Completed
Push — master ( c67af0...01a0a2 )
by Florent
02:34
created

ECKey::loadJWK()   B

Complexity

Conditions 7
Paths 5

Size

Total Lines 18
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 1 Features 0
Metric Value
c 2
b 1
f 0
dl 0
loc 18
rs 8.2222
cc 7
eloc 12
nc 5
nop 1
1
<?php
2
3
/*
4
 * The MIT License (MIT)
5
 *
6
 * Copyright (c) 2014-2015 Spomky-Labs
7
 *
8
 * This software may be modified and distributed under the terms
9
 * of the MIT license.  See the LICENSE file for details.
10
 */
11
12
namespace Jose\KeyConverter;
13
14
use Base64Url\Base64Url;
15
use FG\ASN1\ExplicitlyTaggedObject;
16
use FG\ASN1\Object;
17
use FG\ASN1\Universal\BitString;
18
use FG\ASN1\Universal\Integer;
19
use FG\ASN1\Universal\ObjectIdentifier;
20
use FG\ASN1\Universal\OctetString;
21
use FG\ASN1\Universal\Sequence;
22
use Jose\JWKInterface;
23
24
final class ECKey extends Sequence
25
{
26
    /**
27
     * @var bool
28
     */
29
    private $private = false;
30
31
    /**
32
     * @var array
33
     */
34
    private $values = [];
35
36
    /**
37
     * @param \Jose\JWKInterface|string|array $data
38
     */
39
    public function __construct($data)
40
    {
41
        parent::__construct();
42
43
        if ($data instanceof JWKInterface) {
44
            $this->loadJWK($data->getValues());
45
        } elseif (is_array($data)) {
46
            $this->loadJWK($data);
47
        } elseif (is_string($data)) {
48
            $this->loadPEM($data);
49
        } else {
50
            throw new \InvalidArgumentException('Unsupported input');
51
        }
52
        $this->private = isset($this->values['d']);
53
    }
54
55
    /**
56
     * @param $data
57
     *
58
     * @throws \Exception
59
     * @throws \FG\ASN1\Exception\ParserException
60
     *
61
     * @return array
62
     */
63
    private function loadPEM($data)
64
    {
65
        $data = base64_decode(preg_replace('#-.*-|\r|\n#', '', $data));
66
        $asnObject = Object::fromBinary($data);
67
68
        if (!$asnObject instanceof Sequence) {
69
            throw new \Exception('Unable to load the key');
70
        }
71
        $children = $asnObject->getChildren();
72
        if (4 === count($children)) {
73
            return $this->loadPrivatePEM($children);
74
        } elseif (2 === count($children)) {
75
            return $this->loadPublicPEM($children);
76
        }
77
        throw new \Exception('Unable to load the key');
78
    }
79
80
    /**
81
     * @param array $jwk
82
     */
83
    private function loadJWK(array $jwk)
84
    {
85
        if (!array_key_exists('kty', $jwk) || 'EC' !== $jwk['kty']) {
86
            throw new \InvalidArgumentException('JWK is not an Elliptic Curve key');
87
        }
88
        if (!array_key_exists('crv', $jwk)) {
89
            throw new \InvalidArgumentException('Curve parameter is missing');
90
        }
91
        if (!array_key_exists('x', $jwk) || !array_key_exists('y', $jwk)) {
92
            throw new \InvalidArgumentException('Point parameters are missing');
93
        }
94
        $this->values = $jwk;
95
        if (array_key_exists('d', $jwk)) {
96
            $this->initPrivateKey();
97
        } else {
98
            $this->initPublicKey();
99
        }
100
    }
101
102
    /**
103
     * @throws \Exception
104
     */
105
    private function initPublicKey()
106
    {
107
        $oid_sequence = new Sequence();
108
        $oid_sequence->addChild(new ObjectIdentifier('1.2.840.10045.2.1'));
109
        $oid_sequence->addChild(new ObjectIdentifier($this->getOID($this->values['crv'])));
110
        $this->addChild($oid_sequence);
111
112
        $bits = '04';
113
        $bits .= bin2hex(Base64Url::decode($this->values['x']));
114
        $bits .= bin2hex(Base64Url::decode($this->values['y']));
115
        $this->addChild(new BitString($bits));
116
    }
117
118
    /**
119
     *
120
     */
121
    private function initPrivateKey()
122
    {
123
        $this->addChild(new Integer(1));
124
        $this->addChild(new OctetString(bin2hex(Base64Url::decode($this->values['d']))));
125
126
        $oid = new ObjectIdentifier($this->getOID($this->values['crv']));
127
        $this->addChild(new ExplicitlyTaggedObject(0, $oid));
128
129
        $bits = '04';
130
        $bits .= bin2hex(Base64Url::decode($this->values['x']));
131
        $bits .= bin2hex(Base64Url::decode($this->values['y']));
132
        $bit = new BitString($bits);
133
        $this->addChild(new ExplicitlyTaggedObject(1, $bit));
134
    }
135
136
    /**
137
     * @param array $children
138
     *
139
     * @throws \Exception
140
     *
141
     * @return array
142
     */
143
    private function loadPublicPEM(array $children)
144
    {
145
        if (!$children[0] instanceof Sequence) {
146
            throw new \Exception('Unable to load the key');
147
        }
148
        $sub = $children[0]->getChildren();
149
        if (!$sub[0] instanceof ObjectIdentifier || '1.2.840.10045.2.1' !== $sub[0]->getContent()) {
150
            throw new \Exception('Unsupported key type');
151
        }
152
        if (!$sub[1] instanceof ObjectIdentifier) {
153
            throw new \Exception('Unsupported key type');
154
        }
155
156
        if (!$children[1] instanceof BitString) {
157
            throw new \Exception('Unable to load the key');
158
        }
159
160
        $bits = $children[1]->getContent();
161
162
        if (substr($bits, 0, 2) !== '04') {
163
            throw new \Exception('Unsupported key type');
164
        }
165
166
        $this->values['kty'] = 'EC';
167
        $this->values['crv'] = $this->getCurve($sub[1]->getContent());
168
        $this->values['x'] = Base64Url::encode(hex2bin(substr($bits, 2, (strlen($bits) - 2) / 2)));
169
        $this->values['y'] = Base64Url::encode(hex2bin(substr($bits, (strlen($bits) - 2) / 2 + 2, (strlen($bits) - 2) / 2)));
170
    }
171
172
    /**
173
     * @param \FG\ASN1\Object $children
174
     *
175
     * @throws \Exception
176
     */
177
    private function verifyVersion(Object $children)
178
    {
179
        if (!$children instanceof Integer || 1 !== (int) $children->getContent()) {
180
            throw new \Exception('Unable to load the key');
181
        }
182
    }
183
184
    /**
185
     * @param \FG\ASN1\Object $children
186
     * @param string|null     $x
187
     * @param string|null     $y
188
     *
189
     * @throws \Exception
190
     */
191
    private function getXAndY(Object $children, &$x, &$y)
192
    {
193
        if (!$children instanceof ExplicitlyTaggedObject) {
194
            throw new \Exception('Unable to load the key');
195
        }
196
        if (!$children->getContent() instanceof BitString) {
197
            throw new \Exception('Unable to load the key');
198
        }
199
200
        $bits = $children->getContent()->getContent();
201
202
        if (substr($bits, 0, 2) !== '04') {
203
            throw new \Exception('Unsupported key type');
204
        }
205
206
        $x = substr($bits, 2, (strlen($bits) - 2) / 2);
207
        $y = substr($bits, (strlen($bits) - 2) / 2 + 2, (strlen($bits) - 2) / 2);
208
    }
209
210
    /**
211
     * @param \FG\ASN1\Object $children
212
     *
213
     * @throws \Exception
214
     *
215
     * @return string
216
     */
217
    private function getD(Object $children)
218
    {
219
        if (!$children instanceof OctetString) {
220
            throw new \Exception('Unable to load the key');
221
        }
222
223
        return $children->getContent();
224
    }
225
226
    /**
227
     * @param array $children
228
     *
229
     * @throws \Exception
230
     *
231
     * @return array
232
     */
233
    private function loadPrivatePEM(array $children)
234
    {
235
        $this->verifyVersion($children[0]);
236
237
        $x = null;
238
        $y = null;
239
        $d = $this->getD($children[1]);
240
        $this->getXAndY($children[3], $x, $y);
241
242
        if (!$children[2] instanceof ExplicitlyTaggedObject) {
243
            throw new \Exception('Unable to load the key');
244
        }
245
        if (!$children[2]->getContent() instanceof ObjectIdentifier) {
246
            throw new \Exception('Unable to load the key');
247
        }
248
        $curve = $children[2]->getContent()->getContent();
249
250
        $this->private = true;
251
        $this->values['kty'] = 'EC';
252
        $this->values['crv'] = $this->getCurve($curve);
253
        $this->values['d'] = Base64Url::encode(hex2bin($d));
254
        $this->values['x'] = Base64Url::encode(hex2bin($x));
255
        $this->values['y'] = Base64Url::encode(hex2bin($y));
256
    }
257
258
    /**
259
     * @return bool
260
     */
261
    public function isPrivate()
262
    {
263
        return $this->private;
264
    }
265
266
    /**
267
     * @param \Jose\KeyConverter\ECKey $private
268
     *
269
     * @return \Jose\KeyConverter\ECKey
270
     */
271
    public static function toPublic(ECKey $private)
272
    {
273
        $data = $private->toArray();
274
        if (array_key_exists('d', $data)) {
275
            unset($data['d']);
276
        }
277
278
        return new self($data);
279
    }
280
281
    public function __toString()
282
    {
283
        return $this->toPEM();
284
    }
285
286
    /**
287
     * @return array
288
     */
289
    public function toArray()
290
    {
291
        return $this->values;
292
    }
293
294
    /**
295
     * @return string
296
     */
297
    public function toPEM()
298
    {
299
        $result = '-----BEGIN '.($this->private ? 'EC PRIVATE' : 'PUBLIC').' KEY-----'.PHP_EOL;
300
        $result .= chunk_split(base64_encode($this->getBinary()), 64, PHP_EOL);
301
        $result .= '-----END '.($this->private ? 'EC PRIVATE' : 'PUBLIC').' KEY-----'.PHP_EOL;
302
303
        return $result;
304
    }
305
306
    /**
307
     * @param $curve
308
     *
309
     * @return string
310
     */
311
    private function getOID($curve)
312
    {
313
        $curves = $this->getSupportedCurves();
314
        $oid = array_key_exists($curve, $curves) ? $curves[$curve] : null;
315
        if (null === $oid) {
316
            throw new \InvalidArgumentException('Unsupported curve');
317
        }
318
319
        return $oid;
320
    }
321
322
    /**
323
     * @param string $oid
324
     *
325
     * @return string
326
     */
327
    private function getCurve($oid)
328
    {
329
        $curves = $this->getSupportedCurves();
330
        $curve = array_search($oid, $curves, true);
331
        if (false === $curve) {
332
            throw new \InvalidArgumentException('Unsupported OID');
333
        }
334
335
        return $curve;
336
    }
337
338
    private function getSupportedCurves()
339
    {
340
        return [
341
            'P-192' => '1.2.840.10045.3.1.1',
342
            'P-256' => '1.2.840.10045.3.1.7',
343
            'P-384' => '1.3.132.0.34',
344
            'P-521' => '1.3.132.0.35',
345
        ];
346
    }
347
}
348