Completed
Push — v2.0.x ( 94c950...c40792 )
by Florent
02:29
created

KeyConverter::loadKeyFromFile()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 10
rs 9.4285
cc 2
eloc 6
nc 2
nop 3
1
<?php
2
3
/*
4
 * The MIT License (MIT)
5
 *
6
 * Copyright (c) 2014-2016 Spomky-Labs
7
 *
8
 * This software may be modified and distributed under the terms
9
 * of the MIT license.  See the LICENSE file for details.
10
 */
11
12
namespace Jose\KeyConverter;
13
14
use Base64Url\Base64Url;
15
use phpseclib\Crypt\RSA;
16
17
/**
18
 * This class will help you to load an EC key or a RSA key/certificate (private or public) and get values to create a JWK object.
19
 */
20
final class KeyConverter
21
{
22
    /**
23
     * @param string $file
24
     *
25
     * @throws \InvalidArgumentException
26
     *
27
     * @return array
28
     */
29
    public static function loadKeyFromCertificateFile($file)
30
    {
31
        if (!file_exists($file)) {
32
            throw new \InvalidArgumentException(sprintf('File "%s" does not exist.', $file));
33
        }
34
        $content = file_get_contents($file);
35
36
        return self::loadKeyFromCertificate($content);
37
    }
38
39
    /**
40
     * @param string $certificate
41
     *
42
     * @throws \InvalidArgumentException
43
     *
44
     * @return array
45
     */
46
    public static function loadKeyFromCertificate($certificate)
47
    {
48
        try {
49
            $res = openssl_x509_read($certificate);
50
        } catch (\Exception $e) {
51
            $certificate = self::convertDerToPem($certificate);
52
            $res = openssl_x509_read($certificate);
53
        }
54
        if (false === $res) {
55
            throw new \InvalidArgumentException('Unable to load the certificate');
56
        }
57
        $values = self::loadKeyFromX509Resource($res);
58
        openssl_x509_free($res);
59
60
        return $values;
61
    }
62
63
    /**
64
     * @param resource $res
65
     *
66
     * @throws \Exception
67
     *
68
     * @return array
69
     */
70
    public static function loadKeyFromX509Resource($res)
71
    {
72
        $key = openssl_get_publickey($res);
73
74
        $details = openssl_pkey_get_details($key);
75
        if (isset($details['key'])) {
76
            $values = self::loadKeyFromPEM($details['key']);
77
            if (function_exists('openssl_x509_fingerprint')) {
78
                $values['x5t'] = Base64Url::encode(openssl_x509_fingerprint($res, 'sha1', true));
79
                $values['x5t#256'] = Base64Url::encode(openssl_x509_fingerprint($res, 'sha256', true));
80
            } else {
81
                openssl_x509_export($res, $pem);
82
                $values['x5t'] = Base64Url::encode(self::calculateX509Fingerprint($pem, 'sha1', true));
83
                $values['x5t#256'] = Base64Url::encode(self::calculateX509Fingerprint($pem, 'sha256', true));
84
            }
85
86
            return $values;
87
        }
88
        throw new \InvalidArgumentException('Unable to load the certificate');
89
    }
90
91
    /**
92
     * @param string      $file
93
     * @param null|string $password
94
     *
95
     * @throws \Exception
96
     *
97
     * @return array
98
     */
99
    public static function loadFromKeyFile($file, $password = null)
100
    {
101
        $content = file_get_contents($file);
102
103
        return self::loadFromKey($content, $password);
104
    }
105
106
    /**
107
     * @param string      $key
108
     * @param null|string $password
109
     *
110
     * @throws \Exception
111
     *
112
     * @return array
113
     */
114
    public static function loadFromKey($key, $password = null)
115
    {
116
        try {
117
            return self::loadKeyFromDER($key, $password);
118
        } catch (\Exception $e) {
119
            return self::loadKeyFromPEM($key, $password);
120
        }
121
    }
122
123
    /**
124
     * @param string      $der
125
     * @param null|string $password
126
     *
127
     * @throws \Exception
128
     *
129
     * @return array
130
     */
131
    private static function loadKeyFromDER($der, $password = null)
132
    {
133
        $pem = self::convertDerToPem($der);
134
135
        return self::loadKeyFromPEM($pem, $password);
136
    }
137
138
    /**
139
     * @param string      $pem
140
     * @param null|string $password
141
     *
142
     * @throws \Exception
143
     *
144
     * @return array
145
     */
146
    private static function loadKeyFromPEM($pem, $password = null)
147
    {
148
        if (preg_match('#DEK-Info: (.+),(.+)#', $pem, $matches)) {
149
            $pem = self::decodePEM($pem, $matches, $password);
150
        }
151
152
        $res = openssl_pkey_get_private($pem);
153
        if ($res === false) {
154
            $res = openssl_pkey_get_public($pem);
155
        }
156
        if ($res === false) {
157
            throw new \InvalidArgumentException('Unable to load the key');
158
        }
159
160
        $details = openssl_pkey_get_details($res);
161
        if (!is_array($details) || !array_key_exists('type', $details)) {
162
            throw new \Exception('Unable to get details of the key');
163
        }
164
165
        switch ($details['type']) {
166
            case OPENSSL_KEYTYPE_EC:
167
                $ec_key = new ECKey($pem);
168
169
                return $ec_key->toArray();
170
            case OPENSSL_KEYTYPE_RSA:
171
                $temp = [
172
                    'kty' => 'RSA',
173
                ];
174
175
                foreach ([
176
                    'n' => 'n',
177
                    'e' => 'e',
178
                    'd' => 'd',
179
                    'p' => 'p',
180
                    'q' => 'q',
181
                    'dp' => 'dmp1',
182
                    'dq' => 'dmq1',
183
                    'qi' => 'iqmp',
184
                        ] as $A => $B) {
185
                    if (array_key_exists($B, $details['rsa'])) {
186
                        $temp[$A] = Base64Url::encode($details['rsa'][$B]);
187
                    }
188
                }
189
190
                return $temp;
191
                /*
192
                 * The following lines will be used when FGrosse/PHPASN1 v1.4.0 will be available
193
                 * (not available because of current version of mdanter/phpecc.
194
                 * $rsa_key = new RSAKey($pem);
195
                 *
196
                 * return $rsa_key->toArray();
197
                 */
198
            default:
199
                throw new \InvalidArgumentException('Unsupported key type');
200
        }
201
    }
202
203
    /**
204
     * @param array $data
205
     *
206
     * @throws \Exception
207
     *
208
     * @return \phpseclib\Crypt\RSA
209
     */
210
    public static function fromArrayToRSACrypt(array $data)
211
    {
212
        $xml = self::fromArrayToXML($data);
213
        $rsa = new RSA();
214
        $rsa->loadKey($xml);
215
216
        return $rsa;
217
    }
218
219
    /**
220
     * @param array $data
221
     *
222
     * @throws \Exception
223
     *
224
     * @return string
225
     */
226
    public static function fromArrayToXML(array $data)
227
    {
228
        $result = "<RSAKeyPair>\n";
229
        foreach ($data as $key => $value) {
230
            $element = self::getElement($key);
231
            $value = strtr($value, '-_', '+/');
232
233
            switch (strlen($value) % 4) {
234
                case 0:
235
                    break; // No pad chars in this case
236
                case 2:
237
                    $value .= '==';
238
                    break; // Two pad chars
239
                case 3:
240
                    $value .= '=';
241
                    break; // One pad char
242
                default:
243
                    throw new \Exception('Invalid data');
244
            }
245
246
            $result .= "\t<$element>$value</$element>\n";
247
        }
248
        $result .= '</RSAKeyPair>';
249
250
        return $result;
251
    }
252
253
    /**
254
     * @param $key
255
     *
256
     * @return string
257
     */
258
    private static function getElement($key)
259
    {
260
        $values = [
261
            'n'  => 'Modulus',
262
            'e'  => 'Exponent',
263
            'p'  => 'P',
264
            'd'  => 'D',
265
            'q'  => 'Q',
266
            'dp' => 'DP',
267
            'dq' => 'DQ',
268
            'qi' => 'InverseQ',
269
        ];
270
        if (array_key_exists($key, $values)) {
271
            return $values[$key];
272
        } else {
273
            throw new \InvalidArgumentException('Unsupported key data');
274
        }
275
    }
276
277
    /**
278
     * @param string      $pem
279
     * @param string[]    $matches
280
     * @param null|string $password
281
     *
282
     * @return string
283
     */
284
    private static function decodePem($pem, array $matches, $password = null)
285
    {
286
        if (null === $password) {
287
            throw new \InvalidArgumentException('Password required for encrypted keys.');
288
        }
289
        $iv = pack('H*', trim($matches[2]));
290
        $symkey = pack('H*', md5($password.substr($iv, 0, 8)));
291
        $symkey .= pack('H*', md5($symkey.$password.substr($iv, 0, 8)));
292
        $key = preg_replace('#^(?:Proc-Type|DEK-Info): .*#m', '', $pem);
293
        $ciphertext = base64_decode(preg_replace('#-.*-|\r|\n#', '', $key));
294
295
        $decoded = openssl_decrypt($ciphertext, strtolower($matches[1]), $symkey, true, $iv);
296
297
        $number = preg_match_all('#-{5}.*-{5}#', $pem, $result);
298
        if (2 !== $number) {
299
            throw new \InvalidArgumentException('Unable to load the key');
300
        }
301
        $pem = $result[0][0].PHP_EOL;
302
        $pem .= chunk_split(base64_encode($decoded), 64);
303
        $pem .= $result[0][1].PHP_EOL;
304
305
        return $pem;
306
    }
307
308
    /**
309
     * @param string $der_data
310
     *
311
     * @return string
312
     */
313
    private static function convertDerToPem($der_data)
314
    {
315
        $pem = chunk_split(base64_encode($der_data), 64, PHP_EOL);
316
        $pem = '-----BEGIN CERTIFICATE-----'.PHP_EOL.$pem.'-----END CERTIFICATE-----'.PHP_EOL;
317
318
        return $pem;
319
    }
320
321
    /**
322
     * @param string $pem
323
     * @param string $algorithm
324
     * @param bool   $binary
325
     *
326
     * @return string
327
     */
328
    private static function calculateX509Fingerprint($pem, $algorithm, $binary = false)
329
    {
330
        $pem = preg_replace('#-.*-|\r|\n#', '', $pem);
331
        $bin = base64_decode($pem);
332
333
        return hash($algorithm, $bin, $binary);
334
    }
335
}
336