Skip to content

Commit

Permalink
feat(webcrypto): allow generate* modules extractable: false override
Browse files Browse the repository at this point in the history
  • Loading branch information
panva committed May 12, 2021
1 parent b84d6a3 commit afae428
Show file tree
Hide file tree
Showing 8 changed files with 74 additions and 11 deletions.
11 changes: 8 additions & 3 deletions src/runtime/browser/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import crypto from './webcrypto.js'
import { JOSENotSupported } from '../../util/errors.js'
import random from './random.js'
import type { GenerateKeyPairOptions } from '../../util/generate_key_pair.js'
import type { GenerateSecretOptions } from '../../util/generate_secret.js'

export async function generateSecret(alg: string) {
export async function generateSecret(alg: string, options?: GenerateSecretOptions) {
let length: number
let algorithm: AesKeyGenParams | HmacKeyGenParams
let keyUsages: KeyUsage[]
Expand Down Expand Up @@ -41,7 +42,9 @@ export async function generateSecret(alg: string) {
throw new JOSENotSupported('unsupported or invalid JWK "alg" (Algorithm) Parameter value')
}

return <Promise<CryptoKey>>crypto.subtle.generateKey(algorithm, false, keyUsages)
return <Promise<CryptoKey>>(
crypto.subtle.generateKey(algorithm, options?.extractable ?? false, keyUsages)
)
}

function getModulusLengthOption(options?: GenerateKeyPairOptions) {
Expand Down Expand Up @@ -116,5 +119,7 @@ export async function generateKeyPair(alg: string, options?: GenerateKeyPairOpti
throw new JOSENotSupported('unsupported or invalid JWK "alg" (Algorithm) Parameter value')
}

return <Promise<CryptoKeyPair>>crypto.subtle.generateKey(algorithm, false, keyUsages)
return <Promise<CryptoKeyPair>>(
crypto.subtle.generateKey(algorithm, options?.extractable ?? false, keyUsages)
)
}
2 changes: 1 addition & 1 deletion src/runtime/browser/key_to_jwk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const keyToJWK: JWKConvertFunction = async (key: unknown): Promise<JWK> => {
throw new TypeError('invalid key input')
}
if (!key.extractable) {
throw new TypeError('non-extractable key cannot be extracted as a JWK')
throw new TypeError('non-extractable CryptoKey cannot be exported as a JWK')
}
// eslint-disable-next-line @typescript-eslint/naming-convention
const { ext, key_ops, alg, use, ...jwk } = await crypto.subtle.exportKey('jwk', key)
Expand Down
5 changes: 4 additions & 1 deletion src/runtime/node/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@ import random from './random.js'
import { setModulusLength } from './check_modulus_length.js'
import { JOSENotSupported } from '../../util/errors.js'
import type { GenerateKeyPairOptions } from '../../util/generate_key_pair.js'
import type { GenerateSecretOptions } from '../../util/generate_secret.js'

const generate = promisify(generateKeyPairCb)

export async function generateSecret(alg: string) {
// @ts-expect-error
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export async function generateSecret(alg: string, options?: GenerateSecretOptions) {
let length: number
switch (alg) {
case 'HS256':
Expand Down
9 changes: 8 additions & 1 deletion src/util/generate_key_pair.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ export interface GenerateKeyPairOptions {
* (Key size in bits). JOSE requires 2048 bits or larger. Default is 2048.
*/
modulusLength?: number

/**
* (Web Cryptography API specific) The value to use as
* [SubtleCrypto.generateKey()](https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/generateKey)
* `extractable` argument. Default is false.
*/
extractable?: boolean
}

/**
Expand All @@ -22,7 +29,7 @@ export interface GenerateKeyPairOptions {
* `generateSecret` function.
*
* Note: Under Web Cryptography API runtime the `privateKey` is generated with
* `extractable` set to `false`.
* `extractable` set to `false` by default.
*
* @example ESM import
* ```js
Expand Down
16 changes: 13 additions & 3 deletions src/util/generate_secret.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
import { generateSecret as generate } from '../runtime/generate.js'
import type { KeyLike } from '../types.d'

export interface GenerateSecretOptions {
/**
* (Web Cryptography API specific) The value to use as
* [SubtleCrypto.generateKey()](https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/generateKey)
* `extractable` argument. Default is false.
*/
extractable?: boolean
}

/**
* Generates a symmetric secret key for a given JWA algorithm identifier.
*
* Note: Under Web Cryptography API runtime the secret key is generated with
* `extractable` set to `false`.
* `extractable` set to `false` by default.
*
* @example ESM import
* ```js
Expand All @@ -24,9 +33,10 @@ import type { KeyLike } from '../types.d'
* ```
*
* @param alg JWA Algorithm Identifier to be used with the generated secret.
* @param options Additional options passed down to the secret generation.
*/
async function generateSecret(alg: string): Promise<KeyLike> {
return generate(alg)
async function generateSecret(alg: string, options?: GenerateSecretOptions): Promise<KeyLike> {
return generate(alg, options)
}

export { generateSecret }
Expand Down
9 changes: 9 additions & 0 deletions test-browser/generate_keys.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,15 @@ QUnit.test('Generate PS256', async (assert) => {
assert.ok(await generateKeyPair('PS256'));
});

QUnit.test('extractable', async (assert) => {
let { privateKey, publicKey } = await generateKeyPair('PS256');
assert.true(publicKey.extractable);
assert.false(privateKey.extractable);

({ privateKey } = await generateKeyPair('PS256', { extractable: true }));
assert.true(privateKey.extractable);
});

QUnit.test('Generate PS384', async (assert) => {
assert.ok(await generateKeyPair('PS384'));
});
Expand Down
7 changes: 7 additions & 0 deletions test-browser/generate_secrets.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ QUnit.test('HS256', async (assert) => {
assert.ok(await generateSecret('HS256'));
});

QUnit.test('extractable', async (assert) => {
let secret = await generateSecret('HS256');
assert.false(secret.extractable);
secret = await generateSecret('HS256', { extractable: true });
assert.true(secret.extractable);
});

QUnit.test('HS384', async (assert) => {
assert.ok(await generateSecret('HS384'));
});
Expand Down
26 changes: 24 additions & 2 deletions test/util/generators.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,14 @@ Promise.all([
if (key.algorithm.modulusLength !== undefined) {
t.is(key.algorithm.modulusLength, (options && options.modulusLength) || 2048);
}

t.true(publicKey.extractable);

if (options && options.extractable) {
t.true(privateKey.extractable);
} else {
t.false(privateKey.extractable);
}
} else {
// KeyObject
// Test OKP sub types are set properly
Expand Down Expand Up @@ -134,6 +142,10 @@ Promise.all([
test(`crv: ${crv}`, testKeyPair, 'ECDH-ES+A256KW', { crv });
}

if ('WEBCRYPTO' in process.env || 'CRYPTOKEY' in process.env) {
test('with extractable: true', testKeyPair, 'PS256', { extractable: true });
}

function conditional({ webcrypto = 1, electron = 1 } = {}, ...args) {
let run = test;
if ((!webcrypto && 'WEBCRYPTO' in process.env) || 'CRYPTOKEY' in process.env) {
Expand Down Expand Up @@ -183,9 +195,9 @@ Promise.all([
);
}

async function testSecret(t, alg, expectedLength) {
async function testSecret(t, alg, expectedLength, options) {
return t.notThrowsAsync(async () => {
const secret = await generateSecret(alg);
const secret = await generateSecret(alg, options);

if ('symmetricKeySize' in secret) {
t.is(secret.symmetricKeySize, expectedLength >> 3);
Expand All @@ -195,6 +207,12 @@ Promise.all([
t.is(secret.algorithm.length, expectedLength);
t.true('type' in secret);
t.is(secret.type, 'secret');

if (options && options.extractable) {
t.true(secret.extractable);
} else {
t.false(secret.extractable);
}
} else if (secret instanceof Uint8Array) {
t.is(secret.length, expectedLength >> 3);
} else {
Expand All @@ -219,6 +237,10 @@ Promise.all([
test(testSecret, 'A128GCM', 128);
test(testSecret, 'A192GCM', 192);
test(testSecret, 'A256GCM', 256);

if ('WEBCRYPTO' in process.env || 'CRYPTOKEY' in process.env) {
test('with extractable: true', testSecret, 'HS256', 256, { extractable: true });
}
},
(err) => {
test('failed to import', (t) => {
Expand Down

0 comments on commit afae428

Please sign in to comment.