Skip to content

Commit

Permalink
feat: support KeyObject inputs in WebCryptoAPI runtimes given compati…
Browse files Browse the repository at this point in the history
…bility
  • Loading branch information
panva committed Jun 27, 2024
1 parent be0f830 commit e178b8f
Show file tree
Hide file tree
Showing 29 changed files with 233 additions and 150 deletions.
21 changes: 9 additions & 12 deletions src/lib/check_key_type.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { withAlg as invalidKeyInput } from './invalid_key_input.js'
import isKeyLike, { types } from '../runtime/is_key_like.js'

// @ts-expect-error
const tag = (key: unknown): string => key?.[Symbol.toStringTag]

const symmetricTypeCheck = (alg: string, key: unknown) => {
if (key instanceof Uint8Array) return

Expand All @@ -9,9 +12,7 @@ const symmetricTypeCheck = (alg: string, key: unknown) => {
}

if (key.type !== 'secret') {
throw new TypeError(
`${types.join(' or ')} instances for symmetric algorithms must be of type "secret"`,
)
throw new TypeError(`${tag(key)} instances for symmetric algorithms must be of type "secret"`)
}
}

Expand All @@ -22,37 +23,33 @@ const asymmetricTypeCheck = (alg: string, key: unknown, usage: string) => {

if (key.type === 'secret') {
throw new TypeError(
`${types.join(' or ')} instances for asymmetric algorithms must not be of type "secret"`,
`${tag(key)} instances for asymmetric algorithms must not be of type "secret"`,
)
}

if (usage === 'sign' && key.type === 'public') {
throw new TypeError(
`${types.join(' or ')} instances for asymmetric algorithm signing must be of type "private"`,
`${tag(key)} instances for asymmetric algorithm signing must be of type "private"`,
)
}

if (usage === 'decrypt' && key.type === 'public') {
throw new TypeError(
`${types.join(
' or ',
)} instances for asymmetric algorithm decryption must be of type "private"`,
`${tag(key)} instances for asymmetric algorithm decryption must be of type "private"`,
)
}

// KeyObject allows this but CryptoKey does not.
if ((<CryptoKey>key).algorithm && usage === 'verify' && key.type === 'private') {
throw new TypeError(
`${types.join(' or ')} instances for asymmetric algorithm verifying must be of type "public"`,
`${tag(key)} instances for asymmetric algorithm verifying must be of type "public"`,
)
}

// KeyObject allows this but CryptoKey does not.
if ((<CryptoKey>key).algorithm && usage === 'encrypt' && key.type === 'private') {
throw new TypeError(
`${types.join(
' or ',
)} instances for asymmetric algorithm encryption must be of type "public"`,
`${tag(key)} instances for asymmetric algorithm encryption must be of type "public"`,
)
}
}
Expand Down
7 changes: 7 additions & 0 deletions src/lib/decrypt_key_management.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import * as ECDH from '../runtime/ecdhes.js'
import { decrypt as pbes2Kw } from '../runtime/pbes2kw.js'
import { decrypt as rsaEs } from '../runtime/rsaes.js'
import { decode as base64url } from '../runtime/base64url.js'
import * as normalize from '../runtime/normalize_key.js'

