Completed
Push — master ( 26ae43...5f93f7 )
by Florent
10:44 queued 10:44
created

KeyConverter::loadKeyFromCertificateFile()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
c 1
b 1
f 0
dl 0
loc 9
rs 9.6667
cc 2
eloc 5
nc 2
nop 1
1
<?php
2
3
/*
4
 * The MIT License (MIT)
5
 *
6
 * Copyright (c) 2014-2015 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
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 $file
0 ignored issues
show
Bug introduced by
There is no parameter named $file. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
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   $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
     * @param bool        $is_DER
95
     *
96
     * @throws \Exception
97
     *
98
     * @return array
99
     */
100
    public static function loadKeyFromFile($file, $password = null, $is_DER = false)
101
    {
102
        $content = file_get_contents($file);
103
104
        if (true === $is_DER) {
105
            return self::loadKeyFromDER($content, $password);
106
        } else {
107
            return self::loadKeyFromPEM($content, $password);
108
        }
109
    }
110
111
    /**
112
     * @param string      $der
113
     * @param null|string $password
114
     *
115
     * @throws \Exception
116
     *
117
     * @return array
118
     */
119
    public static function loadKeyFromDER($der, $password = null)
120
    {
121
        $pem = self::convertDerToPem($der);
122
123
        return self::loadKeyFromPEM($pem, $password);
124
    }
125
126
    /**
127
     * @param string      $pem
128
     * @param null|string $password
129
     *
130
     * @throws \Exception
131
     *
132
     * @return array
133
     */
134
    public static function loadKeyFromPEM($pem, $password = null)
135
    {
136
        if (preg_match('#DEK-Info: (.+),(.+)#', $pem, $matches)) {
137
            $pem = self::decodePEM($pem, $matches, $password);
138
        }
139
140
        $res = openssl_pkey_get_private($pem);
141
        if ($res === false) {
142
            $res = openssl_pkey_get_public($pem);
143
        }
144
        if ($res === false) {
145
            throw new \InvalidArgumentException('Unable to load the key');
146
        }
147
148
        $details = openssl_pkey_get_details($res);
149
        if (!is_array($details) || !array_key_exists('type', $details)) {
150
            throw new \Exception('Unable to get details of the key');
151
        }
152
153
        switch ($details['type']) {
154
            case OPENSSL_KEYTYPE_EC:
155
                $ec_key = new ECKey($pem);
156
157
                return $ec_key->toArray();
158
            case OPENSSL_KEYTYPE_RSA:
159
                $temp = [
160
                    'kty' => 'RSA',
161
                ];
162
163
                foreach ([
164
                    'n' => 'n',
165
                    'e' => 'e',
166
                    'd' => 'd',
167
                    'p' => 'p',
168
                    'q' => 'q',
169
                    'dp' => 'dmp1',
170
                    'dq' => 'dmq1',
171
                    'qi' => 'iqmp',
172
                        ] as $A => $B) {
173
                    if (array_key_exists($B, $details['rsa'])) {
174
                        $temp[$A] = Base64Url::encode($details['rsa'][$B]);
175
                    }
176
                }
177
178
                return $temp;
179
                /*
180
                 * The following lines will be used when FGrosse/PHPASN1 v1.4.0 will be available
181
                 * (not available because of current version of mdanter/phpecc.
182
                 * $rsa_key = new RSAKey($pem);
183
                 *
184
                 * return $rsa_key->toArray();
185
                 */
186
            default:
187
                throw new \InvalidArgumentException('Unsupported key type');
188
        }
189
    }
190
191
    /**
192
     * @param array $data
193
     *
194
     * @throws \Exception
195
     *
196
     * @return \phpseclib\Crypt\RSA
197
     */
198
    public static function fromArrayToRSACrypt(array $data)
199
    {
200
        $xml = self::fromArrayToXML($data);
201
        $rsa = new RSA();
202
        $rsa->loadKey($xml);
203
204
        return $rsa;
205
    }
