From 163e1b02ed5b64368110d750c9f5f5c3d247042d Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Wed, 18 Oct 2023 10:49:48 +0200 Subject: [PATCH] refactor: importJWK always returns a Uint8Array for symmetric key inputs BREAKING CHANGE: importJWK "octAsKeyObject" option was removed. importJWK will no longer return CryptoKey or KeyObject for "oct" (octet sequence) JWK key types, it will instead always return a Uint8Array formed from the "k" (Key Value) Parameter regardless of the other JWK Parameters that may be present. --- src/jwk/embedded.ts | 2 +- src/key/import.ts | 10 ------- src/runtime/browser/jwk_to_key.ts | 43 ------------------------------- src/runtime/node/jwk_to_key.ts | 11 ++------ tap/jwk.ts | 37 +++++++++++++++++--------- test/jwk/jwk2key.test.mjs | 41 +---------------------------- 6 files changed, 29 insertions(+), 115 deletions(-) diff --git a/src/jwk/embedded.ts b/src/jwk/embedded.ts index ff4c7f5721..c2c6a7894f 100644 --- a/src/jwk/embedded.ts +++ b/src/jwk/embedded.ts @@ -36,7 +36,7 @@ export async function EmbeddedJWK( throw new JWSInvalid('"jwk" (JSON Web Key) Header Parameter must be a JSON object') } - const key = await importJWK({ ...joseHeader.jwk, ext: true }, joseHeader.alg!, true) + const key = await importJWK({ ...joseHeader.jwk, ext: true }, joseHeader.alg!) if (key instanceof Uint8Array || key.type !== 'public') { throw new JWSInvalid('"jwk" (JSON Web Key) Header Parameter must be a public key') diff --git a/src/key/import.ts b/src/key/import.ts index 3be6cf6355..22b3cfaa8a 100644 --- a/src/key/import.ts +++ b/src/key/import.ts @@ -152,13 +152,10 @@ export async function importPKCS8( * with the imported key. Default is the "alg" property on the JWK, its presence is only enforced * in Web Crypto API runtimes. See * {@link https://github.com/panva/jose/issues/210 Algorithm Key Requirements}. - * @param octAsKeyObject Forces a symmetric key to be imported to a KeyObject or CryptoKey. Default - * is true unless JWK "ext" (Extractable) is true. */ export async function importJWK( jwk: JWK, alg?: string, - octAsKeyObject?: boolean, ): Promise { if (!isObject(jwk)) { throw new TypeError('JWK must be an object') @@ -172,13 +169,6 @@ export async function importJWK( throw new TypeError('missing "k" (Key Value) Parameter value') } - octAsKeyObject ??= jwk.ext !== true - - if (octAsKeyObject) { - // @ts-ignore - return asKeyObject({ ...jwk, alg, ext: jwk.ext ?? false }) - } - return decodeBase64URL(jwk.k) case 'RSA': if (jwk.oth !== undefined) { diff --git a/src/runtime/browser/jwk_to_key.ts b/src/runtime/browser/jwk_to_key.ts index 7c72e3db79..249e55f543 100644 --- a/src/runtime/browser/jwk_to_key.ts +++ b/src/runtime/browser/jwk_to_key.ts @@ -2,7 +2,6 @@ import crypto from './webcrypto.js' import type { JWKImportFunction } from '../interfaces.d' import { JOSENotSupported } from '../../util/errors.js' import type { JWK } from '../../types.d' -import { decode as base64url } from './base64url.js' function subtleMapping(jwk: JWK): { algorithm: RsaHashedImportParams | EcKeyAlgorithm | Algorithm @@ -12,44 +11,6 @@ function subtleMapping(jwk: JWK): { let keyUsages: KeyUsage[] switch (jwk.kty) { - case 'oct': { - switch (jwk.alg) { - case 'HS256': - case 'HS384': - case 'HS512': - algorithm = { name: 'HMAC', hash: `SHA-${jwk.alg.slice(-3)}` } - keyUsages = ['sign', 'verify'] - break - case 'A128CBC-HS256': - case 'A192CBC-HS384': - case 'A256CBC-HS512': - throw new JOSENotSupported(`${jwk.alg} keys cannot be imported as CryptoKey instances`) - case 'A128GCM': - case 'A192GCM': - case 'A256GCM': - case 'A128GCMKW': - case 'A192GCMKW': - case 'A256GCMKW': - algorithm = { name: 'AES-GCM' } - keyUsages = ['encrypt', 'decrypt'] - break - case 'A128KW': - case 'A192KW': - case 'A256KW': - algorithm = { name: 'AES-KW' } - keyUsages = ['wrapKey', 'unwrapKey'] - break - case 'PBES2-HS256+A128KW': - case 'PBES2-HS384+A192KW': - case 'PBES2-HS512+A256KW': - algorithm = { name: 'PBKDF2' } - keyUsages = ['deriveBits'] - break - default: - throw new JOSENotSupported('Invalid or unsupported JWK "alg" (Algorithm) Parameter value') - } - break - } case 'RSA': { switch (jwk.alg) { case 'PS256': @@ -142,10 +103,6 @@ const parse: JWKImportFunction = async (jwk: JWK): Promise => { jwk.key_ops ?? keyUsages, ] - if (algorithm.name === 'PBKDF2') { - return crypto.subtle.importKey('raw', base64url(jwk.k!), ...rest) - } - const keyData: JWK = { ...jwk } delete keyData.alg delete keyData.use diff --git a/src/runtime/node/jwk_to_key.ts b/src/runtime/node/jwk_to_key.ts index 9df3dc57ed..ee80cdfa67 100644 --- a/src/runtime/node/jwk_to_key.ts +++ b/src/runtime/node/jwk_to_key.ts @@ -1,17 +1,10 @@ -import { createPrivateKey, createPublicKey, createSecretKey } from 'node:crypto' +import { createPrivateKey, createPublicKey } from 'node:crypto' import type { KeyObject } from 'node:crypto' import type { JWKImportFunction } from '../interfaces.d' -import { decode as base64url } from './base64url.js' import type { JWK } from '../../types.d' const parse: JWKImportFunction = (jwk: JWK): KeyObject => { - if (jwk.kty === 'oct') { - return createSecretKey(base64url(jwk.k!)) - } - - return jwk.d - ? createPrivateKey({ format: 'jwk', key: jwk }) - : createPublicKey({ format: 'jwk', key: jwk }) + return (jwk.d ? createPrivateKey : createPublicKey)({ format: 'jwk', key: jwk }) } export default parse diff --git a/tap/jwk.ts b/tap/jwk.ts index 509c508192..cba6204ad7 100644 --- a/tap/jwk.ts +++ b/tap/jwk.ts @@ -94,27 +94,40 @@ export default (QUnit: QUnit, lib: typeof jose) => { } } + test('alg argument and jwk.alg is ignored for oct JWKs', async (t) => { + const oct = { + k: 'FyCq1CKBflh3I5gikEjpYrdOXllzxB_yc02za8ERknI', + kty: 'oct', + } + await lib.importJWK(oct) + t.ok(1) + }) + if (env.isNodeCrypto || env.isElectron) { - test('alg argument and jwk.alg is ignored', async (t) => { - const oct = { - k: 'FyCq1CKBflh3I5gikEjpYrdOXllzxB_yc02za8ERknI', - kty: 'oct', + test('alg argument is ignored if jwk does not have alg for asymmetric keys', async (t) => { + const jwk = { + kty: 'EC', + crv: 'P-256', + x: 'jJ6Flys3zK9jUhnOHf6G49Dyp5hah6CNP84-gY-n9eo', + y: 'nhI6iD5eFXgBTLt_1p3aip-5VbZeMhxeFSpjfEAf7Ww', } - await lib.importJWK(oct) + await lib.importJWK(jwk) t.ok(1) }) } else { - test('alg argument must be present if jwk does not have alg', async (t) => { - const oct = { - k: 'FyCq1CKBflh3I5gikEjpYrdOXllzxB_yc02za8ERknI', - kty: 'oct', + test('alg argument must be present if jwk does not have alg for asymmetric keys', async (t) => { + const jwk = { + kty: 'EC', + crv: 'P-256', + x: 'jJ6Flys3zK9jUhnOHf6G49Dyp5hah6CNP84-gY-n9eo', + y: 'nhI6iD5eFXgBTLt_1p3aip-5VbZeMhxeFSpjfEAf7Ww', } await t.rejects( - lib.importJWK(oct), + lib.importJWK(jwk), '"alg" argument is required when "jwk.alg" is not present', ) - await lib.importJWK(oct, 'HS256') - await lib.importJWK({ ...oct, alg: 'HS256' }) + await lib.importJWK(jwk, 'ES256') + await lib.importJWK({ ...jwk, alg: 'ES256' }) }) } } diff --git a/test/jwk/jwk2key.test.mjs b/test/jwk/jwk2key.test.mjs index 448fa9537f..557c916c23 100644 --- a/test/jwk/jwk2key.test.mjs +++ b/test/jwk/jwk2key.test.mjs @@ -52,11 +52,10 @@ test('RSA JWK with oth is not supported', async (t) => { }) }) -test('oct JWK (ext: true)', async (t) => { +test('oct JWK', async (t) => { const oct = { k: 'FyCq1CKBflh3I5gikEjpYrdOXllzxB_yc02za8ERknI', kty: 'oct', - ext: true, } t.deepEqual( @@ -66,44 +65,6 @@ test('oct JWK (ext: true)', async (t) => { 196, 31, 242, 115, 77, 179, 107, 193, 17, 146, 114, ], ) - - const k = await importJWK(oct, 'HS256', true) - t.true('type' in k) - t.is(k.type, 'secret') - if ('extractable' in k) { - t.is(k.extractable, true) - } -}) - -test('oct JWK (ext: false)', async (t) => { - const oct = { - k: 'FyCq1CKBflh3I5gikEjpYrdOXllzxB_yc02za8ERknI', - kty: 'oct', - ext: false, - } - - const k = await importJWK(oct, 'HS256', true) - - t.true('type' in k) - t.is(k.type, 'secret') - if ('extractable' in k) { - t.is(k.extractable, false) - } -}) - -test('oct JWK (ext missing)', async (t) => { - const oct = { - k: 'FyCq1CKBflh3I5gikEjpYrdOXllzxB_yc02za8ERknI', - kty: 'oct', - } - - const k = await importJWK(oct, 'HS256', true) - - t.true('type' in k) - t.is(k.type, 'secret') - if ('extractable' in k) { - t.is(k.extractable, false) - } }) test('Uin8tArray can be transformed to a JWK', async (t) => {