1<?php
2
3/**
4 * OpenSSH Formatted EC Key Handler
5 *
6 * PHP version 5
7 *
8 * Place in $HOME/.ssh/authorized_keys
9 *
10 * @category  Crypt
11 * @package   EC
12 * @author    Jim Wigginton <terrafrost@php.net>
13 * @copyright 2015 Jim Wigginton
14 * @license   http://www.opensource.org/licenses/mit-license.html  MIT License
15 * @link      http://phpseclib.sourceforge.net
16 */
17
18namespace phpseclib3\Crypt\EC\Formats\Keys;
19
20use phpseclib3\Common\Functions\Strings;
21use phpseclib3\Crypt\Common\Formats\Keys\OpenSSH as Progenitor;
22use phpseclib3\Crypt\EC\BaseCurves\Base as BaseCurve;
23use phpseclib3\Crypt\EC\Curves\Ed25519;
24use phpseclib3\Exception\UnsupportedCurveException;
25use phpseclib3\Math\BigInteger;
26
27/**
28 * OpenSSH Formatted EC Key Handler
29 *
30 * @package EC
31 * @author  Jim Wigginton <terrafrost@php.net>
32 * @access  public
33 */
34abstract class OpenSSH extends Progenitor
35{
36    use Common;
37
38    /**
39     * Supported Key Types
40     *
41     * @var array
42     */
43    protected static $types = [
44        'ecdsa-sha2-nistp256',
45        'ecdsa-sha2-nistp384',
46        'ecdsa-sha2-nistp521',
47        'ssh-ed25519'
48    ];
49
50    /**
51     * Break a public or private key down into its constituent components
52     *
53     * @access public
54     * @param string $key
55     * @param string $password optional
56     * @return array
57     */
58    public static function load($key, $password = '')
59    {
60        $parsed = parent::load($key, $password);
61
62        if (isset($parsed['paddedKey'])) {
63            $paddedKey = $parsed['paddedKey'];
64            list($type) = Strings::unpackSSH2('s', $paddedKey);
65            if ($type != $parsed['type']) {
66                throw new \RuntimeException("The public and private keys are not of the same type ($type vs $parsed[type])");
67            }
68            if ($type == 'ssh-ed25519') {
69                list(, $key, $comment) = Strings::unpackSSH2('sss', $paddedKey);
70                $key = libsodium::load($key);
71                $key['comment'] = $comment;
72                return $key;
73            }
74            list($curveName, $publicKey, $privateKey, $comment) = Strings::unpackSSH2('ssis', $paddedKey);
75            $curve = self::loadCurveByParam(['namedCurve' => $curveName]);
76            $curve->rangeCheck($privateKey);
77            return [
78                'curve' => $curve,
79                'dA' => $privateKey,
80                'QA' => self::extractPoint("\0$publicKey", $curve),
81                'comment' => $comment
82            ];
83        }
84
85        if ($parsed['type'] == 'ssh-ed25519') {
86            if (Strings::shift($parsed['publicKey'], 4) != "\0\0\0\x20") {
87                throw new \RuntimeException('Length of ssh-ed25519 key should be 32');
88            }
89
90            $curve = new Ed25519();
91            $qa = self::extractPoint($parsed['publicKey'], $curve);
92        } else {
93            list($curveName, $publicKey) = Strings::unpackSSH2('ss', $parsed['publicKey']);
94            $curveName = '\phpseclib3\Crypt\EC\Curves\\' . $curveName;
95            $curve = new $curveName();
96
97            $qa = self::extractPoint("\0" . $publicKey, $curve);
98        }
99
100        return [
101            'curve' => $curve,
102            'QA' => $qa,
103            'comment' => $parsed['comment']
104        ];
105    }
106
107    /**
108     * Returns the alias that corresponds to a curve
109     *
110     * @return string
111     */
112    private static function getAlias(BaseCurve $curve)
113    {
114        self::initialize_static_variables();
115
116        $reflect = new \ReflectionClass($curve);
117        $name = $reflect->getShortName();
118
119        $oid = self::$curveOIDs[$name];
120        $aliases = array_filter(self::$curveOIDs, function ($v) use ($oid) {
121            return $v == $oid;
122        });
123        $aliases = array_keys($aliases);
124
125        for ($i = 0; $i < count($aliases); $i++) {
126            if (in_array('ecdsa-sha2-' . $aliases[$i], self::$types)) {
127                $alias = $aliases[$i];
128                break;
129            }
130        }
131
132        if (!isset($alias)) {
133            throw new UnsupportedCurveException($name . ' is not a curve that the OpenSSH plugin supports');
134        }
135
136        return $alias;
137    }
138
139    /**
140     * Convert an EC public key to the appropriate format
141     *
142     * @access public
143     * @param \phpseclib3\Crypt\EC\BaseCurves\Base $curve
144     * @param \phpseclib3\Math\Common\FiniteField\Integer[] $publicKey
145     * @param array $options optional
146     * @return string
147     */
148    public static function savePublicKey(BaseCurve $curve, array $publicKey, array $options = [])
149    {
150        $comment = isset($options['comment']) ? $options['comment'] : self::$comment;
151
152        if ($curve instanceof Ed25519) {
153            $key = Strings::packSSH2('ss', 'ssh-ed25519', $curve->encodePoint($publicKey));
154
155            if (isset($options['binary']) ? $options['binary'] : self::$binary) {
156                return $key;
157            }
158
159            $key = 'ssh-ed25519 ' . base64_encode($key) . ' ' . $comment;
160            return $key;
161        }
162
163        $alias = self::getAlias($curve);
164
165        $points = "\4" . $publicKey[0]->toBytes() . $publicKey[1]->toBytes();
166        $key = Strings::packSSH2('sss', 'ecdsa-sha2-' . $alias, $alias, $points);
167
168        if (isset($options['binary']) ? $options['binary'] : self::$binary) {
169            return $key;
170        }
171
172        $key = 'ecdsa-sha2-' . $alias . ' ' . base64_encode($key) . ' ' . $comment;
173
174        return $key;
175    }
176
177    /**
178     * Convert a private key to the appropriate format.
179     *
180     * @access public
181     * @param \phpseclib3\Math\BigInteger $privateKey
182     * @param \phpseclib3\Crypt\EC\Curves\Ed25519 $curve
183     * @param \phpseclib3\Math\Common\FiniteField\Integer[] $publicKey
184     * @param string $password optional
185     * @param array $options optional
186     * @return string
187     */
188    public static function savePrivateKey(BigInteger $privateKey, BaseCurve $curve, array $publicKey, $password = '', array $options = [])
189    {
190        if ($curve instanceof Ed25519) {
191            if (!isset($privateKey->secret)) {
192                throw new \RuntimeException('Private Key does not have a secret set');
193            }
194            if (strlen($privateKey->secret) != 32) {
195                throw new \RuntimeException('Private Key secret is not of the correct length');
196            }
197
198            $pubKey = $curve->encodePoint($publicKey);
199
200            $publicKey = Strings::packSSH2('ss', 'ssh-ed25519', $pubKey);
201            $privateKey = Strings::packSSH2('sss', 'ssh-ed25519', $pubKey, $privateKey->secret . $pubKey);
202
203            return self::wrapPrivateKey($publicKey, $privateKey, $password, $options);
204        }
205
206        $alias = self::getAlias($curve);
207
208        $points = "\4" . $publicKey[0]->toBytes() . $publicKey[1]->toBytes();
209        $publicKey = self::savePublicKey($curve, $publicKey, ['binary' => true]);
210
211        $privateKey = Strings::packSSH2('sssi', 'ecdsa-sha2-' . $alias, $alias, $points, $privateKey);
212
213        return self::wrapPrivateKey($publicKey, $privateKey, $password, $options);
214    }
215}
216