206
207
    /**
208
     * @param array $data
209
     *
210
     * @throws \Exception
211
     *
212
     * @return string
213
     */
214
    public static function fromArrayToXML(array $data)
215
    {
216
        $result = "<RSAKeyPair>\n";
217
        foreach ($data as $key => $value) {
218
            $element = self::getElement($key);
219
            $value = strtr($value, '-_', '+/');
220
221
            switch (strlen($value) % 4) {
222
                case 0:
223
                    break; // No pad chars in this case
224
                case 2:
225
                    $value .= '==';
226
                    break; // Two pad chars
227
                case 3:
228
                    $value .= '=';
229
                    break; // One pad char
230
                default:
231
                    throw new \Exception('Invalid data');
232
            }
233
234
            $result .= "\t<$element>$value</$element>\n";
235
        }
236
        $result .= '</RSAKeyPair>';
237
238
        return $result;
239
    }
240
241
    /**
242
     * @param $key
243
     *
244
     * @return mixed
245
     */
246
    private static function getElement($key)
247
    {
248
        $values = [
249
            'n'  => 'Modulus',
250
            'e'  => 'Exponent',
251
            'p'  => 'P',
252
            'd'  => 'D',
253
            'q'  => 'Q',
254
            'dp' => 'DP',
255
            'dq' => 'DQ',
256
            'qi' => 'InverseQ',
257
        ];
258
        if (array_key_exists($key, $values)) {
259
            return $values[$key];
260
        } else {
261
            throw new \InvalidArgumentException('Unsupported key data');
262
        }
263
    }
264
265
    /**
266
     * @param string      $pem
267
     * @param array       $matches
268
     * @param null|string $password
269
     *
270
     * @return string
271
     */
272
    private static function decodePem($pem, array $matches, $password = null)
273
    {
274
        if (null === $password) {
275
            throw new \InvalidArgumentException('Password required for encrypted keys.');
276
        }
277
        $iv = pack('H*', trim($matches[2]));
278
        $symkey = pack('H*', md5($password.substr($iv, 0, 8)));
279
        $symkey .= pack('H*', md5($symkey.$password.substr($iv, 0, 8)));
280
        $key = preg_replace('#^(?:Proc-Type|DEK-Info): .*#m', '', $pem);
281
        $ciphertext = base64_decode(preg_replace('#-.*-|\r|\n#', '', $key));
282
283
        $decoded = openssl_decrypt($ciphertext, strtolower($matches[1]), $symkey, true, $iv);
284
285
        $number = preg_match_all('#-{5}.*-{5}#', $pem, $result);
286
        if (2 !== $number) {
287
            throw new \InvalidArgumentException('Unable to load the key');
288
        }
289
        $pem = $result[0][0].PHP_EOL;
290
        $pem .= chunk_split(base64_encode($decoded), 64);
291
        $pem .= $result[0][1].PHP_EOL;
292
293
        return $pem;
294
    }
295
296
    /**
297
     * @param string $der_data
298
     *
299
     * @return string
300
     */
301
    private static function convertDerToPem($der_data)
302
    {
303
        $pem = chunk_split(base64_encode($der_data), 64, PHP_EOL);
304
        $pem = '-----BEGIN CERTIFICATE-----'.PHP_EOL.$pem.'-----END CERTIFICATE-----'.PHP_EOL;
305
306
        return $pem;
307
    }
308
309
    /**
310
     * @param string $pem
311
     * @param string $algorithm
312
     * @param bool   $binary
313
     *
314
     * @return string
315
     */
316
    private static function calculateX509Fingerprint($pem, $algorithm, $binary = false)
317
    {
318
        $pem = preg_replace('#-.*-|\r|\n#', '', $pem);
319
        $bin = base64_decode($pem);
320
321
        return hash($algorithm, $bin, $binary);
322
    }
323
}
324