Completed
Push — master ( 022625...2b82d4 )
by Florent
05:58
created

ECKey::loadPEM()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 15
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 15
rs 9.4285
cc 3
eloc 10
nc 3
nop 1
1
<?php
2
3
/*
4
 * The MIT License (MIT)
5
 *
6
 * Copyright (c) 2014-2016 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 Assert\Assertion;
15
use Base64Url\Base64Url;
16
use FG\ASN1\ExplicitlyTaggedObject;
17
use FG\ASN1\Object;
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 Jose\Object\JWKInterface;
24
25
final class ECKey extends Sequence
26
{
27
    /**
28
     * @var bool
29
     */
30
    private $private = false;
31
32
    /**
33
     * @var array
34
     */
35
    private $values = [];
36
37
    /**
38
     * @param \Jose\Object\JWKInterface|string|array $data
39
     */
40
    public function __construct($data)
41
    {
42
        parent::__construct();
43
44
        if ($data instanceof JWKInterface) {
45
            $this->loadJWK($data->getAll());
46
        } elseif (is_array($data)) {
47
            $this->loadJWK($data);
48
        } elseif (is_string($data)) {
49
            $this->loadPEM($data);
50
        } else {
51
            throw new \InvalidArgumentException('Unsupported input');
52
        }
53
        $this->private = isset($this->values['d']);
54
    }
55
56
    /**
57
     * @param string $data
58
     *
59
     * @throws \Exception
60
     * @throws \FG\ASN1\Exception\ParserException
61
     *
62
     * @return array
63
     */
64
    private function loadPEM($data)
65
    {
66
        $data = base64_decode(preg_replace('#-.*-|\r|\n#', '', $data));
67
        $asnObject = Object::fromBinary($data);
68
69
        Assertion::isInstanceOf($asnObject, Sequence::class);
70
71
        $children = $asnObject->getChildren();
0 ignored issues
show
Bug introduced by
The method getChildren does only exist in FG\ASN1\Construct, but not in FG\ASN1\AbstractString a...d FG\ASN1\UnknownObject.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
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
        Assertion::true(array_key_exists('kty', $jwk), 'JWK is not an Elliptic Curve key');
86
        Assertion::eq($jwk['kty'], 'EC', 'JWK is not an Elliptic Curve key');
87
        Assertion::true(array_key_exists('crv', $jwk), 'Curve parameter is missing');
88
        Assertion::true(array_key_exists('x', $jwk), 'Point parameters are missing');
89
        Assertion::true(array_key_exists('y', $jwk), 'Point parameters are missing');
90
91
        $this->values = $jwk;
92
        if (array_key_exists('d', $jwk)) {
93
            $this->initPrivateKey();
94
        } else {
95
            $this->initPublicKey();
96
        }
97
    }
98
99
    private function initPublicKey()
100
    {
101
        $oid_sequence = new Sequence();
102
        $oid_sequence->addChild(new ObjectIdentifier('1.2.840.10045.2.1'));
103
        $oid_sequence->addChild(new ObjectIdentifier($this->getOID($this->values['crv'])));
104
        $this->addChild($oid_sequence);
105
106
        $bits = '04';
107
        $bits .= bin2hex(Base64Url::decode($this->values['x']));
108
        $bits .= bin2hex(Base64Url::decode($this->values['y']));
109
        $this->addChild(new BitString($bits));
110
    }
111
112
    private function initPrivateKey()
113
    {
114
        $this->addChild(new Integer(1));
115
        $this->addChild(new OctetString(bin2hex(Base64Url::decode($this->values['d']))));
116
117
        $oid = new ObjectIdentifier($this->getOID($this->values['crv']));
118
        $this->addChild(new ExplicitlyTaggedObject(0, $oid));
119
120
        $bits = '04';
121
        $bits .= bin2hex(Base64Url::decode($this->values['x']));
122
        $bits .= bin2hex(Base64Url::decode($this->values['y']));
123
        $bit = new BitString($bits);
124
        $this->addChild(new ExplicitlyTaggedObject(1, $bit));
125
    }
126
127
    /**
128
     * @param array $children
129
     *
130
     * @throws \Exception
131
     *
132
     * @return array
133
     */
134
    private function loadPublicPEM(array $children)
135
    {
136
        Assertion::isInstanceOf($children[0], Sequence::class, 'Unsupported key type');
137
138
        $sub = $children[0]->getChildren();
139
        Assertion::isInstanceOf($sub[0], ObjectIdentifier::class, 'Unsupported key type');
140
        Assertion::eq('1.2.840.10045.2.1', $sub[0]->getContent(), 'Unsupported key type');
141
142
        Assertion::isInstanceOf($sub[1], ObjectIdentifier::class, 'Unsupported key type');
143
        Assertion::isInstanceOf($children[1], BitString::class, 'Unable to load the key');
144
145
        $bits = $children[1]->getContent();
146
        $bits_length = mb_strlen($bits, '8bit');
147
148
        Assertion::eq('04', mb_substr($bits, 0, 2, '8bit'), 'Unsupported key type');
149
150
        $this->values['kty'] = 'EC';
151
        $this->values['crv'] = $this->getCurve($sub[1]->getContent());
152
        $this->values['x'] = Base64Url::encode(hex2bin(mb_substr($bits, 2, ($bits_length - 2) / 2, '8bit')));
153
        $this->values['y'] = Base64Url::encode(hex2bin(mb_substr($bits, ($bits_length - 2) / 2 + 2, ($bits_length - 2) / 2, '8bit')));
154
    }
