Subversion Repositories oidplus

Rev

Rev 846 | Rev 1042 | 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
 * OpenSSH Key Handler
5
 *
6
 * PHP version 5
7
 *
8
 * Place in $HOME/.ssh/authorized_keys
9
 *
874 daniel-mar 10
 * @category  Crypt
11
 * @package   Common
827 daniel-mar 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
 *
874 daniel-mar 28
 * @package Common
827 daniel-mar 29
 * @author  Jim Wigginton <terrafrost@php.net>
874 daniel-mar 30
 * @access  public
827 daniel-mar 31
 */
32
abstract class OpenSSH
33
{
34
    /**
35
     * Default comment
36
     *
37
     * @var string
874 daniel-mar 38
     * @access private
827 daniel-mar 39
     */
40
    protected static $comment = 'phpseclib-generated-key';
41
 
42
    /**
43
     * Binary key flag
44
     *
45
     * @var bool
874 daniel-mar 46
     * @access private
827 daniel-mar 47
     */
48
    protected static $binary = false;
49
 
50
    /**
51
     * Sets the default comment
52
     *
874 daniel-mar 53
     * @access public
827 daniel-mar 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
     *
874 daniel-mar 66
     * @access public
827 daniel-mar 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
     *
874 daniel-mar 172
     * @access public
827 daniel-mar 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
     *
874 daniel-mar 183
     * @access private
827 daniel-mar 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
     *
874 daniel-mar 196
     * @access public
827 daniel-mar 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
}