2021-12-07 13:18:08 -05:00

184 lines
4.3 KiB
JavaScript

const { inspect } = require('util')
const isObject = require('../help/is_object')
const { generate, generateSync } = require('../jwk/generate')
const { USES_MAPPING } = require('../help/consts')
const { isKey, asKey: importKey } = require('../jwk')
const keyscore = (key, { alg, use, ops }) => {
let score = 0
if (alg && key.alg) {
score++
}
if (use && key.use) {
score++
}
if (ops && key.key_ops) {
score++
}
return score
}
class KeyStore {
constructor (...keys) {
while (keys.some(Array.isArray)) {
keys = keys.flat
? keys.flat()
: keys.reduce((acc, val) => {
if (Array.isArray(val)) {
return [...acc, ...val]
}
acc.push(val)
return acc
}, [])
}
if (keys.some(k => !isKey(k) || !k.kty)) {
throw new TypeError('all keys must be instances of a key instantiated by JWK.asKey')
}
this._keys = new Set(keys)
}
all ({ alg, kid, thumbprint, use, kty, key_ops: ops, x5t, 'x5t#S256': x5t256, crv } = {}) {
if (ops !== undefined && (!Array.isArray(ops) || !ops.length || ops.some(x => typeof x !== 'string'))) {
throw new TypeError('`key_ops` must be a non-empty array of strings')
}
const search = { alg, use, ops }
return [...this._keys]
.filter((key) => {
let candidate = true
if (candidate && kid !== undefined && key.kid !== kid) {
candidate = false
}
if (candidate && thumbprint !== undefined && key.thumbprint !== thumbprint) {
candidate = false
}
if (candidate && x5t !== undefined && key.x5t !== x5t) {
candidate = false
}
if (candidate && x5t256 !== undefined && key['x5t#S256'] !== x5t256) {
candidate = false
}
if (candidate && kty !== undefined && key.kty !== kty) {
candidate = false
}
if (candidate && crv !== undefined && (key.crv !== crv)) {
candidate = false
}
if (alg !== undefined && !key.algorithms().has(alg)) {
candidate = false
}
if (candidate && use !== undefined && (key.use !== undefined && key.use !== use)) {
candidate = false
}
// TODO:
if (candidate && ops !== undefined && (key.key_ops !== undefined || key.use !== undefined)) {
let keyOps
if (key.key_ops) {
keyOps = new Set(key.key_ops)
} else {
keyOps = USES_MAPPING[key.use]
}
if (ops.some(x => !keyOps.has(x))) {
candidate = false
}
}
return candidate
})
.sort((first, second) => keyscore(second, search) - keyscore(first, search))
}
get (...args) {
return this.all(...args)[0]
}
add (key) {
if (!isKey(key) || !key.kty) {
throw new TypeError('key must be an instance of a key instantiated by JWK.asKey')
}
this._keys.add(key)
}
remove (key) {
if (!isKey(key)) {
throw new TypeError('key must be an instance of a key instantiated by JWK.asKey')
}
this._keys.delete(key)
}
toJWKS (priv = false) {
return {
keys: [...this._keys.values()].map(
key => key.toJWK(priv && (key.private || (key.secret && key.k)))
)
}
}
async generate (...args) {
this._keys.add(await generate(...args))
}
generateSync (...args) {
this._keys.add(generateSync(...args))
}
get size () {
return this._keys.size
}
/* c8 ignore next 8 */
[inspect.custom] () {
return `${this.constructor.name} ${inspect(this.toJWKS(false), {
depth: Infinity,
colors: process.stdout.isTTY,
compact: false,
sorted: true
})}`
}
* [Symbol.iterator] () {
for (const key of this._keys) {
yield key
}
}
}
function asKeyStore (jwks, { ignoreErrors = false, calculateMissingRSAPrimes = false } = {}) {
if (!isObject(jwks) || !Array.isArray(jwks.keys) || jwks.keys.some(k => !isObject(k) || !('kty' in k))) {
throw new TypeError('jwks must be a JSON Web Key Set formatted object')
}
const keys = jwks.keys.map((jwk) => {
try {
return importKey(jwk, { calculateMissingRSAPrimes })
} catch (err) {
if (!ignoreErrors) {
throw err
}
return undefined
}
}).filter(Boolean)
return new KeyStore(...keys)
}
module.exports = { KeyStore, asKeyStore }