Failed Conditions
Push — v7 ( 61ffea...f7e5f1 )
by Florent
04:20
created

RSAKey::__toString()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 4
rs 10
c 0
b 0
f 0
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\KeyManagement\KeyConverter;
15
16
use Assert\Assertion;
17
use Base64Url\Base64Url;
18
use FG\ASN1\Exception\ParserException;
19
use FG\ASN1\Universal\BitString;
20
use FG\ASN1\Universal\Integer;
21
use FG\ASN1\Universal\NullObject;
22
use FG\ASN1\Universal\ObjectIdentifier;
23
use FG\ASN1\Universal\OctetString;
24
use FG\ASN1\Universal\Sequence;
25
use Jose\Component\Core\JWK;
26
use Jose\Component\Core\Util\BigInteger;
27
28
final class RSAKey
29
{
30
    /**
31
     * @var Sequence
32
     */
33
    private $sequence;
34
35
    /**
36
     * @var array
37
     */
38
    private $values = [];
39
40
    /**
41
     * @var BigInteger
42
     */
43
    private $modulus;
44
45
    /**
46
     * @var int
47
     */
48
    private $modulus_length;
49
50
    /**
51
     * @var BigInteger
52
     */
53
    private $public_exponent;
54
55
    /**
56
     * @var BigInteger|null
57
     */
58
    private $private_exponent = null;
59
60
    /**
61
     * @var BigInteger[]
62
     */
63
    private $primes = [];
64
65
    /**
66
     * @var BigInteger[]
67
     */
68
    private $exponents = [];
69
70
    /**
71
     * @var BigInteger|null
72
     */
73
    private $coefficient = null;
74
75
    /**
76
     * @param JWK|string|array $data
77
     */
78
    private function __construct($data)
79
    {
80
        $this->sequence = new Sequence();
81
82
        if ($data instanceof JWK) {
83
            $this->loadJWK($data->all());
84
        } elseif (is_array($data)) {
85
            $this->loadJWK($data);
86
        } elseif (is_string($data)) {
87
            $this->loadPEM($data);
88
        } else {
89
            throw new \InvalidArgumentException('Unsupported input');
90
        }
91
92
        $this->populateBigIntegers();
93
    }
94
95
    /**
96
     * @param JWK $jwk
97
     *
98
     * @return RSAKey
99
     */
100
    public static function createFromJWK(JWK $jwk): RSAKey
101
    {
102
        return new self($jwk);
103
    }
104
105
    /**
106
     * @param string $pem
107
     *
108
     * @return RSAKey
109
     */
110
    public static function createFromPEM(string $pem): RSAKey
111
    {
112
        return new self($pem);
113
    }
114
115
    /**
116
     * @return bool
117
     */
118
    public function isPublic(): bool
119
    {
120
        return !$this->isPrivate();
121
    }
122
123
    /**
124
     * @return bool
125
     */
126
    public function isPrivate(): bool
127
    {
128
        return array_key_exists('d', $this->values);
129
    }
130
131
    /**
132
     * @return BigInteger
133
     */
134
    public function getModulus(): BigInteger
135
    {
136
        return $this->modulus;
137
    }
138
139
    /**
140
     * @return int
141
     */
142
    public function getModulusLength(): int
143
    {
144
        return $this->modulus_length;
145
    }
146
147
    /**
148
     * @return BigInteger
149
     */
150
    public function getExponent(): BigInteger
151
    {
152
        $d = $this->getPrivateExponent();
153
        if (null !== $d) {
154
            return $d;
155
        }
156
157
        return $this->getPublicExponent();
158
    }
159
160
    /**
161
     * @return BigInteger
162
     */
163
    public function getPublicExponent(): BigInteger
164
    {
165
        return $this->public_exponent;
166
    }
167
168
    /**
169
     * @return BigInteger|null
170
     */
171
    public function getPrivateExponent(): ?BigInteger
172
    {
173
        return $this->private_exponent;
174
    }
175
176
    /**
177
     * @return BigInteger[]
178
     */
179
    public function getPrimes(): array
180
    {
181
        return $this->primes;
182
    }
183
184
    /**
185
     * @return BigInteger[]
186
     */
187
    public function getExponents(): array
188
    {
189
        return $this->exponents;
190
    }
191
192
    /**
193
     * @return BigInteger|null
194
     */
195
    public function getCoefficient(): ?BigInteger
196
    {
197
        return $this->coefficient;
198
    }
199
200
    /**
201
     * @param RSAKey $private
202
     *
203
     * @return RSAKey
204
     */
205
    public static function toPublic(RSAKey $private): RSAKey
206
    {
207
        $data = $private->toArray();
208
        $keys = ['p', 'd', 'q', 'dp', 'dq', 'qi'];
209
        foreach ($keys as $key) {
210
            if (array_key_exists($key, $data)) {
211
                unset($data[$key]);
212
            }
213
        }
214
215
        return new self($data);
216
    }
217
218
    /**
219
     * @return array
220
     */
221
    public function toArray(): array
222
    {
223
        return $this->values;
224
    }
225
226
    /**
227
     * @return string
228
     */
229
    public function toPEM(): string
230
    {
231
        $result = '-----BEGIN '.($this->isPrivate() ? 'RSA PRIVATE' : 'PUBLIC').' KEY-----'.PHP_EOL;
232
        $result .= chunk_split(base64_encode($this->sequence->getBinary()), 64, PHP_EOL);
233
        $result .= '-----END '.($this->isPrivate() ? 'RSA PRIVATE' : 'PUBLIC').' KEY-----'.PHP_EOL;
234
235
        return $result;
236
    }
237
238
    /**
239
     * @param string $data
240
     *
241
     * @throws \Exception
242
     * @throws ParserException
243
     */
244
    private function loadPEM(string $data)
245
    {
246
        $res = openssl_pkey_get_private($data);
247
        if (false === $res) {
248
            $res = openssl_pkey_get_public($data);
249
        }
250
        Assertion::false(false === $res, 'Unable to load the key');
251
252
        $details = openssl_pkey_get_details($res);
253
        Assertion::keyExists($details, 'rsa', 'Unable to load the key');
254
255
        $this->values['kty'] = 'RSA';
256
        $keys = [
257
            'n' => 'n',
258
            'e' => 'e',
259
            'd' => 'd',
260
            'p' => 'p',
261
            'q' => 'q',
262
            'dp' => 'dmp1',
263
            'dq' => 'dmq1',
264
            'qi' => 'iqmp',
265
        ];
266
        foreach ($details['rsa'] as $key => $value) {
267
            if (in_array($key, $keys)) {
268
                $value = Base64Url::encode($value);
269
                $this->values[array_search($key, $keys)] = $value;
270
            }
271
        }
272
    }
273
274
    /**
275
     * @param array $jwk
276
     */
277
    private function loadJWK(array $jwk)
278
    {
279
        Assertion::keyExists($jwk, 'kty', 'The key parameter "kty" is missing.');
280
        Assertion::eq($jwk['kty'], 'RSA', 'The JWK is not a RSA key');
281
282
        $this->values = $jwk;
283
        if (array_key_exists('d', $jwk)) {
284
            $this->populateCRT();
285
            $this->initPrivateKey();
286
        } else {
287
            $this->initPublicKey();
288
        }
289
    }
290
291
    /**
292
     * This method adds Chinese Remainder Theorem (CRT) parameters if primes 'p' and 'q' are available.
293
     */
294
    private function populateCRT()
295
    {
296
        if (!array_key_exists('p', $this->values) && !array_key_exists('q', $this->values)) {
297
            $d = BigInteger::createFromBinaryString(Base64Url::decode($this->values['d']));
298
            $e = BigInteger::createFromBinaryString(Base64Url::decode($this->values['e']));
299
            $n = BigInteger::createFromBinaryString(Base64Url::decode($this->values['n']));
300
301
            list($p, $q) = $this->findPrimeFactors($d, $e, $n);
302
            $this->values['p'] = Base64Url::encode($p->toBytes());
303
            $this->values['q'] = Base64Url::encode($q->toBytes());
304
        }
305
306
        if (array_key_exists('dp', $this->values) && array_key_exists('dq', $this->values) && array_key_exists('qi', $this->values)) {
307
            return;
308
        }
309
310
        $one = BigInteger::createFromDecimal(1);
311
        $d = BigInteger::createFromBinaryString(Base64Url::decode($this->values['d']));
312
        $p = BigInteger::createFromBinaryString(Base64Url::decode($this->values['p']));
313
        $q = BigInteger::createFromBinaryString(Base64Url::decode($this->values['q']));
314
315
        $this->values['dp'] = Base64Url::encode($d->mod($p->subtract($one))->toBytes());
316
        $this->values['dq'] = Base64Url::encode($d->mod($q->subtract($one))->toBytes());
317
        $this->values['qi'] = Base64Url::encode($q->modInverse($p)->toBytes());
318
    }
319
320
    /**
321
     * @throws \Exception
322
     */
323
    private function initPublicKey()
324
    {
325
        $oid_sequence = new Sequence();
326
        $oid_sequence->addChild(new ObjectIdentifier('1.2.840.113549.1.1.1'));
327
        $oid_sequence->addChild(new NullObject());
328
        $this->sequence->addChild($oid_sequence);
329
330
        $n = new Integer($this->fromBase64ToInteger($this->values['n']));
331
        $e = new Integer($this->fromBase64ToInteger($this->values['e']));
332
333
        $key_sequence = new Sequence();
334
        $key_sequence->addChild($n);
335
        $key_sequence->addChild($e);
336
        $key_bit_string = new BitString(bin2hex($key_sequence->getBinary()));
337
        $this->sequence->addChild($key_bit_string);
338
    }
339
340
    private function initPrivateKey()
341
    {
342
        $this->sequence->addChild(new Integer(0));
343
344
        $oid_sequence = new Sequence();
345
        $oid_sequence->addChild(new ObjectIdentifier('1.2.840.113549.1.1.1'));
346
        $oid_sequence->addChild(new NullObject());
347
        $this->sequence->addChild($oid_sequence);
348
349
        $v = new Integer(0);
350
        $n = new Integer($this->fromBase64ToInteger($this->values['n']));
351
        $e = new Integer($this->fromBase64ToInteger($this->values['e']));
352
        $d = new Integer($this->fromBase64ToInteger($this->values['d']));
353
        $p = new Integer($this->fromBase64ToInteger($this->values['p']));
354
        $q = new Integer($this->fromBase64ToInteger($this->values['q']));
355
        $dp = array_key_exists('dp', $this->values) ? new Integer($this->fromBase64ToInteger($this->values['dp'])) : new Integer(0);
356
        $dq = array_key_exists('dq', $this->values) ? new Integer($this->fromBase64ToInteger($this->values['dq'])) : new Integer(0);
357
        $qi = array_key_exists('qi', $this->values) ? new Integer($this->fromBase64ToInteger($this->values['qi'])) : new Integer(0);
358
359
        $key_sequence = new Sequence();
360
        $key_sequence->addChild($v);
361
        $key_sequence->addChild($n);
362
        $key_sequence->addChild($e);
363
        $key_sequence->addChild($d);
364
        $key_sequence->addChild($p);
365
        $key_sequence->addChild($q);
366
        $key_sequence->addChild($dp);
367
        $key_sequence->addChild($dq);
368
        $key_sequence->addChild($qi);
369
        $key_octet_string = new OctetString(bin2hex($key_sequence->getBinary()));
370
        $this->sequence->addChild($key_octet_string);
371
    }
372
373
    /**
374
     * @param string $value
375
     *
376
     * @return string
377
     */
378
    private function fromBase64ToInteger(string $value): string
379
    {
380
        return gmp_strval(gmp_init(current(unpack('H*', Base64Url::decode($value))), 16), 10);
381
    }
382
383
    private function populateBigIntegers()
384
    {
385
        $this->modulus = $this->convertBase64StringToBigInteger($this->values['n']);
386
        $this->modulus_length = mb_strlen($this->getModulus()->toBytes(), '8bit');
387
        $this->public_exponent = $this->convertBase64StringToBigInteger($this->values['e']);
388
389
        if (true === $this->isPrivate()) {
390
            $this->private_exponent = $this->convertBase64StringToBigInteger($this->values['d']);
391
392
            if (array_key_exists('p', $this->values) && array_key_exists('q', $this->values)) {
393
                $this->primes = [
394
                    $this->convertBase64StringToBigInteger($this->values['p']),
395
                    $this->convertBase64StringToBigInteger($this->values['q']),
396
                ];
397
                $this->exponents = [
398
                    $this->convertBase64StringToBigInteger($this->values['dp']),
399
                    $this->convertBase64StringToBigInteger($this->values['dq']),
400
                ];
401
                $this->coefficient = $this->convertBase64StringToBigInteger($this->values['qi']);
402
            }
403
        }
404
    }
405
406
    /**
407
     * @param string $value
408
     *
409
     * @return BigInteger
410
     */
411
    private function convertBase64StringToBigInteger(string $value): BigInteger
412
    {
413
        return BigInteger::createFromBinaryString(Base64Url::decode($value));
414
    }
415
416
    /**
417
     * @param BigInteger $d
418
     * @param BigInteger $e
419
     * @param BigInteger $n
420
     *
421
     * @return BigInteger[]
422
     */
423
    private function findPrimeFactors(BigInteger $d, BigInteger $e, BigInteger $n): array
424
    {
425
        $zero = BigInteger::createFromDecimal(0);
426
        $one = BigInteger::createFromDecimal(1);
427
        $two = BigInteger::createFromDecimal(2);
428
429
        $k = $d->multiply($e)->subtract($one);
430
431
        if ($k->isEven()) {
432
            $r = $k;
433
            $t = $zero;
434
435
            do {
436
                $r = $r->divide($two);
437
                $t = $t->add($one);
438
            } while ($r->isEven());
439
440
            $found = false;
441
            $y = null;
442
443
            for ($i = 1; $i <= 100; ++$i) {
444
                $g = BigInteger::random($n->subtract($one));
445
                $y = $g->modPow($r, $n);
446
447
                if ($y->equals($one) || $y->equals($n->subtract($one))) {
448
                    continue;
449
                }
450
451
                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...
452
                    $x = $y->modPow($two, $n);
453
454
                    if ($x->equals($one)) {
455
                        $found = true;
456
                        break;
457
                    }
458
459
                    if ($x->equals($n->subtract($one))) {
460
                        continue;
461
                    }
462
463
                    $y = $x;
464
                }
465
466
                $x = $y->modPow($two, $n);
467
                if ($x->equals($one)) {
468
                    $found = true;
469
                    break;
470
                }
471
            }
472
473
            if (true === $found) {
474
                $p = $y->subtract($one)->gcd($n);
475
                $q = $n->divide($p);
476
477
                return [$p, $q];
478
            }
479
        }
480
481
        throw new \InvalidArgumentException('Unable to find prime factors.');
482
    }
483
}
484