JWKSet::sortKeys()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 8
rs 10
c 0
b 0
f 0
cc 3
nc 3
nop 2
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * The MIT License (MIT)
7
 *
8
 * Copyright (c) 2014-2019 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
use ArrayIterator;
17
use Countable;
18
use InvalidArgumentException;
19
use IteratorAggregate;
20
use JsonSerializable;
21
use Traversable;
22
23
class JWKSet implements Countable, IteratorAggregate, JsonSerializable
24
{
25
    /**
26
     * @var array
27
     */
28
    private $keys = [];
29
30
    /**
31
     * @param JWK[] $keys
32
     *
33
     * @throws InvalidArgumentException if the list is invalid
34
     */
35
    public function __construct(array $keys)
36
    {
37
        foreach ($keys as $k => $key) {
38
            if (!$key instanceof JWK) {
39
                throw new InvalidArgumentException('Invalid list. Should only contains JWK objects');
40
            }
41
42
            if ($key->has('kid')) {
43
                unset($keys[$k]);
44
                $this->keys[$key->get('kid')] = $key;
45
            } else {
46
                $this->keys[] = $key;
47
            }
48
        }
49
    }
50
51
    /**
52
     * Creates a JWKSet object using the given values.
53
     *
54
     * @throws InvalidArgumentException if the keyset is not valid
55
     *
56
     * @return JWKSet
57
     */
58
    public static function createFromKeyData(array $data): self
59
    {
60
        if (!isset($data['keys'])) {
61
            throw new InvalidArgumentException('Invalid data.');
62
        }
63
        if (!\is_array($data['keys'])) {
64
            throw new InvalidArgumentException('Invalid data.');
65
        }
66
67
        $jwkset = new self([]);
68
        foreach ($data['keys'] as $key) {
69
            $jwk = new JWK($key);
70
            if ($jwk->has('kid')) {
71
                $jwkset->keys[$jwk->get('kid')] = $jwk;
72
            } else {
73
                $jwkset->keys[] = $jwk;
74
            }
75
        }
76
77
        return $jwkset;
78
    }
79
80
    /**
81
     * Creates a JWKSet object using the given Json string.
82
     *
83
     * @throws InvalidArgumentException if the data is not valid
84
     *
85
     * @return JWKSet
86
     */
87
    public static function createFromJson(string $json): self
88
    {
89
        $data = json_decode($json, true);
90
        if (!\is_array($data)) {
91
            throw new InvalidArgumentException('Invalid argument.');
92
        }
93
94
        return self::createFromKeyData($data);
95
    }
96
97
    /**
98
     * Returns an array of keys stored in the key set.
99
     *
100
     * @return JWK[]
101
     */
102
    public function all(): array
103
    {
104
        return $this->keys;
105
    }
106
107
    /**
108
     * Add key to store in the key set.
109
     * This method is immutable and will return a new object.
110
     *
111
     * @return JWKSet
112
     */
113
    public function with(JWK $jwk): self
114
    {
115
        $clone = clone $this;
116
117
        if ($jwk->has('kid')) {
118
            $clone->keys[$jwk->get('kid')] = $jwk;
119
        } else {
120
            $clone->keys[] = $jwk;
121
        }
122
123
        return $clone;
124
    }
125
126
    /**
127
     * Remove key from the key set.
128
     * This method is immutable and will return a new object.
129
     *
130
     * @param int|string $key Key to remove from the key set
131
     *
132
     * @return JWKSet
133
     */
134
    public function without($key): self
135
    {
136
        if (!$this->has($key)) {
137
            return $this;
138
        }
139
140
        $clone = clone $this;
141
        unset($clone->keys[$key]);
142
143
        return $clone;
144
    }
145
146
    /**
147
     * Returns true if the key set contains a key with the given index.
148
     *
149
     * @param int|string $index
150
     */
151
    public function has($index): bool
152
    {
153
        return \array_key_exists($index, $this->keys);
154
    }
155
156
    /**
157
     * Returns the key with the given index. Throws an exception if the index is not present in the key store.
158
     *
159
     * @param int|string $index
160
     *
161
     * @throws InvalidArgumentException if the index is not defined
162
     */
163
    public function get($index): JWK
164
    {
165
        if (!$this->has($index)) {
166
            throw new InvalidArgumentException('Undefined index.');
167
        }
168
169
        return $this->keys[$index];
170
    }
171
172
    /**
173
     * Returns the values to be serialized.
174
     */
175
    public function jsonSerialize(): array
176
    {
177
        return ['keys' => array_values($this->keys)];
178
    }
179
180
    /**
181
     * Returns the number of keys in the key set.
182
     *
183
     * @param int $mode
184
     */
185
    public function count($mode = COUNT_NORMAL): int
186
    {
187
        return \count($this->keys, $mode);
188
    }
189
190
    /**
191
     * Try to find a key that fits on the selected requirements.
192
     * Returns null if not found.
193
     *
194
     * @param string         $type         Must be 'sig' (signature) or 'enc' (encryption)
195
     * @param null|Algorithm $algorithm    Specifies the algorithm to be used
196
     * @param array          $restrictions More restrictions such as 'kid' or 'kty'
197
     *
198
     * @throws InvalidArgumentException if the key type is not valid (must be "sig" or "enc")
199
     */
200
    public function selectKey(string $type, ?Algorithm $algorithm = null, array $restrictions = []): ?JWK
201
    {
202
        if (!\in_array($type, ['enc', 'sig'], true)) {
203
            throw new InvalidArgumentException('Allowed key types are "sig" or "enc".');
204
        }
205
206
        $result = [];
207
        foreach ($this->keys as $key) {
208
            $ind = 0;
209
210
            $can_use = $this->canKeyBeUsedFor($type, $key);
211
            if (false === $can_use) {
212
                continue;
213
            }
214
            $ind += $can_use;
215
216
            $alg = $this->canKeyBeUsedWithAlgorithm($algorithm, $key);
217
            if (false === $alg) {
218
                continue;
219
            }
220
            $ind += $alg;
221
222
            if (false === $this->doesKeySatisfyRestrictions($restrictions, $key)) {
223
                continue;
224
            }
225
226
            $result[] = ['key' => $key, 'ind' => $ind];
227
        }
228
229
        if (0 === \count($result)) {
230
            return null;
231
        }
232
233
        usort($result, [$this, 'sortKeys']);
234
235
        return $result[0]['key'];
236
    }
237
238
    /**
239
     * Internal method only. Should not be used.
240
     *
241
     * @internal
242
     * @internal
243
     */
244
    public static function sortKeys(array $a, array $b): int
245
    {
246
        if ($a['ind'] === $b['ind']) {
247
            return 0;
248
        }
249
250
        return ($a['ind'] > $b['ind']) ? -1 : 1;
251
    }
252
253
    /**
254
     * Internal method only. Should not be used.
255
     *
256
     * @internal
257
     */
258
    public function getIterator(): Traversable
259
    {
260
        return new ArrayIterator($this->keys);
261
    }
262
263
    /**
264
     * @throws InvalidArgumentException if the key does not fulfill with the "key_ops" constraint
265
     *
266
     * @return bool|int
267
     */
268
    private function canKeyBeUsedFor(string $type, JWK $key)
269
    {
270
        if ($key->has('use')) {
271
            return $type === $key->get('use') ? 1 : false;
272
        }
273
        if ($key->has('key_ops')) {
274
            $key_ops = $key->get('key_ops');
275
            if (!\is_array($key_ops)) {
276
                throw new InvalidArgumentException('Invalid key parameter "key_ops". Should be a list of key operations');
277
            }
278
279
            return $type === self::convertKeyOpsToKeyUse($key_ops) ? 1 : false;
280
        }
281
282
        return 0;
283
    }
284
285
    /**
286
     * @return bool|int
287
     */
288
    private function canKeyBeUsedWithAlgorithm(?Algorithm $algorithm, JWK $key)
289
    {
290
        if (null === $algorithm) {
291
            return 0;
292
        }
293
        if (!\in_array($key->get('kty'), $algorithm->allowedKeyTypes(), true)) {
294
            return false;
295
        }
296
        if ($key->has('alg')) {
297
            return $algorithm->name() === $key->get('alg') ? 2 : false;
298
        }
299
300
        return 1;
301
    }
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
     * @throws InvalidArgumentException if the key operation is not supported
316
     */
317
    private static function convertKeyOpsToKeyUse(array $key_ops): string
318
    {
319
        switch (true) {
320
            case \in_array('verify', $key_ops, true):
321
            case \in_array('sign', $key_ops, true):
322
                return 'sig';
323
            case \in_array('encrypt', $key_ops, true):
324
            case \in_array('decrypt', $key_ops, true):
325
            case \in_array('wrapKey', $key_ops, true):
326
            case \in_array('unwrapKey', $key_ops, true):
327
            case \in_array('deriveKey', $key_ops, true):
328
            case \in_array('deriveBits', $key_ops, true):
329
                return 'enc';
330
            default:
331
                throw new InvalidArgumentException(sprintf('Unsupported key operation value "%s"', $key_ops));
332
        }
333
    }
334
}
335