Failed Conditions
Push — v7 ( 61ffea...f7e5f1 )
by Florent
04:20
created

JWKSet::getPEM()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 11
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 8
nc 3
nop 1
dl 0
loc 11
rs 9.4285
c 0
b 0
f 0
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\Core;
15
16
/**
17
 * Class JWKSet.
18
 */
19
final class JWKSet implements \Countable, \Iterator, \JsonSerializable
20
{
21
    /**
22
     * @var array
23
     */
24
    private $keys = [];
25
26
    /**
27
     * JWKSet constructor.
28
     *
29
     * @param JWK[] $keys
30
     */
31
    private function __construct(array $keys)
32
    {
33
        $this->keys = $keys;
34
    }
35
36
    /**
37
     * @param array $data
38
     *
39
     * @return JWKSet
40
     */
41
    public static function createFromKeyData(array $data): JWKSet
42
    {
43
        if (!array_key_exists('keys', $data) || !is_array($data['keys'])) {
44
            throw new \InvalidArgumentException('Invalid data.');
45
        }
46
47
        $keys = [];
48
        foreach ($data['keys'] as $key) {
49
            $jwk = JWK::create($key);
50
            if ($jwk->has('kid')) {
51
                $keys[$jwk->get('kid')] = $jwk;
52
                continue;
53
            }
54
            $keys[] = $jwk;
55
        }
56
57
        return new self($keys);
58
    }
59
60
    /**
61
     * @param JWK[] $keys
62
     *
63
     * @return JWKSet
64
     */
65
    public static function createFromKeys(array $keys): JWKSet
66
    {
67
        $keys = array_filter($keys, function () {
68
            return true;
69
        });
70
        foreach ($keys as $k => $v) {
71
            if ($v->has('kid')) {
72
                $keys[$v->get('kid')] = $v;
73
                unset($keys[$k]);
74
            }
75
        }
76
77
        return new self($keys);
78
    }
79
80
    /**
81
     * Returns all keys in the key set.
82
     *
83
     * @return JWK[] An array of keys stored in the key set
84
     */
85
    public function getKeys(): array
86
    {
87
        return $this->keys;
88
    }
89
90
    /**
91
     * Add key in the key set.
92
     *
93
     * @param JWK $jwk A key to store in the key set
94
     *
95
     * @return JWKSet
96
     */
97
    public function withKey(JWK $jwk): JWKSet
98
    {
99
        $clone = clone $this;
100
101
        if ($jwk->has('kid')) {
102
            $clone->keys[$jwk->get('kid')] = $jwk;
103
        } else {
104
            $clone->keys[] = $jwk;
105
        }
106
107
        return $clone;
108
    }
109
110
    /**
111
     * Remove key from the key set.
112
     *
113
     * @param int|string $key Key to remove from the key set
114
     *
115
     * @return JWKSet
116
     */
117
    public function withoutKey($key): JWKSet
118
    {
119
        if (!$this->hasKey($key)) {
120
            return $this;
121
        }
122
123
        $clone = clone $this;
124
        unset($clone->keys[$key]);
125
126
        return $clone;
127
    }
128
129
    /**
130
     * @param int|string $index
131
     *
132
     * @return bool
133
     */
134
    public function hasKey($index): bool
135
    {
136
        return array_key_exists($index, $this->keys);
137
    }
138
139
    /**
140
     * @param int|string $index
141
     *
142
     * @return JWK
143
     */
144
    public function getKey($index): JWK
145
    {
146
        if (!$this->hasKey($index)) {
147
            throw new \InvalidArgumentException('Undefined index.');
148
        }
149
150
        return $this->keys[$index];
151
    }
152
153
    /**
154
     * @return array
155
     */
156
    public function jsonSerialize(): array
157
    {
158
        return ['keys' => $this->keys];
159
    }
160
161
    /**
162
     * @param int $mode
163
     *
164
     * @return int
165
     */
166
    public function count($mode = COUNT_NORMAL): int
167
    {
168
        return count($this->keys, $mode);
169
    }
170
171
    /**
172
     * @return JWK|null
173
     */
174
    public function current(): ?JWK
175
    {
176
        $key = $this->key();
177
        if (null === $key) {
178
            return null;
179
        }
180
181
        return $this->hasKey($key) ? $this->getKey($key) : null;
182
    }
183
184
    /**
185
     * @return int|string|null
186
     */
187
    public function key()
188
    {
189
        return key($this->keys);
190
    }
191
192
    public function next()
193
    {
194
        next($this->keys);
195
    }
196
197
    public function rewind()
198
    {
199
        reset($this->keys);
200
    }
201
202
    /**
203
     * @return bool
204
     */
205
    public function valid(): bool
206
    {
207
        return $this->current() instanceof JWK;
208
    }
209
210
    /**
211
     * @param string      $type         Must be 'sig' (signature) or 'enc' (encryption)
212
     * @param string|null $algorithm    Specifies the algorithm to be used
213
     * @param array       $restrictions More restrictions such as 'kid' or 'kty'
214
     *
215
     * @return JWK|null
216
     */
217
    public function selectKey(string $type, ?string $algorithm = null, array $restrictions = []): ?JWK
218
    {
219
        if (!in_array($type, ['enc', 'sig'])) {
220
            throw new \InvalidArgumentException('Allowed key types are "sig" or "enc".');
221
        }
222
223
        $result = [];
224
        foreach ($this->keys as $key) {
225
            $ind = 0;
226
227
            // Check usage
228
            $can_use = $this->canKeyBeUsedFor($type, $key);
229
            if (false === $can_use) {
230
                continue;
231
            }
232
            $ind += $can_use;
233
234
            // Check algorithm
235
            $alg = $this->canKeyBeUsedWithAlgorithm($algorithm, $key);
236
            if (false === $alg) {
237
                continue;
238
            }
239
            $ind += $alg;
240
241
            // Validate restrictions
242
            if (false === $this->doesKeySatisfyRestrictions($restrictions, $key)) {
243
                continue;
244
            }
245
246
            // Add to the list with trust indicator
247
            $result[] = ['key' => $key, 'ind' => $ind];
248
        }
249
250
        //Return null if no key
251
        if (empty($result)) {
252
            return null;
253
        }
254
255
        //Sort by trust indicator
256
        usort($result, [$this, 'sortKeys']);
257
        //Return the highest trust indicator (first key)
258
        return $result[0]['key'];
259
    }
260
261
    /**
262
     * @param string $type
263
     * @param JWK    $key
264
     *
265
     * @return bool|int
266
     */
267
    private function canKeyBeUsedFor(string $type, JWK $key)
268
    {
269
        if ($key->has('use')) {
270
            return $type === $key->get('use') ? 1 : false;
271
        }
272
        if ($key->has('key_ops')) {
273
            return $type === self::convertKeyOpsToKeyUse($key->get('use')) ? 1 : false;
274
        }
275
276
        return 0;
277
    }
278
279
    /**
280
     * @param null|string $algorithm
281
     * @param JWK         $key
282
     *
283
     * @return bool|int
284
     */
285
    private function canKeyBeUsedWithAlgorithm(?string $algorithm, JWK $key)
286
    {
287
        if (null === $algorithm) {
288
            return 0;
289
        }
290
        if ($key->has('alg')) {
291
            return $algorithm === $key->get('alg') ? 1 : false;
292
        }
293
294
        return 0;
295
    }
296
297
    /**
298
     * @param array $restrictions
299
     * @param JWK   $key
300
     *
301
     * @return bool
302
     */
303
    private function doesKeySatisfyRestrictions(array $restrictions, JWK $key): bool
304
    {
305
        foreach ($restrictions as $k => $v) {
306
            if (!$key->has($k) || $v !== $key->get($k)) {
307
                return false;
308
            }
309
        }
310
311
        return true;
312
    }
313
314
    /**
315
     * @param string $key_ops
316
     *
317
     * @return string
318
     */
319
    private static function convertKeyOpsToKeyUse(string $key_ops): string
320
    {
321
        switch ($key_ops) {
322
            case 'verify':
323
            case 'sign':
324
                return 'sig';
325
            case 'encrypt':
326
            case 'decrypt':
327
            case 'wrapKey':
328
            case 'unwrapKey':
329
                return 'enc';
330
            default:
331
                throw new \InvalidArgumentException(sprintf('Unsupported key operation value "%s"', $key_ops));
332
        }
333
    }
334
335
    /**
336
     * @param array $a
337
     * @param array $b
338
     *
339
     * @return int
340
     */
341
    public function sortKeys(array $a, array $b): int
342
    {
343
        if ($a['ind'] === $b['ind']) {
344
            return 0;
345
        }
346
347
        return ($a['ind'] > $b['ind']) ? -1 : 1;
348
    }
349
}
350