mirror of
https://github.com/danbulant/jose
synced 2026-05-20 12:58:36 +00:00
feat: decrypt allowlists for both key management and content encryption
BREAKING CHANGE: the `JWE.decrypt` option `algorithms` was removed and replaced with contentEncryptionAlgorithms (handles `enc` allowlist) and keyManagementAlgorithms (handles `alg` allowlist)
This commit is contained in:
parent
87c1562537
commit
30e5c46ecf
4 changed files with 98 additions and 54 deletions
|
|
@ -1488,9 +1488,12 @@ operation.
|
|||
Syntax) matches. Any `JWK.asKey()` compatible input also works. `<JWK.Key>` instances are
|
||||
recommended for performance purposes when re-using the same key for every operation.
|
||||
- `options`: `<Object>`
|
||||
- `algorithms`: `string[]` Array of Algorithms to accept, when the JWE does not use an
|
||||
Key Management algorithm from this list the decryption will fail. **Default:** 'undefined' -
|
||||
accepts all algorithms available on the keys
|
||||
- `contentEncryptionAlgorithms`: `string[]` Array of algorithms to accept as the `enc` (content
|
||||
encryption), when the JWE does not use an Key Management algorithm from this list the decryption
|
||||
will fail. **Default:** 'undefined' - accepts all content encryption algorithms.
|
||||
- `keyManagementAlgorithms`: `string[]` Array of algorithms to accept as the `alg` (key management),
|
||||
when the JWE does not use an Key Management algorithm from this list the decryption will fail.
|
||||
**Default:** 'undefined' - accepts all algorithms available on the key or key store.
|
||||
- `complete`: `<boolean>` When true returns an object with the parsed headers, verified
|
||||
AAD, the content encryption key, the key that was used to unwrap or derive the content
|
||||
encryption key, and cleartext instead of only the cleartext.
|
||||
|
|
|
|||
|
|
@ -37,17 +37,26 @@ const combineHeader = (prot = {}, unprotected = {}, header = {}) => {
|
|||
}
|
||||
}
|
||||
|
||||
const validateAlgorithms = (algorithms, option) => {
|
||||
if (algorithms !== undefined && (!Array.isArray(algorithms) || algorithms.some(s => typeof s !== 'string' || !s))) {
|
||||
throw new TypeError(`"${option}" option must be an array of non-empty strings`)
|
||||
}
|
||||
|
||||
if (!algorithms) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return new Set(algorithms)
|
||||
}
|
||||
|
||||
/*
|
||||
* @public
|
||||
*/
|
||||
const jweDecrypt = (skipValidateHeaders, serialization, jwe, key, { crit = [], complete = false, algorithms } = {}) => {
|
||||
const jweDecrypt = (skipValidateHeaders, serialization, jwe, key, { crit = [], complete = false, keyManagementAlgorithms, contentEncryptionAlgorithms } = {}) => {
|
||||
key = getKey(key, true)
|
||||
|
||||
if (algorithms !== undefined && (!Array.isArray(algorithms) || algorithms.some(s => typeof s !== 'string' || !s))) {
|
||||
throw new TypeError('"algorithms" option must be an array of non-empty strings')
|
||||
} else if (algorithms) {
|
||||
algorithms = new Set(algorithms)
|
||||
}
|
||||
keyManagementAlgorithms = validateAlgorithms(keyManagementAlgorithms, 'keyManagementAlgorithms')
|
||||
contentEncryptionAlgorithms = validateAlgorithms(contentEncryptionAlgorithms, 'contentEncryptionAlgorithms')
|
||||
|
||||
if (!Array.isArray(crit) || crit.some(s => typeof s !== 'string' || !s)) {
|
||||
throw new TypeError('"crit" option must be an array of non-empty strings')
|
||||
|
|
@ -82,8 +91,12 @@ const jweDecrypt = (skipValidateHeaders, serialization, jwe, key, { crit = [], c
|
|||
|
||||
;({ alg, enc } = opts)
|
||||
|
||||
if (algorithms && !algorithms.has(alg === 'dir' ? enc : alg)) {
|
||||
throw new errors.JOSEAlgNotWhitelisted('alg not whitelisted')
|
||||
if (keyManagementAlgorithms && !keyManagementAlgorithms.has(alg)) {
|
||||
throw new errors.JOSEAlgNotWhitelisted('key management algorithm not whitelisted')
|
||||
}
|
||||
|
||||
if (contentEncryptionAlgorithms && !contentEncryptionAlgorithms.has(enc)) {
|
||||
throw new errors.JOSEAlgNotWhitelisted('content encryption algorithm not whitelisted')
|
||||
}
|
||||
|
||||
if (key instanceof KeyStore) {
|
||||
|
|
@ -106,7 +119,12 @@ const jweDecrypt = (skipValidateHeaders, serialization, jwe, key, { crit = [], c
|
|||
const errs = []
|
||||
for (const key of keys) {
|
||||
try {
|
||||
return jweDecrypt(true, serialization, jwe, key, { crit, complete, algorithms: algorithms ? [...algorithms] : undefined })
|
||||
return jweDecrypt(true, serialization, jwe, key, {
|
||||
crit,
|
||||
complete,
|
||||
contentEncryptionAlgorithms: contentEncryptionAlgorithms ? [...contentEncryptionAlgorithms] : undefined,
|
||||
keyManagementAlgorithms: keyManagementAlgorithms ? [...keyManagementAlgorithms] : undefined
|
||||
})
|
||||
} catch (err) {
|
||||
errs.push(err)
|
||||
continue
|
||||
|
|
@ -187,7 +205,12 @@ const jweDecrypt = (skipValidateHeaders, serialization, jwe, key, { crit = [], c
|
|||
const errs = []
|
||||
for (const recipient of recipients) {
|
||||
try {
|
||||
return jweDecrypt(true, 'flattened', { ...root, ...recipient }, key, { crit, complete, algorithms: algorithms ? [...algorithms] : undefined })
|
||||
return jweDecrypt(true, 'flattened', { ...root, ...recipient }, key, {
|
||||
crit,
|
||||
complete,
|
||||
contentEncryptionAlgorithms: contentEncryptionAlgorithms ? [...contentEncryptionAlgorithms] : undefined,
|
||||
keyManagementAlgorithms: keyManagementAlgorithms ? [...keyManagementAlgorithms] : undefined
|
||||
})
|
||||
} catch (err) {
|
||||
errs.push(err)
|
||||
continue
|
||||
|
|
|
|||
|
|
@ -3,22 +3,41 @@ const test = require('ava')
|
|||
const base64url = require('../../lib/help/base64url')
|
||||
const { JWKS, JWK: { generateSync }, JWE, errors } = require('../..')
|
||||
|
||||
test('algorithms option be an array of strings', t => {
|
||||
test('keyManagementAlgorithms option be an array of strings', t => {
|
||||
;[{}, new Object(), false, null, Infinity, 0, '', Buffer.from('foo')].forEach((val) => { // eslint-disable-line no-new-object
|
||||
t.throws(() => {
|
||||
JWE.decrypt({
|
||||
header: { alg: 'HS256' },
|
||||
payload: 'foo',
|
||||
ciphertext: 'bar'
|
||||
}, generateSync('oct'), { algorithms: val })
|
||||
}, { instanceOf: TypeError, message: '"algorithms" option must be an array of non-empty strings' })
|
||||
}, generateSync('oct'), { keyManagementAlgorithms: val })
|
||||
}, { instanceOf: TypeError, message: '"keyManagementAlgorithms" option must be an array of non-empty strings' })
|
||||
t.throws(() => {
|
||||
JWE.decrypt({
|
||||
header: { alg: 'HS256' },
|
||||
payload: 'foo',
|
||||
ciphertext: 'bar'
|
||||
}, generateSync('oct'), { algorithms: [val] })
|
||||
}, { instanceOf: TypeError, message: '"algorithms" option must be an array of non-empty strings' })
|
||||
}, generateSync('oct'), { keyManagementAlgorithms: [val] })
|
||||
}, { instanceOf: TypeError, message: '"keyManagementAlgorithms" option must be an array of non-empty strings' })
|
||||
})
|
||||
})
|
||||
|
||||
test('contentEncryptionAlgorithms option be an array of strings', t => {
|
||||
;[{}, new Object(), false, null, Infinity, 0, '', Buffer.from('foo')].forEach((val) => { // eslint-disable-line no-new-object
|
||||
t.throws(() => {
|
||||
JWE.decrypt({
|
||||
header: { alg: 'HS256' },
|
||||
payload: 'foo',
|
||||
ciphertext: 'bar'
|
||||
}, generateSync('oct'), { contentEncryptionAlgorithms: val })
|
||||
}, { instanceOf: TypeError, message: '"contentEncryptionAlgorithms" option must be an array of non-empty strings' })
|
||||
t.throws(() => {
|
||||
JWE.decrypt({
|
||||
header: { alg: 'HS256' },
|
||||
payload: 'foo',
|
||||
ciphertext: 'bar'
|
||||
}, generateSync('oct'), { contentEncryptionAlgorithms: [val] })
|
||||
}, { instanceOf: TypeError, message: '"contentEncryptionAlgorithms" option must be an array of non-empty strings' })
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -433,42 +452,40 @@ test('JWE prot, unprot and per-recipient headers must be disjoint', t => {
|
|||
}, { instanceOf: errors.JWEInvalid, code: 'ERR_JWE_INVALID', message: 'JWE Shared Protected, JWE Shared Unprotected and JWE Per-Recipient Header Parameter names must be disjoint' })
|
||||
})
|
||||
|
||||
if (!('electron' in process.versions)) {
|
||||
test('JWE decrypt algorithms whitelist', t => {
|
||||
const k = generateSync('oct')
|
||||
const jwe = JWE.encrypt('foo', k, { alg: 'PBES2-HS256+A128KW' })
|
||||
JWE.decrypt(jwe, k, { algorithms: ['PBES2-HS256+A128KW', 'PBES2-HS384+A192KW'] })
|
||||
|
||||
t.throws(() => {
|
||||
JWE.decrypt(jwe, k, { algorithms: ['PBES2-HS384+A192KW'] })
|
||||
}, { instanceOf: errors.JOSEAlgNotWhitelisted, code: 'ERR_JOSE_ALG_NOT_WHITELISTED', message: 'alg not whitelisted' })
|
||||
})
|
||||
|
||||
test('JWE decrypt algorithms whitelist with a keystore', t => {
|
||||
const k = generateSync('oct')
|
||||
const k2 = generateSync('oct')
|
||||
const ks = new JWKS.KeyStore(k, k2)
|
||||
|
||||
const jwe = JWE.encrypt('foo', k2, { alg: 'PBES2-HS256+A128KW' })
|
||||
JWE.decrypt(jwe, ks, { algorithms: ['PBES2-HS256+A128KW', 'PBES2-HS384+A192KW'] })
|
||||
|
||||
t.throws(() => {
|
||||
JWE.decrypt(jwe, ks, { algorithms: ['PBES2-HS384+A192KW'] })
|
||||
}, { instanceOf: errors.JOSEAlgNotWhitelisted, code: 'ERR_JOSE_ALG_NOT_WHITELISTED' })
|
||||
})
|
||||
}
|
||||
|
||||
test('JWE decrypt algorithms whitelist with direct encryption', t => {
|
||||
const k = generateSync('oct')
|
||||
const jwe = JWE.encrypt('foo', k, { alg: 'dir' })
|
||||
JWE.decrypt(jwe, k, { algorithms: ['A128CBC-HS256'] })
|
||||
test('JWE decrypt keyManagementAlgorithms whitelist', t => {
|
||||
const k = generateSync('oct', 128)
|
||||
const jwe = JWE.encrypt('foo', k, { alg: 'A128GCMKW' })
|
||||
JWE.decrypt(jwe, k, { keyManagementAlgorithms: ['A128GCMKW', 'A192GCMKW'] })
|
||||
|
||||
t.throws(() => {
|
||||
JWE.decrypt(jwe, k, { algorithms: ['PBES2-HS384+A192KW'] })
|
||||
JWE.decrypt(jwe, k, { keyManagementAlgorithms: ['A192GCMKW'] })
|
||||
}, { instanceOf: errors.JOSEAlgNotWhitelisted, code: 'ERR_JOSE_ALG_NOT_WHITELISTED', message: 'key management algorithm not whitelisted' })
|
||||
})
|
||||
|
||||
test('JWE decrypt keyManagementAlgorithms whitelist with a keystore', t => {
|
||||
const k = generateSync('oct')
|
||||
const k2 = generateSync('oct', 128)
|
||||
const ks = new JWKS.KeyStore(k, k2)
|
||||
|
||||
const jwe = JWE.encrypt('foo', k2, { alg: 'A128GCMKW' })
|
||||
JWE.decrypt(jwe, ks, { keyManagementAlgorithms: ['A128GCMKW', 'A192GCMKW'] })
|
||||
|
||||
t.throws(() => {
|
||||
JWE.decrypt(jwe, ks, { keyManagementAlgorithms: ['A192GCMKW'] })
|
||||
}, { instanceOf: errors.JOSEAlgNotWhitelisted, code: 'ERR_JOSE_ALG_NOT_WHITELISTED' })
|
||||
})
|
||||
|
||||
test('JWE decrypt algorithms whitelist (multi-recipient)', t => {
|
||||
test('JWE decrypt contentEncryptionAlgorithms whitelist', t => {
|
||||
const k = generateSync('oct')
|
||||
const jwe = JWE.encrypt('foo', k, { alg: 'dir' })
|
||||
JWE.decrypt(jwe, k, { contentEncryptionAlgorithms: ['A128CBC-HS256'] })
|
||||
|
||||
t.throws(() => {
|
||||
JWE.decrypt(jwe, k, { contentEncryptionAlgorithms: ['PBES2-HS384+A192KW'] })
|
||||
}, { instanceOf: errors.JOSEAlgNotWhitelisted, code: 'ERR_JOSE_ALG_NOT_WHITELISTED' })
|
||||
})
|
||||
|
||||
test('JWE decrypt keyManagementAlgorithms whitelist (multi-recipient)', t => {
|
||||
const k = generateSync('oct')
|
||||
const k2 = generateSync('RSA')
|
||||
|
||||
|
|
@ -477,12 +494,12 @@ test('JWE decrypt algorithms whitelist (multi-recipient)', t => {
|
|||
encrypt.recipient(k2)
|
||||
const jwe = encrypt.encrypt('general')
|
||||
|
||||
JWE.decrypt(jwe, k, { algorithms: ['electron' in process.versions ? 'A256GCMKW' : 'A256KW'] })
|
||||
JWE.decrypt(jwe, k2, { algorithms: ['RSA-OAEP'] })
|
||||
JWE.decrypt(jwe, k, { keyManagementAlgorithms: ['electron' in process.versions ? 'A256GCMKW' : 'A256KW'] })
|
||||
JWE.decrypt(jwe, k2, { keyManagementAlgorithms: ['RSA-OAEP'] })
|
||||
let err
|
||||
|
||||
err = t.throws(() => {
|
||||
JWE.decrypt(jwe, k, { algorithms: ['RSA-OAEP'] })
|
||||
JWE.decrypt(jwe, k, { keyManagementAlgorithms: ['RSA-OAEP'] })
|
||||
}, { instanceOf: errors.JOSEMultiError, code: 'ERR_JOSE_MULTIPLE_ERRORS' })
|
||||
;[...err].forEach((e, i) => {
|
||||
if (i === 0) {
|
||||
|
|
@ -493,7 +510,7 @@ test('JWE decrypt algorithms whitelist (multi-recipient)', t => {
|
|||
})
|
||||
|
||||
err = t.throws(() => {
|
||||
JWE.decrypt(jwe, k2, { algorithms: ['electron' in process.versions ? 'A256GCMKW' : 'A256KW'] })
|
||||
JWE.decrypt(jwe, k2, { keyManagementAlgorithms: ['electron' in process.versions ? 'A256GCMKW' : 'A256KW'] })
|
||||
}, { instanceOf: errors.JOSEMultiError, code: 'ERR_JOSE_MULTIPLE_ERRORS' })
|
||||
;[...err].forEach((e, i) => {
|
||||
if (i === 0) {
|
||||
|
|
|
|||
3
types/index.d.ts
vendored
3
types/index.d.ts
vendored
|
|
@ -399,7 +399,8 @@ export namespace JWE {
|
|||
interface DecryptOptions {
|
||||
complete?: boolean;
|
||||
crit?: string[];
|
||||
algorithms?: string[];
|
||||
contentEncryptionAlgorithms?: string[];
|
||||
keyManagementAlgorithms?: string[];
|
||||
}
|
||||
|
||||
interface completeDecrypt {
|
||||
|
|
|
|||
Loading…
Reference in a new issue