import type { DecryptOptions, JWEHeaderParameters, KeyLike, JWK } from '../types.d'
import { JOSENotSupported, JWEInvalid } from '../util/errors.js'
Expand All @@ -19,6 +20,12 @@ async function decryptKeyManagement(
joseHeader: JWEHeaderParameters,
options?: DecryptOptions,
): Promise<KeyLike | Uint8Array> {
// @ts-ignore
if (normalize.normalizePrivateKey) {
// @ts-ignore
key = await normalize.normalizePrivateKey(key, alg)
}

checkKeyType(alg, key, 'decrypt')

switch (alg) {
Expand Down
7 changes: 7 additions & 0 deletions src/lib/encrypt_key_management.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import * as ECDH from '../runtime/ecdhes.js'
import { encrypt as pbes2Kw } from '../runtime/pbes2kw.js'
import { encrypt as rsaEs } from '../runtime/rsaes.js'
import { encode as base64url } from '../runtime/base64url.js'
import * as normalize from '../runtime/normalize_key.js'

import type {
KeyLike,
Expand Down Expand Up @@ -31,6 +32,12 @@ async function encryptKeyManagement(
let parameters: (JWEHeaderParameters & { epk?: JWK }) | undefined
let cek: KeyLike | Uint8Array

// @ts-ignore
if (normalize.normalizePublicKey) {
// @ts-ignore
key = await normalize.normalizePublicKey(key, alg)
}

checkKeyType(alg, key, 'encrypt')

switch (alg) {
Expand Down
15 changes: 14 additions & 1 deletion src/runtime/browser/get_sign_verify_key.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,21 @@ import crypto, { isCryptoKey } from './webcrypto.js'
import { checkSigCryptoKey } from '../../lib/crypto_key.js'
import invalidKeyInput from '../../lib/invalid_key_input.js'
import { types } from './is_key_like.js'
import * as normalize from '../normalize_key.js'

export default async function getCryptoKey(alg: string, key: unknown, usage: KeyUsage) {
// @ts-ignore
if (normalize.normalizePrivateKey && usage === 'sign') {
// @ts-ignore
key = await normalize.normalizePrivateKey(key, alg)
}

// @ts-ignore
if (normalize.normalizePublicKey && usage === 'verify') {
// @ts-ignore
key = await normalize.normalizePublicKey(key, alg)
}

export default function getCryptoKey(alg: string, key: unknown, usage: KeyUsage) {
if (isCryptoKey(key)) {
checkSigCryptoKey(key, alg, usage)
return key
Expand Down
7 changes: 6 additions & 1 deletion src/runtime/browser/is_key_like.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@ import type { KeyLike } from '../../types.d'
import { isCryptoKey } from './webcrypto.js'

export default (key: unknown): key is KeyLike => {
return isCryptoKey(key)
if (isCryptoKey(key)) {
return true
}

// @ts-expect-error
return key?.[Symbol.toStringTag] === 'KeyObject'
}

export const types = ['CryptoKey']
41 changes: 41 additions & 0 deletions src/runtime/browser/normalize_key.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import type { KeyLike } from '../../types.d'
import { decode } from '../base64url.js'
import importJWK from '../jwk_to_key.js'

const normalizeSecretKey = (k: string) => decode(k)

export const normalizePublicKey = async (key: KeyLike | Uint8Array | unknown, alg: string) => {
// @ts-expect-error
if (key?.[Symbol.toStringTag] === 'KeyObject') {
// @ts-expect-error
let jwk = key.export({ format: 'jwk' })
delete jwk.d
delete jwk.dp
delete jwk.dq
delete jwk.p
delete jwk.q
delete jwk.qi
if (jwk.k) {
return normalizeSecretKey(jwk.k)
}

return importJWK({ ...jwk, alg })
}

return <KeyLike | Uint8Array>key
}

export const normalizePrivateKey = async (key: KeyLike | Uint8Array | unknown, alg: string) => {
// @ts-expect-error
if (key?.[Symbol.toStringTag] === 'KeyObject') {
// @ts-expect-error
let jwk = key.export({ format: 'jwk' })
if (jwk.k) {
return normalizeSecretKey(jwk.k)
}

return importJWK({ ...jwk, alg })
}

return <KeyLike | Uint8Array>key
}
1 change: 1 addition & 0 deletions src/runtime/node/normalize_key.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {}
21 changes: 14 additions & 7 deletions tap/.node.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,31 +13,38 @@
source .node_flags.sh

node tap/run-node.mjs '#dist'
NODE_CRYPTO_API=$?
NODE_CRYPTO=$?

node -e 'process.exit(parseInt(process.versions.node, 10))' &> /dev/null
NODE_VERSION=$?

if [[ "$NODE_VERSION" -le 14 ]]; then
exit $NODE_CRYPTO_API
exit $NODE_CRYPTO
fi

node tap/run-node.mjs '#dist/webapi'
WEB_CRYPTO_API=$?

node tap/run-node.mjs '#dist/hybrid'
HYBRID=$?
node tap/run-node.mjs '#dist/node-crypto-with-cryptokey'
NODE_WITH_CRYPTOKEY=$?

node tap/run-node.mjs '#dist/webcrypto-with-keyobject'
WEB_CRYPTO_API_WITH_KEYOBJECT=$?

echo ""
echo "node:crypto"
test $NODE_CRYPTO_API -eq 0 && echo " passed" || echo " failed"
test $NODE_CRYPTO -eq 0 && echo " passed" || echo " failed"

echo ""
echo "WebCryptoAPI"
test $WEB_CRYPTO_API -eq 0 && echo " passed" || echo " failed"

echo ""
echo "node:crypto with CryptoKey"
test $HYBRID -eq 0 && echo " passed" || echo " failed"
test $NODE_WITH_CRYPTOKEY -eq 0 && echo " passed" || echo " failed"

echo ""
echo "WebCryptoAPI with KeyObject"
test $WEB_CRYPTO_API_WITH_KEYOBJECT -eq 0 && echo " passed" || echo " failed"

test $WEB_CRYPTO_API -eq 0 && test $NODE_CRYPTO_API -eq 0 && test $HYBRID -eq 0
test $WEB_CRYPTO_API -eq 0 && test $NODE_CRYPTO -eq 0 && test $WEB_CRYPTO_API_WITH_KEYOBJECT -eq 0 && test $NODE_WITH_CRYPTOKEY -eq 0
4 changes: 2 additions & 2 deletions tap/aes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type * as jose from '../src/index.js'
import random from './random.js'
import * as roundtrip from './encrypt.js'

export default (QUnit: QUnit, lib: typeof jose) => {
export default (QUnit: QUnit, lib: typeof jose, keys: typeof jose) => {
const { module, test } = QUnit
module('aes.ts')

Expand All @@ -30,7 +30,7 @@ export default (QUnit: QUnit, lib: typeof jose) => {

function secretsFor(enc: string) {
return [
lib.generateSecret(enc),
keys.generateSecret(enc),
random(parseInt(enc.endsWith('GCM') ? enc.slice(1, 4) : enc.slice(-3)) >> 3),
]
}
Expand Down
4 changes: 2 additions & 2 deletions tap/aeskw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type * as jose from '../src/index.js'
import random from './random.js'
import * as roundtrip from './encrypt.js'

export default (QUnit: QUnit, lib: typeof jose) => {
export default (QUnit: QUnit, lib: typeof jose, keys: typeof jose) => {
const { module, test } = QUnit
module('aeskw.ts')

Expand All @@ -29,7 +29,7 @@ export default (QUnit: QUnit, lib: typeof jose) => {
}

function secretsFor(alg: string) {
return [lib.generateSecret(alg), random(parseInt(alg.slice(1, 4), 10) >> 3)]
return [keys.generateSecret(alg), random(parseInt(alg.slice(1, 4), 10) >> 3)]
}

for (const vector of algorithms) {
Expand Down
18 changes: 9 additions & 9 deletions tap/cookbook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ if (env.isWebKitAbove17) {
ed25519.reproducible = false
}

export default (QUnit: QUnit, lib: typeof jose) => {
export default (QUnit: QUnit, lib: typeof jose, keys: typeof jose) => {
const { module, test } = QUnit

const encode = TextEncoder.prototype.encode.bind(new TextEncoder())
Expand Down Expand Up @@ -50,8 +50,8 @@ export default (QUnit: QUnit, lib: typeof jose) => {
const execute = (vector: any) => async (t: typeof QUnit.assert) => {
const reproducible = !!vector.reproducible

const privateKey = await lib.importJWK(vector.input.key, vector.input.alg)
const publicKey = await lib.importJWK(pubjwk(vector.input.key), vector.input.alg)
const privateKey = await keys.importJWK(vector.input.key, vector.input.alg)
const publicKey = await keys.importJWK(pubjwk(vector.input.key), vector.input.alg)

if (reproducible) {
// sign and compare results are the same
Expand Down Expand Up @@ -144,7 +144,7 @@ export default (QUnit: QUnit, lib: typeof jose) => {
}

function supported(vector: any) {
if (vector.webcrypto === false && !(env.isNodeCrypto || env.isElectron)) {
if (vector.webcrypto === false && lib.cryptoRuntime !== 'node:crypto') {
return false
}
if (env.isElectron && vector.electron === false) {
Expand Down Expand Up @@ -220,15 +220,15 @@ export default (QUnit: QUnit, lib: typeof jose) => {

if (vector.encrypting_key && vector.encrypting_key.epk) {
keyManagementParameters.epk = <jose.KeyLike>(
await lib.importJWK(vector.encrypting_key.epk, vector.input.alg)
await keys.importJWK(vector.encrypting_key.epk, vector.input.alg)
)
}

if (Object.keys(keyManagementParameters).length !== 0) {
encrypt.setKeyManagementParameters(keyManagementParameters)
}

const publicKey = await lib.importJWK(
const publicKey = await keys.importJWK(
pubjwk(toJWK(vector.input.pwd || vector.input.key)),
dir ? vector.input.enc : vector.input.alg,
)
Expand All @@ -255,7 +255,7 @@ export default (QUnit: QUnit, lib: typeof jose) => {
}

const privateKey = <jose.KeyLike>(
await lib.importJWK(
await keys.importJWK(
toJWK(vector.input.pwd || vector.input.key),
dir ? vector.input.enc : vector.input.alg,
)
Expand All @@ -264,7 +264,7 @@ export default (QUnit: QUnit, lib: typeof jose) => {
if (privateKey.type === 'secret') {
publicKey = privateKey
} else {
publicKey = await lib.importJWK(
publicKey = await keys.importJWK(
pubjwk(toJWK(vector.input.pwd || vector.input.key)),
dir ? vector.input.enc : vector.input.alg,
)
Expand All @@ -277,7 +277,7 @@ export default (QUnit: QUnit, lib: typeof jose) => {
})
}

const privateKey = await lib.importJWK(
const privateKey = await keys.importJWK(
toJWK(vector.input.pwd || vector.input.key),
dir ? vector.input.enc : vector.input.alg,
)
Expand Down
6 changes: 3 additions & 3 deletions tap/ecdh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import * as env from './env.js'
import type * as jose from '../src/index.js'
import * as roundtrip from './encrypt.js'

export default (QUnit: QUnit, lib: typeof jose) => {
export default (QUnit: QUnit, lib: typeof jose, keys: typeof jose) => {
const { module, test } = QUnit
module('ecdh.ts')

Expand Down Expand Up @@ -39,14 +39,14 @@ export default (QUnit: QUnit, lib: typeof jose) => {

const execute = async (t: typeof QUnit.assert) => {
if (!kps[k]) {
kps[k] = await lib.generateKeyPair(alg, options)
kps[k] = await keys.generateKeyPair(alg, options)
}
await roundtrip.jwe(t, lib, alg, 'A128GCM', kps[k])
}

const jwt = async (t: typeof QUnit.assert) => {
if (!kps[k]) {
kps[k] = await lib.generateKeyPair(alg, options)
kps[k] = await keys.generateKeyPair(alg, options)
}
await roundtrip.jwt(t, lib, alg, 'A128GCM', kps[k])
}
Expand Down
6 changes: 0 additions & 6 deletions tap/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,6 @@ export const isElectron = typeof process !== 'undefined' && process.versions.ele
// @ts-ignore
export const isNode = !isBun && !isElectron && typeof process !== 'undefined'

// @ts-ignore
export const isNodeCrypto = isNode && [...process.argv].reverse()[0] === '#dist'

// @ts-ignore
export const isNodeWebCrypto = isNode && [...process.argv].reverse()[0] !== '#dist'

// @ts-ignore
export const isDeno = typeof Deno !== 'undefined'

Expand Down
Loading

0 comments on commit e178b8f

Please sign in to comment.