Subversion Repositories oidplus

Rev

Rev 846 | Go to most recent revision | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
827 daniel-mar 1
<?php
2
 
3
/**
4
 * PuTTY Formatted Key Handler
5
 *
6
 * See PuTTY's SSHPUBK.C and https://tartarus.org/~simon/putty-snapshots/htmldoc/AppendixC.html
7
 *
8
 * PHP version 5
9
 *
874 daniel-mar 10
 * @category  Crypt
11
 * @package   Common
827 daniel-mar 12
 * @author    Jim Wigginton <terrafrost@php.net>
13
 * @copyright 2016 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 ParagonIE\ConstantTime\Hex;
22
use phpseclib3\Common\Functions\Strings;
23
use phpseclib3\Crypt\AES;
24
use phpseclib3\Crypt\Hash;
25
use phpseclib3\Crypt\Random;
26
use phpseclib3\Exception\UnsupportedAlgorithmException;
27
 
28
/**
29
 * PuTTY Formatted Key Handler
30
 *
874 daniel-mar 31
 * @package Common
827 daniel-mar 32
 * @author  Jim Wigginton <terrafrost@php.net>
874 daniel-mar 33
 * @access  public
827 daniel-mar 34
 */
35
abstract class PuTTY
36
{
37
    /**
38
     * Default comment
39
     *
40
     * @var string
874 daniel-mar 41
     * @access private
827 daniel-mar 42
     */
43
    private static $comment = 'phpseclib-generated-key';
44
 
45
    /**
46
     * Default version
47
     *
48
     * @var int
874 daniel-mar 49
     * @access private
827 daniel-mar 50
     */
51
    private static $version = 2;
52
 
53
    /**
54
     * Sets the default comment
55
     *
874 daniel-mar 56
     * @access public
827 daniel-mar 57
     * @param string $comment
58
     */
59
    public static function setComment($comment)
60
    {
61
        self::$comment = str_replace(["\r", "\n"], '', $comment);
62
    }
63
 
64
    /**
65
     * Sets the default version
66
     *
874 daniel-mar 67
     * @access public
827 daniel-mar 68
     * @param int $version
69
     */
70
    public static function setVersion($version)
71
    {
72
        if ($version != 2 && $version != 3) {
73
            throw new \RuntimeException('Only supported versions are 2 and 3');
74
        }
75
        self::$version = $version;
76
    }
77
 
78
    /**
79
     * Generate a symmetric key for PuTTY v2 keys
80
     *
874 daniel-mar 81
     * @access private
827 daniel-mar 82
     * @param string $password
83
     * @param int $length
84
     * @return string
85
     */
86
    private static function generateV2Key($password, $length)
87
    {
88
        $symkey = '';
89
        $sequence = 0;
90
        while (strlen($symkey) < $length) {
91
            $temp = pack('Na*', $sequence++, $password);
92
            $symkey .= Hex::decode(sha1($temp));
93
        }
94
        return substr($symkey, 0, $length);
95
    }
96
 
97
    /**
98
     * Generate a symmetric key for PuTTY v3 keys
99
     *
874 daniel-mar 100
     * @access private
827 daniel-mar 101
     * @param string $password
102
     * @param string $flavour
103
     * @param int $memory
104
     * @param int $passes
105
     * @param string $salt
106
     * @return array
107
     */
108
    private static function generateV3Key($password, $flavour, $memory, $passes, $salt)
109
    {
110
        if (!function_exists('sodium_crypto_pwhash')) {
111
            throw new \RuntimeException('sodium_crypto_pwhash needs to exist for Argon2 password hasing');
112
        }
113
 
114
        switch ($flavour) {
115
            case 'Argon2i':
116
                $flavour = SODIUM_CRYPTO_PWHASH_ALG_ARGON2I13;
117
                break;
118
            case 'Argon2id':
119
                $flavour = SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13;
120
                break;
121
            default:
122
                throw new UnsupportedAlgorithmException('Only Argon2i and Argon2id are supported');
123
        }
124
 
125
        $length = 80; // keylen + ivlen + mac_keylen
126
        $temp = sodium_crypto_pwhash($length, $password, $salt, $passes, $memory << 10, $flavour);
127
 
128
        $symkey = substr($temp, 0, 32);
129
        $symiv = substr($temp, 32, 16);
130
        $hashkey = substr($temp, -32);
131
 
132
        return compact('symkey', 'symiv', 'hashkey');
133
    }
134
 
135
    /**
136
     * Break a public or private key down into its constituent components
137
     *
874 daniel-mar 138
     * @access public
827 daniel-mar 139
     * @param string $key
140
     * @param string $password
141
     * @return array
142
     */
143
    public static function load($key, $password)
144
    {
145
        if (!Strings::is_stringable($key)) {
146
            throw new \UnexpectedValueException('Key should be a string - not a ' . gettype($key));
147
        }
148
 
149
        if (strpos($key, 'BEGIN SSH2 PUBLIC KEY') !== false) {
150
            $lines = preg_split('#[\r\n]+#', $key);
151
            switch (true) {
152
                case $lines[0] != '---- BEGIN SSH2 PUBLIC KEY ----':
153
                    throw new \UnexpectedValueException('Key doesn\'t start with ---- BEGIN SSH2 PUBLIC KEY ----');
154
                case $lines[count($lines) - 1] != '---- END SSH2 PUBLIC KEY ----':
155
                    throw new \UnexpectedValueException('Key doesn\'t end with ---- END SSH2 PUBLIC KEY ----');
156
            }
157
            $lines = array_splice($lines, 1, -1);
158
            $lines = array_map(function ($line) {
159
                return rtrim($line, "\r\n");
160
            }, $lines);
161
            $data = $current = '';
162
            $values = [];
163
            $in_value = false;
164
            foreach ($lines as $line) {
165
                switch (true) {
166
                    case preg_match('#^(.*?): (.*)#', $line, $match):
167
                        $in_value = $line[strlen($line) - 1] == '\\';
168
                        $current = strtolower($match[1]);
169
                        $values[$current] = $in_value ? substr($match[2], 0, -1) : $match[2];
170
                        break;
171
                    case $in_value:
172
                        $in_value = $line[strlen($line) - 1] == '\\';
173
                        $values[$current] .= $in_value ? substr($line, 0, -1) : $line;
174
                        break;
175
                    default:
176
                        $data .= $line;
177
                }
178
            }
179
 
180
            $components = call_user_func([static::PUBLIC_HANDLER, 'load'], $data);
181
            if ($components === false) {
182
                throw new \UnexpectedValueException('Unable to decode public key');
183
            }
184
            $components += $values;
185
            $components['comment'] = str_replace(['\\\\', '\"'], ['\\', '"'], $values['comment']);
186
 
187
            return $components;
188
        }
189
 
190
        $components = [];
191
 
192
        $key = preg_split('#\r\n|\r|\n#', trim($key));
193
        if (Strings::shift($key[0], strlen('PuTTY-User-Key-File-')) != 'PuTTY-User-Key-File-') {
194
            return false;
195
        }
196
        $version = (int) Strings::shift($key[0], 3); // should be either "2: " or "3: 0" prior to int casting
197
        if ($version != 2 && $version != 3) {
198
            throw new \RuntimeException('Only v2 and v3 PuTTY private keys are supported');
199
        }
200
        $components['type'] = $type = rtrim($key[0]);
201
        if (!in_array($type, static::$types)) {
202
            $error = count(static::$types) == 1 ?
203
                'Only ' . static::$types[0] . ' keys are supported. ' :
204
                '';
205
            throw new UnsupportedAlgorithmException($error . 'This is an unsupported ' . $type . ' key');
206
        }
207
        $encryption = trim(preg_replace('#Encryption: (.+)#', '$1', $key[1]));
208
        $components['comment'] = trim(preg_replace('#Comment: (.+)#', '$1', $key[2]));
209
 
210
        $publicLength = trim(preg_replace('#Public-Lines: (\d+)#', '$1', $key[3]));
211
        $public = Base64::decode(implode('', array_map('trim', array_slice($key, 4, $publicLength))));
212
 
213
        $source = Strings::packSSH2('ssss', $type, $encryption, $components['comment'], $public);
214
 
215
        extract(unpack('Nlength', Strings::shift($public, 4)));
216
        $newtype = Strings::shift($public, $length);
217
        if ($newtype != $type) {
218
            throw new \RuntimeException('The binary type does not match the human readable type field');
219
        }
220
 
221
        $components['public'] = $public;
222
 
223
        switch ($version) {
224
            case 3:
225
                $hashkey = '';
226
                break;
227
            case 2:
228
                $hashkey = 'putty-private-key-file-mac-key';
229
        }
230
 
231
        $offset = $publicLength + 4;
232
        switch ($encryption) {
233
            case 'aes256-cbc':
234
                $crypto = new AES('cbc');
235
                switch ($version) {
236
                    case 3:
237
                        $flavour = trim(preg_replace('#Key-Derivation: (.*)#', '$1', $key[$offset++]));
238
                        $memory = trim(preg_replace('#Argon2-Memory: (\d+)#', '$1', $key[$offset++]));
239
                        $passes = trim(preg_replace('#Argon2-Passes: (\d+)#', '$1', $key[$offset++]));
240
                        $parallelism = trim(preg_replace('#Argon2-Parallelism: (\d+)#', '$1', $key[$offset++]));
241
                        $salt = Hex::decode(trim(preg_replace('#Argon2-Salt: ([0-9a-f]+)#', '$1', $key[$offset++])));
242
 
243
                        extract(self::generateV3Key($password, $flavour, $memory, $passes, $salt));
244
 
245
                        break;
246
                    case 2:
247
                        $symkey = self::generateV2Key($password, 32);
248
                        $symiv = str_repeat("\0", $crypto->getBlockLength() >> 3);
249
                        $hashkey .= $password;
250
                }
251
        }
252
 
253
        switch ($version) {
254
            case 3:
255
                $hash = new Hash('sha256');
256
                $hash->setKey($hashkey);
257
                break;
258
            case 2:
259
                $hash = new Hash('sha1');
260
                $hash->setKey(sha1($hashkey, true));
261
        }
262
 
263
        $privateLength = trim(preg_replace('#Private-Lines: (\d+)#', '$1', $key[$offset++]));
264
        $private = Base64::decode(implode('', array_map('trim', array_slice($key, $offset, $privateLength))));
265
 
266
        if ($encryption != 'none') {
267
            $crypto->setKey($symkey);
268
            $crypto->setIV($symiv);
269
            $crypto->disablePadding();
270
            $private = $crypto->decrypt($private);
271
        }
272
 
273
        $source .= Strings::packSSH2('s', $private);
274
 
275
        $hmac = trim(preg_replace('#Private-MAC: (.+)#', '$1', $key[$offset + $privateLength]));
276
        $hmac = Hex::decode($hmac);
277
 
278
        if (!hash_equals($hash->hash($source), $hmac)) {
279
            throw new \UnexpectedValueException('MAC validation error');
280
        }
281
 
282
        $components['private'] = $private;
283
 
284
        return $components;
285
    }
286
 
287
    /**
288
     * Wrap a private key appropriately
289
     *
874 daniel-mar 290
     * @access private
827 daniel-mar 291
     * @param string $public
292
     * @param string $private
293
     * @param string $type
294
     * @param string $password
295
     * @param array $options optional
296
     * @return string
297
     */
298
    protected static function wrapPrivateKey($public, $private, $type, $password, array $options = [])
299
    {
300
        $encryption = (!empty($password) || is_string($password)) ? 'aes256-cbc' : 'none';
301
        $comment = isset($options['comment']) ? $options['comment'] : self::$comment;
302
        $version = isset($options['version']) ? $options['version'] : self::$version;
303
 
304
        $key = "PuTTY-User-Key-File-$version: $type\r\n";
305
        $key .= "Encryption: $encryption\r\n";
306
        $key .= "Comment: $comment\r\n";
307
 
308
        $public = Strings::packSSH2('s', $type) . $public;
309
 
310
        $source = Strings::packSSH2('ssss', $type, $encryption, $comment, $public);
311
 
312
        $public = Base64::encode($public);
313
        $key .= "Public-Lines: " . ((strlen($public) + 63) >> 6) . "\r\n";
314
        $key .= chunk_split($public, 64);
315
 
316
        if (empty($password) && !is_string($password)) {
317
            $source .= Strings::packSSH2('s', $private);
318
            switch ($version) {
319
                case 3:
320
                    $hash = new Hash('sha256');
321
                    $hash->setKey('');
322
                    break;
323
                case 2:
324
                    $hash = new Hash('sha1');
325
                    $hash->setKey(sha1('putty-private-key-file-mac-key', true));
326
            }
327
        } else {
328
            $private .= Random::string(16 - (strlen($private) & 15));
329
            $source .= Strings::packSSH2('s', $private);
330
            $crypto = new AES('cbc');
331
 
332
            switch ($version) {
333
                case 3:
334
                    $salt = Random::string(16);
335
                    $key .= "Key-Derivation: Argon2id\r\n";
336
                    $key .= "Argon2-Memory: 8192\r\n";
337
                    $key .= "Argon2-Passes: 13\r\n";
338
                    $key .= "Argon2-Parallelism: 1\r\n";
339
                    $key .= "Argon2-Salt: " . Hex::encode($salt) . "\r\n";
340
                    extract(self::generateV3Key($password, 'Argon2id', 8192, 13, $salt));
341
 
342
                    $hash = new Hash('sha256');
343
                    $hash->setKey($hashkey);
344
 
345
                    break;
346
                case 2:
347
                    $symkey = self::generateV2Key($password, 32);
348
                    $symiv = str_repeat("\0", $crypto->getBlockLength() >> 3);
349
                    $hashkey = 'putty-private-key-file-mac-key' . $password;
350
 
351
                    $hash = new Hash('sha1');
352
                    $hash->setKey(sha1($hashkey, true));
353
            }
354
 
355
            $crypto->setKey($symkey);
356
            $crypto->setIV($symiv);
357
            $crypto->disablePadding();
358
            $private = $crypto->encrypt($private);
359
            $mac = $hash->hash($source);
360
        }
361
 
362
        $private = Base64::encode($private);
363
        $key .= 'Private-Lines: ' . ((strlen($private) + 63) >> 6) . "\r\n";
364
        $key .= chunk_split($private, 64);
365
        $key .= 'Private-MAC: ' . Hex::encode($hash->hash($source)) . "\r\n";
366
 
367
        return $key;
368
    }
369
 
370
    /**
371
     * Wrap a public key appropriately
372
     *
373
     * This is basically the format described in RFC 4716 (https://tools.ietf.org/html/rfc4716)
374
     *
874 daniel-mar 375
     * @access private
827 daniel-mar 376
     * @param string $key
377
     * @param string $type
378
     * @return string
379
     */
380
    protected static function wrapPublicKey($key, $type)
381
    {
382
        $key = pack('Na*a*', strlen($type), $type, $key);
383
        $key = "---- BEGIN SSH2 PUBLIC KEY ----\r\n" .
384
               'Comment: "' . str_replace(['\\', '"'], ['\\\\', '\"'], self::$comment) . "\"\r\n" .
385
               chunk_split(Base64::encode($key), 64) .
386
               '---- END SSH2 PUBLIC KEY ----';
387
        return $key;
388
    }
389
}