Subversion Repositories oidplus

Rev

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
}