UNPKG

react-native-quick-crypto

Version:

A fast implementation of Node's `crypto` module written in C/C++ JSI

1,733 lines (1,587 loc) 130 kB
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<string, AnyAlgorithm> | null = null; function getCanonicalAlgorithmNames(): Map<string, AnyAlgorithm> { if (_canonicalAlgorithmNames === null) { const map = new Map<string, AnyAlgorithm>(); 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<string, Record<string, string | null>> = { '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<string, unknown>; const kRequiredFields: Record<string, IdlField[]> = { 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<string, unknown>; 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<string, unknown>)[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<string, number> = { '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>('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<ArrayBuffer> { 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>('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<ArrayBuffer> { 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<ArrayBuffer> { // 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>('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<ArrayBuffer> { // 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>('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<ArrayBuffer> { 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>('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<ArrayBuffer> { 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<ArrayBuffer> { 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<ArrayBuffer> { 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>('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<ArrayBuffer> { 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>('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<CryptoKey> { 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<CryptoKey> { // 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<CryptoKey> { 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>('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<CryptoKey> { 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>('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) {