Failed Conditions
Push — v7 ( d25f5c...477009 )
by Florent
02:07
created

ECKey::createFromJWK()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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