Rev 846 | Go to most recent revision | Details | 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 | * @category Crypt |
||
11 | * @package Common |
||
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 | |||
18 | namespace phpseclib3\Crypt\Common\Formats\Keys; |
||
19 | |||
20 | use ParagonIE\ConstantTime\Base64; |
||
21 | use phpseclib3\Common\Functions\Strings; |
||
22 | use phpseclib3\Crypt\Random; |
||
23 | use phpseclib3\Exception\UnsupportedFormatException; |
||
24 | |||
25 | /** |
||
26 | * OpenSSH Formatted RSA Key Handler |
||
27 | * |
||
28 | * @package Common |
||
29 | * @author Jim Wigginton <terrafrost@php.net> |
||
30 | * @access public |
||
31 | */ |
||
32 | abstract class OpenSSH |
||
33 | { |
||
34 | /** |
||
35 | * Default comment |
||
36 | * |
||
37 | * @var string |
||
38 | * @access private |
||
39 | */ |
||
40 | protected static $comment = 'phpseclib-generated-key'; |
||
41 | |||
42 | /** |
||
43 | * Binary key flag |
||
44 | * |
||
45 | * @var bool |
||
46 | * @access private |
||
47 | */ |
||
48 | protected static $binary = false; |
||
49 | |||
50 | /** |
||
51 | * Sets the default comment |
||
52 | * |
||
53 | * @access public |
||
54 | * @param string $comment |
||
55 | */ |
||
56 | public static function setComment($comment) |
||
57 | { |
||
58 | self::$comment = str_replace(["\r", "\n"], '', $comment); |
||
59 | } |
||
60 | |||
61 | /** |
||
62 | * Break a public or private key down into its constituent components |
||
63 | * |
||
64 | * $type can be either ssh-dss or ssh-rsa |
||
65 | * |
||
66 | * @access public |
||
67 | * @param string $key |
||
68 | * @param string $password |
||
69 | * @return array |
||
70 | */ |
||
71 | public static function load($key, $password = '') |
||
72 | { |
||
73 | if (!Strings::is_stringable($key)) { |
||
74 | throw new \UnexpectedValueException('Key should be a string - not a ' . gettype($key)); |
||
75 | } |
||
76 | |||
77 | // key format is described here: |
||
78 | // https://cvsweb.openbsd.org/cgi-bin/cvsweb/src/usr.bin/ssh/PROTOCOL.key?annotate=HEAD |
||
79 | |||
80 | if (strpos($key, 'BEGIN OPENSSH PRIVATE KEY') !== false) { |
||
81 | $key = preg_replace('#(?:^-.*?-[\r\n]*$)|\s#ms', '', $key); |
||
82 | $key = Base64::decode($key); |
||
83 | $magic = Strings::shift($key, 15); |
||
84 | if ($magic != "openssh-key-v1\0") { |
||
85 | throw new \RuntimeException('Expected openssh-key-v1'); |
||
86 | } |
||
87 | list($ciphername, $kdfname, $kdfoptions, $numKeys) = Strings::unpackSSH2('sssN', $key); |
||
88 | if ($numKeys != 1) { |
||
89 | // if we wanted to support multiple keys we could update PublicKeyLoader to preview what the # of keys |
||
90 | // would be; it'd then call Common\Keys\OpenSSH.php::load() and get the paddedKey. it'd then pass |
||
91 | // that to the appropriate key loading parser $numKey times or something |
||
92 | throw new \RuntimeException('Although the OpenSSH private key format supports multiple keys phpseclib does not'); |
||
93 | } |
||
94 | if (strlen($kdfoptions) || $kdfname != 'none' || $ciphername != 'none') { |
||
95 | /* |
||
96 | OpenSSH private keys use a customized version of bcrypt. specifically, instead of encrypting |
||
97 | OrpheanBeholderScryDoubt 64 times OpenSSH's bcrypt variant encrypts |
||
98 | OxychromaticBlowfishSwatDynamite 64 times. so we can't use crypt(). |
||
99 | |||
100 | bcrypt is basically Blowfish with an altered key expansion. whereas Blowfish just runs the |
||
101 | key through the key expansion bcrypt interleaves the key expansion with the salt and |
||
102 | password. this renders openssl / mcrypt unusuable. this forces us to use a pure-PHP implementation |
||
103 | of bcrypt. the problem with that is that pure-PHP is too slow to be practically useful. |
||
104 | |||
105 | in addition to encrypting a different string 64 times the OpenSSH implementation also performs bcrypt |
||
106 | from scratch $rounds times. calling crypt() 64x with bcrypt takes 0.7s. PHP is going to be naturally |
||
107 | slower. pure-PHP is 215x slower than OpenSSL for AES and pure-PHP is 43x slower for bcrypt. |
||
108 | 43 * 0.7 = 30s. no one wants to wait 30s to load a private key. |
||
109 | |||
110 | another way to think about this.. according to wikipedia's article on Blowfish, |
||
111 | "Each new key requires pre-processing equivalent to encrypting about 4 kilobytes of text". |
||
112 | key expansion is done (9+64*2)*160 times. multiply that by 4 and it turns out that Blowfish, |
||
113 | OpenSSH style, is the equivalent of encrypting ~80mb of text. |
||
114 | |||
115 | more supporting evidence: sodium_compat does not implement Argon2 (another password hashing |
||
116 | algorithm) because "It's not feasible to polyfill scrypt or Argon2 into PHP and get reasonable |
||
117 | performance. Users would feel motivated to select parameters that downgrade security to avoid |
||
118 | denial of service (DoS) attacks. The only winning move is not to play" |
||
119 | -- https://github.com/paragonie/sodium_compat/blob/master/README.md |
||
120 | */ |
||
121 | throw new \RuntimeException('Encrypted OpenSSH private keys are not supported'); |
||
122 | //list($salt, $rounds) = Strings::unpackSSH2('sN', $kdfoptions); |
||
123 | } |
||
124 | |||
125 | list($publicKey, $paddedKey) = Strings::unpackSSH2('ss', $key); |
||
126 | list($type) = Strings::unpackSSH2('s', $publicKey); |
||
127 | list($checkint1, $checkint2) = Strings::unpackSSH2('NN', $paddedKey); |
||
128 | // any leftover bytes in $paddedKey are for padding? but they should be sequential bytes. eg. 1, 2, 3, etc. |
||
129 | if ($checkint1 != $checkint2) { |
||
130 | throw new \RuntimeException('The two checkints do not match'); |
||
131 | } |
||
132 | self::checkType($type); |
||
133 | |||
134 | return compact('type', 'publicKey', 'paddedKey'); |
||
135 | } |
||
136 | |||
137 | $parts = explode(' ', $key, 3); |
||
138 | |||
139 | if (!isset($parts[1])) { |
||
140 | $key = base64_decode($parts[0]); |
||
141 | $comment = isset($parts[1]) ? $parts[1] : false; |
||
142 | } else { |
||
143 | $asciiType = $parts[0]; |
||
144 | self::checkType($parts[0]); |
||
145 | $key = base64_decode($parts[1]); |
||
146 | $comment = isset($parts[2]) ? $parts[2] : false; |
||
147 | } |
||
148 | if ($key === false) { |
||
149 | throw new \UnexpectedValueException('Key should be a string - not a ' . gettype($key)); |
||
150 | } |
||
151 | |||
152 | list($type) = Strings::unpackSSH2('s', $key); |
||
153 | self::checkType($type); |
||
154 | if (isset($asciiType) && $asciiType != $type) { |
||
155 | throw new \RuntimeException('Two different types of keys are claimed: ' . $asciiType . ' and ' . $type); |
||
156 | } |
||
157 | if (strlen($key) <= 4) { |
||
158 | throw new \UnexpectedValueException('Key appears to be malformed'); |
||
159 | } |
||
160 | |||
161 | $publicKey = $key; |
||
162 | |||
163 | return compact('type', 'publicKey', 'comment'); |
||
164 | } |
||
165 | |||
166 | /** |
||
167 | * Toggle between binary and printable keys |
||
168 | * |
||
169 | * Printable keys are what are generated by default. These are the ones that go in |
||
170 | * $HOME/.ssh/authorized_key. |
||
171 | * |
||
172 | * @access public |
||
173 | * @param bool $enabled |
||
174 | */ |
||
175 | public static function setBinaryOutput($enabled) |
||
176 | { |
||
177 | self::$binary = $enabled; |
||
178 | } |
||
179 | |||
180 | /** |
||
181 | * Checks to see if the type is valid |
||
182 | * |
||
183 | * @access private |
||
184 | * @param string $candidate |
||
185 | */ |
||
186 | private static function checkType($candidate) |
||
187 | { |
||
188 | if (!in_array($candidate, static::$types)) { |
||
189 | throw new \RuntimeException("The key type ($candidate) is not equal to: " . implode(',', static::$types)); |
||
190 | } |
||
191 | } |
||
192 | |||
193 | /** |
||
194 | * Wrap a private key appropriately |
||
195 | * |
||
196 | * @access public |
||
197 | * @param string $publicKey |
||
198 | * @param string $privateKey |
||
199 | * @param string $password |
||
200 | * @param array $options |
||
201 | * @return string |
||
202 | */ |
||
203 | protected static function wrapPrivateKey($publicKey, $privateKey, $password, $options) |
||
204 | { |
||
205 | if (!empty($password) && is_string($password)) { |
||
206 | throw new UnsupportedFormatException('Encrypted OpenSSH private keys are not supported'); |
||
207 | } |
||
208 | |||
209 | list(, $checkint) = unpack('N', Random::string(4)); |
||
210 | |||
211 | $comment = isset($options['comment']) ? $options['comment'] : self::$comment; |
||
212 | $paddedKey = Strings::packSSH2('NN', $checkint, $checkint) . |
||
213 | $privateKey . |
||
214 | Strings::packSSH2('s', $comment); |
||
215 | |||
216 | /* |
||
217 | from http://tools.ietf.org/html/rfc4253#section-6 : |
||
218 | |||
219 | Note that the length of the concatenation of 'packet_length', |
||
220 | 'padding_length', 'payload', and 'random padding' MUST be a multiple |
||
221 | of the cipher block size or 8, whichever is larger. |
||
222 | */ |
||
223 | $paddingLength = (7 * strlen($paddedKey)) % 8; |
||
224 | for ($i = 1; $i <= $paddingLength; $i++) { |
||
225 | $paddedKey .= chr($i); |
||
226 | } |
||
227 | $key = Strings::packSSH2('sssNss', 'none', 'none', '', 1, $publicKey, $paddedKey); |
||
228 | $key = "openssh-key-v1\0$key"; |
||
229 | |||
230 | return "-----BEGIN OPENSSH PRIVATE KEY-----\n" . |
||
231 | chunk_split(Base64::encode($key), 70, "\n") . |
||
232 | "-----END OPENSSH PRIVATE KEY-----\n"; |
||
233 | } |
||
234 | } |