Failed Conditions
Push — v7 ( e9e51b...d9c0af )
by Florent
03:20
created

ECKey::loadPublicPEM()   C

Complexity

Conditions 7
Paths 7

Size

Total Lines 31
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 31
rs 6.7272
c 0
b 0
f 0
cc 7
eloc 20
nc 7
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\KeyManagement\KeyConverter;
15
16
use Base64Url\Base64Url;
17
use FG\ASN1\Exception\ParserException;
18
use FG\ASN1\ExplicitlyTaggedObject;
19
use FG\ASN1\Object;
20
use FG\ASN1\Universal\BitString;
21
use FG\ASN1\Universal\Integer;
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 function MongoDB\BSON\toJSON;
27
28
final class ECKey
29
{
30
    /**
31
     * @var null|Sequence
32
     */
33
    private $sequence = null;
34
35
    /**
36
     * @var bool
37
     */
38
    private $private = false;
39
40
    /**
41
     * @var array
42
     */
43
    private $values = [];
44
45
    /**
46
     * ECKey constructor.
47
     *
48
     * @param $data
49
     */
50
    private function __construct($data)
51
    {
52
        $this->sequence = new Sequence();
53
54
        if ($data instanceof JWK) {
55
            $this->loadJWK($data->all());
56
        } elseif (is_array($data)) {
57
            $this->loadJWK($data);
58
        } elseif (is_string($data)) {
59
            $this->loadPEM($data);
60
        } else {
61
            throw new \InvalidArgumentException('Unsupported input');
62
        }
63
        $this->private = isset($this->values['d']);
64
    }
65
66
    /**
67
     * @param JWK $jwk
68
     *
69
     * @return ECKey
70
     */
71
    public static function createFromJWK(JWK $jwk): ECKey
72
    {
73
        return new self($jwk);
74
    }
75
76
    /**
77
     * @param string $pem
78
     *
79
     * @return ECKey
80
     */
81
    public static function createFromPEM(string $pem): ECKey
82
    {
83
        return new self($pem);
84
    }
85
86
    /**
87
     * @param string $data
88
     *
89
     * @throws \Exception
90
     * @throws ParserException
91
     */
92
    private function loadPEM(string $data)
93
    {
94
        $data = base64_decode(preg_replace('#-.*-|\r|\n#', '', $data));
95
        $asnObject = Object::fromBinary($data);
96
97
        if (!$asnObject instanceof Sequence) {
98
            throw new \InvalidArgumentException('Unable to load the key.');
99
        }
100
        $children = $asnObject->getChildren();
101
        if (self::isPKCS8($children)) {
102
            $children = self::loadPKCS8($children);
103
        }
104
105
        if (4 === count($children)) {
106
            $this->loadPrivatePEM($children);
107
108
            return;
109
        } elseif (2 === count($children)) {
110
            $this->loadPublicPEM($children);
111
112
            return;
113
        }
114
        throw new \Exception('Unable to load the key.');
115
    }
116
117
    /**
118
     * @param array $children
119
     *
120
     * @return array
121
     */
122
    private function loadPKCS8(array $children): array
123
    {
124
        $binary = hex2bin($children[2]->getContent());
125
        $asnObject = Object::fromBinary($binary);
126
        if (!$asnObject instanceof Sequence) {
127
            throw new \InvalidArgumentException('Unable to load the key.');
128
        }
129
130
        return $asnObject->getChildren();
131
    }
132
133
    /**
134
     * @param array $children
135
     *
136
     * @return bool
137
     */
138
    private function isPKCS8(array $children): bool
139
    {
140
        if (3 !== count($children)) {
141
            return false;
142
        }
143
144
        $classes = [0 => Integer::class, 1 => Sequence::class, 2 => OctetString::class];
145
        foreach ($classes as $k => $class) {
146
            if (!$children[$k] instanceof $class) {
147
                return false;
148
            }
149
        }
150
151
        return true;
152
    }
153
154
    /**
155
     * @param array $jwk
156
     */
157
    private function loadJWK(array $jwk)
158
    {
159
        foreach (['kty', 'x', 'y', 'crv'] as $k) {
160
            if (!array_key_exists($k, $jwk)) {
161
                throw new \InvalidArgumentException(sprintf('The key parameter "%s" is missing.', $k));
162
            }
163
        }
164
        if ('EC' !== $jwk['kty']) {
165
            throw new \InvalidArgumentException('JWK is not an Elliptic Curve key');
166
        }
167
168
        $this->values = $jwk;
169
        if (array_key_exists('d', $jwk)) {
170
            $this->initPrivateKey();
171
        } else {
172
            $this->initPublicKey();
173
        }
174
    }
175
176
    private function initPublicKey()
177
    {
178
        $oid_sequence = new Sequence();
179
        $oid_sequence->addChild(new ObjectIdentifier('1.2.840.10045.2.1'));
180
        $oid_sequence->addChild(new ObjectIdentifier($this->getOID($this->values['crv'])));
181
        $this->sequence->addChild($oid_sequence);
182
183
        $bits = '04';
184
        $bits .= bin2hex(Base64Url::decode($this->values['x']));
185
        $bits .= bin2hex(Base64Url::decode($this->values['y']));
186
        $this->sequence->addChild(new BitString($bits));
187
    }
188
189
    private function initPrivateKey()
190
    {
191
        $this->sequence->addChild(new Integer(1));
192
        $this->sequence->addChild(new OctetString(bin2hex(Base64Url::decode($this->values['d']))));
193
194
        $oid = new ObjectIdentifier($this->getOID($this->values['crv']));
195
        $this->sequence->addChild(new ExplicitlyTaggedObject(0, $oid));
196
197
        $bits = '04';
198
        $bits .= bin2hex(Base64Url::decode($this->values['x']));
199
        $bits .= bin2hex(Base64Url::decode($this->values['y']));
200
        $bit = new BitString($bits);
201
        $this->sequence->addChild(new ExplicitlyTaggedObject(1, $bit));
202
    }
203
204
    /**
205
     * @param array $children
206
     *
207
     * @throws \Exception
208
     */
209
    private function loadPublicPEM(array $children)
210
    {
211
        if (!$children[0] instanceof Sequence) {
212
            throw new \InvalidArgumentException('Unsupported key type.');
213
        }
214
215
        $sub = $children[0]->getChildren();
216
        if (!$sub[0] instanceof ObjectIdentifier) {
217
            throw new \InvalidArgumentException('Unsupported key type.');
218
        }
219
        if ('1.2.840.10045.2.1' !== $sub[0]->getContent()) {
220
            throw new \InvalidArgumentException('Unsupported key type.');
221
        }
222
        if (!$sub[1] instanceof ObjectIdentifier) {
223
            throw new \InvalidArgumentException('Unsupported key type.');
224
        }
225
        if (!$children[1] instanceof BitString) {
226
            throw new \InvalidArgumentException('Unable to load the key.');
227
        }
228
229
        $bits = $children[1]->getContent();
230
        $bits_length = mb_strlen($bits, '8bit');
231
        if ('04' !== mb_substr($bits, 0, 2, '8bit')) {
232
            throw new \InvalidArgumentException('Unsupported key type');
233
        }
234
235
        $this->values['kty'] = 'EC';
236
        $this->values['crv'] = $this->getCurve($sub[1]->getContent());
237
        $this->values['x'] = Base64Url::encode(hex2bin(mb_substr($bits, 2, ($bits_length - 2) / 2, '8bit')));
238
        $this->values['y'] = Base64Url::encode(hex2bin(mb_substr($bits, ($bits_length - 2) / 2 + 2, ($bits_length - 2) / 2, '8bit')));
239
    }
240
241
    /**
242
     * @param object $children
243
     */
244
    private function verifyVersion(Object $children)
245
    {
246
        if (!$children instanceof Integer || '1' !== $children->getContent()) {
0 ignored issues
show
Unused Code Bug introduced by
The strict comparison !== seems to always evaluate to true as the types of '1' (string) and $children->getContent() (integer) can never be identical. Maybe you want to use a loose comparison != instead?
Loading history...
247
            throw new \InvalidArgumentException('Unable to load the key.');
248
        }
249
    }
250
251
    /**
252
     * @param object      $children
253
     * @param string|null $x
254
     * @param string|null $y
255
     */
256
    private function getXAndY(Object $children, ?string &$x, ?string &$y)
257
    {
258
        if (!$children instanceof ExplicitlyTaggedObject || !is_array($children->getContent())) {
259
            throw new \InvalidArgumentException('Unable to load the key.');
260
        }
261
        if (!$children->getContent()[0] instanceof BitString) {
262
            throw new \InvalidArgumentException('Unable to load the key.');
263
        }
264
265
        $bits = $children->getContent()[0]->getContent();
266
        $bits_length = mb_strlen($bits, '8bit');
267
268
        if ('04' !== mb_substr($bits, 0, 2, '8bit')) {
269
            throw new \InvalidArgumentException('Unsupported key type');
270
        }
271
272
        $x = mb_substr($bits, 2, ($bits_length - 2) / 2, '8bit');
273
        $y = mb_substr($bits, ($bits_length - 2) / 2 + 2, ($bits_length - 2) / 2, '8bit');
274
    }
275
276
    /**
277
     * @param object $children
278
     *
279
     * @return string
280
     */
281
    private function getD(Object $children): string
282
    {
283
        if (!$children instanceof OctetString) {
284
            throw new \InvalidArgumentException('Unable to load the key.');
285
        }
286
287
        return $children->getContent();
288
    }
289
290
    /**
291
     * @param array $children
292
     */
293
    private function loadPrivatePEM(array $children)
294
    {
295
        $this->verifyVersion($children[0]);
296
        $x = null;
297
        $y = null;
298
        $d = $this->getD($children[1]);
299
        $this->getXAndY($children[3], $x, $y);
300
301
        if (!$children[2] instanceof ExplicitlyTaggedObject || !is_array($children[2]->getContent())) {
302
            throw new \InvalidArgumentException('Unable to load the key.');
303
        }
304
        if (!$children[2]->getContent()[0] instanceof ObjectIdentifier) {
305
            throw new \InvalidArgumentException('Unable to load the key.');
306
        }
307
308
        $curve = $children[2]->getContent()[0]->getContent();
309
310
        $this->private = true;
311
        $this->values['kty'] = 'EC';
312
        $this->values['crv'] = $this->getCurve($curve);
313
        $this->values['d'] = Base64Url::encode(hex2bin($d));
314
        $this->values['x'] = Base64Url::encode(hex2bin($x));
315
        $this->values['y'] = Base64Url::encode(hex2bin($y));
316
    }
317
318
    /**
319
     * @param ECKey $private
320
     *
321
     * @return ECKey
322
     */
323
    public static function toPublic(ECKey $private): ECKey
324
    {
325
        $data = $private->toArray();
326
        if (array_key_exists('d', $data)) {
327
            unset($data['d']);
328
        }
329
330
        return new self($data);
331
    }
332
333
    /**
334
     * @return array
335
     */
336
    public function toArray()
337
    {
338
        return $this->values;
339
    }
340
341
    /**
342
     * @return string
343
     */
344
    public function toPEM(): string
345
    {
346
        $result = '-----BEGIN '.($this->private ? 'EC PRIVATE' : 'PUBLIC').' KEY-----'.PHP_EOL;
347
        $result .= chunk_split(base64_encode($this->sequence->getBinary()), 64, PHP_EOL);
348
        $result .= '-----END '.($this->private ? 'EC PRIVATE' : 'PUBLIC').' KEY-----'.PHP_EOL;
349
350
        return $result;
351
    }
352
353
    /**
354
     * @param $curve
355
     *
356
     * @return string
357
     */
358
    private function getOID(string $curve): string
359
    {
360
        $curves = $this->getSupportedCurves();
361
        $oid = array_key_exists($curve, $curves) ? $curves[$curve] : null;
362
        if (!is_string($oid)) {
363
            throw new \InvalidArgumentException('Unsupported curve.');
364
        }
365
366
        return $oid;
367
    }
368
369
    /**
370
     * @param string $oid
371
     *
372
     * @return string
373
     */
374
    private function getCurve(string $oid): string
375
    {
376
        $curves = $this->getSupportedCurves();
377
        $curve = array_search($oid, $curves, true);
378
        if (!is_string($curve)) {
379
            throw new \InvalidArgumentException('Unsupported OID.');
380
        }
381
382
        return $curve;
383
    }
384
385
    /**
386
     * @return array
387
     */
388
    private function getSupportedCurves(): array
389
    {
390
        return [
391
            'P-256' => '1.2.840.10045.3.1.7',
392
            'P-384' => '1.3.132.0.34',
393
            'P-521' => '1.3.132.0.35',
394
        ];
395
    }
396
}
397