diff --git a/src/lib/crypto_key.ts b/src/lib/crypto_key.ts index f69b944d..44e77f1e 100644 --- a/src/lib/crypto_key.ts +++ b/src/lib/crypto_key.ts @@ -1,5 +1,3 @@ -import { isCloudflareWorkers, isNodeJs } from '../runtime/env.js' - function unusable(name: string | number, prop = 'algorithm.name') { return new TypeError(`CryptoKey does not support this operation, its ${prop} must be ${name}`) } @@ -71,13 +69,10 @@ export function checkSigCryptoKey(key: CryptoKey, alg: string, ...usages: KeyUsa if (actual !== expected) throw unusable(`SHA-${expected}`, 'algorithm.hash') break } - case isNodeJs() && 'EdDSA': { - if (key.algorithm.name !== 'NODE-ED25519' && key.algorithm.name !== 'NODE-ED448') - throw unusable('NODE-ED25519 or NODE-ED448') - break - } - case isCloudflareWorkers() && 'EdDSA': { - if (!isAlgorithm(key.algorithm, 'NODE-ED25519')) throw unusable('NODE-ED25519') + case 'EdDSA': { + if (key.algorithm.name !== 'Ed25519' && key.algorithm.name !== 'Ed448') { + throw unusable('Ed25519 or Ed448') + } break } case 'ES256': @@ -116,9 +111,17 @@ export function checkEncCryptoKey(key: CryptoKey, alg: string, ...usages: KeyUsa if (actual !== expected) throw unusable(expected, 'algorithm.length') break } - case 'ECDH': - if (!isAlgorithm(key.algorithm, 'ECDH')) throw unusable('ECDH') + case 'ECDH': { + switch (key.algorithm.name) { + case 'ECDH': + case 'X25519': + case 'X448': + break + default: + throw unusable('ECDH, X25519, or X448') + } break + } case 'PBES2-HS256+A128KW': case 'PBES2-HS384+A192KW': case 'PBES2-HS512+A256KW': diff --git a/src/runtime/browser/asn1.ts b/src/runtime/browser/asn1.ts index 0c11c017..d07f16fa 100644 --- a/src/runtime/browser/asn1.ts +++ b/src/runtime/browser/asn1.ts @@ -1,4 +1,3 @@ -import { isCloudflareWorkers, isNodeJs } from './env.js' import crypto, { isCryptoKey } from './webcrypto.js' import type { PEMExportFunction, PEMImportFunction } from '../interfaces.d' import invalidKeyInput from '../../lib/invalid_key_input.js' @@ -60,9 +59,13 @@ const getNamedCurve = (keyData: Uint8Array): string => { return 'P-384' case findOid(keyData, [0x2b, 0x81, 0x04, 0x00, 0x23]): return 'P-521' - case (isCloudflareWorkers() || isNodeJs()) && findOid(keyData, [0x2b, 0x65, 0x70]): + case findOid(keyData, [0x2b, 0x65, 0x6e]): + return 'X25519' + case findOid(keyData, [0x2b, 0x65, 0x6f]): + return 'X448' + case findOid(keyData, [0x2b, 0x65, 0x70]): return 'Ed25519' - case isNodeJs() && findOid(keyData, [0x2b, 0x65, 0x71]): + case findOid(keyData, [0x2b, 0x65, 0x71]): return 'Ed448' default: throw new JOSENotSupported('Invalid or unsupported EC Key Curve or OKP Key Sub Type') @@ -126,12 +129,12 @@ const genericImport = async ( case 'ECDH-ES+A128KW': case 'ECDH-ES+A192KW': case 'ECDH-ES+A256KW': - algorithm = { name: 'ECDH', namedCurve: getNamedCurve(keyData) } + const namedCurve = getNamedCurve(keyData) + algorithm = namedCurve.startsWith('P-') ? { name: 'ECDH', namedCurve } : { name: namedCurve } keyUsages = isPublic ? [] : ['deriveBits'] break - case (isCloudflareWorkers() || isNodeJs()) && 'EdDSA': - const namedCurve = getNamedCurve(keyData).toUpperCase() - algorithm = { name: `NODE-${namedCurve}`, namedCurve: `NODE-${namedCurve}` } + case 'EdDSA': + algorithm = { name: getNamedCurve(keyData) } keyUsages = isPublic ? ['verify'] : ['sign'] break default: diff --git a/src/runtime/browser/ecdhes.ts b/src/runtime/browser/ecdhes.ts index b3eb8508..73cc05f3 100644 --- a/src/runtime/browser/ecdhes.ts +++ b/src/runtime/browser/ecdhes.ts @@ -28,14 +28,24 @@ export async function deriveKey( uint32be(keyLength), ) + let length: number + if (publicKey.algorithm.name === 'X25519') { + length = 256 + } else if (publicKey.algorithm.name === 'X448') { + length = 448 + } else { + length = + Math.ceil(parseInt((publicKey.algorithm).namedCurve.substr(-3), 10) / 8) << 3 + } + const sharedSecret = new Uint8Array( await crypto.subtle.deriveBits( { - name: 'ECDH', + name: publicKey.algorithm.name, public: publicKey, }, privateKey, - Math.ceil(parseInt((privateKey.algorithm).namedCurve.slice(-3), 10) / 8) << 3, + length, ), ) @@ -54,5 +64,9 @@ export function ecdhAllowed(key: unknown) { if (!isCryptoKey(key)) { throw new TypeError(invalidKeyInput(key, ...types)) } - return ['P-256', 'P-384', 'P-521'].includes((key.algorithm).namedCurve) + return ( + ['P-256', 'P-384', 'P-521'].includes((key.algorithm).namedCurve) || + key.algorithm.name === 'X25519' || + key.algorithm.name === 'X448' + ) } diff --git a/src/runtime/browser/env.ts b/src/runtime/browser/env.ts index 52195baf..36bb043e 100644 --- a/src/runtime/browser/env.ts +++ b/src/runtime/browser/env.ts @@ -1,13 +1,4 @@ -export function isCloudflareWorkers(): boolean { +export function isCloudflareWorkers() { // @ts-expect-error return typeof WebSocketPair === 'function' } - -export function isNodeJs(): boolean { - try { - // @deno-expect-error - return process.versions.node !== undefined - } catch { - return false - } -} diff --git a/src/runtime/browser/generate.ts b/src/runtime/browser/generate.ts index dae971f2..9c7aac6b 100644 --- a/src/runtime/browser/generate.ts +++ b/src/runtime/browser/generate.ts @@ -1,4 +1,3 @@ -import { isCloudflareWorkers, isNodeJs } from './env.js' import crypto from './webcrypto.js' import { JOSENotSupported } from '../../util/errors.js' import random from './random.js' @@ -59,7 +58,7 @@ function getModulusLengthOption(options?: GenerateKeyPairOptions) { } export async function generateKeyPair(alg: string, options?: GenerateKeyPairOptions) { - let algorithm: RsaHashedKeyGenParams | EcKeyGenParams + let algorithm: RsaHashedKeyGenParams | EcKeyGenParams | KeyAlgorithm let keyUsages: KeyUsage[] switch (alg) { @@ -109,16 +108,13 @@ export async function generateKeyPair(alg: string, options?: GenerateKeyPairOpti algorithm = { name: 'ECDSA', namedCurve: 'P-521' } keyUsages = ['sign', 'verify'] break - case (isCloudflareWorkers() || isNodeJs()) && 'EdDSA': - switch (options?.crv) { - case undefined: + case 'EdDSA': + keyUsages = ['sign', 'verify'] + const crv = options?.crv ?? 'Ed25519' + switch (crv) { case 'Ed25519': - algorithm = { name: 'NODE-ED25519', namedCurve: 'NODE-ED25519' } - keyUsages = ['sign', 'verify'] - break - case isNodeJs() && 'Ed448': - algorithm = { name: 'NODE-ED448', namedCurve: 'NODE-ED448' } - keyUsages = ['sign', 'verify'] + case 'Ed448': + algorithm = { name: crv } break default: throw new JOSENotSupported( @@ -129,10 +125,27 @@ export async function generateKeyPair(alg: string, options?: GenerateKeyPairOpti case 'ECDH-ES': case 'ECDH-ES+A128KW': case 'ECDH-ES+A192KW': - case 'ECDH-ES+A256KW': - algorithm = { name: 'ECDH', namedCurve: options?.crv ?? 'P-256' } + case 'ECDH-ES+A256KW': { keyUsages = ['deriveKey', 'deriveBits'] + const crv = options?.crv ?? 'P-256' + switch (crv) { + case 'P-256': + case 'P-384': + case 'P-521': { + algorithm = { name: 'ECDH', namedCurve: crv } + break + } + case 'X25519': + case 'X448': + algorithm = { name: crv } + break + default: + throw new JOSENotSupported( + 'Invalid or unsupported crv option provided, supported values are P-256, P-384, P-521, X25519, and X448', + ) + } break + } default: throw new JOSENotSupported('Invalid or unsupported JWK "alg" (Algorithm) Parameter value') } diff --git a/src/runtime/browser/jwk_to_key.ts b/src/runtime/browser/jwk_to_key.ts index 8c84155a..6ea9749b 100644 --- a/src/runtime/browser/jwk_to_key.ts +++ b/src/runtime/browser/jwk_to_key.ts @@ -1,4 +1,3 @@ -import { isCloudflareWorkers, isNodeJs } from './env.js' import crypto from './webcrypto.js' import type { JWKImportFunction } from '../interfaces.d' import { JOSENotSupported } from '../../util/errors.js' @@ -106,25 +105,24 @@ function subtleMapping(jwk: JWK): { } break } - case (isCloudflareWorkers() || isNodeJs()) && 'OKP': - if (jwk.alg !== 'EdDSA') { - throw new JOSENotSupported('Invalid or unsupported JWK "alg" (Algorithm) Parameter value') - } - switch (jwk.crv) { - case 'Ed25519': - algorithm = { name: 'NODE-ED25519', namedCurve: 'NODE-ED25519' } + case 'OKP': { + switch (jwk.alg) { + case 'EdDSA': + algorithm = { name: jwk.crv! } keyUsages = jwk.d ? ['sign'] : ['verify'] break - case isNodeJs() && 'Ed448': - algorithm = { name: 'NODE-ED448', namedCurve: 'NODE-ED448' } - keyUsages = jwk.d ? ['sign'] : ['verify'] + case 'ECDH-ES': + case 'ECDH-ES+A128KW': + case 'ECDH-ES+A192KW': + case 'ECDH-ES+A256KW': + algorithm = { name: jwk.crv! } + keyUsages = jwk.d ? ['deriveBits'] : [] break default: - throw new JOSENotSupported( - 'Invalid or unsupported JWK "crv" (Subtype of Key Pair) Parameter value', - ) + throw new JOSENotSupported('Invalid or unsupported JWK "alg" (Algorithm) Parameter value') } break + } default: throw new JOSENotSupported('Invalid or unsupported JWK "kty" (Key Type) Parameter value') } diff --git a/src/runtime/browser/subtle_dsa.ts b/src/runtime/browser/subtle_dsa.ts index 0a20f84a..47ebccec 100644 --- a/src/runtime/browser/subtle_dsa.ts +++ b/src/runtime/browser/subtle_dsa.ts @@ -1,4 +1,3 @@ -import { isCloudflareWorkers, isNodeJs } from './env.js' import { JOSENotSupported } from '../../util/errors.js' export default function subtleDsa(alg: string, algorithm: KeyAlgorithm | EcKeyAlgorithm) { @@ -21,9 +20,8 @@ export default function subtleDsa(alg: string, algorithm: KeyAlgorithm | EcKeyAl case 'ES384': case 'ES512': return { hash, name: 'ECDSA', namedCurve: (algorithm).namedCurve } - case (isCloudflareWorkers() || isNodeJs()) && 'EdDSA': - const { namedCurve } = algorithm - return { name: namedCurve, namedCurve } + case 'EdDSA': + return { name: algorithm.name } default: throw new JOSENotSupported( `alg ${alg} is not supported either by JOSE or your javascript runtime`, diff --git a/src/runtime/node/env.ts b/src/runtime/node/env.ts index d3d7a4f2..e1ad91c0 100644 --- a/src/runtime/node/env.ts +++ b/src/runtime/node/env.ts @@ -1,6 +1,3 @@ export function isCloudflareWorkers() { return false } -export function isNodeJs() { - return true -} diff --git a/test/jwe/smoke.test.mjs b/test/jwe/smoke.test.mjs index bcf6cf8e..69aeeff2 100644 --- a/test/jwe/smoke.test.mjs +++ b/test/jwe/smoke.test.mjs @@ -299,12 +299,12 @@ test('as keyobject', smoke, 'oct256gcm', ['encrypt'], ['decrypt'], true) test(smoke, 'oct256c') test(smoke, 'oct384c') test(smoke, 'oct512c') +test(smoke, 'x25519dir') conditional({ webcrypto: 0 })(smoke, 'rsa1_5') -conditional({ webcrypto: 0, electron: 0 })(smoke, 'x25519kw') -conditional({ webcrypto: 0 })(smoke, 'x25519dir') -conditional({ webcrypto: 0, electron: 0 })(smoke, 'x448kw') -conditional({ webcrypto: 0, electron: 0 })(smoke, 'x448dir') +conditional({ electron: 0 })(smoke, 'x25519kw') +conditional({ electron: 0 })(smoke, 'x448kw') +conditional({ electron: 0 })(smoke, 'x448dir') conditional({ webcrypto: 0 })('as keyobject', smoke, 'oct256c', undefined, undefined, true) conditional({ webcrypto: 0 })('as keyobject', smoke, 'oct384c', undefined, undefined, true) conditional({ webcrypto: 0 })('as keyobject', smoke, 'oct512c', undefined, undefined, true) diff --git a/test/jwk/jwk2key.test.mjs b/test/jwk/jwk2key.test.mjs index 42c04d46..79c7eb4f 100644 --- a/test/jwk/jwk2key.test.mjs +++ b/test/jwk/jwk2key.test.mjs @@ -173,10 +173,17 @@ const rsa = { qi: 'htPHLViOVG6QrldfuHn9evfdlD-UEuViOWNx8aKR3IBv0qegpJ78vYB4hdAcJZtBslKI97En5rzOAN3Y6Y8MbI4oN77WeiePJl2cMrS64evmlERvjJ6ZTs8jK0iV5q_gIZ9Qg9drmolUgb_CccQOBFbqSL6YkXwCBxlkCrzTlhc', kty: 'RSA', } +const x25519 = { + crv: 'X25519', + x: 'axR8Q7PEd74nY9nWaAoAYpMe3gp5sWbau6V6X1inPw4', + d: 'aCvvb3jEBnxJJBjCIN2a9ZDTL-HG6LVgBbij4m8-d3Y', + kty: 'OKP', +} test(testKeyImportExport, { ...rsa, alg: 'RS256' }) test(testKeyImportExport, { ...rsa, alg: 'PS256' }) test(testKeyImportExport, { ...rsa, alg: 'RSA-OAEP' }) test(testKeyImportExport, { ...rsa, alg: 'RSA-OAEP-256' }) +test(testKeyImportExport, { ...x25519, alg: 'ECDH-ES' }) test('Uin8tArray can be transformed to a JWK', async (t) => { t.deepEqual( @@ -229,17 +236,10 @@ const ed448 = { kty: 'OKP', } conditional({ webcrypto: 1, electron: 0 })(testKeyImportExport, { ...ed448, alg: 'EdDSA' }) -const x25519 = { - crv: 'X25519', - x: 'axR8Q7PEd74nY9nWaAoAYpMe3gp5sWbau6V6X1inPw4', - d: 'aCvvb3jEBnxJJBjCIN2a9ZDTL-HG6LVgBbij4m8-d3Y', - kty: 'OKP', -} -conditional({ webcrypto: 0 })(testKeyImportExport, { ...x25519, alg: 'ECDH-ES' }) const x448 = { crv: 'X448', x: 'z8s0Ej7D4pgIDu233UHoDW48EbiEm5eFv8_LuFwRr0xVREHhCtdxH75x6J8egZbjDGweOSbeHbY', d: 'xBrCwLlrHa1ov2cbmD4eMw4t6DoN_MWsBT_mxcA_QWsCS_9sKMRyFpphNN9_2iKrGPTC9pWCS5w', kty: 'OKP', } -conditional({ webcrypto: 0, electron: 0 })(testKeyImportExport, { ...x448, alg: 'ECDH-ES' }) +conditional({ electron: 0 })(testKeyImportExport, { ...x448, alg: 'ECDH-ES' }) diff --git a/test/key/importexport.test.mjs b/test/key/importexport.test.mjs index 8d8f7c00..7324e16d 100644 --- a/test/key/importexport.test.mjs +++ b/test/key/importexport.test.mjs @@ -174,25 +174,15 @@ for (const alg of ['EdDSA']) { } for (const alg of ['ECDH-ES', 'ECDH-ES+A128KW', 'ECDH-ES+A192KW', 'ECDH-ES+A256KW']) { - conditional({ webcrypto: 0, electron: 1 })( - `import SPKI x25519 for ${alg}`, - testSPKI, - keys.x25519.publicKey, - alg, - ) - conditional({ webcrypto: 0, electron: 1 })( - `import PKCS8 x25519 for ${alg}`, - testPKCS8, - keys.x25519.privateKey, - alg, - ) - conditional({ webcrypto: 0, electron: 0 })( + test(`import SPKI x25519 for ${alg}`, testSPKI, keys.x25519.publicKey, alg) + test(`import PKCS8 x25519 for ${alg}`, testPKCS8, keys.x25519.privateKey, alg) + conditional({ electron: 0 })( `import SPKI x448 for ${alg}`, testSPKI, keys.x448.publicKey, alg, ) - conditional({ webcrypto: 0, electron: 0 })( + conditional({ electron: 0 })( `import PKCS8 x448 for ${alg}`, testPKCS8, keys.x448.privateKey, diff --git a/test/util/generators.test.mjs b/test/util/generators.test.mjs index 9df982d8..d72a9e45 100644 --- a/test/util/generators.test.mjs +++ b/test/util/generators.test.mjs @@ -143,22 +143,22 @@ conditional({ webcrypto: 0 })('with modulusLength', testKeyPair, 'RSA1_5', { modulusLength: 4096, }) for (const crv of ['X25519', 'X448']) { - conditional({ webcrypto: 0, electron: crv === 'X25519' })(`crv: ${crv}`, testKeyPair, 'ECDH-ES', { + conditional({ electron: crv === 'X25519' })(`crv: ${crv}`, testKeyPair, 'ECDH-ES', { crv, }) - conditional({ webcrypto: 0, electron: crv === 'X25519' })( + conditional({ electron: crv === 'X25519' })( `crv: ${crv}`, testKeyPair, 'ECDH-ES+A128KW', { crv }, ) - conditional({ webcrypto: 0, electron: crv === 'X25519' })( + conditional({ electron: crv === 'X25519' })( `crv: ${crv}`, testKeyPair, 'ECDH-ES+A192KW', { crv }, ) - conditional({ webcrypto: 0, electron: crv === 'X25519' })( + conditional({ electron: crv === 'X25519' })( `crv: ${crv}`, testKeyPair, 'ECDH-ES+A256KW',