Skip to content

Commit

Permalink
fix: limit default PBES2 alg's computational expense
Browse files Browse the repository at this point in the history
Also adds an option to opt-in to higher computation expense.
  • Loading branch information
panva committed Sep 1, 2022
1 parent 5309e00 commit d530c30
Show file tree
Hide file tree
Showing 17 changed files with 89 additions and 62 deletions.
4 changes: 2 additions & 2 deletions dist/browser/jwe/flattened/decrypt.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,10 +95,10 @@ async function flattenedDecrypt(jwe, key, options) {
}
let cek;
try {
cek = await decryptKeyManagement(alg, key, encryptedKey, joseHeader);
cek = await decryptKeyManagement(alg, key, encryptedKey, joseHeader, options);
}
catch (err) {
if (err instanceof TypeError) {
if (err instanceof TypeError || err instanceof JWEInvalid || err instanceof JOSENotSupported) {
throw err;
}
cek = generateCek(enc);
Expand Down
5 changes: 4 additions & 1 deletion dist/browser/lib/decrypt_key_management.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { bitLengths as cekLengths } from '../lib/cek.js';
import { importJWK } from '../key/import.js';
import checkKeyType from './check_key_type.js';
import isObject from './is_object.js';
async function decryptKeyManagement(alg, key, encryptedKey, joseHeader) {
async function decryptKeyManagement(alg, key, encryptedKey, joseHeader, options) {
checkKeyType(alg, key, 'decrypt');
switch (alg) {
case 'dir': {
Expand Down Expand Up @@ -63,6 +63,9 @@ async function decryptKeyManagement(alg, key, encryptedKey, joseHeader) {
throw new JWEInvalid('JWE Encrypted Key missing');
if (typeof joseHeader.p2c !== 'number')
throw new JWEInvalid(`JOSE Header "p2c" (PBES2 Count) missing or invalid`);
const p2cLimit = (options === null || options === void 0 ? void 0 : options.maxPBES2Count) || 10000;
if (joseHeader.p2c > p2cLimit)
throw new JWEInvalid(`JOSE Header "p2c" (PBES2 Count) out is of acceptable bounds`);
if (typeof joseHeader.p2s !== 'string')
throw new JWEInvalid(`JOSE Header "p2s" (PBES2 Salt) missing or invalid`);
return pbes2Kw(alg, key, encryptedKey, joseHeader.p2c, base64url(joseHeader.p2s));
Expand Down
45 changes: 0 additions & 45 deletions dist/deno/README.md

This file was deleted.

4 changes: 2 additions & 2 deletions dist/deno/jwe/flattened/decrypt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,9 +204,9 @@ async function flattenedDecrypt(

let cek: KeyLike | Uint8Array
try {
cek = await decryptKeyManagement(alg, key, encryptedKey, joseHeader)
cek = await decryptKeyManagement(alg, key, encryptedKey, joseHeader, options)
} catch (err) {
if (err instanceof TypeError) {
if (err instanceof TypeError || err instanceof JWEInvalid || err instanceof JOSENotSupported) {
throw err
}
// https://tools.ietf.org/html/rfc7516#section-11.5
Expand Down
8 changes: 7 additions & 1 deletion dist/deno/lib/decrypt_key_management.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { decrypt as rsaEs } from '../runtime/rsaes.ts'
import { unwrap as aesGcmKw } from '../runtime/aesgcmkw.ts'
import { decode as base64url } from '../runtime/base64url.ts'

import type { JWEHeaderParameters, KeyLike, JWK } from '../types.d.ts'
import type { DecryptOptions, JWEHeaderParameters, KeyLike, JWK } from '../types.d.ts'
import { JOSENotSupported, JWEInvalid } from '../util/errors.ts'
import { bitLengths as cekLengths } from '../lib/cek.ts'
import { importJWK } from '../key/import.ts'
Expand All @@ -17,6 +17,7 @@ async function decryptKeyManagement(
key: KeyLike | Uint8Array,
encryptedKey: Uint8Array | undefined,
joseHeader: JWEHeaderParameters,
options?: DecryptOptions,
): Promise<KeyLike | Uint8Array> {
checkKeyType(alg, key, 'decrypt')

Expand Down Expand Up @@ -96,6 +97,11 @@ async function decryptKeyManagement(
if (typeof joseHeader.p2c !== 'number')
throw new JWEInvalid(`JOSE Header "p2c" (PBES2 Count) missing or invalid`)

const p2cLimit = options?.maxPBES2Count || 10_000

if (joseHeader.p2c > p2cLimit)
throw new JWEInvalid(`JOSE Header "p2c" (PBES2 Count) out is of acceptable bounds`)

if (typeof joseHeader.p2s !== 'string')
throw new JWEInvalid(`JOSE Header "p2s" (PBES2 Salt) missing or invalid`)

Expand Down
7 changes: 7 additions & 0 deletions dist/deno/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,13 @@ export interface DecryptOptions extends CritOption {
* when you expect JWEs with compressed plaintext.
*/
inflateRaw?: InflateFunction

/**
* (PBES2 Key Management Algorithms only) Maximum allowed "p2c" (PBES2 Count) Header Parameter
* value. The PBKDF2 iteration count defines the algorithm's computational expense. By default
* this value is set to 10000.
*/
maxPBES2Count?: number
}

/**
Expand Down
4 changes: 2 additions & 2 deletions dist/node/cjs/jwe/flattened/decrypt.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,10 +98,10 @@ async function flattenedDecrypt(jwe, key, options) {
}
let cek;
try {
cek = await (0, decrypt_key_management_js_1.default)(alg, key, encryptedKey, joseHeader);
cek = await (0, decrypt_key_management_js_1.default)(alg, key, encryptedKey, joseHeader, options);
}
catch (err) {
if (err instanceof TypeError) {
if (err instanceof TypeError || err instanceof errors_js_1.JWEInvalid || err instanceof errors_js_1.JOSENotSupported) {
throw err;
}
cek = (0, cek_js_1.default)(enc);
Expand Down
5 changes: 4 additions & 1 deletion dist/node/cjs/lib/decrypt_key_management.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const cek_js_1 = require("../lib/cek.js");
const import_js_1 = require("../key/import.js");
const check_key_type_js_1 = require("./check_key_type.js");
const is_object_js_1 = require("./is_object.js");
async function decryptKeyManagement(alg, key, encryptedKey, joseHeader) {
async function decryptKeyManagement(alg, key, encryptedKey, joseHeader, options) {
(0, check_key_type_js_1.default)(alg, key, 'decrypt');
switch (alg) {
case 'dir': {
Expand Down Expand Up @@ -65,6 +65,9 @@ async function decryptKeyManagement(alg, key, encryptedKey, joseHeader) {
throw new errors_js_1.JWEInvalid('JWE Encrypted Key missing');
if (typeof joseHeader.p2c !== 'number')
throw new errors_js_1.JWEInvalid(`JOSE Header "p2c" (PBES2 Count) missing or invalid`);
const p2cLimit = (options === null || options === void 0 ? void 0 : options.maxPBES2Count) || 10000;
if (joseHeader.p2c > p2cLimit)
throw new errors_js_1.JWEInvalid(`JOSE Header "p2c" (PBES2 Count) out is of acceptable bounds`);
if (typeof joseHeader.p2s !== 'string')
throw new errors_js_1.JWEInvalid(`JOSE Header "p2s" (PBES2 Salt) missing or invalid`);
return (0, pbes2kw_js_1.decrypt)(alg, key, encryptedKey, joseHeader.p2c, (0, base64url_js_1.decode)(joseHeader.p2s));
Expand Down
4 changes: 2 additions & 2 deletions dist/node/esm/jwe/flattened/decrypt.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,10 +95,10 @@ async function flattenedDecrypt(jwe, key, options) {
}
let cek;
try {
cek = await decryptKeyManagement(alg, key, encryptedKey, joseHeader);
cek = await decryptKeyManagement(alg, key, encryptedKey, joseHeader, options);
}
catch (err) {
if (err instanceof TypeError) {
if (err instanceof TypeError || err instanceof JWEInvalid || err instanceof JOSENotSupported) {
throw err;
}
cek = generateCek(enc);
Expand Down
5 changes: 4 additions & 1 deletion dist/node/esm/lib/decrypt_key_management.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { bitLengths as cekLengths } from '../lib/cek.js';
import { importJWK } from '../key/import.js';
import checkKeyType from './check_key_type.js';
import isObject from './is_object.js';
async function decryptKeyManagement(alg, key, encryptedKey, joseHeader) {
async function decryptKeyManagement(alg, key, encryptedKey, joseHeader, options) {
checkKeyType(alg, key, 'decrypt');
switch (alg) {
case 'dir': {
Expand Down Expand Up @@ -63,6 +63,9 @@ async function decryptKeyManagement(alg, key, encryptedKey, joseHeader) {
throw new JWEInvalid('JWE Encrypted Key missing');
if (typeof joseHeader.p2c !== 'number')
throw new JWEInvalid(`JOSE Header "p2c" (PBES2 Count) missing or invalid`);
const p2cLimit = (options === null || options === void 0 ? void 0 : options.maxPBES2Count) || 10000;
if (joseHeader.p2c > p2cLimit)
throw new JWEInvalid(`JOSE Header "p2c" (PBES2 Count) out is of acceptable bounds`);
if (typeof joseHeader.p2s !== 'string')
throw new JWEInvalid(`JOSE Header "p2s" (PBES2 Salt) missing or invalid`);
return pbes2Kw(alg, key, encryptedKey, joseHeader.p2c, base64url(joseHeader.p2s));
Expand Down
1 change: 1 addition & 0 deletions dist/types/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ export interface DecryptOptions extends CritOption {
keyManagementAlgorithms?: string[]
contentEncryptionAlgorithms?: string[]
inflateRaw?: InflateFunction
maxPBES2Count?: number
}
export interface EncryptOptions extends CritOption {
deflateRaw?: DeflateFunction
Expand Down
11 changes: 11 additions & 0 deletions docs/interfaces/jwt_decrypt.JWTDecryptOptions.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Combination of JWE Decryption options and JWT Claims Set verification options.
- [inflateRaw](jwt_decrypt.JWTDecryptOptions.md#inflateraw)
- [issuer](jwt_decrypt.JWTDecryptOptions.md#issuer)
- [keyManagementAlgorithms](jwt_decrypt.JWTDecryptOptions.md#keymanagementalgorithms)
- [maxPBES2Count](jwt_decrypt.JWTDecryptOptions.md#maxpbes2count)
- [maxTokenAge](jwt_decrypt.JWTDecryptOptions.md#maxtokenage)
- [subject](jwt_decrypt.JWTDecryptOptions.md#subject)
- [typ](jwt_decrypt.JWTDecryptOptions.md#typ)
Expand Down Expand Up @@ -110,6 +111,16 @@ A list of accepted JWE "alg" (Algorithm) Header Parameter values.

___

### maxPBES2Count

`Optional` **maxPBES2Count**: `number`

(PBES2 Key Management Algorithms only) Maximum allowed "p2c" (PBES2 Count) Header Parameter
value. The PBKDF2 iteration count defines the algorithm's computational expense. By default
this value is set to 10000.

___

### maxTokenAge

`Optional` **maxTokenAge**: `string` \| `number`
Expand Down
11 changes: 11 additions & 0 deletions docs/interfaces/types.DecryptOptions.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ JWE Decryption options.
- [crit](types.DecryptOptions.md#crit)
- [inflateRaw](types.DecryptOptions.md#inflateraw)
- [keyManagementAlgorithms](types.DecryptOptions.md#keymanagementalgorithms)
- [maxPBES2Count](types.DecryptOptions.md#maxpbes2count)

## Properties

Expand Down Expand Up @@ -66,3 +67,13 @@ ___
`Optional` **keyManagementAlgorithms**: `string`[]

A list of accepted JWE "alg" (Algorithm) Header Parameter values.

___

### maxPBES2Count

`Optional` **maxPBES2Count**: `number`

(PBES2 Key Management Algorithms only) Maximum allowed "p2c" (PBES2 Count) Header Parameter
value. The PBKDF2 iteration count defines the algorithm's computational expense. By default
this value is set to 10000.
4 changes: 2 additions & 2 deletions src/jwe/flattened/decrypt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,9 +204,9 @@ async function flattenedDecrypt(

let cek: KeyLike | Uint8Array
try {
cek = await decryptKeyManagement(alg, key, encryptedKey, joseHeader)
cek = await decryptKeyManagement(alg, key, encryptedKey, joseHeader, options)
} catch (err) {
if (err instanceof TypeError) {
if (err instanceof TypeError || err instanceof JWEInvalid || err instanceof JOSENotSupported) {
throw err
}
// https://tools.ietf.org/html/rfc7516#section-11.5
Expand Down
8 changes: 7 additions & 1 deletion src/lib/decrypt_key_management.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { decrypt as rsaEs } from '../runtime/rsaes.js'
import { unwrap as aesGcmKw } from '../runtime/aesgcmkw.js'
import { decode as base64url } from '../runtime/base64url.js'

import type { JWEHeaderParameters, KeyLike, JWK } from '../types.d'
import type { DecryptOptions, JWEHeaderParameters, KeyLike, JWK } from '../types.d'
import { JOSENotSupported, JWEInvalid } from '../util/errors.js'
import { bitLengths as cekLengths } from '../lib/cek.js'
import { importJWK } from '../key/import.js'
Expand All @@ -17,6 +17,7 @@ async function decryptKeyManagement(
key: KeyLike | Uint8Array,
encryptedKey: Uint8Array | undefined,
joseHeader: JWEHeaderParameters,
options?: DecryptOptions,
): Promise<KeyLike | Uint8Array> {
checkKeyType(alg, key, 'decrypt')

Expand Down Expand Up @@ -96,6 +97,11 @@ async function decryptKeyManagement(
if (typeof joseHeader.p2c !== 'number')
throw new JWEInvalid(`JOSE Header "p2c" (PBES2 Count) missing or invalid`)

const p2cLimit = options?.maxPBES2Count || 10_000

if (joseHeader.p2c > p2cLimit)
throw new JWEInvalid(`JOSE Header "p2c" (PBES2 Count) out is of acceptable bounds`)

if (typeof joseHeader.p2s !== 'string')
throw new JWEInvalid(`JOSE Header "p2s" (PBES2 Salt) missing or invalid`)

Expand Down
7 changes: 7 additions & 0 deletions src/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,13 @@ export interface DecryptOptions extends CritOption {
* when you expect JWEs with compressed plaintext.
*/
inflateRaw?: InflateFunction

/**
* (PBES2 Key Management Algorithms only) Maximum allowed "p2c" (PBES2 Count) Header Parameter
* value. The PBKDF2 iteration count defines the algorithm's computational expense. By default
* this value is set to 10000.
*/
maxPBES2Count?: number
}

/**
Expand Down
18 changes: 16 additions & 2 deletions test/jwe/flattened.decrypt.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -180,8 +180,8 @@ Promise.all([
jwe.encrypted_key = 'foo';

await t.throwsAsync(flattenedDecrypt(jwe, t.context.secret), {
message: 'decryption operation failed',
code: 'ERR_JWE_DECRYPTION_FAILED',
message: 'Encountered unexpected JWE Encrypted Key',
code: 'ERR_JWE_INVALID',
});
}
});
Expand Down Expand Up @@ -220,6 +220,20 @@ Promise.all([
});
}
});

if (!('electron' in process.versions)) {
test('decrypt PBES2 p2c limit', async (t) => {
const jwe = await new FlattenedEncrypt(new Uint8Array(0))
.setProtectedHeader({ alg: 'PBES2-HS256+A128KW', enc: 'A128CBC-HS256' })
.setKeyManagementParameters({ p2c: 2049 })
.encrypt(new Uint8Array(32));

await t.throwsAsync(flattenedDecrypt(jwe, new Uint8Array(32), { maxPBES2Count: 2048 }), {
code: 'ERR_JWE_INVALID',
message: 'JOSE Header "p2c" (PBES2 Count) out is of acceptable bounds',
});
});
}
},
(err) => {
test('failed to import', (t) => {
Expand Down

0 comments on commit d530c30

Please sign in to comment.