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

ECKey   B

Complexity

Total Complexity 54

Size/Duplication

Total Lines 370
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 9

Importance

Changes 0
Metric Value
wmc 54
lcom 1
cbo 9
dl 0
loc 370
rs 7.0642
c 0
b 0
f 0

20 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
A createFromJWK() 0 4 1
A createFromPEM() 0 6 1
B loadPEM() 0 21 5
A loadPKCS8() 0 10 2
C loadPublicPEM() 0 33 7
A getCurve() 0 10 2
A getSupportedCurves() 0 8 1
A verifyVersion() 0 6 3
B getXAndY() 0 19 5
A getD() 0 8 2
B loadPrivatePEM() 0 25 4
A isPKCS8() 0 15 4
A toPublic() 0 9 2
A toArray() 0 4 1
B loadJWK() 0 24 5
A initPublicKey() 0 12 1
A initPrivateKey() 0 14 1
A toPEM() 0 8 3
A getOID() 0 11 3

How to fix   Complexity   

Complex Class

Complex classes like ECKey often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ECKey, and based on these observations, apply Extract Interface, too.

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