ECKey::loadJWK()   A
last analyzed

Complexity

Conditions 4
Paths 5

Size

Total Lines 19
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

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