Skip to content

Commit

Permalink
refactor: importJWK always returns a Uint8Array for symmetric key inputs
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
panva committed Oct 25, 2023
1 parent b5aee54 commit 163e1b0
Show file tree
Hide file tree
Showing 6 changed files with 29 additions and 115 deletions.
2 changes: 1 addition & 1 deletion src/jwk/embedded.ts
Expand Up @@ -36,7 +36,7 @@ export async function EmbeddedJWK<T extends KeyLike = KeyLike>(
throw new JWSInvalid('"jwk" (JSON Web Key) Header Parameter must be a JSON object')
}

const key = await importJWK<T>({ ...joseHeader.jwk, ext: true }, joseHeader.alg!, true)
const key = await importJWK<T>({ ...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')
Expand Down
10 changes: 0 additions & 10 deletions src/key/import.ts
Expand Up @@ -152,13 +152,10 @@ export async function importPKCS8<T extends KeyLike = KeyLike>(
* 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<T extends KeyLike = KeyLike>(
jwk: JWK,
alg?: string,
octAsKeyObject?: boolean,
): Promise<T | Uint8Array> {
if (!isObject(jwk)) {
throw new TypeError('JWK must be an object')
Expand All @@ -172,13 +169,6 @@ export async function importJWK<T extends KeyLike = KeyLike>(
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) {
Expand Down
43 changes: 0 additions & 43 deletions src/runtime/browser/jwk_to_key.ts
Expand Up @@ -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
Expand All @@ -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':
Expand Down Expand Up @@ -142,10 +103,6 @@ const parse: JWKImportFunction = async (jwk: JWK): Promise<CryptoKey> => {
<KeyUsage[]>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
Expand Down
11 changes: 2 additions & 9 deletions 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
37 changes: 25 additions & 12 deletions tap/jwk.ts
Expand Up @@ -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' })
})
}
}
41 changes: 1 addition & 40 deletions test/jwk/jwk2key.test.mjs
Expand Up @@ -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(
Expand All @@ -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) => {
Expand Down

0 comments on commit 163e1b0

Please sign in to comment.