Rev 1042 | Details | Compare with Previous | Last modification | View Log | RSS feed
Rev | Author | Line No. | Line |
---|---|---|---|
827 | daniel-mar | 1 | <?php |
2 | |||
3 | /** |
||
4 | * OpenSSH Key Handler |
||
5 | * |
||
6 | * PHP version 5 |
||
7 | * |
||
8 | * Place in $HOME/.ssh/authorized_keys |
||
9 | * |
||
10 | * @author Jim Wigginton <terrafrost@php.net> |
||
11 | * @copyright 2015 Jim Wigginton |
||
12 | * @license http://www.opensource.org/licenses/mit-license.html MIT License |
||
13 | * @link http://phpseclib.sourceforge.net |
||
14 | */ |
||
15 | |||
16 | namespace phpseclib3\Crypt\Common\Formats\Keys; |
||
17 | |||
18 | use phpseclib3\Common\Functions\Strings; |
||
1042 | daniel-mar | 19 | use phpseclib3\Crypt\AES; |
827 | daniel-mar | 20 | use phpseclib3\Crypt\Random; |
1471 | daniel-mar | 21 | use phpseclib3\Exception\BadDecryptionException; |
827 | daniel-mar | 22 | |
23 | /** |
||
24 | * OpenSSH Formatted RSA Key Handler |
||
25 | * |
||
26 | * @author Jim Wigginton <terrafrost@php.net> |
||
27 | */ |
||
28 | abstract class OpenSSH |
||
29 | { |
||
30 | /** |
||
31 | * Default comment |
||
32 | * |
||
33 | * @var string |
||
34 | */ |
||
35 | protected static $comment = 'phpseclib-generated-key'; |
||
36 | |||
37 | /** |
||
38 | * Binary key flag |
||
39 | * |
||
40 | * @var bool |
||
41 | */ |
||
42 | protected static $binary = false; |
||
43 | |||
44 | /** |
||
45 | * Sets the default comment |
||
46 | * |
||
47 | * @param string $comment |
||
48 | */ |
||
49 | public static function setComment($comment) |
||
50 | { |
||
51 | self::$comment = str_replace(["\r", "\n"], '', $comment); |
||
52 | } |
||
53 | |||
54 | /** |
||
55 | * Break a public or private key down into its constituent components |
||
56 | * |
||
57 | * $type can be either ssh-dss or ssh-rsa |
||
58 | * |
||
59 | * @param string $key |
||
60 | * @param string $password |
||
61 | * @return array |
||
62 | */ |
||
63 | public static function load($key, $password = '') |
||
64 | { |
||
65 | if (!Strings::is_stringable($key)) { |
||
66 | throw new \UnexpectedValueException('Key should be a string - not a ' . gettype($key)); |
||
67 | } |
||
68 | |||
69 | // key format is described here: |
||
70 | // https://cvsweb.openbsd.org/cgi-bin/cvsweb/src/usr.bin/ssh/PROTOCOL.key?annotate=HEAD |
||
71 | |||
72 | if (strpos($key, 'BEGIN OPENSSH PRIVATE KEY') !== false) { |
||
73 | $key = preg_replace('#(?:^-.*?-[\r\n]*$)|\s#ms', '', $key); |
||
1042 | daniel-mar | 74 | $key = Strings::base64_decode($key); |
827 | daniel-mar | 75 | $magic = Strings::shift($key, 15); |
76 | if ($magic != "openssh-key-v1\0") { |
||
77 | throw new \RuntimeException('Expected openssh-key-v1'); |
||
78 | } |
||
79 | list($ciphername, $kdfname, $kdfoptions, $numKeys) = Strings::unpackSSH2('sssN', $key); |
||
80 | if ($numKeys != 1) { |
||
81 | // if we wanted to support multiple keys we could update PublicKeyLoader to preview what the # of keys |
||
82 | // would be; it'd then call Common\Keys\OpenSSH.php::load() and get the paddedKey. it'd then pass |
||
83 | // that to the appropriate key loading parser $numKey times or something |
||
84 | throw new \RuntimeException('Although the OpenSSH private key format supports multiple keys phpseclib does not'); |
||
85 | } |
||
1042 | daniel-mar | 86 | switch ($ciphername) { |
87 | case 'none': |
||
88 | break; |
||
89 | case 'aes256-ctr': |
||
90 | if ($kdfname != 'bcrypt') { |
||
91 | throw new \RuntimeException('Only the bcrypt kdf is supported (' . $kdfname . ' encountered)'); |
||
92 | } |
||
93 | list($salt, $rounds) = Strings::unpackSSH2('sN', $kdfoptions); |
||
94 | $crypto = new AES('ctr'); |
||
95 | //$crypto->setKeyLength(256); |
||
96 | //$crypto->disablePadding(); |
||
97 | $crypto->setPassword($password, 'bcrypt', $salt, $rounds, 32); |
||
98 | break; |
||
99 | default: |
||
1471 | daniel-mar | 100 | throw new \RuntimeException('The only supported ciphers are: none, aes256-ctr (' . $ciphername . ' is being used)'); |
827 | daniel-mar | 101 | } |
102 | |||
103 | list($publicKey, $paddedKey) = Strings::unpackSSH2('ss', $key); |
||
104 | list($type) = Strings::unpackSSH2('s', $publicKey); |
||
1042 | daniel-mar | 105 | if (isset($crypto)) { |
106 | $paddedKey = $crypto->decrypt($paddedKey); |
||
107 | } |
||
827 | daniel-mar | 108 | list($checkint1, $checkint2) = Strings::unpackSSH2('NN', $paddedKey); |
109 | // any leftover bytes in $paddedKey are for padding? but they should be sequential bytes. eg. 1, 2, 3, etc. |
||
110 | if ($checkint1 != $checkint2) { |
||
1471 | daniel-mar | 111 | if (isset($crypto)) { |
112 | throw new BadDecryptionException('Unable to decrypt key - please verify the password you are using'); |
||
113 | } |
||
114 | throw new \RuntimeException("The two checkints do not match ($checkint1 vs. $checkint2)"); |
||
827 | daniel-mar | 115 | } |
116 | self::checkType($type); |
||
117 | |||
118 | return compact('type', 'publicKey', 'paddedKey'); |
||
119 | } |
||
120 | |||
121 | $parts = explode(' ', $key, 3); |
||
122 | |||
123 | if (!isset($parts[1])) { |
||
124 | $key = base64_decode($parts[0]); |
||
1042 | daniel-mar | 125 | $comment = false; |
827 | daniel-mar | 126 | } else { |
127 | $asciiType = $parts[0]; |
||
128 | self::checkType($parts[0]); |
||
129 | $key = base64_decode($parts[1]); |
||
130 | $comment = isset($parts[2]) ? $parts[2] : false; |
||
131 | } |
||
132 | if ($key === false) { |
||
133 | throw new \UnexpectedValueException('Key should be a string - not a ' . gettype($key)); |
||
134 | } |
||
135 | |||
136 | list($type) = Strings::unpackSSH2('s', $key); |
||
137 | self::checkType($type); |
||
138 | if (isset($asciiType) && $asciiType != $type) { |
||
139 | throw new \RuntimeException('Two different types of keys are claimed: ' . $asciiType . ' and ' . $type); |
||
140 | } |
||
141 | if (strlen($key) <= 4) { |
||
142 | throw new \UnexpectedValueException('Key appears to be malformed'); |
||
143 | } |
||
144 | |||
145 | $publicKey = $key; |
||
146 | |||
147 | return compact('type', 'publicKey', 'comment'); |
||
148 | } |
||
149 | |||
150 | /** |
||
151 | * Toggle between binary and printable keys |
||
152 | * |
||
153 | * Printable keys are what are generated by default. These are the ones that go in |
||
154 | * $HOME/.ssh/authorized_key. |
||
155 | * |
||
156 | * @param bool $enabled |
||
157 | */ |
||
158 | public static function setBinaryOutput($enabled) |
||
159 | { |
||
160 | self::$binary = $enabled; |
||
161 | } |
||
162 | |||
163 | /** |
||
164 | * Checks to see if the type is valid |
||
165 | * |
||
166 | * @param string $candidate |
||
167 | */ |
||
168 | private static function checkType($candidate) |
||
169 | { |
||
170 | if (!in_array($candidate, static::$types)) { |
||
171 | throw new \RuntimeException("The key type ($candidate) is not equal to: " . implode(',', static::$types)); |
||
172 | } |
||
173 | } |
||
174 | |||
175 | /** |
||
176 | * Wrap a private key appropriately |
||
177 | * |
||
178 | * @param string $publicKey |
||
179 | * @param string $privateKey |
||
180 | * @param string $password |
||
181 | * @param array $options |
||
182 | * @return string |
||
183 | */ |
||
184 | protected static function wrapPrivateKey($publicKey, $privateKey, $password, $options) |
||
185 | { |
||
186 | list(, $checkint) = unpack('N', Random::string(4)); |
||
187 | |||
188 | $comment = isset($options['comment']) ? $options['comment'] : self::$comment; |
||
189 | $paddedKey = Strings::packSSH2('NN', $checkint, $checkint) . |
||
190 | $privateKey . |
||
191 | Strings::packSSH2('s', $comment); |
||
192 | |||
1042 | daniel-mar | 193 | $usesEncryption = !empty($password) && is_string($password); |
194 | |||
827 | daniel-mar | 195 | /* |
196 | from http://tools.ietf.org/html/rfc4253#section-6 : |
||
197 | |||
198 | Note that the length of the concatenation of 'packet_length', |
||
199 | 'padding_length', 'payload', and 'random padding' MUST be a multiple |
||
200 | of the cipher block size or 8, whichever is larger. |
||
201 | */ |
||
1042 | daniel-mar | 202 | $blockSize = $usesEncryption ? 16 : 8; |
203 | $paddingLength = (($blockSize - 1) * strlen($paddedKey)) % $blockSize; |
||
827 | daniel-mar | 204 | for ($i = 1; $i <= $paddingLength; $i++) { |
205 | $paddedKey .= chr($i); |
||
206 | } |
||
1042 | daniel-mar | 207 | if (!$usesEncryption) { |
208 | $key = Strings::packSSH2('sssNss', 'none', 'none', '', 1, $publicKey, $paddedKey); |
||
209 | } else { |
||
210 | $rounds = isset($options['rounds']) ? $options['rounds'] : 16; |
||
211 | $salt = Random::string(16); |
||
212 | $kdfoptions = Strings::packSSH2('sN', $salt, $rounds); |
||
213 | $crypto = new AES('ctr'); |
||
214 | $crypto->setPassword($password, 'bcrypt', $salt, $rounds, 32); |
||
215 | $paddedKey = $crypto->encrypt($paddedKey); |
||
216 | $key = Strings::packSSH2('sssNss', 'aes256-ctr', 'bcrypt', $kdfoptions, 1, $publicKey, $paddedKey); |
||
217 | } |
||
827 | daniel-mar | 218 | $key = "openssh-key-v1\0$key"; |
219 | |||
220 | return "-----BEGIN OPENSSH PRIVATE KEY-----\n" . |
||
1042 | daniel-mar | 221 | chunk_split(Strings::base64_encode($key), 70, "\n") . |
827 | daniel-mar | 222 | "-----END OPENSSH PRIVATE KEY-----\n"; |
223 | } |
||
224 | } |