Failed Conditions
Push — master ( 7e91e5...a0678f )
by Florent
56:34
created

JWKSet::valid()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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