Subversion Repositories oidplus

Rev

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
}