JWKSet::sortKeys()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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