Failed Conditions
Push — APNKeysSupport ( 8a74c9 )
by Florent
03:00
created

ECKey::loadPEM()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 25
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

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