1 | const { deprecate, inspect } = require('util')
|
2 |
|
3 | const isObject = require('../help/is_object')
|
4 | const { generate, generateSync } = require('../jwk/generate')
|
5 | const { USES_MAPPING } = require('../help/consts')
|
6 | const { isKey, asKey: importKey } = require('../jwk')
|
7 |
|
8 | const keyscore = (key, { alg, use, ops }) => {
|
9 | let score = 0
|
10 |
|
11 | if (alg && key.alg) {
|
12 | score++
|
13 | }
|
14 |
|
15 | if (use && key.use) {
|
16 | score++
|
17 | }
|
18 |
|
19 | if (ops && key.key_ops) {
|
20 | score++
|
21 | }
|
22 |
|
23 | return score
|
24 | }
|
25 |
|
26 | class KeyStore {
|
27 | constructor (...keys) {
|
28 | while (keys.some(Array.isArray)) {
|
29 | keys = keys.flat ? keys.flat() : keys.reduce((acc, val) => {
|
30 | if (Array.isArray(val)) {
|
31 | return [...acc, ...val]
|
32 | }
|
33 |
|
34 | acc.push(val)
|
35 | return acc
|
36 | }, [])
|
37 | }
|
38 | if (keys.some(k => !isKey(k) || !k.kty)) {
|
39 | throw new TypeError('all keys must be instances of a key instantiated by JWK.asKey')
|
40 | }
|
41 |
|
42 | this._keys = new Set(keys)
|
43 | }
|
44 |
|
45 | all ({ alg, kid, thumbprint, use, kty, key_ops: ops, x5t, 'x5t#S256': x5t256, crv } = {}) {
|
46 | if (ops !== undefined && (!Array.isArray(ops) || !ops.length || ops.some(x => typeof x !== 'string'))) {
|
47 | throw new TypeError('`key_ops` must be a non-empty array of strings')
|
48 | }
|
49 |
|
50 | const search = { alg, use, ops }
|
51 | return [...this._keys]
|
52 | .filter((key) => {
|
53 | let candidate = true
|
54 |
|
55 | if (candidate && kid !== undefined && key.kid !== kid) {
|
56 | candidate = false
|
57 | }
|
58 |
|
59 | if (candidate && thumbprint !== undefined && key.thumbprint !== thumbprint) {
|
60 | candidate = false
|
61 | }
|
62 |
|
63 | if (candidate && x5t !== undefined && key.x5t !== x5t) {
|
64 | candidate = false
|
65 | }
|
66 |
|
67 | if (candidate && x5t256 !== undefined && key['x5t#S256'] !== x5t256) {
|
68 | candidate = false
|
69 | }
|
70 |
|
71 | if (candidate && kty !== undefined && key.kty !== kty) {
|
72 | candidate = false
|
73 | }
|
74 |
|
75 | if (candidate && crv !== undefined && (key.crv !== crv)) {
|
76 | candidate = false
|
77 | }
|
78 |
|
79 | if (alg !== undefined && !key.algorithms().has(alg)) {
|
80 | candidate = false
|
81 | }
|
82 |
|
83 | if (candidate && use !== undefined && (key.use !== undefined && key.use !== use)) {
|
84 | candidate = false
|
85 | }
|
86 |
|
87 |
|
88 | if (candidate && ops !== undefined && (key.key_ops !== undefined || key.use !== undefined)) {
|
89 | let keyOps
|
90 | if (key.key_ops) {
|
91 | keyOps = new Set(key.key_ops)
|
92 | } else {
|
93 | keyOps = USES_MAPPING[key.use]
|
94 | }
|
95 | if (ops.some(x => !keyOps.has(x))) {
|
96 | candidate = false
|
97 | }
|
98 | }
|
99 |
|
100 | return candidate
|
101 | })
|
102 | .sort((first, second) => keyscore(second, search) - keyscore(first, search))
|
103 | }
|
104 |
|
105 | get (...args) {
|
106 | return this.all(...args)[0]
|
107 | }
|
108 |
|
109 | add (key) {
|
110 | if (!isKey(key) || !key.kty) {
|
111 | throw new TypeError('key must be an instance of a key instantiated by JWK.asKey')
|
112 | }
|
113 |
|
114 | this._keys.add(key)
|
115 | }
|
116 |
|
117 | remove (key) {
|
118 | if (!isKey(key)) {
|
119 | throw new TypeError('key must be an instance of a key instantiated by JWK.asKey')
|
120 | }
|
121 |
|
122 | this._keys.delete(key)
|
123 | }
|
124 |
|
125 | toJWKS (priv = false) {
|
126 | return {
|
127 | keys: [...this._keys.values()].map(
|
128 | key => key.toJWK(priv && (key.private || (key.secret && key.k)))
|
129 | )
|
130 | }
|
131 | }
|
132 |
|
133 | async generate (...args) {
|
134 | this._keys.add(await generate(...args))
|
135 | }
|
136 |
|
137 | generateSync (...args) {
|
138 | this._keys.add(generateSync(...args))
|
139 | }
|
140 |
|
141 | get size () {
|
142 | return this._keys.size
|
143 | }
|
144 |
|
145 |
|
146 | [inspect.custom] () {
|
147 | return `${this.constructor.name} ${inspect(this.toJWKS(false), {
|
148 | depth: Infinity,
|
149 | colors: process.stdout.isTTY,
|
150 | compact: false,
|
151 | sorted: true
|
152 | })}`
|
153 | }
|
154 |
|
155 | * [Symbol.iterator] () {
|
156 | for (const key of this._keys) {
|
157 | yield key
|
158 | }
|
159 | }
|
160 | }
|
161 |
|
162 | function asKeyStore (jwks, { ignoreErrors = false, calculateMissingRSAPrimes = false } = {}) {
|
163 | if (!isObject(jwks) || !Array.isArray(jwks.keys) || jwks.keys.some(k => !isObject(k) || !('kty' in k))) {
|
164 | throw new TypeError('jwks must be a JSON Web Key Set formatted object')
|
165 | }
|
166 |
|
167 | const keys = jwks.keys.map((jwk) => {
|
168 | try {
|
169 | return importKey(jwk, { calculateMissingRSAPrimes })
|
170 | } catch (err) {
|
171 | if (!ignoreErrors) {
|
172 | throw err
|
173 | }
|
174 | }
|
175 | }).filter(Boolean)
|
176 |
|
177 | return new KeyStore(...keys)
|
178 | }
|
179 |
|
180 | Object.defineProperty(KeyStore, 'fromJWKS', {
|
181 | value: deprecate(jwks => asKeyStore(jwks, { calculateMissingRSAPrimes: true }), 'JWKS.KeyStore.fromJWKS() is deprecated, use JWKS.asKeyStore() instead'),
|
182 | enumerable: false
|
183 | })
|
184 |
|
185 | module.exports = { KeyStore, asKeyStore }
|