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:
Filip Skokan 2020-09-05 14:23:03 +02:00
parent 87c1562537
commit 30e5c46ecf
4 changed files with 98 additions and 54 deletions

View file

@ -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.

View file

@ -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

View file

@ -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
View file

@ -399,7 +399,8 @@ export namespace JWE {
interface DecryptOptions {
complete?: boolean;
crit?: string[];
algorithms?: string[];
contentEncryptionAlgorithms?: string[];
keyManagementAlgorithms?: string[];
}
interface completeDecrypt {