Failed Conditions
Push — v7 ( 348606...04dca0 )
by Florent
02:16
created

RSAKey::loadPEM()   B

Complexity

Conditions 6
Paths 10

Size

Total Lines 35
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 35
rs 8.439
c 0
b 0
f 0
cc 6
eloc 24
nc 10
nop 1
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * The MIT License (MIT)
7
 *
8
 * Copyright (c) 2014-2017 Spomky-Labs
9
 *
10
 * This software may be modified and distributed under the terms
11
 * of the MIT license.  See the LICENSE file for details.
12
 */
13
14
namespace Jose\Component\Core\Util;
15
16
use Base64Url\Base64Url;
17
use FG\ASN1\Universal\BitString;
18
use FG\ASN1\Universal\Integer;
19
use FG\ASN1\Universal\NullObject;
20
use FG\ASN1\Universal\ObjectIdentifier;
21
use FG\ASN1\Universal\OctetString;
22
use FG\ASN1\Universal\Sequence;
23
use Jose\Component\Core\JWK;
24
25
final class RSAKey
26
{
27
    /**
28
     * @var Sequence
29
     */
30
    private $sequence;
31
32
    /**
33
     * @var array
34
     */
35
    private $values = [];
36
37
    /**
38
     * @var BigInteger
39
     */
40
    private $modulus;
41
42
    /**
43
     * @var int
44
     */
45
    private $modulus_length;
46
47
    /**
48
     * @var BigInteger
49
     */
50
    private $public_exponent;
51
52
    /**
53
     * @var BigInteger|null
54
     */
55
    private $private_exponent = null;
56
57
    /**
58
     * @var BigInteger[]
59
     */
60
    private $primes = [];
61
62
    /**
63
     * @var BigInteger[]
64
     */
65
    private $exponents = [];
66
67
    /**
68
     * @var BigInteger|null
69
     */
70
    private $coefficient = null;
71
72
    /**
73
     * @param JWK $data
74
     */
75
    private function __construct(JWK $data)
76
    {
77
        $this->sequence = new Sequence();
78
        $this->loadJWK($data->all());
79
        $this->populateBigIntegers();
80
    }
81
82
    /**
83
     * @param string $pem
84
     *
85
     * @return RSAKey
86
     */
87
    public static function createFromPEM(string $pem): RSAKey
88
    {
89
        $data = self::loadPEM($pem);
90
91
        return new self(JWK::create($data));
92
    }
93
94
    /**
95
     * @param string $data
96
     *
97
     * @return array
98
     */
99
    private static function loadPEM(string $data): array
100
    {
101
        $res = openssl_pkey_get_private($data);
102
        if (false === $res) {
103
            $res = openssl_pkey_get_public($data);
104
        }
105
        if (false === $res) {
106
            throw new \InvalidArgumentException('Unable to load the key.');
107
        }
108
109
        $details = openssl_pkey_get_details($res);
110
        if (!array_key_exists('rsa', $details)) {
111
            throw new \InvalidArgumentException('Unable to load the key.');
112
        }
113
114
        $values['kty'] = 'RSA';
0 ignored issues
show
Coding Style Comprehensibility introduced by
$values was never initialized. Although not strictly required by PHP, it is generally a good practice to add $values = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
115
        $keys = [
116
            'n' => 'n',
117
            'e' => 'e',
118
            'd' => 'd',
119
            'p' => 'p',
120
            'q' => 'q',
121
            'dp' => 'dmp1',
122
            'dq' => 'dmq1',
123
            'qi' => 'iqmp',
124
        ];
125
        foreach ($details['rsa'] as $key => $value) {
126
            if (in_array($key, $keys)) {
127
                $value = Base64Url::encode($value);
128
                $values[array_search($key, $keys)] = $value;
129
            }
130
        }
131
132
        return $values;
133
    }
134
135
    /**
136
     * @param JWK $jwk
137
     *
138
     * @return RSAKey
139
     */
140
    public static function createFromJWK(JWK $jwk): RSAKey
141
    {
142
        return new self($jwk);
143
    }
144
145
    /**
146
     * @return BigInteger
147
     */
148
    public function getModulus(): BigInteger
149
    {
150
        return $this->modulus;
151
    }
152
153
    /**
154
     * @return int
155
     */
156
    public function getModulusLength(): int
157
    {
158
        return $this->modulus_length;
159
    }
160
161
    /**
162
     * @return BigInteger
163
     */
164
    public function getExponent(): BigInteger
165
    {
166
        $d = $this->getPrivateExponent();
167
        if (null !== $d) {
168
            return $d;
169
        }
170
171
        return $this->getPublicExponent();
172
    }
173
174
    /**
175
     * @return BigInteger
176
     */
177
    public function getPublicExponent(): BigInteger
178
    {
179
        return $this->public_exponent;
180
    }
181
182
    /**
183
     * @return BigInteger|null
184
     */
185
    public function getPrivateExponent(): ?BigInteger
186
    {
187
        return $this->private_exponent;
188
    }
189
190
    /**
191
     * @return BigInteger[]
192
     */
193
    public function getPrimes(): array
194
    {
195
        return $this->primes;
196
    }
197
198
    /**
199
     * @return BigInteger[]
200
     */
201
    public function getExponents(): array
202
    {
203
        return $this->exponents;
204
    }
205
206
    /**
207
     * @return BigInteger|null
208
     */
209
    public function getCoefficient(): ?BigInteger
210
    {
211
        return $this->coefficient;
212
    }
213
214
    /**
215
     * @return bool
216
     */
217
    public function isPublic(): bool
218
    {
219
        return !array_key_exists('d', $this->values);
220
    }
221
222
    /**
223
     * @param RSAKey $private
224
     *
225
     * @return RSAKey
226
     */
227
    public static function toPublic(RSAKey $private): RSAKey
228
    {
229
        $data = $private->toArray();
230
        $keys = ['p', 'd', 'q', 'dp', 'dq', 'qi'];
231
        foreach ($keys as $key) {
232
            if (array_key_exists($key, $data)) {
233
                unset($data[$key]);
234
            }
235
        }
236
237
        return new self(JWK::create($data));
238
    }
239
240
    /**
241
     * @return array
242
     */
243
    public function toArray(): array
244
    {
245
        return $this->values;
246
    }
247
248
    /**
249
     * @return string
250
     */
251
    public function toPEM(): string
252
    {
253
        $result = '-----BEGIN '.($this->isPublic() ? 'PUBLIC' : 'RSA PRIVATE').' KEY-----'.PHP_EOL;
254
        $result .= chunk_split(base64_encode($this->sequence->getBinary()), 64, PHP_EOL);
255
        $result .= '-----END '.($this->isPublic() ? 'PUBLIC' : 'RSA PRIVATE').' KEY-----'.PHP_EOL;
256
257
        return $result;
258
    }
259
260
    /**
261
     * @param array $jwk
262
     */
263
    private function loadJWK(array $jwk)
264
    {
265
        if (!array_key_exists('kty', $jwk)) {
266
            throw new \InvalidArgumentException('The key parameter "kty" is missing.');
267
        }
268
        if ('RSA' !== $jwk['kty']) {
269
            throw new \InvalidArgumentException('The JWK is not a RSA key.');
270
        }
271
272
        $this->values = $jwk;
273
        if (array_key_exists('d', $jwk)) {
274
            $this->populateCRT();
275
            $this->initPrivateKey();
276
        } else {
277
            $this->initPublicKey();
278
        }
279
    }
280
281
    /**
282
     * This method adds Chinese Remainder Theorem (CRT) parameters if primes 'p' and 'q' are available.
283
     */
284
    private function populateCRT()
285
    {
286
        if (!array_key_exists('p', $this->values) && !array_key_exists('q', $this->values)) {
287
            $d = BigInteger::createFromBinaryString(Base64Url::decode($this->values['d']));
288
            $e = BigInteger::createFromBinaryString(Base64Url::decode($this->values['e']));
289
            $n = BigInteger::createFromBinaryString(Base64Url::decode($this->values['n']));
290
291
            list($p, $q) = $this->findPrimeFactors($d, $e, $n);
292
            $this->values['p'] = Base64Url::encode($p->toBytes());
293
            $this->values['q'] = Base64Url::encode($q->toBytes());
294
        }
295
296
        if (array_key_exists('dp', $this->values) && array_key_exists('dq', $this->values) && array_key_exists('qi', $this->values)) {
297
            return;
298
        }
299
300
        $one = BigInteger::createFromDecimal(1);
301
        $d = BigInteger::createFromBinaryString(Base64Url::decode($this->values['d']));
302
        $p = BigInteger::createFromBinaryString(Base64Url::decode($this->values['p']));
303
        $q = BigInteger::createFromBinaryString(Base64Url::decode($this->values['q']));
304
305
        $this->values['dp'] = Base64Url::encode($d->mod($p->subtract($one))->toBytes());
306
        $this->values['dq'] = Base64Url::encode($d->mod($q->subtract($one))->toBytes());
307
        $this->values['qi'] = Base64Url::encode($q->modInverse($p)->toBytes());
308
    }
309
310
    /**
311
     * @throws \Exception
312
     */
313
    private function initPublicKey()
314
    {
315
        $oid_sequence = new Sequence();
316
        $oid_sequence->addChild(new ObjectIdentifier('1.2.840.113549.1.1.1'));
317
        $oid_sequence->addChild(new NullObject());
318
        $this->sequence->addChild($oid_sequence);
319
320
        $n = new Integer($this->fromBase64ToInteger($this->values['n']));
321
        $e = new Integer($this->fromBase64ToInteger($this->values['e']));
322
323
        $key_sequence = new Sequence();
324
        $key_sequence->addChild($n);
325
        $key_sequence->addChild($e);
326
        $key_bit_string = new BitString(bin2hex($key_sequence->getBinary()));
327
        $this->sequence->addChild($key_bit_string);
328
    }
329
330
    private function initPrivateKey()
331
    {
332
        $this->sequence->addChild(new Integer(0));
333
334
        $oid_sequence = new Sequence();
335
        $oid_sequence->addChild(new ObjectIdentifier('1.2.840.113549.1.1.1'));
336
        $oid_sequence->addChild(new NullObject());
337
        $this->sequence->addChild($oid_sequence);
338
339
        $v = new Integer(0);
340
        $n = new Integer($this->fromBase64ToInteger($this->values['n']));
341
        $e = new Integer($this->fromBase64ToInteger($this->values['e']));
342
        $d = new Integer($this->fromBase64ToInteger($this->values['d']));
343
        $p = new Integer($this->fromBase64ToInteger($this->values['p']));
344
        $q = new Integer($this->fromBase64ToInteger($this->values['q']));
345
        $dp = array_key_exists('dp', $this->values) ? new Integer($this->fromBase64ToInteger($this->values['dp'])) : new Integer(0);
346
        $dq = array_key_exists('dq', $this->values) ? new Integer($this->fromBase64ToInteger($this->values['dq'])) : new Integer(0);
347
        $qi = array_key_exists('qi', $this->values) ? new Integer($this->fromBase64ToInteger($this->values['qi'])) : new Integer(0);
348
349
        $key_sequence = new Sequence();
350
        $key_sequence->addChild($v);
351
        $key_sequence->addChild($n);
352
        $key_sequence->addChild($e);
353
        $key_sequence->addChild($d);
354
        $key_sequence->addChild($p);
355
        $key_sequence->addChild($q);
356
        $key_sequence->addChild($dp);
357
        $key_sequence->addChild($dq);
358
        $key_sequence->addChild($qi);
359
        $key_octet_string = new OctetString(bin2hex($key_sequence->getBinary()));
360
        $this->sequence->addChild($key_octet_string);
361
    }
362
363
    /**
364
     * @param string $value
365
     *
366
     * @return string
367
     */
368
    private function fromBase64ToInteger(string $value): string
369
    {
370
        return gmp_strval(gmp_init(current(unpack('H*', Base64Url::decode($value))), 16), 10);
371
    }
372
373
    private function populateBigIntegers()
374
    {
375
        $this->modulus = $this->convertBase64StringToBigInteger($this->values['n']);
376
        $this->modulus_length = mb_strlen($this->getModulus()->toBytes(), '8bit');
377
        $this->public_exponent = $this->convertBase64StringToBigInteger($this->values['e']);
378
379
        if (!$this->isPublic()) {
380
            $this->private_exponent = $this->convertBase64StringToBigInteger($this->values['d']);
381
382
            if (array_key_exists('p', $this->values) && array_key_exists('q', $this->values)) {
383
                $this->primes = [
384
                    $this->convertBase64StringToBigInteger($this->values['p']),
385
                    $this->convertBase64StringToBigInteger($this->values['q']),
386
                ];
387
                $this->exponents = [
388
                    $this->convertBase64StringToBigInteger($this->values['dp']),
389
                    $this->convertBase64StringToBigInteger($this->values['dq']),
390
                ];
391
                $this->coefficient = $this->convertBase64StringToBigInteger($this->values['qi']);
392
            }
393
        }
394
    }
395
396
    /**
397
     * @param string $value
398
     *
399
     * @return BigInteger
400
     */
401
    private function convertBase64StringToBigInteger(string $value): BigInteger
402
    {
403
        return BigInteger::createFromBinaryString(Base64Url::decode($value));
404
    }
405
406
    /**
407
     * @param BigInteger $d
408
     * @param BigInteger $e
409
     * @param BigInteger $n
410
     *
411
     * @return BigInteger[]
412
     */
413
    private function findPrimeFactors(BigInteger $d, BigInteger $e, BigInteger $n): array
414
    {
415
        $zero = BigInteger::createFromDecimal(0);
416
        $one = BigInteger::createFromDecimal(1);
417
        $two = BigInteger::createFromDecimal(2);
418
419
        $k = $d->multiply($e)->subtract($one);
420
421
        if ($k->isEven()) {
422
            $r = $k;
423
            $t = $zero;
424
425
            do {
426
                $r = $r->divide($two);
427
                $t = $t->add($one);
428
            } while ($r->isEven());
429
430
            $found = false;
431
            $y = null;
432
433
            for ($i = 1; $i <= 100; ++$i) {
434
                $g = BigInteger::random($n->subtract($one));
435
                $y = $g->modPow($r, $n);
436
437
                if ($y->equals($one) || $y->equals($n->subtract($one))) {
438
                    continue;
439
                }
440
441
                for ($j = $one; $j->lowerThan($t->subtract($one)); $j = $j->add($one)) {
0 ignored issues
show
Performance Best Practice introduced by
Consider avoiding function calls on each iteration of the for loop.

If you have a function call in the test part of a for loop, this function is executed on each iteration. Often such a function, can be moved to the initialization part and be cached.

// count() is called on each iteration
for ($i=0; $i < count($collection); $i++) { }

// count() is only called once
for ($i=0, $c=count($collection); $i<$c; $i++) { }
Loading history...
442
                    $x = $y->modPow($two, $n);
443
444
                    if ($x->equals($one)) {
445
                        $found = true;
446
447
                        break;
448
                    }
449
450
                    if ($x->equals($n->subtract($one))) {
451
                        continue;
452
                    }
453
454
                    $y = $x;
455
                }
456
457
                $x = $y->modPow($two, $n);
458
                if ($x->equals($one)) {
459
                    $found = true;
460
461
                    break;
462
                }
463
            }
464
465
            if (true === $found) {
466
                $p = $y->subtract($one)->gcd($n);
467
                $q = $n->divide($p);
468
469
                return [$p, $q];
470
            }
471
        }
472
473
        throw new \InvalidArgumentException('Unable to find prime factors.');
474
    }
475
}
476