Failed Conditions
Push — master ( 6aefc6...e9b4ba )
by Florent
02:15
created

JWKSet::without()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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