155
156
    /**
157
     * @param \FG\ASN1\Object $children
158
     */
159
    private function verifyVersion(Object $children)
160
    {
161
        Assertion::isInstanceOf($children, Integer::class, 'Unable to load the key');
162
        Assertion::eq(1, $children->getContent(), 'Unable to load the key');
163
    }
164
165
    /**
166
     * @param \FG\ASN1\Object $children
167
     * @param string|null     $x
168
     * @param string|null     $y
169
     */
170
    private function getXAndY(Object $children, &$x, &$y)
171
    {
172
        Assertion::isInstanceOf($children, ExplicitlyTaggedObject::class, 'Unable to load the key');
173
        Assertion::isArray($children->getContent(), 'Unable to load the key');
174
        Assertion::isInstanceOf($children->getContent()[0], BitString::class, 'Unable to load the key');
175
176
        $bits = $children->getContent()[0]->getContent();
177
        $bits_length = mb_strlen($bits, '8bit');
178
179
        Assertion::eq('04', mb_substr($bits, 0, 2, '8bit'), 'Unsupported key type');
180
181
        $x = mb_substr($bits, 2, ($bits_length - 2) / 2, '8bit');
182
        $y = mb_substr($bits, ($bits_length - 2) / 2 + 2, ($bits_length - 2) / 2, '8bit');
183
    }
184
185
    /**
186
     * @param \FG\ASN1\Object $children
187
     *
188
     * @return string
189
     */
190
    private function getD(Object $children)
191
    {
192
        Assertion::isInstanceOf($children, '\FG\ASN1\Universal\OctetString', 'Unable to load the key');
193
194
        return $children->getContent();
195
    }
196
197
    /**
198
     * @param array $children
199
     *
200
     * @return array
201
     */
202
    private function loadPrivatePEM(array $children)
203
    {
204
        $this->verifyVersion($children[0]);
205
206
        $x = null;
207
        $y = null;
208
        $d = $this->getD($children[1]);
209
        $this->getXAndY($children[3], $x, $y);
210
211
        Assertion::isInstanceOf($children[2], ExplicitlyTaggedObject::class, 'Unable to load the key');
212
        Assertion::isArray($children[2]->getContent(), 'Unable to load the key');
213
        Assertion::isInstanceOf($children[2]->getContent()[0], ObjectIdentifier::class, 'Unable to load the key');
214
215
        $curve = $children[2]->getContent()[0]->getContent();
216
217
        $this->private = true;
218
        $this->values['kty'] = 'EC';
219
        $this->values['crv'] = $this->getCurve($curve);
220
        $this->values['d'] = Base64Url::encode(hex2bin($d));
221
        $this->values['x'] = Base64Url::encode(hex2bin($x));
222
        $this->values['y'] = Base64Url::encode(hex2bin($y));
223
    }
224
225
    /**
226
     * @return bool
227
     */
228
    public function isPrivate()
229
    {
230
        return $this->private;
231
    }
232
233
    /**
234
     * @param \Jose\KeyConverter\ECKey $private
235
     *
236
     * @return \Jose\KeyConverter\ECKey
237
     */
238
    public static function toPublic(ECKey $private)
239
    {
240
        $data = $private->toArray();
241
        if (array_key_exists('d', $data)) {
242
            unset($data['d']);
243
        }
244
245
        return new self($data);
246
    }
247
248
    /**
249
     * @return string
250
     */
251
    public function __toString()
252
    {
253
        return $this->toPEM();
254
    }
255
256
    /**
257
     * @return array
258
     */
259
    public function toArray()
260
    {
261
        return $this->values;
262
    }
263
264
    /**
265
     * @return string
266
     */
267
    public function toDER()
268
    {
269
        return $this->getBinary();
270
    }
271
272
    /**
273
     * @return string
274
     */
275
    public function toPEM()
276
    {
277
        $result = '-----BEGIN '.($this->private ? 'EC PRIVATE' : 'PUBLIC').' KEY-----'.PHP_EOL;
278
        $result .= chunk_split(base64_encode($this->getBinary()), 64, PHP_EOL);
279
        $result .= '-----END '.($this->private ? 'EC PRIVATE' : 'PUBLIC').' KEY-----'.PHP_EOL;
280
281
        return $result;
282
    }
283
284
    /**
285
     * @param $curve
286
     *
287
     * @return string
288
     */
289
    private function getOID($curve)
290
    {
291
        $curves = $this->getSupportedCurves();
292
        $oid = array_key_exists($curve, $curves) ? $curves[$curve] : null;
293
294
        Assertion::notNull($oid, 'Unsupported curve');
295
296
        return $oid;
297
    }
298
299
    /**
300
     * @param string $oid
301
     *
302
     * @return string
303
     */
304
    private function getCurve($oid)
305
    {
306
        $curves = $this->getSupportedCurves();
307
        $curve = array_search($oid, $curves, true);
308
        Assertion::string($curve, 'Unsupported OID');
309
310
        return $curve;
311
    }
312
313
    /**
314
     * @return array
315
     */
316
    private function getSupportedCurves()
317
    {
318
        return [
319
            'P-256' => '1.2.840.10045.3.1.7',
320
            'P-384' => '1.3.132.0.34',
321
            'P-521' => '1.3.132.0.35',
322
        ];
323
    }
324
}
325