import { Buffer as SBuffer } from 'safe-buffer'; import type { SubtleAlgorithm, KeyUsage, BinaryLike, BufferLike, JWK, AnyAlgorithm, ImportFormat, AesKeyGenParams, EncryptDecryptParams, Operation, AesCtrParams, AesCbcParams, AesGcmParams, AesOcbParams, RsaOaepParams, ChaCha20Poly1305Params, } from './utils'; import { KFormatType, KeyEncoding, KeyType, kNamedCurveAliases } from './utils'; import { Buffer } from '@craftzdog/react-native-buffer'; import { CryptoKey, KeyObject, PublicKeyObject, PrivateKeyObject, SecretKeyObject, } from './keys'; import type { CryptoKeyPair } from './utils/types'; import { binaryLikeToArrayBuffer, bufferLikeToArrayBuffer, } from './utils/conversion'; import { argon2Sync } from './argon2'; import { lazyDOMException } from './utils/errors'; import { normalizeHashName, HashContext } from './utils/hashnames'; import { validateJwkStructure, validateMaxBufferLength, } from './utils/validation'; import { asyncDigest } from './hash'; import { createSecretKey, createPublicKey } from './keys'; import { NitroModules } from 'react-native-nitro-modules'; import type { KeyObjectHandle } from './specs/keyObjectHandle.nitro'; import type { RsaCipher } from './specs/rsaCipher.nitro'; import type { CipherFactory } from './specs/cipher.nitro'; import { pbkdf2DeriveBits } from './pbkdf2'; import { ecImportKey, ecdsaSignVerify, ec_generateKeyPair, ecDeriveBits, } from './ec'; import { rsa_generateKeyPair } from './rsa'; import { getRandomValues } from './random'; import { createHmac } from './hmac'; import type { Kmac } from './specs/kmac.nitro'; import { timingSafeEqual } from './utils/timingSafeEqual'; import { createSign, createVerify } from './keys/signVerify'; import { ed_generateKeyPairWebCrypto, x_generateKeyPairWebCrypto, xDeriveBits, Ed, } from './ed'; import { mldsa_generateKeyPairWebCrypto, type MlDsaVariant } from './mldsa'; import { slhdsa_generateKeyPairWebCrypto, SLH_DSA_VARIANTS, type SlhDsaVariant, } from './slhdsa'; import { mlkem_generateKeyPairWebCrypto, type MlKemVariant, MlKem, } from './mlkem'; import type { EncapsulateResult } from './utils'; import { hkdfDeriveBits, type HkdfAlgorithm } from './hkdf'; // Temporary enums that need to be defined enum KWebCryptoKeyFormat { kWebCryptoKeyFormatRaw, kWebCryptoKeyFormatSPKI, kWebCryptoKeyFormatPKCS8, } enum CipherOrWrapMode { kWebCryptoCipherEncrypt, kWebCryptoCipherDecrypt, } // Placeholder functions that need to be implemented function hasAnyNotIn(usages: KeyUsage[], allowed: KeyUsage[]): boolean { return usages.some(usage => !allowed.includes(usage)); } // Mirrors webidl.requiredArguments. Node throws TypeError when a SubtleCrypto // method is called with fewer than the spec-required number of arguments // (webcrypto.js:866 etc.); we relied on TypeScript types alone, which apps // catching `instanceof TypeError` could not see at runtime. function requireArgs(actual: number, required: number, method: string): void { if (actual < required) { throw new TypeError( `Failed to execute '${method}' on 'SubtleCrypto': ${required} arguments required, but only ${actual} present.`, ); } } // WebCrypto §18.4.4: algorithm name lookup is case-insensitive, but the // canonical mixed-case form is preserved in the resulting `name` field // (e.g. "aes-gcm" → "AES-GCM"). This map is built lazily on first call so // the registry of canonical names below can stay declared after the // function. Without this, callers who pass lowercase strings bypass the // downstream `SUPPORTED_ALGORITHMS` set comparisons silently. // // The map's value type is `AnyAlgorithm` so callers can use the lookup // result directly without re-asserting. The `as AnyAlgorithm` at insertion // is the single contract boundary: every name in `SUPPORTED_ALGORITHMS` is // already a member of `AnyAlgorithm` by construction. let _canonicalAlgorithmNames: Map | null = null; function getCanonicalAlgorithmNames(): Map { if (_canonicalAlgorithmNames === null) { const map = new Map(); for (const set of Object.values(SUPPORTED_ALGORITHMS)) { if (!set) continue; for (const name of set) { map.set(name.toLowerCase(), name as AnyAlgorithm); } } _canonicalAlgorithmNames = map; } return _canonicalAlgorithmNames; } // Per-algorithm WebIDL converter table. Mirrors Node's kAlgorithmDefinitions // (lib/internal/crypto/util.js): each (algorithm, operation) pair maps to a // dictionary converter name, or null when only the `name` member is required. // Operation keys are missing when an algorithm cannot perform that operation, // causing `normalizeAlgorithm` to reject the call. const kAlgorithmDefinitions: Record> = { 'AES-CBC': { generateKey: 'AesKeyGenParams', exportKey: null, importKey: null, encrypt: 'AesCbcParams', decrypt: 'AesCbcParams', 'get key length': 'AesDerivedKeyParams', }, 'AES-CTR': { generateKey: 'AesKeyGenParams', exportKey: null, importKey: null, encrypt: 'AesCtrParams', decrypt: 'AesCtrParams', 'get key length': 'AesDerivedKeyParams', }, 'AES-GCM': { generateKey: 'AesKeyGenParams', exportKey: null, importKey: null, encrypt: 'AeadParams', decrypt: 'AeadParams', 'get key length': 'AesDerivedKeyParams', }, 'AES-KW': { generateKey: 'AesKeyGenParams', exportKey: null, importKey: null, 'get key length': 'AesDerivedKeyParams', wrapKey: null, unwrapKey: null, }, 'AES-OCB': { generateKey: 'AesKeyGenParams', exportKey: null, importKey: null, encrypt: 'AeadParams', decrypt: 'AeadParams', 'get key length': 'AesDerivedKeyParams', }, Argon2d: { deriveBits: 'Argon2Params', 'get key length': null, importKey: null, }, Argon2i: { deriveBits: 'Argon2Params', 'get key length': null, importKey: null, }, Argon2id: { deriveBits: 'Argon2Params', 'get key length': null, importKey: null, }, 'ChaCha20-Poly1305': { generateKey: null, exportKey: null, importKey: null, encrypt: 'AeadParams', decrypt: 'AeadParams', 'get key length': null, }, ECDH: { generateKey: 'EcKeyGenParams', exportKey: null, importKey: 'EcKeyImportParams', deriveBits: 'EcdhKeyDeriveParams', }, ECDSA: { generateKey: 'EcKeyGenParams', exportKey: null, importKey: 'EcKeyImportParams', sign: 'EcdsaParams', verify: 'EcdsaParams', }, Ed25519: { generateKey: null, exportKey: null, importKey: null, sign: null, verify: null, }, Ed448: { generateKey: null, exportKey: null, importKey: null, sign: 'ContextParams', verify: 'ContextParams', }, HKDF: { importKey: null, deriveBits: 'HkdfParams', 'get key length': null, }, HMAC: { generateKey: 'HmacKeyGenParams', exportKey: null, importKey: 'HmacImportParams', sign: null, verify: null, 'get key length': 'HmacImportParams', }, KMAC128: { generateKey: 'KmacKeyGenParams', exportKey: null, importKey: 'KmacImportParams', sign: 'KmacParams', verify: 'KmacParams', 'get key length': 'KmacImportParams', }, KMAC256: { generateKey: 'KmacKeyGenParams', exportKey: null, importKey: 'KmacImportParams', sign: 'KmacParams', verify: 'KmacParams', 'get key length': 'KmacImportParams', }, 'ML-DSA-44': { generateKey: null, exportKey: null, importKey: null, sign: 'ContextParams', verify: 'ContextParams', }, 'ML-DSA-65': { generateKey: null, exportKey: null, importKey: null, sign: 'ContextParams', verify: 'ContextParams', }, 'ML-DSA-87': { generateKey: null, exportKey: null, importKey: null, sign: 'ContextParams', verify: 'ContextParams', }, 'ML-KEM-512': { generateKey: null, exportKey: null, importKey: null, encapsulateBits: null, decapsulateBits: null, encapsulateKey: null, decapsulateKey: null, }, 'ML-KEM-768': { generateKey: null, exportKey: null, importKey: null, encapsulateBits: null, decapsulateBits: null, encapsulateKey: null, decapsulateKey: null, }, 'ML-KEM-1024': { generateKey: null, exportKey: null, importKey: null, encapsulateBits: null, decapsulateBits: null, encapsulateKey: null, decapsulateKey: null, }, PBKDF2: { importKey: null, deriveBits: 'Pbkdf2Params', 'get key length': null, }, 'RSA-OAEP': { generateKey: 'RsaHashedKeyGenParams', exportKey: null, importKey: 'RsaHashedImportParams', encrypt: 'RsaOaepParams', decrypt: 'RsaOaepParams', }, 'RSA-PSS': { generateKey: 'RsaHashedKeyGenParams', exportKey: null, importKey: 'RsaHashedImportParams', sign: 'RsaPssParams', verify: 'RsaPssParams', }, 'RSASSA-PKCS1-v1_5': { generateKey: 'RsaHashedKeyGenParams', exportKey: null, importKey: 'RsaHashedImportParams', sign: null, verify: null, }, 'SHA-1': { digest: null }, 'SHA-256': { digest: null }, 'SHA-384': { digest: null }, 'SHA-512': { digest: null }, 'SHA3-256': { digest: null }, 'SHA3-384': { digest: null }, 'SHA3-512': { digest: null }, cSHAKE128: { digest: 'CShakeParams' }, cSHAKE256: { digest: 'CShakeParams' }, KT128: { digest: 'KangarooTwelveParams' }, KT256: { digest: 'KangarooTwelveParams' }, TurboSHAKE128: { digest: 'TurboShakeParams' }, TurboSHAKE256: { digest: 'TurboShakeParams' }, X25519: { generateKey: null, exportKey: null, importKey: null, deriveBits: 'EcdhKeyDeriveParams', }, X448: { generateKey: null, exportKey: null, importKey: null, deriveBits: 'EcdhKeyDeriveParams', }, }; for (const v of SLH_DSA_VARIANTS) { kAlgorithmDefinitions[v] = { generateKey: null, exportKey: null, importKey: null, sign: null, verify: null, }; } // WebIDL dictionary member specs. Mirrors Node's per-converter // `createDictionaryConverter` definitions in lib/internal/crypto/webidl.js. // `required: true` causes `normalizeAlgorithm` to throw a TypeError when the // member is missing — matching the spec'd WebCrypto behavior that // `SubtleCrypto.supports` relies on via try/catch. interface IdlField { key: string; required?: boolean; } type NormalizedAlgorithmRecord = SubtleAlgorithm & Record; const kRequiredFields: Record = { AesKeyGenParams: [{ key: 'length', required: true }], AesDerivedKeyParams: [{ key: 'length', required: true }], AesCbcParams: [{ key: 'iv', required: true }], AesCtrParams: [ { key: 'counter', required: true }, { key: 'length', required: true }, ], AeadParams: [ { key: 'iv', required: true }, { key: 'tagLength' }, { key: 'additionalData' }, ], EcKeyGenParams: [{ key: 'namedCurve', required: true }], EcKeyImportParams: [{ key: 'namedCurve', required: true }], EcdsaParams: [{ key: 'hash', required: true }], EcdhKeyDeriveParams: [{ key: 'public', required: true }], HmacKeyGenParams: [{ key: 'hash', required: true }, { key: 'length' }], HmacImportParams: [{ key: 'hash', required: true }, { key: 'length' }], HkdfParams: [ { key: 'hash', required: true }, { key: 'salt', required: true }, { key: 'info', required: true }, ], Pbkdf2Params: [ { key: 'hash', required: true }, { key: 'iterations', required: true }, { key: 'salt', required: true }, ], RsaHashedKeyGenParams: [ { key: 'modulusLength', required: true }, { key: 'publicExponent', required: true }, { key: 'hash', required: true }, ], RsaHashedImportParams: [{ key: 'hash', required: true }], RsaOaepParams: [{ key: 'label' }], RsaPssParams: [{ key: 'saltLength', required: true }], ContextParams: [{ key: 'context' }], Argon2Params: [ { key: 'nonce', required: true }, { key: 'parallelism', required: true }, { key: 'memory', required: true }, { key: 'passes', required: true }, { key: 'version' }, { key: 'secretValue' }, { key: 'associatedData' }, ], KmacKeyGenParams: [{ key: 'length' }], KmacImportParams: [{ key: 'length' }], KmacParams: [ { key: 'outputLength', required: true }, { key: 'customization' }, ], CShakeParams: [ { key: 'outputLength', required: true }, { key: 'functionName' }, { key: 'customization' }, ], KangarooTwelveParams: [ { key: 'outputLength', required: true }, { key: 'customization' }, ], TurboShakeParams: [ { key: 'outputLength', required: true }, { key: 'domainSeparation' }, ], }; function isBufferSource(value: unknown): value is BufferLike { return value instanceof ArrayBuffer || ArrayBuffer.isView(value); } function validateBufferSource( algorithm: NormalizedAlgorithmRecord, key: string, ): ArrayBuffer | undefined { const value = algorithm[key]; if (value === undefined) return undefined; if (!isBufferSource(value)) { throw new TypeError( `Failed to normalize algorithm: '${key}' must be a BufferSource`, ); } return bufferLikeToArrayBuffer(value); } function validateBinaryLike( algorithm: NormalizedAlgorithmRecord, key: string, ): ArrayBuffer | undefined { const value = algorithm[key]; if (value === undefined) return undefined; try { return binaryLikeToArrayBuffer(value as BinaryLike); } catch { throw new TypeError( `Failed to normalize algorithm: '${key}' must be a BufferSource`, ); } } function validateByteLength( buffer: ArrayBuffer | undefined, key: string, length: number, message?: string, ): void { if (buffer !== undefined && buffer.byteLength !== length) { throw lazyDOMException( message ?? `${key} must be ${length} bytes`, 'OperationError', ); } } function validateUnsignedInteger( algorithm: NormalizedAlgorithmRecord, key: string, ): number | undefined { const value = algorithm[key]; if (value === undefined) return undefined; const numberValue = Number(value); if ( !Number.isFinite(numberValue) || !Number.isInteger(numberValue) || numberValue < 0 ) { throw new TypeError( `Failed to normalize algorithm: '${key}' must be an unsigned integer`, ); } algorithm[key] = numberValue; return numberValue; } function validateHashAlgorithm( algorithm: NormalizedAlgorithmRecord, converterName: string, ): void { const hash = algorithm.hash as string | { name: string } | undefined; if (hash === undefined) return; const normalizedHash = normalizeHashName(hash, HashContext.WebCrypto); if ( ![ 'SHA-1', 'SHA-256', 'SHA-384', 'SHA-512', 'SHA3-256', 'SHA3-384', 'SHA3-512', ].includes(normalizedHash) ) { throw lazyDOMException( `Unsupported ${converterName}.hash`, 'NotSupportedError', ); } algorithm.hash = { name: normalizedHash }; } function validateAesLength(length: number | undefined): void { if ( length !== undefined && length !== 128 && length !== 192 && length !== 256 ) { throw lazyDOMException('Invalid key length', 'OperationError'); } } function validateMacLength( algorithm: NormalizedAlgorithmRecord, key: string, zeroError: 'DataError' | 'OperationError', ): void { const length = validateUnsignedInteger(algorithm, key); if (length === undefined) return; if (length === 0) { throw lazyDOMException(`${key} cannot be 0`, zeroError); } if (length % 8) { throw lazyDOMException(`Unsupported ${key}`, 'NotSupportedError'); } } function validateAeadParams(algorithm: NormalizedAlgorithmRecord): void { const iv = validateBufferSource(algorithm, 'iv'); const tagLength = validateUnsignedInteger(algorithm, 'tagLength'); validateBufferSource(algorithm, 'additionalData'); switch (algorithm.name) { case 'AES-GCM': if ( tagLength !== undefined && ![32, 64, 96, 104, 112, 120, 128].includes(tagLength) ) { throw lazyDOMException( `${tagLength} is not a valid AES-GCM tag length`, 'OperationError', ); } break; case 'AES-OCB': if (iv !== undefined && (iv.byteLength < 1 || iv.byteLength > 15)) { throw lazyDOMException( 'AES-OCB algorithm.iv must be between 1 and 15 bytes', 'OperationError', ); } if (tagLength !== undefined && ![64, 96, 128].includes(tagLength)) { throw lazyDOMException( `${tagLength} is not a valid AES-OCB tag length`, 'OperationError', ); } break; case 'ChaCha20-Poly1305': validateByteLength( iv, 'algorithm.iv', 12, 'ChaCha20-Poly1305 IV must be exactly 12 bytes', ); if (tagLength !== undefined && tagLength !== 128) { throw lazyDOMException( `${tagLength} is not a valid ChaCha20-Poly1305 tag length`, 'OperationError', ); } break; } } function validateNormalizedAlgorithm( converterName: string, algorithm: NormalizedAlgorithmRecord, ): void { switch (converterName) { case 'AesKeyGenParams': case 'AesDerivedKeyParams': validateAesLength(validateUnsignedInteger(algorithm, 'length')); break; case 'AesCbcParams': validateByteLength( validateBufferSource(algorithm, 'iv'), 'algorithm.iv', 16, 'algorithm.iv must contain exactly 16 bytes', ); break; case 'AesCtrParams': { validateByteLength( validateBufferSource(algorithm, 'counter'), 'algorithm.counter', 16, ); const length = validateUnsignedInteger(algorithm, 'length'); if (length !== undefined && (length === 0 || length > 128)) { throw lazyDOMException( 'AES-CTR algorithm.length must be between 1 and 128', 'OperationError', ); } break; } case 'AeadParams': validateAeadParams(algorithm); break; case 'EcdsaParams': case 'HmacKeyGenParams': case 'HmacImportParams': case 'HkdfParams': case 'Pbkdf2Params': case 'RsaHashedKeyGenParams': case 'RsaHashedImportParams': validateHashAlgorithm(algorithm, converterName); if (converterName === 'HmacKeyGenParams') { validateMacLength(algorithm, 'length', 'OperationError'); } if (converterName === 'HkdfParams') { validateBinaryLike(algorithm, 'salt'); validateBinaryLike(algorithm, 'info'); } else if (converterName === 'Pbkdf2Params') { const iterations = validateUnsignedInteger(algorithm, 'iterations'); if (iterations === 0) { throw lazyDOMException('iterations cannot be zero', 'OperationError'); } validateBinaryLike(algorithm, 'salt'); } else if (converterName === 'RsaHashedKeyGenParams') { validateUnsignedInteger(algorithm, 'modulusLength'); validateBufferSource(algorithm, 'publicExponent'); } break; case 'RsaPssParams': validateUnsignedInteger(algorithm, 'saltLength'); break; case 'RsaOaepParams': validateBufferSource(algorithm, 'label'); break; case 'ContextParams': validateBufferSource(algorithm, 'context'); break; case 'EcdhKeyDeriveParams': if (!(algorithm.public instanceof CryptoKey)) { throw lazyDOMException( 'algorithm.public must be a public key', 'InvalidAccessError', ); } break; case 'Argon2Params': { validateBufferSource(algorithm, 'nonce'); const parallelism = validateUnsignedInteger(algorithm, 'parallelism'); const memory = validateUnsignedInteger(algorithm, 'memory'); validateUnsignedInteger(algorithm, 'passes'); const version = validateUnsignedInteger(algorithm, 'version'); validateBufferSource(algorithm, 'secretValue'); validateBufferSource(algorithm, 'associatedData'); if ( parallelism !== undefined && (parallelism === 0 || parallelism > 2 ** 24 - 1) ) { throw lazyDOMException( 'parallelism must be > 0 and < 16777215', 'OperationError', ); } if ( memory !== undefined && parallelism !== undefined && memory < 8 * parallelism ) { throw lazyDOMException( 'memory must be at least 8 times the degree of parallelism', 'OperationError', ); } if (version !== undefined && version !== 0x13) { throw lazyDOMException( `${version} is not a valid Argon2 version`, 'OperationError', ); } break; } case 'KmacKeyGenParams': validateMacLength(algorithm, 'length', 'OperationError'); break; case 'KmacImportParams': validateMacLength(algorithm, 'length', 'DataError'); break; case 'KmacParams': validateMacLength(algorithm, 'outputLength', 'OperationError'); validateBufferSource(algorithm, 'customization'); break; case 'CShakeParams': case 'KangarooTwelveParams': { const outputLength = validateUnsignedInteger(algorithm, 'outputLength'); if ( outputLength !== undefined && (outputLength === 0 || outputLength % 8) ) { throw lazyDOMException( `Invalid ${converterName} outputLength`, 'OperationError', ); } validateBufferSource(algorithm, 'functionName'); validateBufferSource(algorithm, 'customization'); break; } case 'TurboShakeParams': { const outputLength = validateUnsignedInteger(algorithm, 'outputLength'); const domainSeparation = validateUnsignedInteger( algorithm, 'domainSeparation', ); if ( outputLength !== undefined && (outputLength === 0 || outputLength % 8) ) { throw lazyDOMException( 'Invalid TurboShakeParams outputLength', 'OperationError', ); } if ( domainSeparation !== undefined && (domainSeparation < 0x01 || domainSeparation > 0x7f) ) { throw lazyDOMException( 'TurboShakeParams.domainSeparation must be in range 0x01-0x7f', 'OperationError', ); } break; } } } // WebCrypto §18.4.4 algorithm normalization. Mirrors Node's normalizeAlgorithm // in lib/internal/crypto/util.js: canonicalizes `name`, looks up the // (name, op) → converter mapping, and rejects inputs that omit required // dictionary members. Callers in Subtle.supports rely on this throwing for // invalid params — without it, supports() over-reports capability (#1025). function normalizeAlgorithm( algorithm: SubtleAlgorithm | AnyAlgorithm, operation: Operation | string, ): SubtleAlgorithm { if (typeof algorithm === 'string') { return normalizeAlgorithm({ name: algorithm }, operation); } const name = (algorithm as { name?: unknown }).name; if (typeof name !== 'string') { throw new TypeError("Algorithm: 'name' is required"); } const map = getCanonicalAlgorithmNames(); const canonical = map.get(name.toLowerCase()); if (canonical === undefined) { throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError'); } const opMap = kAlgorithmDefinitions[canonical]; if (!opMap || !(operation in opMap)) { throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError'); } const converterName = opMap[operation]; if (converterName == null) { return { name: canonical }; } const fields = kRequiredFields[converterName]; if (!fields) { return { ...algorithm, name: canonical }; } const out = { name: canonical } as NormalizedAlgorithmRecord; const src = algorithm as Record; for (const field of fields) { const value = src[field.key]; if (value === undefined) { if (field.required) { throw new TypeError( `Failed to normalize algorithm: '${field.key}' is required in '${converterName}'`, ); } continue; } (out as Record)[field.key] = value; } validateNormalizedAlgorithm(converterName, out); return out; } function getAlgorithmName(name: string, length: number): string { switch (name) { case 'AES-CBC': return `A${length}CBC`; case 'AES-CTR': return `A${length}CTR`; case 'AES-GCM': return `A${length}GCM`; case 'AES-KW': return `A${length}KW`; case 'AES-OCB': return `A${length}OCB`; case 'ChaCha20-Poly1305': return 'C20P'; default: return `${name}${length}`; } } // Mirrors Node's aliasKeyFormat (lib/internal/crypto/webcrypto.js): for // algorithms whose import/export accepts both 'raw' and the disambiguated // 'raw-secret' / 'raw-public', collapse the latter to 'raw'. Used per-algorithm // — algorithms that demand the disambiguated form (KMAC, AES-OCB, // ChaCha20-Poly1305, Argon2*, ML-DSA, ML-KEM) MUST NOT alias. function aliasKeyFormat(format: ImportFormat): ImportFormat { if (format === 'raw-secret' || format === 'raw-public') return 'raw'; return format; } const kUncompressedSpkiLength: Record = { 'P-256': 91, 'P-384': 120, 'P-521': 158, }; function ecExportKey(key: CryptoKey, format: KWebCryptoKeyFormat): ArrayBuffer { const keyObject = key.keyObject; if (format === KWebCryptoKeyFormat.kWebCryptoKeyFormatRaw) { return bufferLikeToArrayBuffer(keyObject.handle.exportKey()); } else if (format === KWebCryptoKeyFormat.kWebCryptoKeyFormatSPKI) { const exported = bufferLikeToArrayBuffer( keyObject.export({ format: 'der', type: 'spki' }), ); // WebCrypto requires uncompressed point format for SPKI exports. // If the key was imported in compressed form, re-export as uncompressed // by reconstructing the point from the JWK x,y coordinates and // round-tripping through initECRaw. const namedCurve = key.algorithm.namedCurve; const expected = namedCurve === undefined ? undefined : kUncompressedSpkiLength[namedCurve]; if (expected !== undefined && exported.byteLength !== expected) { const jwk = keyObject.handle.exportJwk({}, false); if (!jwk.x || !jwk.y) { throw lazyDOMException( 'Failed to re-export EC public key as uncompressed SPKI', 'OperationError', ); } const x = Buffer.from(jwk.x, 'base64url'); const y = Buffer.from(jwk.y, 'base64url'); const raw = new Uint8Array(1 + x.length + y.length); raw[0] = 0x04; raw.set(x, 1); raw.set(y, 1 + x.length); const tmp = NitroModules.createHybridObject('KeyObjectHandle'); const curveAlias = kNamedCurveAliases[namedCurve as keyof typeof kNamedCurveAliases]; if (!tmp.initECRaw(curveAlias, raw.buffer as ArrayBuffer)) { throw lazyDOMException( 'Failed to re-export EC public key as uncompressed SPKI', 'OperationError', ); } return bufferLikeToArrayBuffer( tmp.exportKey(KFormatType.DER, KeyEncoding.SPKI), ); } return exported; } else if (format === KWebCryptoKeyFormat.kWebCryptoKeyFormatPKCS8) { const exported = keyObject.export({ format: 'der', type: 'pkcs8' }); return bufferLikeToArrayBuffer(exported); } else { throw new Error(`Unsupported EC export format: ${format}`); } } function rsaExportKey( key: CryptoKey, format: KWebCryptoKeyFormat, ): ArrayBuffer { const keyObject = key.keyObject; if (format === KWebCryptoKeyFormat.kWebCryptoKeyFormatSPKI) { // Export public key in SPKI format const exported = keyObject.export({ format: 'der', type: 'spki' }); return bufferLikeToArrayBuffer(exported); } else if (format === KWebCryptoKeyFormat.kWebCryptoKeyFormatPKCS8) { // Export private key in PKCS8 format const exported = keyObject.export({ format: 'der', type: 'pkcs8' }); return bufferLikeToArrayBuffer(exported); } else { throw new Error(`Unsupported RSA export format: ${format}`); } } async function rsaCipher( mode: CipherOrWrapMode, key: CryptoKey, data: ArrayBuffer, algorithm: EncryptDecryptParams, ): Promise { const rsaParams = algorithm as RsaOaepParams; // Validate key type matches operation const expectedType = mode === CipherOrWrapMode.kWebCryptoCipherEncrypt ? 'public' : 'private'; if (key.type !== expectedType) { throw lazyDOMException( 'The requested operation is not valid for the provided key', 'InvalidAccessError', ); } // Get hash algorithm from key const hashAlgorithm = normalizeHashName(key.algorithm.hash); // Prepare label (optional) const label = rsaParams.label ? bufferLikeToArrayBuffer(rsaParams.label) : undefined; // Create RSA cipher instance const rsaCipherModule = NitroModules.createHybridObject('RsaCipher'); // RSA-OAEP padding constant = 4 const RSA_PKCS1_OAEP_PADDING = 4; if (mode === CipherOrWrapMode.kWebCryptoCipherEncrypt) { // Encrypt with public key return rsaCipherModule.encrypt( key.keyObject.handle, data, RSA_PKCS1_OAEP_PADDING, hashAlgorithm, label, ); } else { // Decrypt with private key return rsaCipherModule.decrypt( key.keyObject.handle, data, RSA_PKCS1_OAEP_PADDING, hashAlgorithm, label, ); } } async function aesCipher( mode: CipherOrWrapMode, key: CryptoKey, data: ArrayBuffer, algorithm: EncryptDecryptParams, ): Promise { const { name } = algorithm; switch (name) { case 'AES-CTR': return aesCtrCipher(mode, key, data, algorithm as AesCtrParams); case 'AES-CBC': return aesCbcCipher(mode, key, data, algorithm as AesCbcParams); case 'AES-GCM': return aesGcmCipher(mode, key, data, algorithm as AesGcmParams); case 'AES-OCB': return aesOcbCipher(mode, key, data, algorithm as AesOcbParams); default: throw lazyDOMException( `Unsupported AES algorithm: ${name}`, 'NotSupportedError', ); } } async function aesCtrCipher( mode: CipherOrWrapMode, key: CryptoKey, data: ArrayBuffer, algorithm: AesCtrParams, ): Promise { // Validate counter and length if (!algorithm.counter || algorithm.counter.byteLength !== 16) { throw lazyDOMException( 'AES-CTR algorithm.counter must be 16 bytes', 'OperationError', ); } if (algorithm.length < 1 || algorithm.length > 128) { throw lazyDOMException( 'AES-CTR algorithm.length must be between 1 and 128', 'OperationError', ); } // Get cipher type based on key length const keyLength = (key.algorithm as { length: number }).length; const cipherType = `aes-${keyLength}-ctr`; // Create cipher const factory = NitroModules.createHybridObject('CipherFactory'); const cipher = factory.createCipher({ isCipher: mode === CipherOrWrapMode.kWebCryptoCipherEncrypt, cipherType, cipherKey: bufferLikeToArrayBuffer(key.keyObject.export()), iv: bufferLikeToArrayBuffer(algorithm.counter), }); // Process data const updated = cipher.update(data); const final = cipher.final(); // Concatenate results const result = new Uint8Array(updated.byteLength + final.byteLength); result.set(new Uint8Array(updated), 0); result.set(new Uint8Array(final), updated.byteLength); return result.buffer; } async function aesCbcCipher( mode: CipherOrWrapMode, key: CryptoKey, data: ArrayBuffer, algorithm: AesCbcParams, ): Promise { // Validate IV const iv = bufferLikeToArrayBuffer(algorithm.iv); if (iv.byteLength !== 16) { throw lazyDOMException( 'algorithm.iv must contain exactly 16 bytes', 'OperationError', ); } // Get cipher type based on key length const keyLength = (key.algorithm as { length: number }).length; const cipherType = `aes-${keyLength}-cbc`; // Create cipher const factory = NitroModules.createHybridObject('CipherFactory'); const cipher = factory.createCipher({ isCipher: mode === CipherOrWrapMode.kWebCryptoCipherEncrypt, cipherType, cipherKey: bufferLikeToArrayBuffer(key.keyObject.export()), iv, }); // Process data const updated = cipher.update(data); const final = cipher.final(); // Concatenate results const result = new Uint8Array(updated.byteLength + final.byteLength); result.set(new Uint8Array(updated), 0); result.set(new Uint8Array(final), updated.byteLength); return result.buffer; } interface AeadCipherConfig { algorithmName: string; validTagLengths: number[]; cipherSuffix: string; iv: ArrayBuffer; } async function aesAeadCipher( mode: CipherOrWrapMode, key: CryptoKey, data: ArrayBuffer, config: AeadCipherConfig, additionalData?: BufferLike, tagLength: number = 128, ): Promise { if (!config.validTagLengths.includes(tagLength)) { throw lazyDOMException( `${tagLength} is not a valid ${config.algorithmName} tag length`, 'OperationError', ); } const tagByteLength = tagLength / 8; const keyLength = (key.algorithm as { length: number }).length; const cipherType = `aes-${keyLength}-${config.cipherSuffix}`; const factory = NitroModules.createHybridObject('CipherFactory'); const cipher = factory.createCipher({ isCipher: mode === CipherOrWrapMode.kWebCryptoCipherEncrypt, cipherType, cipherKey: bufferLikeToArrayBuffer(key.keyObject.export()), iv: config.iv, authTagLen: tagByteLength, }); let processData: ArrayBuffer; if (mode === CipherOrWrapMode.kWebCryptoCipherDecrypt) { const dataView = new Uint8Array(data); if (dataView.byteLength < tagByteLength) { throw lazyDOMException( 'The provided data is too small.', 'OperationError', ); } const ciphertextLength = dataView.byteLength - tagByteLength; processData = dataView.slice(0, ciphertextLength).buffer; const authTag = dataView.slice(ciphertextLength).buffer; cipher.setAuthTag(authTag); } else { processData = data; } if (additionalData) { cipher.setAAD(bufferLikeToArrayBuffer(additionalData)); } const updated = cipher.update(processData); const final = cipher.final(); if (mode === CipherOrWrapMode.kWebCryptoCipherEncrypt) { const tag = cipher.getAuthTag(); const result = new Uint8Array( updated.byteLength + final.byteLength + tag.byteLength, ); result.set(new Uint8Array(updated), 0); result.set(new Uint8Array(final), updated.byteLength); result.set(new Uint8Array(tag), updated.byteLength + final.byteLength); return result.buffer; } else { const result = new Uint8Array(updated.byteLength + final.byteLength); result.set(new Uint8Array(updated), 0); result.set(new Uint8Array(final), updated.byteLength); return result.buffer; } } async function aesGcmCipher( mode: CipherOrWrapMode, key: CryptoKey, data: ArrayBuffer, algorithm: AesGcmParams, ): Promise { return aesAeadCipher( mode, key, data, { algorithmName: 'AES-GCM', validTagLengths: [32, 64, 96, 104, 112, 120, 128], cipherSuffix: 'gcm', iv: bufferLikeToArrayBuffer(algorithm.iv), }, algorithm.additionalData, algorithm.tagLength, ); } async function aesOcbCipher( mode: CipherOrWrapMode, key: CryptoKey, data: ArrayBuffer, algorithm: AesOcbParams, ): Promise { const ivBuffer = bufferLikeToArrayBuffer(algorithm.iv); if (ivBuffer.byteLength < 1 || ivBuffer.byteLength > 15) { throw lazyDOMException( 'AES-OCB algorithm.iv must be between 1 and 15 bytes', 'OperationError', ); } return aesAeadCipher( mode, key, data, { algorithmName: 'AES-OCB', validTagLengths: [64, 96, 128], cipherSuffix: 'ocb', iv: ivBuffer, }, algorithm.additionalData, algorithm.tagLength, ); } async function aesKwCipher( mode: CipherOrWrapMode, key: CryptoKey, data: ArrayBuffer, ): Promise { const isWrap = mode === CipherOrWrapMode.kWebCryptoCipherEncrypt; // AES-KW requires input to be a multiple of 8 bytes (64 bits) if (data.byteLength % 8 !== 0) { throw lazyDOMException( `AES-KW input length must be a multiple of 8 bytes, got ${data.byteLength}`, 'OperationError', ); } // AES-KW requires at least 16 bytes of input (128 bits) if (isWrap && data.byteLength < 16) { throw lazyDOMException( `AES-KW input must be at least 16 bytes, got ${data.byteLength}`, 'OperationError', ); } // Get cipher type based on key length const keyLength = (key.algorithm as { length: number }).length; // Use aes*-wrap for both operations (matching Node.js) const cipherType = `aes${keyLength}-wrap`; // Export key material const exportedKey = key.keyObject.export(); const cipherKey = bufferLikeToArrayBuffer(exportedKey); // AES-KW uses a default IV as specified in RFC 3394 const defaultWrapIV = new Uint8Array([ 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, ]); const factory = NitroModules.createHybridObject('CipherFactory'); const cipher = factory.createCipher({ isCipher: isWrap, cipherType, cipherKey, iv: defaultWrapIV.buffer, // RFC 3394 default IV for AES-KW }); // Process data const updated = cipher.update(data); const final = cipher.final(); // Concatenate results const result = new Uint8Array(updated.byteLength + final.byteLength); result.set(new Uint8Array(updated), 0); result.set(new Uint8Array(final), updated.byteLength); return result.buffer; } async function chaCha20Poly1305Cipher( mode: CipherOrWrapMode, key: CryptoKey, data: ArrayBuffer, algorithm: ChaCha20Poly1305Params, ): Promise { const { iv, additionalData, tagLength = 128 } = algorithm; // Validate IV (must be 12 bytes for ChaCha20-Poly1305) const ivBuffer = bufferLikeToArrayBuffer(iv); if (!ivBuffer || ivBuffer.byteLength !== 12) { throw lazyDOMException( 'ChaCha20-Poly1305 IV must be exactly 12 bytes', 'OperationError', ); } // Validate tag length (only 128-bit supported) if (tagLength !== 128) { throw lazyDOMException( 'ChaCha20-Poly1305 only supports 128-bit auth tags', 'NotSupportedError', ); } const tagByteLength = 16; // 128 bits = 16 bytes // Create cipher using existing ChaCha20-Poly1305 implementation const factory = NitroModules.createHybridObject('CipherFactory'); const cipher = factory.createCipher({ isCipher: mode === CipherOrWrapMode.kWebCryptoCipherEncrypt, cipherType: 'chacha20-poly1305', cipherKey: bufferLikeToArrayBuffer(key.keyObject.export()), iv: ivBuffer, authTagLen: tagByteLength, }); let processData: ArrayBuffer; let authTag: ArrayBuffer | undefined; if (mode === CipherOrWrapMode.kWebCryptoCipherDecrypt) { // For decryption, extract auth tag from end of data const dataView = new Uint8Array(data); if (dataView.byteLength < tagByteLength) { throw lazyDOMException( 'The provided data is too small.', 'OperationError', ); } // Split data and tag const ciphertextLength = dataView.byteLength - tagByteLength; processData = dataView.slice(0, ciphertextLength).buffer; authTag = dataView.slice(ciphertextLength).buffer; // Set auth tag for verification cipher.setAuthTag(authTag); } else { processData = data; } // Set additional authenticated data if provided if (additionalData) { cipher.setAAD(bufferLikeToArrayBuffer(additionalData)); } // Process data const updated = cipher.update(processData); const final = cipher.final(); if (mode === CipherOrWrapMode.kWebCryptoCipherEncrypt) { // For encryption, append auth tag to result const tag = cipher.getAuthTag(); const result = new Uint8Array( updated.byteLength + final.byteLength + tag.byteLength, ); result.set(new Uint8Array(updated), 0); result.set(new Uint8Array(final), updated.byteLength); result.set(new Uint8Array(tag), updated.byteLength + final.byteLength); return result.buffer; } else { // For decryption, just concatenate plaintext const result = new Uint8Array(updated.byteLength + final.byteLength); result.set(new Uint8Array(updated), 0); result.set(new Uint8Array(final), updated.byteLength); return result.buffer; } } async function aesGenerateKey( algorithm: AesKeyGenParams, extractable: boolean, keyUsages: KeyUsage[], ): Promise { const { length } = algorithm; const name = algorithm.name; if (!name) { throw lazyDOMException('Algorithm name is required', 'OperationError'); } // Validate key length if (![128, 192, 256].includes(length)) { throw lazyDOMException( `Invalid AES key length: ${length}. Must be 128, 192, or 256.`, 'OperationError', ); } // Validate usages const validUsages: KeyUsage[] = [ 'encrypt', 'decrypt', 'wrapKey', 'unwrapKey', ]; if (hasAnyNotIn(keyUsages, validUsages)) { throw lazyDOMException(`Unsupported key usage for ${name}`, 'SyntaxError'); } // Generate random key bytes const keyBytes = new Uint8Array(length / 8); getRandomValues(keyBytes); // Create secret key const keyObject = createSecretKey(keyBytes); // Construct algorithm object with guaranteed name const keyAlgorithm: SubtleAlgorithm = { name, length }; return new CryptoKey(keyObject, keyAlgorithm, keyUsages, extractable); } async function hmacGenerateKey( algorithm: SubtleAlgorithm, extractable: boolean, keyUsages: KeyUsage[], ): Promise { // Validate usages if (hasAnyNotIn(keyUsages, ['sign', 'verify'])) { throw lazyDOMException('Unsupported key usage for HMAC key', 'SyntaxError'); } // Get hash algorithm const hash = algorithm.hash; if (!hash) { throw lazyDOMException( 'HMAC algorithm requires a hash parameter', 'TypeError', ); } const hashName = normalizeHashName(hash); // Determine key length let length = algorithm.length; if (length === undefined) { // Use hash output length as default key length switch (hashName) { case 'SHA-1': length = 160; break; case 'SHA-256': length = 256; break; case 'SHA-384': length = 384; break; case 'SHA-512': length = 512; break; default: length = 256; // Default to 256 bits } } if (length === 0) { throw lazyDOMException( 'Zero-length key is not supported', 'OperationError', ); } // Generate random key bytes const keyBytes = new Uint8Array(Math.ceil(length / 8)); getRandomValues(keyBytes); // Create secret key const keyObject = createSecretKey(keyBytes); // Construct algorithm object with hash normalized to { name: string } format per WebCrypto spec const webCryptoHashName = normalizeHashName(hash, HashContext.WebCrypto); const keyAlgorithm: SubtleAlgorithm = { name: 'HMAC', hash: { name: webCryptoHashName }, length, }; return new CryptoKey(keyObject, keyAlgorithm, keyUsages, extractable); } async function kmacGenerateKey( algorithm: SubtleAlgorithm, extractable: boolean, keyUsages: KeyUsage[], ): Promise { const { name } = algorithm; if (hasAnyNotIn(keyUsages, ['sign', 'verify'])) { throw lazyDOMException( `Unsupported key usage for ${name} key`, 'SyntaxError', ); } const defaultLength = name === 'KMAC128' ? 128 : 256; const length = algorithm.length ?? defaultLength; if (length === 0) { throw lazyDOMException( 'Zero-length key is not supported', 'OperationError', ); } const keyBytes = new Uint8Array(Math.ceil(length / 8)); getRandomValues(keyBytes); const keyObject = createSecretKey(keyBytes); const keyAlgorithm: SubtleAlgorithm = { name: name as AnyAlgorithm, length }; return new CryptoKey(keyObject, keyAlgorithm, keyUsages, extractable); } function kmacSignVerify( key: CryptoKey, data: BufferLike, algorithm: SubtleAlgorithm, signature?: BufferLike, ): ArrayBuffer | boolean { const { name } = algorithm; // KmacParams.outputLength is required per // https://wicg.github.io/webcrypto-modern-algos/#KmacParams-dictionary // and the rename from `length` (commit ab8dc2b84c2). Mirror Node's // mac.js:213-223 by reading `outputLength` (in bits). if (typeof algorithm.outputLength !== 'number') { throw lazyDOMException( `${name}Params.outputLength is required`, 'OperationError', ); } const outputLengthBits = algorithm.outputLength; if (outputLengthBits % 8 !== 0) { throw lazyDOMException( `Unsupported ${name}Params outputLength`, 'NotSupportedError', ); } const outputLengthBytes = outputLengthBits / 8; const keyData = key.keyObject.export(); const kmac = NitroModules.createHybridObject('Kmac'); let customizationBuffer: ArrayBuffer | undefined; if (algorithm.customization !== undefined) { customizationBuffer = bufferLikeToArrayBuffer(algorithm.customization); } kmac.createKmac( name, bufferLikeToArrayBuffer(keyData), outputLengthBytes, customizationBuffer, ); kmac.update(bufferLikeToArrayBuffer(data)); const computed = kmac.digest(); if (signature === undefined) { return computed; } const sigBuffer = bufferLikeToArrayBuffer(signature); if (computed.byteLength !== sigBuffer.byteLength) { return false; } return timingSafeEqual(new Uint8Array(computed), new Uint8Array(sigBuffer)); } async function kmacImportKey( algorithm: SubtleAlgorithm, format: ImportFormat, data: BufferLike | JWK, extractable: boolean, keyUsages: KeyUsage[], ): Promise { const { name } = algorithm; let keyObject: KeyObject; if (format === 'jwk') { const jwk = data as JWK; if (!jwk || typeof jwk !== 'object') { throw lazyDOMException('Invalid keyData', 'DataError'); } if (jwk.kty !== 'oct') { throw lazyDOMException('Invalid JWK "kty" Parameter', 'DataError'); } validateJwkStructure(jwk, extractable, keyUsages, 'sig'); const expectedAlg = name === 'KMAC128' ? 'K128' : 'K256'; if (jwk.alg !== undefined && jwk.alg !== expectedAlg) { throw lazyDOMException( 'JWK "alg" Parameter and algorithm name mismatch', 'DataError', ); } if (hasAnyNotIn(keyUsages, ['sign', 'verify'])) { throw lazyDOMException( `Unsupported key usage for ${name} key`, 'SyntaxError', ); } const handle = NitroModules.createHybridObject('KeyObjectHandle'); let keyType: KeyType | undefined; try { keyType = handle.initJwk(jwk, undefined); } catch (err) { throw lazyDOMException('Invalid keyData', { name: 'DataError', cause: err, }); } if (keyType === undefined || keyType !== 0) { throw lazyDOMException('Invalid keyData', 'DataError'); } keyObject = new SecretKeyObject(handle); } else if (format === 'raw-secret') { // KMAC accepts only the disambiguated 'raw-secret' form (Node mac.js:141-145 // returns undefined for plain 'raw' when not HMAC). if (hasAnyNotIn(keyUsages, ['sign', 'verify'])) { throw lazyDOMException( `Unsupported key usage for ${name} key`, 'SyntaxError', ); } keyObject = createSecretKey(data as BinaryLike); } else { throw lazyDOMException( `Unable to import ${name} key with format ${format}`, 'NotSupportedError', ); } const exported = keyObject.export(); const keyLength = exported.byteLength * 8; if (keyLength === 0) { throw lazyDOMException('Zero-length key is not supported', 'DataError'); } if (algorithm.length !== undefined && algorithm.length !== keyLength) { throw lazyDOMException('Invalid key length', 'DataError'); } const keyAlgorithm: SubtleAlgorithm = { name: name as AnyAlgorithm, length: keyLength, }; return new CryptoKey(keyObject, keyAlgorithm, keyUsages, extractable); } function rsaImportKey( format: ImportFormat, data: BufferLike | JWK, algorithm: SubtleAlgorithm, extractable: boolean, keyUsages: KeyUsage[], ): CryptoKey { const { name } = algorithm; let checkSet: KeyUsage[]; switch (name) { case 'RSASSA-PKCS1-v1_5': case 'RSA-PSS': checkSet = ['sign', 'verify']; break; case 'RSA-OAEP': checkSet = ['encrypt', 'decrypt', 'wrapKey', 'unwrapKey']; break; default: throw new Error(`Unsupported RSA algorithm: ${name}`); } const checkUsages = (): void => { if (hasAnyNotIn(keyUsages, checkSet)) { throw lazyDOMException( `Unsupported key usage for ${name} key`, 'SyntaxError', ); } }; let keyObject: KeyObject; if (format === 'jwk') { const jwk = data as JWK; if (!jwk || typeof jwk !== 'object') { throw lazyDOMException('Invalid keyData', 'DataError'); } if (jwk.kty !== 'RSA') { throw lazyDOMException('Invalid JWK "kty" Parameter', 'DataError'); } const expectedUse = name === 'RSA-OAEP' ? 'enc' : 'sig'; validateJwkStructure(jwk, extractable, keyUsages, expectedUse); checkUsages(); if (jwk.alg !== undefined) { let jwkContext: HashContext; switch (name) { case 'RSASSA-PKCS1-v1_5': jwkContext = HashContext.JwkRsa; break; case 'RSA-PSS': jwkContext = HashContext.JwkRsaPss; break; default: jwkContext = HashContext.JwkRsaOaep; } const expectedAlg = normalizeHashName(algorithm.hash, jwkContext); if (jwk.alg !== expectedAlg) { throw lazyDOMException( 'JWK "alg" does not match the requested algorithm', 'DataError', ); } } const handle = NitroModules.createHybridObject('KeyObjectHandle'); let keyType: KeyType | undefined; try { keyType = handle.initJwk(jwk, undefined); } catch (err) { throw lazyDOMException('Invalid keyData', { name: 'DataError', cause: err, }); } if (keyType === undefined) { throw lazyDOMException('Invalid keyData', 'DataError'); } if (keyType === KeyType.PUBLIC) { keyObject = new PublicKeyObject(handle); } else if (keyType === KeyType.PRIVATE) { keyObject = new PrivateKeyObject(handle); } else { throw lazyDOMException('Invalid keyData', 'DataError'); } } else if (format === 'spki') { checkUsages(); const keyData = bufferLikeToArrayBuffer(data as BufferLike); keyObject = KeyObject.createKeyObject( 'public', keyData, KFormatType.DER, KeyEncoding.SPKI, ); } else if (format === 'pkcs8') { checkUsages(); const keyData = bufferLikeToArrayBuffer(data as BufferLike); keyObject = KeyObject.createKeyObject( 'private', keyData, KFormatType.DER, KeyEncoding.PKCS8, ); } else { throw lazyDOMException( `Unsupported format for ${name} import: ${format}`, 'NotSupportedError', ); } // Get the modulus length from the key and add it to the algorithm const keyDetails = (keyObject as PublicKeyObject | PrivateKeyObject) .asymmetricKeyDetails; // Convert publicExponent number to big-endian byte array let publicExponentBytes: Uint8Array | undefined; if (keyDetails?.publicExponent) { const exp = keyDetails.publicExponent; // Convert number to big-endian bytes const bytes: number[] = []; let value = exp; while (value > 0) { bytes.unshift(value & 0xff); value = Math.floor(value / 256); } publicExponentBytes = new Uint8Array(bytes.length > 0 ? bytes : [0]); } // Normalize hash to { name: string } format per WebCrypto spec const hashName = normalizeHashName(algorithm.hash, HashContext.WebCrypto); const normalizedHash = { name: hashName }; const algorithmWithDetails = { ...algorithm, modulusLength: keyDetails?.modulusLength, publicExponent: publicExponentBytes, hash: normalizedHash, }; return new CryptoKey(keyObject, algorithmWithDetails, keyUsages, extractable); } async function hmacImportKey( algorithm: SubtleAlgorithm, format: ImportFormat, data: BufferLike | JWK, extractable: boolean, keyUsages: KeyUsage[], ): Promise { const checkUsages = (): void => { if (hasAnyNotIn(keyUsages, ['sign', 'verify'])) { throw new Error('Unsupported key usage for an HMAC key'); } }; let keyObject: KeyObject; if (format === 'jwk') { const jwk = data as JWK; if (!jwk || typeof jwk !== 'object') { throw new Error('Invalid keyData'); } if (jwk.kty !== 'oct') { throw new Error('Invalid JWK format for HMAC key'); } validateJwkStructure(jwk, extractable, keyUsages, 'sig'); checkUsages(); if (algorithm.length !== undefined) { if (!jwk.k) { throw new Error('JWK missing key data'); } const decoded = SBuffer.from(jwk.k, 'base64'); const keyBitLength = decoded.length * 8; if (algorithm.length === 0) { throw new Error('Zero-length key is not supported'); } if (algorithm.length !== keyBitLength) { throw new Error('Invalid key length'); } } const handle = NitroModules.createHybridObject('KeyObjectHandle'); let keyType: KeyType | undefined; try { keyType = handle.initJwk(jwk, undefined); } catch (err) { throw lazyDOMException('Invalid keyData', { name: 'DataError', cause: err, }); } if (keyType === undefined || keyType !== 0) { throw lazyDOMException('Invalid keyData', 'DataError'); } keyObject = new SecretKeyObject(handle); } else if (format === 'raw' || format === 'raw-secret') { // HMAC accepts both 'raw' and 'raw-secret' (Node mac.js:141-145). checkUsages(); keyObject = createSecretKey(data as BinaryLike); } else { throw lazyDOMException( `Unable to import HMAC key with format ${format}`, 'NotSupportedError', ); } // Normalize hash to { name: string } format per WebCrypto spec const hashName = normalizeHashName(algorithm.hash, HashContext.WebCrypto); const normalizedAlgorithm: SubtleAlgorithm = { ...algorithm, name: 'HMAC', hash: { name: hashName }, }; return new CryptoKey(keyObject, normalizedAlgorithm, keyUsages, extractable); } async function aesImportKey( algorithm: SubtleAlgorithm, format: ImportFormat, data: BufferLike | JWK, extractable: boolean, keyUsages: KeyUsage[], ): Promise { const { name, length } = algorithm; const validUsages: KeyUsage[] = [ 'encrypt', 'decrypt', 'wrapKey', 'unwrapKey', ]; const checkUsages = (): void => { if (hasAnyNotIn(keyUsages, validUsages)) { throw new Error(`Unsupported key usage for ${name}`); } }; // AES-OCB and ChaCha20-Poly1305 require the disambiguated 'raw-secret' form // and reject 'raw' (Node aes.js:243-249, chacha20_poly1305.js:104-134). // Other AES variants accept both 'raw' and 'raw-secret'. const requiresRawSecret = name === 'AES-OCB' || name === 'ChaCha20-Poly1305'; const acceptsRaw = format === 'raw-secret' || (format === 'raw' && !requiresRawSecret); let keyObject: KeyObject; let actualLength: number; if (format === 'jwk') { const jwk = data as JWK; if (jwk.kty !== 'oct') { throw new Error('Invalid JWK format for AES key'); } validateJwkStructure(jwk, extractable, keyUsages, 'enc'); checkUsages(); const handle = NitroModules.createHybridObject('KeyObjectHandle'); let keyType: KeyType | undefined; try { keyType = handle.initJwk(jwk, undefined); } catch (err) { throw lazyDOMException('Invalid keyData', { name: 'DataError', cause: err, }); } if (keyType === undefined || keyType !== 0) { throw lazyDOMException('Invalid keyData', 'DataError'); } keyObject = new SecretKeyObject(handle); const exported = keyObject.export(); actualLength = exported.byteLength * 8; } else if (acceptsRaw) { checkUsages(); const keyData = bufferLikeToArrayBuffer(data as BufferLike); actualLength = keyData.byteLength * 8; if (name === 'ChaCha20-Poly1305') { if (actualLength !== 256) { throw lazyDOMException( 'Invalid ChaCha20-Poly1305 key length', 'DataError', ); } } else if (![128, 192, 256].includes(actualLength)) { throw new Error('Invalid AES key length'); } keyObject = createSecretKey(keyData); } else { throw lazyDOMException( `Unable to import ${name} key with format ${format}`, 'NotSupportedError', ); } // Validate length if specified if (length !== undefined && length !== actualLength) { throw new Error( `Key length mismatch: expected ${length}, got ${actualLength}`, ); } return new CryptoKey( keyObject, { name, length: actualLength }, keyUsages, extractable, ); } function edImportKey( format: ImportFormat, data: BufferLike | JWK, algorithm: SubtleAlgorithm, extractable: boolean, keyUsages: KeyUsage[], ): CryptoKey { const { name } = algorithm; const isX = name === 'X25519' || name === 'X448'; const allowedUsages: KeyUsage[] = isX ? ['deriveKey', 'deriveBits'] : ['sign', 'verify']; const checkUsages = (): void => { if (hasAnyNotIn(keyUsages, allowedUsages)) { throw lazyDOMException( `Unsupported key usage for ${name} key`, 'SyntaxError', ); } }; let keyObject: KeyObject; if (format === 'spki') { checkUsages(); const keyData = bufferLikeToArrayBuffer(data as BufferLike); keyObject = KeyObject.createKeyObject( 'public', keyData, KFormatType.DER, KeyEncoding.SPKI, ); } else if (format === 'pkcs8') { checkUsages(); const keyData = bufferLikeToArrayBuffer(data as BufferLike); keyObject = KeyObject.createKeyObject( 'private', keyData, KFormatType.DER, KeyEncoding.PKCS8, ); } else if (format === 'raw') { checkUsages(); const keyData = bufferLikeToArrayBuffer(data as BufferLike); const handle = NitroModules.createHybridObject('KeyObjectHandle'); handle.init(1, keyData); keyObject = new PublicKeyObject(handle); } else if (format === 'jwk') { const jwkData = data as JWK; if (!jwkData || typeof jwkData !== 'object') { throw lazyDOMException('Invalid keyData', 'DataError'); } if (jwkData.kty !== 'OKP') { throw lazyDOMException('Invalid JWK "kty" Parameter', 'DataError'); } const expectedUse = isX ? 'enc' : 'sig'; validateJwkStructure(jwkData, extractable, keyUsages, expectedUse); if (jwkData.crv !== name) { throw lazyDOMException( 'JWK "crv" Parameter and algorithm name mismatch', 'DataError', ); } if (!isX && jwkData.alg !== undefined) { if (jwkData.alg !== name && jwkData.alg !== 'EdDSA') { throw lazyDOMException( 'JWK "alg" does not match the requested algorithm', 'DataError', ); } } checkUsages(); const handle = NitroModules.createHybridObject('KeyObjectHandle'); let keyType: KeyType | undefined; try { keyType = handle.initJwk(jwkData); } catch (err) { throw lazyDOMException('Invalid JWK data', { name: 'DataError', cause: err, }); } if (keyType === undefined) { throw lazyDOMException('Invalid JWK data', 'DataError'); } if (keyType === KeyType.PRIVATE) { keyObject = new PrivateKeyObject(handle); } else { keyObject = new PublicKeyObject(handle); } } else { throw lazyDOMException( `Unsupported format for ${name} import: ${format}`, 'NotSupportedError', ); } return new CryptoKey(keyObject, { name }, keyUsages, extractable); } // Lengths (in bytes) of seedless ML-DSA / ML-KEM PKCS#8 encodings. A PKCS#8 // blob of exactly this length contains only the expanded private key with no // seed; Node rejects these to keep cross-implementation interop intact. // Refs: node lib/internal/crypto/ml_dsa.js (mlDsaImportKey, pkcs8 case) // node lib/internal/crypto/ml_kem.js (mlKemImportKey, pkcs8 case) export const PQC_SEEDLESS_PKCS8_LENGTHS: Readonly> = { 'ML-DSA-44': 2588, 'ML-DSA-65': 4060, 'ML-DSA-87': 4924, 'ML-KEM-512': 1660, 'ML-KEM-768': 2428, 'ML-KEM-1024': 3196, }; // Map from PQC algorithm name to display family. Used to render the // import-rejection error message in the same form Node emits. const PQC_FAMILY: Readonly> = { 'ML-DSA-44': 'ML-DSA', 'ML-DSA-65': 'ML-DSA', 'ML-DSA-87': 'ML-DSA', 'ML-KEM-512': 'ML-KEM', 'ML-KEM-768': 'ML-KEM', 'ML-KEM-1024': 'ML-KEM', 'SLH-DSA-SHA2-128s': 'SLH-DSA', 'SLH-DSA-SHA2-128f': 'SLH-DSA', 'SLH-DSA-SHA2-192s': 'SLH-DSA', 'SLH-DSA-SHA2-192f': 'SLH-DSA', 'SLH-DSA-SHA2-256s': 'SLH-DSA', 'SLH-DSA-SHA2-256f': 'SLH-DSA', 'SLH-DSA-SHAKE-128s': 'SLH-DSA', 'SLH-DSA-SHAKE-128f': 'SLH-DSA', 'SLH-DSA-SHAKE-192s': 'SLH-DSA', 'SLH-DSA-SHAKE-192f': 'SLH-DSA', 'SLH-DSA-SHAKE-256s': 'SLH-DSA', 'SLH-DSA-SHAKE-256f': 'SLH-DSA', }; function pqcImportKeyObject( format: ImportFormat, data: BufferLike | JWK, name: string, ): { keyObject: KeyObject; isPublic: boolean } { if (format === 'spki') { return { keyObject: KeyObject.createKeyObject( 'public', bufferLikeToArrayBuffer(data as BufferLike), KFormatType.DER, KeyEncoding.SPKI, ), isPublic: true, }; } else if (format === 'pkcs8') { const ab = bufferLikeToArrayBuffer(data as BufferLike); const family = PQC_FAMILY[name]; if ( family !== undefined && ab.byteLength === PQC_SEEDLESS_PKCS8_LENGTHS[name] ) { throw lazyDOMException( `Importing an ${family} PKCS#8 key without a seed is not supported`, 'NotSupportedError', ); } return { keyObject: KeyObject.createKeyObject( 'private', ab, KFormatType.DER, KeyEncoding.PKCS8, ), isPublic: false, }; } else if (format === 'raw-public') { // ML-DSA / ML-KEM reject plain 'raw' — only 'raw-public' is accepted for // public-key import (Node webcrypto.js:493-499, 506-511). const handle = NitroModules.createHybridObject('KeyObjectHandle'); if ( !handle.initPqcRaw( name, bufferLikeToArrayBuffer(data as BufferLike), true, ) ) { throw lazyDOMException( `Failed to import ${name} raw public key`, 'DataError', ); } return { keyObject: new PublicKeyObject(handle), isPublic: true }; } else if (format === 'raw-seed') { const handle = NitroModules.createHybridObject('KeyObjectHandle'); if ( !handle.initPqcRaw( name, bufferLikeToArrayBuffer(data as BufferLike), false, ) ) { throw lazyDOMException(`Failed to import ${name} raw seed`, 'DataError'); } return { keyObject: new PrivateKeyObject(handle), isPublic: false }; } else if (format === 'jwk') { const jwkData = data as JWK; const isPublic = jwkData.priv === undefined; const handle = NitroModules.createHybridObject('KeyObjectHandle'); let keyType: KeyType | undefined; try { keyType = handle.initJwk(jwkData); } catch (err) { throw lazyDOMException('Invalid JWK data', { name: 'DataError', cause: err, }); } if (keyType === undefined) { throw lazyDOMException('Invalid JWK data', 'DataError'); } return { keyObject: isPublic ? new PublicKeyObject(handle) : new PrivateKeyObject(handle), isPublic, }; } throw lazyDOMException( `Unsupported format for ${name} import: ${format}`, 'NotSupportedError', ); } // Per WebCrypto AKP JWK rules, public-vs-private is determined by the presence // of `priv`. For binary formats it follows from the format itself. function pqcIsPublicImport( format: ImportFormat, data: BufferLike | JWK, ): boolean { if (format === 'jwk') { return ( typeof data === 'object' && data !== null && (data as JWK).priv === undefined ); } return format === 'spki' || format === 'raw-public'; } function validatePqcJwk( data: BufferLike | JWK, name: string, extractable: boolean, keyUsages: KeyUsage[], expectedUse: 'sig' | 'enc', ): void { if (typeof data !== 'object' || data === null) { throw lazyDOMException('Invalid keyData', 'DataError'); } const jwk = data as JWK; if (jwk.kty !== 'AKP') { throw lazyDOMException('Invalid JWK "kty" Parameter', 'DataError'); } validateJwkStructure(jwk, extractable, keyUsages, expectedUse); if (jwk.alg !== name) { throw lazyDOMException( 'JWK "alg" Parameter and algorithm name mismatch', 'DataError', ); } } // Validates that `format` is one of the formats PQC algorithms accept; rejects // plain 'raw' early so the format error wins over usage-based errors. function validatePqcFormat(format: ImportFormat, name: string): void { if ( format !== 'spki' && format !== 'pkcs8' && format !== 'raw-public' && format !== 'raw-seed' && format !== 'jwk' ) { throw lazyDOMException( `Unsupported format for ${name} import: ${format}`, 'NotSupportedError', ); } } function mldsaImportKey( format: ImportFormat, data: BufferLike | JWK, algorithm: SubtleAlgorithm, extractable: boolean, keyUsages: KeyUsage[], ): CryptoKey { const { name } = algorithm; validatePqcFormat(format, name); if (format === 'jwk') { validatePqcJwk(data, name, extractable, keyUsages, 'sig'); } const isPublic = pqcIsPublicImport(format, data); if (hasAnyNotIn(keyUsages, isPublic ? ['verify'] : ['sign'])) { throw lazyDOMException( `Unsupported key usage for ${name} key`, 'SyntaxError', ); } const { keyObject } = pqcImportKeyObject(format, data, name); return new CryptoKey(keyObject, { name }, keyUsages, extractable); } function slhdsaImportKey( format: ImportFormat, data: BufferLike | JWK, algorithm: SubtleAlgorithm, extractable: boolean, keyUsages: KeyUsage[], ): CryptoKey { return mldsaImportKey(format, data, algorithm, extractable, keyUsages); } function mlkemImportKey( format: ImportFormat, data: BufferLike | JWK, algorithm: SubtleAlgorithm, extractable: boolean, keyUsages: KeyUsage[], ): CryptoKey { const { name } = algorithm; validatePqcFormat(format, name); if (format === 'jwk') { validatePqcJwk(data, name, extractable, keyUsages, 'enc'); } const isPublic = pqcIsPublicImport(format, data); const allowedUsages: KeyUsage[] = isPublic ? ['encapsulateBits', 'encapsulateKey'] : ['decapsulateBits', 'decapsulateKey']; if (hasAnyNotIn(keyUsages, allowedUsages)) { throw lazyDOMException( `Unsupported key usage for ${name} key`, 'SyntaxError', ); } const { keyObject } = pqcImportKeyObject(format, data, name); return new CryptoKey(keyObject, { name }, keyUsages, extractable); } const exportKeySpki = async ( key: CryptoKey, ): Promise => { switch (key.algorithm.name) { case 'RSASSA-PKCS1-v1_5': // Fall through case 'RSA-PSS': // Fall through case 'RSA-OAEP': if (key.type === 'public') { return rsaExportKey(key, KWebCryptoKeyFormat.kWebCryptoKeyFormatSPKI); } break; case 'ECDSA': // Fall through case 'ECDH': if (key.type === 'public') { return ecExportKey(key, KWebCryptoKeyFormat.kWebCryptoKeyFormatSPKI); } break; case 'Ed25519': // Fall through case 'Ed448': // Fall through case 'X25519': // Fall through case 'X448': if (key.type === 'public') { // Export Ed/X key in SPKI DER format return bufferLikeToArrayBuffer( key.keyObject.handle.exportKey(KFormatType.DER, KeyEncoding.SPKI), ); } break; case 'ML-DSA-44': // Fall through case 'ML-DSA-65': // Fall through case 'ML-DSA-87': // Fall through case 'SLH-DSA-SHA2-128s': case 'SLH-DSA-SHA2-128f': case 'SLH-DSA-SHA2-192s': case 'SLH-DSA-SHA2-192f': case 'SLH-DSA-SHA2-256s': case 'SLH-DSA-SHA2-256f': case 'SLH-DSA-SHAKE-128s': case 'SLH-DSA-SHAKE-128f': case 'SLH-DSA-SHAKE-192s': case 'SLH-DSA-SHAKE-192f': case 'SLH-DSA-SHAKE-256s': case 'SLH-DSA-SHAKE-256f': if (key.type === 'public') { // Export ML-DSA / SLH-DSA key in SPKI DER format return bufferLikeToArrayBuffer( key.keyObject.handle.exportKey(KFormatType.DER, KeyEncoding.SPKI), ); } break; case 'ML-KEM-512': // Fall through case 'ML-KEM-768': // Fall through case 'ML-KEM-1024': if (key.type === 'public') { return bufferLikeToArrayBuffer( key.keyObject.handle.exportKey(KFormatType.DER, KeyEncoding.SPKI), ); } break; } throw new Error( `Unable to export a spki ${key.algorithm.name} ${key.type} key`, ); }; const exportKeyPkcs8 = async ( key: CryptoKey, ): Promise => { switch (key.algorithm.name) { case 'RSASSA-PKCS1-v1_5': // Fall through case 'RSA-PSS': // Fall through case 'RSA-OAEP': if (key.type === 'private') { return rsaExportKey(key, KWebCryptoKeyFormat.kWebCryptoKeyFormatPKCS8); } break; case 'ECDSA': // Fall through case 'ECDH': if (key.type === 'private') { return ecExportKey(key, KWebCryptoKeyFormat.kWebCryptoKeyFormatPKCS8); } break; case 'Ed25519': // Fall through case 'Ed448': // Fall through case 'X25519': // Fall through case 'X448': if (key.type === 'private') { // Export Ed/X key in PKCS8 DER format return bufferLikeToArrayBuffer( key.keyObject.handle.exportKey(KFormatType.DER, KeyEncoding.PKCS8), ); } break; case 'ML-DSA-44': // Fall through case 'ML-DSA-65': // Fall through case 'ML-DSA-87': // Fall through case 'ML-KEM-512': // Fall through case 'ML-KEM-768': // Fall through case 'ML-KEM-1024': if (key.type === 'private') { const ab = bufferLikeToArrayBuffer( key.keyObject.handle.exportKey(KFormatType.DER, KeyEncoding.PKCS8), ); // 22 bytes of PKCS#8 ASN.1 + seed (32 ML-DSA, 64 ML-KEM). Guards // against a seedless KeyObject that was wrapped via toCryptoKey. const expected = key.algorithm.name.startsWith('ML-DSA') ? 54 : 86; if (ab.byteLength !== expected) { throw lazyDOMException( 'The operation failed for an operation-specific reason', 'OperationError', ); } return ab; } break; case 'SLH-DSA-SHA2-128s': case 'SLH-DSA-SHA2-128f': case 'SLH-DSA-SHA2-192s': case 'SLH-DSA-SHA2-192f': case 'SLH-DSA-SHA2-256s': case 'SLH-DSA-SHA2-256f': case 'SLH-DSA-SHAKE-128s': case 'SLH-DSA-SHAKE-128f': case 'SLH-DSA-SHAKE-192s': case 'SLH-DSA-SHAKE-192f': case 'SLH-DSA-SHAKE-256s': case 'SLH-DSA-SHAKE-256f': if (key.type === 'private') { return bufferLikeToArrayBuffer( key.keyObject.handle.exportKey(KFormatType.DER, KeyEncoding.PKCS8), ); } break; } throw new Error( `Unable to export a pkcs8 ${key.algorithm.name} ${key.type} key`, ); }; // Mirrors Node's export key matrix (lib/internal/crypto/webcrypto.js // exportKeyRawSecret / exportKeyRawPublic, lines 472-563): // // raw — AES-CTR/CBC/GCM/KW + HMAC (secret); ECDSA/ECDH/Ed/X (public) // raw-secret — AES-CTR/CBC/GCM/KW + HMAC + AES-OCB + KMAC + ChaCha20-Poly1305 // raw-public — ECDSA/ECDH + Ed/X + ML-DSA + ML-KEM (public) const exportKeyRaw = ( key: CryptoKey, format: 'raw' | 'raw-secret' | 'raw-public', ): ArrayBuffer => { const name = key.algorithm.name; const isPublic = key.type === 'public'; const isSecret = key.type === 'secret'; const exportSecret = (): ArrayBuffer => { const exported = key.keyObject.export(); return exported.buffer.slice( exported.byteOffset, exported.byteOffset + exported.byteLength, ) as ArrayBuffer; }; const exportRawPublic = (): ArrayBuffer => bufferLikeToArrayBuffer(key.keyObject.handle.exportKey()); const fail = (): never => { throw lazyDOMException( `Unable to export ${name} ${key.type} key using ${format} format`, 'NotSupportedError', ); }; // Symmetric: AES-CTR/CBC/GCM/KW and HMAC accept both 'raw' and 'raw-secret'; // AES-OCB / KMAC* / ChaCha20-Poly1305 only 'raw-secret'. switch (name) { case 'AES-CTR': case 'AES-CBC': case 'AES-GCM': case 'AES-KW': case 'HMAC': if (!isSecret) return fail(); if (format === 'raw' || format === 'raw-secret') return exportSecret(); return fail(); case 'AES-OCB': case 'KMAC128': case 'KMAC256': case 'ChaCha20-Poly1305': if (!isSecret) return fail(); if (format === 'raw-secret') return exportSecret(); return fail(); case 'ECDSA': case 'ECDH': if (!isPublic) return fail(); if (format === 'raw' || format === 'raw-public') { return ecExportKey(key, KWebCryptoKeyFormat.kWebCryptoKeyFormatRaw); } return fail(); case 'Ed25519': case 'Ed448': case 'X25519': case 'X448': if (!isPublic) return fail(); if (format === 'raw' || format === 'raw-public') return exportRawPublic(); return fail(); case 'ML-DSA-44': case 'ML-DSA-65': case 'ML-DSA-87': case 'ML-KEM-512': case 'ML-KEM-768': case 'ML-KEM-1024': case 'SLH-DSA-SHA2-128s': case 'SLH-DSA-SHA2-128f': case 'SLH-DSA-SHA2-192s': case 'SLH-DSA-SHA2-192f': case 'SLH-DSA-SHA2-256s': case 'SLH-DSA-SHA2-256f': case 'SLH-DSA-SHAKE-128s': case 'SLH-DSA-SHAKE-128f': case 'SLH-DSA-SHAKE-192s': case 'SLH-DSA-SHAKE-192f': case 'SLH-DSA-SHAKE-256s': case 'SLH-DSA-SHAKE-256f': // ML-DSA / ML-KEM / SLH-DSA keys do not recognize plain 'raw' (Node // webcrypto.js lines 488-510). if (!isPublic) return fail(); if (format === 'raw-public') return exportRawPublic(); return fail(); } return fail(); }; const exportKeyJWK = (key: CryptoKey): ArrayBuffer | unknown => { const jwk = key.keyObject.handle.exportJwk( { key_ops: [...key.usages], ext: key.extractable, }, true, ); switch (key.algorithm.name) { case 'RSASSA-PKCS1-v1_5': jwk.alg = normalizeHashName(key.algorithm.hash, HashContext.JwkRsa); return jwk; case 'RSA-PSS': jwk.alg = normalizeHashName(key.algorithm.hash, HashContext.JwkRsaPss); return jwk; case 'RSA-OAEP': jwk.alg = normalizeHashName(key.algorithm.hash, HashContext.JwkRsaOaep); return jwk; case 'HMAC': jwk.alg = normalizeHashName(key.algorithm.hash, HashContext.JwkHmac); return jwk; case 'KMAC128': jwk.alg = 'K128'; return jwk; case 'KMAC256': jwk.alg = 'K256'; return jwk; case 'ECDSA': // Fall through case 'ECDH': jwk.crv ||= key.algorithm.namedCurve; return jwk; case 'Ed25519': // Fall through case 'Ed448': // Fall through case 'X25519': // Fall through case 'X448': return jwk; case 'ML-DSA-44': // Fall through case 'ML-DSA-65': // Fall through case 'ML-DSA-87': // Fall through case 'ML-KEM-512': // Fall through case 'ML-KEM-768': // Fall through case 'ML-KEM-1024': // Fall through case 'SLH-DSA-SHA2-128s': case 'SLH-DSA-SHA2-128f': case 'SLH-DSA-SHA2-192s': case 'SLH-DSA-SHA2-192f': case 'SLH-DSA-SHA2-256s': case 'SLH-DSA-SHA2-256f': case 'SLH-DSA-SHAKE-128s': case 'SLH-DSA-SHAKE-128f': case 'SLH-DSA-SHAKE-192s': case 'SLH-DSA-SHAKE-192f': case 'SLH-DSA-SHAKE-256s': case 'SLH-DSA-SHAKE-256f': return jwk; case 'AES-CTR': // Fall through case 'AES-CBC': // Fall through case 'AES-GCM': // Fall through case 'AES-KW': // Fall through case 'AES-OCB': // Fall through case 'ChaCha20-Poly1305': if (key.algorithm.length === undefined) { throw lazyDOMException( `Algorithm ${key.algorithm.name} missing required length property`, 'InvalidAccessError', ); } jwk.alg = getAlgorithmName(key.algorithm.name, key.algorithm.length); return jwk; default: // Fall through } throw lazyDOMException( `JWK export not yet supported: ${key.algorithm.name}`, 'NotSupportedError', ); }; // PBKDF2 import. Mirrors Node's importGenericSecretKey ordering // (keys.js:945-971): extractable → usage → format → length. Callers pre-alias // 'raw-secret' / 'raw-public' to 'raw' via aliasKeyFormat // (webcrypto.js:798-808). const pbkdf2ImportKey = async ( { name, length }: SubtleAlgorithm, format: ImportFormat, keyData: BufferLike | BinaryLike, extractable: boolean, keyUsages: KeyUsage[], ): Promise => { if (extractable) { throw lazyDOMException(`${name} keys are not extractable`, 'SyntaxError'); } if (hasAnyNotIn(keyUsages, ['deriveKey', 'deriveBits'])) { throw lazyDOMException( `Unsupported key usage for a ${name} key`, 'SyntaxError', ); } if (format !== 'raw') { throw lazyDOMException( `Unable to import ${name} key with format ${format}`, 'NotSupportedError', ); } const checkLength = typeof keyData === 'string' || SBuffer.isBuffer(keyData) ? keyData.length * 8 : keyData.byteLength * 8; if (length !== undefined && length !== checkLength) { throw lazyDOMException('Invalid key length', 'DataError'); } const keyObject = createSecretKey(keyData as BinaryLike); return new CryptoKey(keyObject, { name }, keyUsages, false); }; // Argon2 import. Node gates the format at the dispatcher level — only // 'raw-secret' enters importGenericSecretKey (webcrypto.js:813-822). To match // that, format is the first check here; remaining ordering matches Node's // importGenericSecretKey. const argon2ImportKey = async ( { name, length }: SubtleAlgorithm, format: ImportFormat, keyData: BufferLike | BinaryLike, extractable: boolean, keyUsages: KeyUsage[], ): Promise => { if (format !== 'raw-secret') { throw lazyDOMException( `Unable to import ${name} key with format ${format}`, 'NotSupportedError', ); } if (extractable) { throw lazyDOMException(`${name} keys are not extractable`, 'SyntaxError'); } if (hasAnyNotIn(keyUsages, ['deriveKey', 'deriveBits'])) { throw lazyDOMException( `Unsupported key usage for a ${name} key`, 'SyntaxError', ); } const checkLength = typeof keyData === 'string' || SBuffer.isBuffer(keyData) ? keyData.length * 8 : keyData.byteLength * 8; if (length !== undefined && length !== checkLength) { throw lazyDOMException('Invalid key length', 'DataError'); } const keyObject = createSecretKey(keyData as BinaryLike); return new CryptoKey(keyObject, { name }, keyUsages, false); }; const hkdfImportKey = async ( format: ImportFormat, keyData: BufferLike | BinaryLike, algorithm: SubtleAlgorithm, extractable: boolean, keyUsages: KeyUsage[], ): Promise => { const { name } = algorithm; // WebCrypto §28.7.6: HKDF keys are never extractable. The previous // implementation passed `extractable` through verbatim, allowing callers // to round-trip the input keying material via `exportKey` — defeating // the whole point of the deriveBits-only usage. if (extractable) { throw lazyDOMException(`${name} keys are not extractable`, 'SyntaxError'); } if (hasAnyNotIn(keyUsages, ['deriveKey', 'deriveBits'])) { throw new Error(`Unsupported key usage for a ${name} key`); } switch (format) { case 'raw': { const keyObject = createSecretKey(keyData as BinaryLike); return new CryptoKey(keyObject, { name }, keyUsages, false); } default: throw new Error(`Unable to import ${name} key with format ${format}`); } }; const checkCryptoKeyPairUsages = (pair: CryptoKeyPair) => { if ( pair.privateKey && pair.privateKey instanceof CryptoKey && pair.privateKey.keyUsages && pair.privateKey.keyUsages.length > 0 ) { return; } throw lazyDOMException( 'Usages cannot be empty when creating a key.', 'SyntaxError', ); }; function argon2DeriveBits( algorithm: SubtleAlgorithm, baseKey: CryptoKey, length: number, ): ArrayBuffer { if (length === 0 || length % 8 !== 0) { throw lazyDOMException( 'Invalid Argon2 derived key length', 'OperationError', ); } if (length < 32) { throw lazyDOMException( 'Argon2 derived key length must be at least 32 bits', 'OperationError', ); } const { nonce, parallelism, memory, passes, secretValue, associatedData } = algorithm; const tagLength = length / 8; const message = baseKey.keyObject.export(); const algName = algorithm.name.toLowerCase(); const result = argon2Sync(algName, { message, nonce: nonce ?? new Uint8Array(0), parallelism: parallelism ?? 1, tagLength, memory: memory ?? 65536, passes: passes ?? 3, secret: secretValue, associatedData, version: algorithm.version, }); return bufferLikeToArrayBuffer(result); } // Type guard to check if result is CryptoKeyPair export function isCryptoKeyPair( result: CryptoKey | CryptoKeyPair, ): result is CryptoKeyPair { return 'publicKey' in result && 'privateKey' in result; } function hmacSignVerify( key: CryptoKey, data: BufferLike, signature?: BufferLike, ): ArrayBuffer | boolean { // Get hash algorithm from key const hashName = normalizeHashName(key.algorithm.hash); // Export the secret key material const keyData = key.keyObject.export(); // Create HMAC and compute digest const hmac = createHmac(hashName, keyData); hmac.update(bufferLikeToArrayBuffer(data)); const computed = hmac.digest(); if (signature === undefined) { // Sign operation - return the HMAC as ArrayBuffer return computed.buffer.slice( computed.byteOffset, computed.byteOffset + computed.byteLength, ); } const sigBuffer = bufferLikeToArrayBuffer(signature); const computedBuffer = computed.buffer.slice( computed.byteOffset, computed.byteOffset + computed.byteLength, ); if (computedBuffer.byteLength !== sigBuffer.byteLength) { return false; } return timingSafeEqual( new Uint8Array(computedBuffer), new Uint8Array(sigBuffer), ); } function rsaSignVerify( key: CryptoKey, data: BufferLike, padding: 'pkcs1' | 'pss', signature?: BufferLike, saltLength?: number, ): ArrayBuffer | boolean { // Get hash algorithm from key const hashName = normalizeHashName(key.algorithm.hash); // Determine RSA padding constant const RSA_PKCS1_PADDING = 1; const RSA_PKCS1_PSS_PADDING = 6; const paddingValue = padding === 'pss' ? RSA_PKCS1_PSS_PADDING : RSA_PKCS1_PADDING; if (signature === undefined) { // Sign operation const signer = createSign(hashName); signer.update(data); const sig = signer.sign({ key: key, padding: paddingValue, saltLength, }); return sig.buffer.slice(sig.byteOffset, sig.byteOffset + sig.byteLength); } // Verify operation const verifier = createVerify(hashName); verifier.update(data); return verifier.verify( { key: key, padding: paddingValue, saltLength, }, signature, ); } function edSignVerify( key: CryptoKey, data: BufferLike, signature?: BufferLike, ): ArrayBuffer | boolean { const isSign = signature === undefined; const expectedKeyType = isSign ? 'private' : 'public'; if (key.type !== expectedKeyType) { throw lazyDOMException( `Key must be a ${expectedKeyType} key`, 'InvalidAccessError', ); } // Get curve type from algorithm name (Ed25519 or Ed448) const algorithmName = key.algorithm.name; const curveType = algorithmName.toLowerCase() as 'ed25519' | 'ed448'; // Create Ed instance with the curve const ed = new Ed(curveType, {}); // Export raw key bytes (exportKey with no format returns raw for Ed keys) const rawKey = key.keyObject.handle.exportKey(); const dataBuffer = bufferLikeToArrayBuffer(data); if (isSign) { // Sign operation - use raw private key const sig = ed.signSync(dataBuffer, rawKey); return sig; } else { // Verify operation - use raw public key const signatureBuffer = bufferLikeToArrayBuffer(signature!); return ed.verifySync(signatureBuffer, dataBuffer, rawKey); } } function mldsaSignVerify( key: CryptoKey, data: BufferLike, signature?: BufferLike, ): ArrayBuffer | boolean { const isSign = signature === undefined; const expectedKeyType = isSign ? 'private' : 'public'; if (key.type !== expectedKeyType) { throw lazyDOMException( `Key must be a ${expectedKeyType} key`, 'InvalidAccessError', ); } const dataBuffer = bufferLikeToArrayBuffer(data); if (isSign) { const signer = createSign(''); signer.update(dataBuffer); const sig = signer.sign({ key: key }); return sig.buffer.slice(sig.byteOffset, sig.byteOffset + sig.byteLength); } else { const signatureBuffer = bufferLikeToArrayBuffer(signature!); const verifier = createVerify(''); verifier.update(dataBuffer); return verifier.verify({ key: key }, signatureBuffer); } } const signVerify = ( algorithm: SubtleAlgorithm, key: CryptoKey, data: BufferLike, signature?: BufferLike, ): ArrayBuffer | boolean => { const usage: Operation = signature === undefined ? 'sign' : 'verify'; algorithm = normalizeAlgorithm(algorithm, usage); if (algorithm.name !== key.algorithm.name) { throw lazyDOMException('Key algorithm mismatch', 'InvalidAccessError'); } if (!key.usages.includes(usage)) { throw lazyDOMException( `Unable to use this key to ${usage}`, 'InvalidAccessError', ); } switch (algorithm.name) { case 'ECDSA': return ecdsaSignVerify(key, data, algorithm, signature); case 'HMAC': return hmacSignVerify(key, data, signature); case 'RSASSA-PKCS1-v1_5': return rsaSignVerify(key, data, 'pkcs1', signature); case 'RSA-PSS': return rsaSignVerify(key, data, 'pss', signature, algorithm.saltLength); case 'Ed25519': case 'Ed448': return edSignVerify(key, data, signature); case 'ML-DSA-44': case 'ML-DSA-65': case 'ML-DSA-87': case 'SLH-DSA-SHA2-128s': case 'SLH-DSA-SHA2-128f': case 'SLH-DSA-SHA2-192s': case 'SLH-DSA-SHA2-192f': case 'SLH-DSA-SHA2-256s': case 'SLH-DSA-SHA2-256f': case 'SLH-DSA-SHAKE-128s': case 'SLH-DSA-SHAKE-128f': case 'SLH-DSA-SHAKE-192s': case 'SLH-DSA-SHAKE-192f': case 'SLH-DSA-SHAKE-256s': case 'SLH-DSA-SHAKE-256f': return mldsaSignVerify(key, data, signature); case 'KMAC128': case 'KMAC256': return kmacSignVerify(key, data, algorithm, signature); } throw lazyDOMException( `Unrecognized algorithm name '${algorithm.name}' for '${usage}'`, 'NotSupportedError', ); }; // Algorithm-mismatch and usage checks live at the public-method call sites // (encrypt / decrypt / wrapKey / unwrapKey) so spec-mandated message and // ordering — algorithm-mismatch first, then usage — is preserved // (Node webcrypto.js, commit 4cb1f284136). const cipherOrWrap = async ( mode: CipherOrWrapMode, algorithm: EncryptDecryptParams, key: CryptoKey, data: ArrayBuffer, ): Promise => { validateMaxBufferLength(data, 'data'); switch (algorithm.name) { case 'RSA-OAEP': return rsaCipher(mode, key, data, algorithm); case 'AES-CTR': // Fall through case 'AES-CBC': // Fall through case 'AES-GCM': // Fall through case 'AES-OCB': return aesCipher(mode, key, data, algorithm); case 'AES-KW': return aesKwCipher(mode, key, data); case 'ChaCha20-Poly1305': return chaCha20Poly1305Cipher( mode, key, data, algorithm as ChaCha20Poly1305Params, ); } }; const SUPPORTED_ALGORITHMS: Record> = { encrypt: new Set([ 'RSA-OAEP', 'AES-CTR', 'AES-CBC', 'AES-GCM', 'AES-OCB', 'ChaCha20-Poly1305', ]), decrypt: new Set([ 'RSA-OAEP', 'AES-CTR', 'AES-CBC', 'AES-GCM', 'AES-OCB', 'ChaCha20-Poly1305', ]), sign: new Set([ 'RSASSA-PKCS1-v1_5', 'RSA-PSS', 'ECDSA', 'HMAC', 'KMAC128', 'KMAC256', 'Ed25519', 'Ed448', 'ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87', ...SLH_DSA_VARIANTS, ]), verify: new Set([ 'RSASSA-PKCS1-v1_5', 'RSA-PSS', 'ECDSA', 'HMAC', 'KMAC128', 'KMAC256', 'Ed25519', 'Ed448', 'ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87', ...SLH_DSA_VARIANTS, ]), digest: new Set([ 'SHA-1', 'SHA-256', 'SHA-384', 'SHA-512', 'SHA3-256', 'SHA3-384', 'SHA3-512', 'cSHAKE128', 'cSHAKE256', 'TurboSHAKE128', 'TurboSHAKE256', 'KT128', 'KT256', ]), generateKey: new Set([ 'RSASSA-PKCS1-v1_5', 'RSA-PSS', 'RSA-OAEP', 'ECDSA', 'ECDH', 'Ed25519', 'Ed448', 'X25519', 'X448', 'AES-CTR', 'AES-CBC', 'AES-GCM', 'AES-KW', 'AES-OCB', 'ChaCha20-Poly1305', 'HMAC', 'KMAC128', 'KMAC256', 'ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87', 'ML-KEM-512', 'ML-KEM-768', 'ML-KEM-1024', ...SLH_DSA_VARIANTS, ]), importKey: new Set([ 'RSASSA-PKCS1-v1_5', 'RSA-PSS', 'RSA-OAEP', 'ECDSA', 'ECDH', 'Ed25519', 'Ed448', 'X25519', 'X448', 'AES-CTR', 'AES-CBC', 'AES-GCM', 'AES-KW', 'AES-OCB', 'ChaCha20-Poly1305', 'HMAC', 'KMAC128', 'KMAC256', 'HKDF', 'PBKDF2', 'Argon2d', 'Argon2i', 'Argon2id', 'ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87', 'ML-KEM-512', 'ML-KEM-768', 'ML-KEM-1024', ...SLH_DSA_VARIANTS, ]), exportKey: new Set([ 'RSASSA-PKCS1-v1_5', 'RSA-PSS', 'RSA-OAEP', 'ECDSA', 'ECDH', 'Ed25519', 'Ed448', 'X25519', 'X448', 'AES-CTR', 'AES-CBC', 'AES-GCM', 'AES-KW', 'AES-OCB', 'ChaCha20-Poly1305', 'HMAC', 'KMAC128', 'KMAC256', 'ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87', 'ML-KEM-512', 'ML-KEM-768', 'ML-KEM-1024', ...SLH_DSA_VARIANTS, ]), deriveBits: new Set([ 'PBKDF2', 'HKDF', 'ECDH', 'X25519', 'X448', 'Argon2d', 'Argon2i', 'Argon2id', ]), wrapKey: new Set([ 'AES-CTR', 'AES-CBC', 'AES-GCM', 'AES-KW', 'AES-OCB', 'ChaCha20-Poly1305', 'RSA-OAEP', ]), unwrapKey: new Set([ 'AES-CTR', 'AES-CBC', 'AES-GCM', 'AES-KW', 'AES-OCB', 'ChaCha20-Poly1305', 'RSA-OAEP', ]), encapsulateBits: new Set(['ML-KEM-512', 'ML-KEM-768', 'ML-KEM-1024']), decapsulateBits: new Set(['ML-KEM-512', 'ML-KEM-768', 'ML-KEM-1024']), encapsulateKey: new Set(['ML-KEM-512', 'ML-KEM-768', 'ML-KEM-1024']), decapsulateKey: new Set(['ML-KEM-512', 'ML-KEM-768', 'ML-KEM-1024']), }; const ASYMMETRIC_ALGORITHMS = new Set([ 'RSASSA-PKCS1-v1_5', 'RSA-PSS', 'RSA-OAEP', 'ECDSA', 'ECDH', 'Ed25519', 'Ed448', 'X25519', 'X448', 'ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87', 'ML-KEM-512', 'ML-KEM-768', 'ML-KEM-1024', ...SLH_DSA_VARIANTS, ]); // Per-algorithm validators for deriveBits (mirrors Node's hkdf.js:141-149, // pbkdf2.js:96-105, argon2.js:194-209). Used by Subtle.supports to reject // length values that the actual deriveBits implementation would reject. function validateKdfDeriveBitsLength( length: number | null | undefined, algName: string, ): void { if (length === null || length === undefined) { throw lazyDOMException('length cannot be null', 'OperationError'); } if (length % 8) { throw lazyDOMException('length must be a multiple of 8', 'OperationError'); } if (algName.startsWith('Argon2') && length < 32) { throw lazyDOMException('length must be >= 32', 'OperationError'); } } // Mirrors Node's webcrypto.js:1652-1731 `check`. Normalizes the algorithm, // looks it up in SUPPORTED_ALGORITHMS, and runs per-algorithm validation // (deriveBits length validators, HMAC+SHA3 generateKey rejection). // `op` is the operation key in SUPPORTED_ALGORITHMS — wrapKey/unwrapKey fall // back to encrypt/decrypt to mirror Node's normalize fallback. function supportsCheck( op: string, alg: SubtleAlgorithm | AnyAlgorithm, length?: number | null, ): boolean { let normalizedAlgorithm: SubtleAlgorithm; try { normalizedAlgorithm = normalizeAlgorithm(alg, op as Operation); } catch { if (op === 'wrapKey') return supportsCheck('encrypt', alg); if (op === 'unwrapKey') return supportsCheck('decrypt', alg); return false; } const supported = SUPPORTED_ALGORITHMS[op]; if (!supported || !supported.has(normalizedAlgorithm.name)) { if (op === 'wrapKey') return supportsCheck('encrypt', alg); if (op === 'unwrapKey') return supportsCheck('decrypt', alg); return false; } if (op === 'deriveBits') { const name = normalizedAlgorithm.name; if (name === 'HKDF' || name === 'PBKDF2' || name.startsWith('Argon2')) { try { validateKdfDeriveBitsLength(length, name); } catch { return false; } } } if (op === 'generateKey' && normalizedAlgorithm.name === 'HMAC') { const hashName = typeof normalizedAlgorithm.hash === 'string' ? normalizedAlgorithm.hash : (normalizedAlgorithm.hash as { name?: string } | undefined)?.name; if ( normalizedAlgorithm.length === undefined && typeof hashName === 'string' && hashName.startsWith('SHA3-') ) { return false; } } return true; } function supportsImpl( operation: string, algorithm: SubtleAlgorithm | AnyAlgorithm, lengthOrAdditionalAlgorithm: unknown, ): boolean { switch (operation) { case 'decapsulateBits': case 'decapsulateKey': case 'decrypt': case 'deriveBits': case 'deriveKey': case 'digest': case 'encapsulateBits': case 'encapsulateKey': case 'encrypt': case 'exportKey': case 'generateKey': case 'getPublicKey': case 'importKey': case 'sign': case 'unwrapKey': case 'verify': case 'wrapKey': break; default: return false; } let length: number | null | undefined; if (operation === 'deriveKey') { // deriveKey decomposes to importKey of derived alg + deriveBits with that // alg's key length. Node webcrypto.js:1547-1563. if (lengthOrAdditionalAlgorithm != null) { const additional = lengthOrAdditionalAlgorithm as | SubtleAlgorithm | AnyAlgorithm; if (!supportsCheck('importKey', additional)) return false; try { length = getKeyLength(normalizeAlgorithm(additional, 'get key length')); } catch { return false; } return supportsCheck('deriveBits', algorithm, length); } // No additional algorithm given — only check the deriveBits side. return supportsCheck('deriveBits', algorithm); } if (operation === 'wrapKey') { // wrapKey decomposes to encrypt of wrapping alg + exportKey of wrapped alg. // Node webcrypto.js:1564-1572. if (lengthOrAdditionalAlgorithm != null) { const additional = lengthOrAdditionalAlgorithm as | SubtleAlgorithm | AnyAlgorithm; if (!supportsCheck('exportKey', additional)) return false; } return supportsCheck('wrapKey', algorithm); } if (operation === 'unwrapKey') { // unwrapKey decomposes to decrypt of wrapping alg + importKey of wrapped // alg. Node webcrypto.js:1573-1581. if (lengthOrAdditionalAlgorithm != null) { const additional = lengthOrAdditionalAlgorithm as | SubtleAlgorithm | AnyAlgorithm; if (!supportsCheck('importKey', additional)) return false; } return supportsCheck('unwrapKey', algorithm); } if (operation === 'deriveBits') { if (lengthOrAdditionalAlgorithm == null) { length = null; } else if (typeof lengthOrAdditionalAlgorithm === 'number') { length = lengthOrAdditionalAlgorithm; } else { return false; } return supportsCheck('deriveBits', algorithm, length); } if (operation === 'getPublicKey') { let normalized: SubtleAlgorithm; try { normalized = normalizeAlgorithm(algorithm, 'exportKey'); } catch { return false; } return ASYMMETRIC_ALGORITHMS.has(normalized.name); } if (operation === 'encapsulateKey' || operation === 'decapsulateKey') { // sharedKeyAlgorithm must support importKey, with HMAC/KMAC limited to // length === undefined or 256 (Node webcrypto.js:1610-1645). const additional = lengthOrAdditionalAlgorithm as | SubtleAlgorithm | AnyAlgorithm; let normalizedAdd: SubtleAlgorithm; try { normalizedAdd = normalizeAlgorithm(additional, 'importKey'); } catch { return false; } switch (normalizedAdd.name) { case 'AES-OCB': case 'AES-KW': case 'AES-GCM': case 'AES-CTR': case 'AES-CBC': case 'ChaCha20-Poly1305': case 'HKDF': case 'PBKDF2': case 'Argon2i': case 'Argon2d': case 'Argon2id': break; case 'HMAC': case 'KMAC128': case 'KMAC256': { const addLen = normalizedAdd.length; if (addLen !== undefined && addLen !== 256) return false; break; } default: return false; } return supportsCheck(operation, algorithm); } return supportsCheck(operation, algorithm); } export class Subtle { // Spec-compliant capability detector. Mirrors Node's webcrypto.js:1506-1649 // `SubtleCrypto.supports`, including: // • composed-operation decomposition (deriveKey, wrapKey, unwrapKey, // encapsulateKey, decapsulateKey, getPublicKey) // • per-algorithm length validators for deriveBits (HKDF/PBKDF2/Argon2) // • HMAC + SHA3 generateKey with no length → false // Static-only per the WICG spec. static supports( operation: string, algorithm: SubtleAlgorithm | AnyAlgorithm, lengthOrAdditionalAlgorithm: unknown = null, ): boolean { return supportsImpl(operation, algorithm, lengthOrAdditionalAlgorithm); } async decrypt( algorithm: EncryptDecryptParams, key: CryptoKey, data: BufferLike, ): Promise { requireArgs(arguments.length, 3, 'decrypt'); const normalizedAlgorithm = normalizeAlgorithm( algorithm, 'decrypt', ) as EncryptDecryptParams; if (normalizedAlgorithm.name !== key.algorithm.name) { throw lazyDOMException('Key algorithm mismatch', 'InvalidAccessError'); } if (!key.usages.includes('decrypt')) { throw lazyDOMException( 'Unable to use this key to decrypt', 'InvalidAccessError', ); } return cipherOrWrap( CipherOrWrapMode.kWebCryptoCipherDecrypt, normalizedAlgorithm, key, bufferLikeToArrayBuffer(data), ); } async digest( algorithm: SubtleAlgorithm | AnyAlgorithm, data: BufferLike, ): Promise { requireArgs(arguments.length, 2, 'digest'); const normalizedAlgorithm = normalizeAlgorithm(algorithm, 'digest'); return asyncDigest(normalizedAlgorithm, data); } async deriveBits( algorithm: SubtleAlgorithm, baseKey: CryptoKey, length: number | null = null, ): Promise { requireArgs(arguments.length, 2, 'deriveBits'); // WebCrypto §SubtleCrypto.deriveBits step 11: throw InvalidAccessError // unless `baseKey.[[usages]]` contains "deriveBits" specifically. The // previous `deriveBits || deriveKey` accept-either branch silently // promoted deriveKey-only keys into deriveBits use, contradicting the // spec usage gate. if (!baseKey.usages.includes('deriveBits')) { throw lazyDOMException( 'baseKey does not have deriveBits usage', 'InvalidAccessError', ); } const normalizedAlgorithm = normalizeAlgorithm(algorithm, 'deriveBits'); if (baseKey.algorithm.name !== normalizedAlgorithm.name) { throw lazyDOMException('Key algorithm mismatch', 'InvalidAccessError'); } switch (normalizedAlgorithm.name) { case 'PBKDF2': if (length === null) { throw lazyDOMException('length cannot be null', 'OperationError'); } return pbkdf2DeriveBits(normalizedAlgorithm, baseKey, length); case 'X25519': // Fall through case 'X448': return xDeriveBits(normalizedAlgorithm, baseKey, length); case 'ECDH': return ecDeriveBits(normalizedAlgorithm, baseKey, length); case 'HKDF': if (length === null) { throw lazyDOMException('length cannot be null', 'OperationError'); } return hkdfDeriveBits( normalizedAlgorithm as unknown as HkdfAlgorithm, baseKey, length, ); case 'Argon2d': case 'Argon2i': case 'Argon2id': if (length === null) { throw lazyDOMException('length cannot be null', 'OperationError'); } return argon2DeriveBits(normalizedAlgorithm, baseKey, length); } throw lazyDOMException( `'subtle.deriveBits()' for ${normalizedAlgorithm.name} is not implemented.`, 'NotSupportedError', ); } async deriveKey( algorithm: SubtleAlgorithm, baseKey: CryptoKey, derivedKeyAlgorithm: SubtleAlgorithm, extractable: boolean, keyUsages: KeyUsage[], ): Promise { requireArgs(arguments.length, 5, 'deriveKey'); const normalizedAlgorithm = normalizeAlgorithm(algorithm, 'deriveBits'); // Validate the derived-key algorithm up front (mirrors Node webcrypto.js:341). // The normalized form is unused — `this.importKey` re-normalizes below. normalizeAlgorithm(derivedKeyAlgorithm, 'importKey'); // Validate baseKey usage if (!baseKey.usages.includes('deriveKey')) { throw lazyDOMException( 'baseKey does not have deriveKey usage', 'InvalidAccessError', ); } if (baseKey.algorithm.name !== normalizedAlgorithm.name) { throw lazyDOMException('Key algorithm mismatch', 'InvalidAccessError'); } // Calculate required key length (may be null for KDF-derived material). // Mirrors Node webcrypto.js:350 — uses the raw derivedKeyAlgorithm with // op='get key length' so AES `length` survives normalization (the // 'importKey' converter for AES is null and strips dictionary members). const length = getKeyLength( normalizeAlgorithm(derivedKeyAlgorithm, 'get key length'), ); // Step 1: Derive bits let derivedBits: ArrayBuffer; switch (normalizedAlgorithm.name) { case 'PBKDF2': if (length === null) { throw lazyDOMException('length cannot be null', 'OperationError'); } derivedBits = await pbkdf2DeriveBits( normalizedAlgorithm, baseKey, length, ); break; case 'X25519': // Fall through case 'X448': derivedBits = await xDeriveBits(normalizedAlgorithm, baseKey, length); break; case 'ECDH': derivedBits = await ecDeriveBits(normalizedAlgorithm, baseKey, length); break; case 'HKDF': if (length === null) { throw lazyDOMException('length cannot be null', 'OperationError'); } derivedBits = hkdfDeriveBits( normalizedAlgorithm as unknown as HkdfAlgorithm, baseKey, length, ); break; case 'Argon2d': case 'Argon2i': case 'Argon2id': if (length === null) { throw lazyDOMException('length cannot be null', 'OperationError'); } derivedBits = argon2DeriveBits(normalizedAlgorithm, baseKey, length); break; default: throw lazyDOMException( `'subtle.deriveKey()' for ${normalizedAlgorithm.name} is not implemented.`, 'NotSupportedError', ); } // Step 2: Import as key. Use 'raw-secret' so derived material flows into // AEADs / KMAC correctly — they reject plain 'raw' (Node webcrypto.js:381-385). return this.importKey( 'raw-secret', derivedBits, derivedKeyAlgorithm, extractable, keyUsages, ); } async encrypt( algorithm: EncryptDecryptParams, key: CryptoKey, data: BufferLike, ): Promise { requireArgs(arguments.length, 3, 'encrypt'); const normalizedAlgorithm = normalizeAlgorithm( algorithm, 'encrypt', ) as EncryptDecryptParams; if (normalizedAlgorithm.name !== key.algorithm.name) { throw lazyDOMException('Key algorithm mismatch', 'InvalidAccessError'); } if (!key.usages.includes('encrypt')) { throw lazyDOMException( 'Unable to use this key to encrypt', 'InvalidAccessError', ); } return cipherOrWrap( CipherOrWrapMode.kWebCryptoCipherEncrypt, normalizedAlgorithm, key, bufferLikeToArrayBuffer(data), ); } async exportKey( format: ImportFormat, key: CryptoKey, ): Promise { requireArgs(arguments.length, 2, 'exportKey'); if (!key.extractable) throw lazyDOMException('key is not extractable', 'InvalidAccessError'); if (format === 'raw-seed') { const pqcAlgos: string[] = [ 'ML-KEM-512', 'ML-KEM-768', 'ML-KEM-1024', 'ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87', ...SLH_DSA_VARIANTS, ]; if (!pqcAlgos.includes(key.algorithm.name)) { throw lazyDOMException( 'raw-seed export only supported for PQC keys', 'NotSupportedError', ); } if (key.type !== 'private') { throw lazyDOMException( 'raw-seed export requires a private key', 'InvalidAccessError', ); } return bufferLikeToArrayBuffer(key.keyObject.handle.exportKey()); } switch (format) { case 'spki': return (await exportKeySpki(key)) as ArrayBuffer; case 'pkcs8': return (await exportKeyPkcs8(key)) as ArrayBuffer; case 'jwk': return exportKeyJWK(key) as JWK; case 'raw': case 'raw-secret': case 'raw-public': return exportKeyRaw(key, format) as ArrayBuffer; } } async wrapKey( format: ImportFormat, key: CryptoKey, wrappingKey: CryptoKey, wrapAlgorithm: EncryptDecryptParams, ): Promise { requireArgs(arguments.length, 4, 'wrapKey'); // Mirrors Node webcrypto.js:923-927: prefer the 'wrapKey' op (only // AES-KW defines it) and fall back to 'encrypt' for cipher-based wrap // algorithms like AES-GCM and RSA-OAEP. let normalizedWrapAlgorithm: EncryptDecryptParams; try { normalizedWrapAlgorithm = normalizeAlgorithm( wrapAlgorithm, 'wrapKey', ) as EncryptDecryptParams; } catch { normalizedWrapAlgorithm = normalizeAlgorithm( wrapAlgorithm, 'encrypt', ) as EncryptDecryptParams; } if (normalizedWrapAlgorithm.name !== wrappingKey.algorithm.name) { throw lazyDOMException('Key algorithm mismatch', 'InvalidAccessError'); } if (!wrappingKey.usages.includes('wrapKey')) { throw lazyDOMException( 'Unable to use this key to wrapKey', 'InvalidAccessError', ); } // Step 1: Export the key const exported = await this.exportKey(format, key); // Step 2: Convert to ArrayBuffer if JWK let keyData: ArrayBuffer; if (format === 'jwk') { const jwkString = JSON.stringify(exported); const buffer = SBuffer.from(jwkString, 'utf8'); // For AES-KW, pad to multiple of 8 bytes (accounting for null terminator) if (normalizedWrapAlgorithm.name === 'AES-KW') { const length = buffer.length; // Add 1 for null terminator, then pad to multiple of 8 const paddedLength = Math.ceil((length + 1) / 8) * 8; const paddedBuffer = SBuffer.alloc(paddedLength); buffer.copy(paddedBuffer); // Null terminator for JSON string (remaining bytes are already zeros from alloc) paddedBuffer.writeUInt8(0, length); keyData = bufferLikeToArrayBuffer(paddedBuffer); } else { keyData = bufferLikeToArrayBuffer(buffer); } } else { keyData = exported as ArrayBuffer; } // Step 3: Encrypt the exported key return cipherOrWrap( CipherOrWrapMode.kWebCryptoCipherEncrypt, normalizedWrapAlgorithm, wrappingKey, keyData, ); } async unwrapKey( format: ImportFormat, wrappedKey: BufferLike, unwrappingKey: CryptoKey, unwrapAlgorithm: EncryptDecryptParams, unwrappedKeyAlgorithm: SubtleAlgorithm | AnyAlgorithm, extractable: boolean, keyUsages: KeyUsage[], ): Promise { requireArgs(arguments.length, 7, 'unwrapKey'); // Mirrors Node webcrypto.js:1006-1010: prefer 'unwrapKey', fall back to // 'decrypt' for cipher-based unwrap algorithms. let normalizedUnwrapAlgorithm: EncryptDecryptParams; try { normalizedUnwrapAlgorithm = normalizeAlgorithm( unwrapAlgorithm, 'unwrapKey', ) as EncryptDecryptParams; } catch { normalizedUnwrapAlgorithm = normalizeAlgorithm( unwrapAlgorithm, 'decrypt', ) as EncryptDecryptParams; } if (normalizedUnwrapAlgorithm.name !== unwrappingKey.algorithm.name) { throw lazyDOMException('Key algorithm mismatch', 'InvalidAccessError'); } if (!unwrappingKey.usages.includes('unwrapKey')) { throw lazyDOMException( 'Unable to use this key to unwrapKey', 'InvalidAccessError', ); } // Step 1: Decrypt the wrapped key const decrypted = await cipherOrWrap( CipherOrWrapMode.kWebCryptoCipherDecrypt, normalizedUnwrapAlgorithm, unwrappingKey, bufferLikeToArrayBuffer(wrappedKey), ); // Step 2: Convert to appropriate format let keyData: BufferLike | JWK; if (format === 'jwk') { const buffer = SBuffer.from(decrypted); // For AES-KW, the data may be padded - find the null terminator let jwkString: string; if (normalizedUnwrapAlgorithm.name === 'AES-KW') { // Find the null terminator (if present) to get the original string const nullIndex = buffer.indexOf(0); if (nullIndex !== -1) { jwkString = buffer.toString('utf8', 0, nullIndex); } else { // No null terminator, try to parse the whole buffer jwkString = buffer.toString('utf8').trim(); } } else { jwkString = buffer.toString('utf8'); } keyData = JSON.parse(jwkString) as JWK; } else { keyData = decrypted; } // Step 3: Import the key return this.importKey( format, keyData, unwrappedKeyAlgorithm, extractable, keyUsages, ); } async generateKey( algorithm: SubtleAlgorithm, extractable: boolean, keyUsages: KeyUsage[], ): Promise { requireArgs(arguments.length, 3, 'generateKey'); algorithm = normalizeAlgorithm(algorithm, 'generateKey'); let result: CryptoKey | CryptoKeyPair; switch (algorithm.name) { case 'RSASSA-PKCS1-v1_5': // Fall through case 'RSA-PSS': // Fall through case 'RSA-OAEP': result = await rsa_generateKeyPair(algorithm, extractable, keyUsages); break; case 'ECDSA': // Fall through case 'ECDH': result = await ec_generateKeyPair( algorithm.name, algorithm.namedCurve!, extractable, keyUsages, ); checkCryptoKeyPairUsages(result as CryptoKeyPair); break; case 'AES-CTR': // Fall through case 'AES-CBC': // Fall through case 'AES-GCM': // Fall through case 'AES-KW': // Fall through case 'AES-OCB': result = await aesGenerateKey( algorithm as AesKeyGenParams, extractable, keyUsages, ); break; case 'ChaCha20-Poly1305': { const length = (algorithm as AesKeyGenParams).length ?? 256; if (length !== 256) { throw lazyDOMException( 'ChaCha20-Poly1305 only supports 256-bit keys', 'NotSupportedError', ); } result = await aesGenerateKey( { name: 'ChaCha20-Poly1305', length: 256, } as unknown as AesKeyGenParams, extractable, keyUsages, ); break; } case 'HMAC': result = await hmacGenerateKey(algorithm, extractable, keyUsages); break; case 'KMAC128': // Fall through case 'KMAC256': result = await kmacGenerateKey(algorithm, extractable, keyUsages); break; case 'Ed25519': // Fall through case 'Ed448': result = await ed_generateKeyPairWebCrypto( algorithm.name.toLowerCase() as 'ed25519' | 'ed448', extractable, keyUsages, ); checkCryptoKeyPairUsages(result as CryptoKeyPair); break; case 'ML-DSA-44': // Fall through case 'ML-DSA-65': // Fall through case 'ML-DSA-87': result = await mldsa_generateKeyPairWebCrypto( algorithm.name as MlDsaVariant, extractable, keyUsages, ); checkCryptoKeyPairUsages(result as CryptoKeyPair); break; case 'SLH-DSA-SHA2-128s': case 'SLH-DSA-SHA2-128f': case 'SLH-DSA-SHA2-192s': case 'SLH-DSA-SHA2-192f': case 'SLH-DSA-SHA2-256s': case 'SLH-DSA-SHA2-256f': case 'SLH-DSA-SHAKE-128s': case 'SLH-DSA-SHAKE-128f': case 'SLH-DSA-SHAKE-192s': case 'SLH-DSA-SHAKE-192f': case 'SLH-DSA-SHAKE-256s': case 'SLH-DSA-SHAKE-256f': result = await slhdsa_generateKeyPairWebCrypto( algorithm.name as SlhDsaVariant, extractable, keyUsages, ); checkCryptoKeyPairUsages(result as CryptoKeyPair); break; case 'X25519': // Fall through case 'X448': result = await x_generateKeyPairWebCrypto( algorithm.name.toLowerCase() as 'x25519' | 'x448', extractable, keyUsages, ); checkCryptoKeyPairUsages(result as CryptoKeyPair); break; case 'ML-KEM-512': // Fall through case 'ML-KEM-768': // Fall through case 'ML-KEM-1024': result = await mlkem_generateKeyPairWebCrypto( algorithm.name as MlKemVariant, extractable, keyUsages, ); checkCryptoKeyPairUsages(result as CryptoKeyPair); break; default: throw new Error( `'subtle.generateKey()' is not implemented for ${algorithm.name}. Unrecognized algorithm name`, ); } return result; } async getPublicKey( key: CryptoKey, keyUsages: KeyUsage[], ): Promise { requireArgs(arguments.length, 2, 'getPublicKey'); if (key.type === 'secret') { throw lazyDOMException('key must be a private key', 'NotSupportedError'); } if (key.type !== 'private') { throw lazyDOMException('key must be a private key', 'InvalidAccessError'); } const publicKeyObject = createPublicKey(key.keyObject); return publicKeyObject.toCryptoKey(key.algorithm, true, keyUsages); } async importKey( format: ImportFormat, data: BufferLike | BinaryLike | JWK, algorithm: SubtleAlgorithm | AnyAlgorithm, extractable: boolean, keyUsages: KeyUsage[], ): Promise { requireArgs(arguments.length, 5, 'importKey'); // Per-algorithm format handling. Some algorithms alias raw-secret/raw-public // to 'raw' (RSA, EC, Ed/X, HMAC, HKDF, PBKDF2); others demand the // disambiguated form (KMAC, AES-OCB, ChaCha20-Poly1305, Argon2, ML-DSA, // ML-KEM). 'raw-seed' is never normalized — PQC import handles it directly. const normalizedAlgorithm = normalizeAlgorithm(algorithm, 'importKey'); let result: CryptoKey; switch (normalizedAlgorithm.name) { case 'RSASSA-PKCS1-v1_5': // Fall through case 'RSA-PSS': // Fall through case 'RSA-OAEP': result = rsaImportKey( aliasKeyFormat(format), data as BufferLike | JWK, normalizedAlgorithm, extractable, keyUsages, ); break; case 'ECDSA': // Fall through case 'ECDH': result = ecImportKey( aliasKeyFormat(format), data, normalizedAlgorithm, extractable, keyUsages, ); break; case 'HMAC': // No aliasing — Node routes HMAC straight into mac.js, which accepts // 'raw' / 'raw-secret' / 'jwk' and rejects everything else // (webcrypto.js:774-781, mac.js:136-174). result = await hmacImportKey( normalizedAlgorithm, format, data as BufferLike | JWK, extractable, keyUsages, ); break; case 'KMAC128': // Fall through case 'KMAC256': result = await kmacImportKey( normalizedAlgorithm, format, data as BufferLike | JWK, extractable, keyUsages, ); break; case 'AES-CTR': // Fall through case 'AES-CBC': // Fall through case 'AES-GCM': // Fall through case 'AES-KW': // Fall through case 'AES-OCB': // Fall through case 'ChaCha20-Poly1305': result = await aesImportKey( normalizedAlgorithm, format, data as BufferLike | JWK, extractable, keyUsages, ); break; case 'PBKDF2': result = await pbkdf2ImportKey( normalizedAlgorithm, aliasKeyFormat(format), data as BufferLike | BinaryLike, extractable, keyUsages, ); break; case 'Argon2d': case 'Argon2i': case 'Argon2id': result = await argon2ImportKey( normalizedAlgorithm, format, data as BufferLike | BinaryLike, extractable, keyUsages, ); break; case 'HKDF': result = await hkdfImportKey( aliasKeyFormat(format), data as BufferLike | BinaryLike, normalizedAlgorithm, extractable, keyUsages, ); break; case 'X25519': // Fall through case 'X448': // Fall through case 'Ed25519': // Fall through case 'Ed448': result = edImportKey( aliasKeyFormat(format), data as BufferLike | JWK, normalizedAlgorithm, extractable, keyUsages, ); break; case 'SLH-DSA-SHA2-128s': case 'SLH-DSA-SHA2-128f': case 'SLH-DSA-SHA2-192s': case 'SLH-DSA-SHA2-192f': case 'SLH-DSA-SHA2-256s': case 'SLH-DSA-SHA2-256f': case 'SLH-DSA-SHAKE-128s': case 'SLH-DSA-SHAKE-128f': case 'SLH-DSA-SHAKE-192s': case 'SLH-DSA-SHAKE-192f': case 'SLH-DSA-SHAKE-256s': case 'SLH-DSA-SHAKE-256f': result = slhdsaImportKey( format, data as BufferLike | JWK, normalizedAlgorithm, extractable, keyUsages, ); break; case 'ML-DSA-44': // Fall through case 'ML-DSA-65': // Fall through case 'ML-DSA-87': result = mldsaImportKey( format, data as BufferLike | JWK, normalizedAlgorithm, extractable, keyUsages, ); break; case 'ML-KEM-512': // Fall through case 'ML-KEM-768': // Fall through case 'ML-KEM-1024': result = mlkemImportKey( format, data as BufferLike | JWK, normalizedAlgorithm, extractable, keyUsages, ); break; default: throw new Error( `"subtle.importKey()" is not implemented for ${normalizedAlgorithm.name}`, ); } if ( (result.type === 'secret' || result.type === 'private') && result.usages.length === 0 ) { throw new Error( `Usages cannot be empty when importing a ${result.type} key.`, ); } return result; } async sign( algorithm: SubtleAlgorithm, key: CryptoKey, data: BufferLike, ): Promise { requireArgs(arguments.length, 3, 'sign'); return signVerify( normalizeAlgorithm(algorithm, 'sign'), key, data, ) as ArrayBuffer; } async verify( algorithm: SubtleAlgorithm, key: CryptoKey, signature: BufferLike, data: BufferLike, ): Promise { requireArgs(arguments.length, 4, 'verify'); return signVerify( normalizeAlgorithm(algorithm, 'verify'), key, data, signature, ) as boolean; } private _encapsulateCore( algorithm: SubtleAlgorithm, key: CryptoKey, ): EncapsulateResult { const normalizedAlgorithm = normalizeAlgorithm( algorithm, 'encapsulateBits' as Operation, ); if (key.algorithm.name !== normalizedAlgorithm.name) { throw lazyDOMException('Key algorithm mismatch', 'InvalidAccessError'); } const variant = normalizedAlgorithm.name as MlKemVariant; const mlkem = new MlKem(variant); const keyData = key.keyObject.handle.exportKey( KFormatType.DER, KeyEncoding.SPKI, ); mlkem.setPublicKey( bufferLikeToArrayBuffer(keyData), KFormatType.DER, KeyEncoding.SPKI, ); return mlkem.encapsulateSync(); } private _decapsulateCore( algorithm: SubtleAlgorithm, key: CryptoKey, ciphertext: BufferLike, ): ArrayBuffer { const normalizedAlgorithm = normalizeAlgorithm( algorithm, 'decapsulateBits' as Operation, ); if (key.algorithm.name !== normalizedAlgorithm.name) { throw lazyDOMException('Key algorithm mismatch', 'InvalidAccessError'); } const variant = normalizedAlgorithm.name as MlKemVariant; const mlkem = new MlKem(variant); const keyData = key.keyObject.handle.exportKey( KFormatType.DER, KeyEncoding.PKCS8, ); mlkem.setPrivateKey( bufferLikeToArrayBuffer(keyData), KFormatType.DER, KeyEncoding.PKCS8, ); return mlkem.decapsulateSync(bufferLikeToArrayBuffer(ciphertext)); } async encapsulateBits( algorithm: SubtleAlgorithm, key: CryptoKey, ): Promise { requireArgs(arguments.length, 2, 'encapsulateBits'); if (!key.usages.includes('encapsulateBits')) { throw lazyDOMException( 'Key does not have encapsulateBits usage', 'InvalidAccessError', ); } return this._encapsulateCore(algorithm, key); } async encapsulateKey( algorithm: SubtleAlgorithm, key: CryptoKey, sharedKeyAlgorithm: SubtleAlgorithm | AnyAlgorithm, extractable: boolean, usages: KeyUsage[], ): Promise<{ key: CryptoKey; ciphertext: ArrayBuffer }> { requireArgs(arguments.length, 5, 'encapsulateKey'); if (!key.usages.includes('encapsulateKey')) { throw lazyDOMException( 'Key does not have encapsulateKey usage', 'InvalidAccessError', ); } const { sharedKey, ciphertext } = this._encapsulateCore(algorithm, key); // Node imports the encapsulated shared bits as 'raw-secret' // (webcrypto.js:1370-1374) so AEADs / KMAC accept the result. const importedKey = await this.importKey( 'raw-secret', sharedKey, sharedKeyAlgorithm, extractable, usages, ); return { key: importedKey, ciphertext }; } async decapsulateBits( algorithm: SubtleAlgorithm, key: CryptoKey, ciphertext: BufferLike, ): Promise { requireArgs(arguments.length, 3, 'decapsulateBits'); if (!key.usages.includes('decapsulateBits')) { throw lazyDOMException( 'Key does not have decapsulateBits usage', 'InvalidAccessError', ); } return this._decapsulateCore(algorithm, key, ciphertext); } async decapsulateKey( algorithm: SubtleAlgorithm, key: CryptoKey, ciphertext: BufferLike, sharedKeyAlgorithm: SubtleAlgorithm | AnyAlgorithm, extractable: boolean, usages: KeyUsage[], ): Promise { requireArgs(arguments.length, 6, 'decapsulateKey'); if (!key.usages.includes('decapsulateKey')) { throw lazyDOMException( 'Key does not have decapsulateKey usage', 'InvalidAccessError', ); } const sharedKey = this._decapsulateCore(algorithm, key, ciphertext); // Node imports the decapsulated shared bits as 'raw-secret' // (webcrypto.js:1490-1494). return this.importKey( 'raw-secret', sharedKey, sharedKeyAlgorithm, extractable, usages, ); } } export const subtle = new Subtle(); // Returns the number of bits to derive for an `importKey` algorithm, mirroring // Node's webcrypto.js:269-306 `getKeyLength`. Returns null for KDF algorithms // (HKDF / PBKDF2 / Argon2) — those carry their full derived secret without a // fixed key length. Throws OperationError on invalid AES / HMAC inputs rather // than silently coercing to a default (Node commit 4cb1f284136 behavior). function getKeyLength(algorithm: SubtleAlgorithm): number | null { const name = algorithm.name; const length = (algorithm as { length?: number }).length; switch (name) { case 'AES-CTR': case 'AES-CBC': case 'AES-GCM': case 'AES-KW': case 'AES-OCB': if (length !== 128 && length !== 192 && length !== 256) { throw lazyDOMException('Invalid key length', 'OperationError'); } return length; case 'HMAC': { if (length === undefined) { return getHmacBlockSize( (algorithm.hash as { name?: string } | undefined)?.name ?? (algorithm.hash as string | undefined), ); } if (typeof length === 'number' && length !== 0) { return length; } throw lazyDOMException('Invalid key length', 'OperationError'); } case 'KMAC128': return typeof length === 'number' ? length : 128; case 'KMAC256': return typeof length === 'number' ? length : 256; case 'ChaCha20-Poly1305': return 256; case 'HKDF': case 'PBKDF2': case 'Argon2d': case 'Argon2i': case 'Argon2id': return null; default: throw lazyDOMException( `Cannot determine key length for ${name}`, 'NotSupportedError', ); } } function getHmacBlockSize(name: string | undefined): number { switch (name) { case 'SHA-1': case 'SHA-256': return 512; case 'SHA-384': case 'SHA-512': return 1024; case 'SHA3-256': case 'SHA3-384': case 'SHA3-512': // SHA-3 / HMAC interaction undefined — Node throws here too // (webcrypto-modern-algos issue #23). throw lazyDOMException( 'Explicit algorithm length member is required', 'NotSupportedError', ); default: throw lazyDOMException('Invalid key length', 'OperationError'); } }