UNPKG

react-native-quick-crypto

Version:

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

1,427 lines (1,340 loc) 51.7 kB
"use strict"; /* eslint-disable @typescript-eslint/no-unused-vars */ import { Buffer as SBuffer } from 'safe-buffer'; import { KFormatType, KeyEncoding } from './utils'; import { CryptoKey, KeyObject, PublicKeyObject, PrivateKeyObject, SecretKeyObject } from './keys'; import { bufferLikeToArrayBuffer } from './utils/conversion'; import { lazyDOMException } from './utils/errors'; import { normalizeHashName, HashContext } from './utils/hashnames'; import { validateMaxBufferLength } from './utils/validation'; import { asyncDigest } from './hash'; import { createSecretKey } from './keys'; import { NitroModules } from 'react-native-nitro-modules'; import { pbkdf2DeriveBits } from './pbkdf2'; import { ecImportKey, ecdsaSignVerify, ec_generateKeyPair } from './ec'; import { rsa_generateKeyPair } from './rsa'; import { getRandomValues } from './random'; import { createHmac } from './hmac'; import { createSign, createVerify } from './keys/signVerify'; import { ed_generateKeyPairWebCrypto, x_generateKeyPairWebCrypto, xDeriveBits, Ed } from './ed'; import { mldsa_generateKeyPairWebCrypto } from './mldsa'; import { hkdfDeriveBits } from './hkdf'; // import { pbkdf2DeriveBits } from './pbkdf2'; // import { aesCipher, aesGenerateKey, aesImportKey, getAlgorithmName } from './aes'; // import { rsaCipher, rsaExportKey, rsaImportKey, rsaKeyGenerate } from './rsa'; // import { normalizeAlgorithm, type Operation } from './algorithms'; // import { hmacImportKey } from './mac'; // Temporary enums that need to be defined var KWebCryptoKeyFormat = /*#__PURE__*/function (KWebCryptoKeyFormat) { KWebCryptoKeyFormat[KWebCryptoKeyFormat["kWebCryptoKeyFormatRaw"] = 0] = "kWebCryptoKeyFormatRaw"; KWebCryptoKeyFormat[KWebCryptoKeyFormat["kWebCryptoKeyFormatSPKI"] = 1] = "kWebCryptoKeyFormatSPKI"; KWebCryptoKeyFormat[KWebCryptoKeyFormat["kWebCryptoKeyFormatPKCS8"] = 2] = "kWebCryptoKeyFormatPKCS8"; return KWebCryptoKeyFormat; }(KWebCryptoKeyFormat || {}); var CipherOrWrapMode = /*#__PURE__*/function (CipherOrWrapMode) { CipherOrWrapMode[CipherOrWrapMode["kWebCryptoCipherEncrypt"] = 0] = "kWebCryptoCipherEncrypt"; CipherOrWrapMode[CipherOrWrapMode["kWebCryptoCipherDecrypt"] = 1] = "kWebCryptoCipherDecrypt"; return CipherOrWrapMode; }(CipherOrWrapMode || {}); // Placeholder functions that need to be implemented function hasAnyNotIn(usages, allowed) { return usages.some(usage => !allowed.includes(usage)); } function normalizeAlgorithm(algorithm, _operation) { if (typeof algorithm === 'string') { return { name: algorithm }; } return algorithm; } function getAlgorithmName(name, length) { return `${name}${length}`; } // Placeholder implementations for missing functions function ecExportKey(key, format) { 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 EC export format: ${format}`); } } function rsaExportKey(key, format) { 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, key, data, algorithm) { const rsaParams = algorithm; // 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, key, data, algorithm) { const { name } = algorithm; switch (name) { case 'AES-CTR': return aesCtrCipher(mode, key, data, algorithm); case 'AES-CBC': return aesCbcCipher(mode, key, data, algorithm); case 'AES-GCM': return aesGcmCipher(mode, key, data, algorithm); default: throw lazyDOMException(`Unsupported AES algorithm: ${name}`, 'NotSupportedError'); } } async function aesCtrCipher(mode, key, data, algorithm) { // 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.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, key, data, algorithm) { // 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.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; } async function aesGcmCipher(mode, key, data, algorithm) { const { tagLength = 128 } = algorithm; // Validate tag length const validTagLengths = [32, 64, 96, 104, 112, 120, 128]; if (!validTagLengths.includes(tagLength)) { throw lazyDOMException(`${tagLength} is not a valid AES-GCM tag length`, 'OperationError'); } const tagByteLength = tagLength / 8; // Get cipher type based on key length const keyLength = key.algorithm.length; const cipherType = `aes-${keyLength}-gcm`; // Create cipher const factory = NitroModules.createHybridObject('CipherFactory'); const cipher = factory.createCipher({ isCipher: mode === CipherOrWrapMode.kWebCryptoCipherEncrypt, cipherType, cipherKey: bufferLikeToArrayBuffer(key.keyObject.export()), iv: bufferLikeToArrayBuffer(algorithm.iv), authTagLen: tagByteLength }); let processData; let authTag; 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 (algorithm.additionalData) { cipher.setAAD(bufferLikeToArrayBuffer(algorithm.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 aesKwCipher(mode, key, data) { 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.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, key, data, algorithm) { 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; let authTag; 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, extractable, keyUsages) { 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 = ['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 = { name, length }; return new CryptoKey(keyObject, keyAlgorithm, keyUsages, extractable); } async function hmacGenerateKey(algorithm, extractable, keyUsages) { // 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 = { name: 'HMAC', hash: { name: webCryptoHashName }, length }; return new CryptoKey(keyObject, keyAlgorithm, keyUsages, extractable); } function rsaImportKey(format, data, algorithm, extractable, keyUsages) { const { name } = algorithm; // Validate usages let checkSet; 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}`); } if (hasAnyNotIn(keyUsages, checkSet)) { throw new Error(`Unsupported key usage for ${name}`); } let keyObject; if (format === 'jwk') { const jwk = data; // Validate JWK if (jwk.kty !== 'RSA') { throw new Error('Invalid JWK format for RSA key'); } const handle = NitroModules.createHybridObject('KeyObjectHandle'); const keyType = handle.initJwk(jwk, undefined); if (keyType === undefined) { throw new Error('Failed to import RSA JWK'); } // Create the appropriate KeyObject based on type if (keyType === 1) { keyObject = new PublicKeyObject(handle); } else if (keyType === 2) { keyObject = new PrivateKeyObject(handle); } else { throw new Error('Unexpected key type from RSA JWK import'); } } else if (format === 'spki') { const keyData = bufferLikeToArrayBuffer(data); keyObject = KeyObject.createKeyObject('public', keyData, KFormatType.DER, KeyEncoding.SPKI); } else if (format === 'pkcs8') { const keyData = bufferLikeToArrayBuffer(data); keyObject = KeyObject.createKeyObject('private', keyData, KFormatType.DER, KeyEncoding.PKCS8); } else { throw new Error(`Unsupported format for RSA import: ${format}`); } // Get the modulus length from the key and add it to the algorithm const keyDetails = keyObject.asymmetricKeyDetails; // Convert publicExponent number to big-endian byte array let publicExponentBytes; if (keyDetails?.publicExponent) { const exp = keyDetails.publicExponent; // Convert number to big-endian bytes const bytes = []; 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, format, data, extractable, keyUsages) { // Validate usages if (hasAnyNotIn(keyUsages, ['sign', 'verify'])) { throw new Error('Unsupported key usage for an HMAC key'); } let keyObject; if (format === 'jwk') { const jwk = data; // Validate JWK if (!jwk || typeof jwk !== 'object') { throw new Error('Invalid keyData'); } if (jwk.kty !== 'oct') { throw new Error('Invalid JWK format for HMAC key'); } // Validate key length if specified if (algorithm.length !== undefined) { if (!jwk.k) { throw new Error('JWK missing key data'); } // Decode to check length 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'); const keyType = handle.initJwk(jwk, undefined); if (keyType === undefined || keyType !== 0) { throw new Error('Failed to import HMAC JWK'); } keyObject = new SecretKeyObject(handle); } else if (format === 'raw') { keyObject = createSecretKey(data); } else { throw new Error(`Unable to import HMAC key with format ${format}`); } // Normalize hash to { name: string } format per WebCrypto spec const hashName = normalizeHashName(algorithm.hash, HashContext.WebCrypto); const normalizedAlgorithm = { ...algorithm, name: 'HMAC', hash: { name: hashName } }; return new CryptoKey(keyObject, normalizedAlgorithm, keyUsages, extractable); } async function aesImportKey(algorithm, format, data, extractable, keyUsages) { const { name, length } = algorithm; // Validate usages const validUsages = ['encrypt', 'decrypt', 'wrapKey', 'unwrapKey']; if (hasAnyNotIn(keyUsages, validUsages)) { throw new Error(`Unsupported key usage for ${name}`); } let keyObject; let actualLength; if (format === 'jwk') { const jwk = data; // Validate JWK if (jwk.kty !== 'oct') { throw new Error('Invalid JWK format for AES key'); } const handle = NitroModules.createHybridObject('KeyObjectHandle'); const keyType = handle.initJwk(jwk, undefined); if (keyType === undefined || keyType !== 0) { throw new Error('Failed to import AES JWK'); } keyObject = new SecretKeyObject(handle); // Get actual key length from imported key const exported = keyObject.export(); actualLength = exported.byteLength * 8; } else if (format === 'raw') { const keyData = bufferLikeToArrayBuffer(data); actualLength = keyData.byteLength * 8; // Validate key length if (![128, 192, 256].includes(actualLength)) { throw new Error('Invalid AES key length'); } keyObject = createSecretKey(keyData); } else { throw new Error(`Unsupported format for AES import: ${format}`); } // 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, data, algorithm, extractable, keyUsages) { const { name } = algorithm; // Validate usages const isX = name === 'X25519' || name === 'X448'; const allowedUsages = isX ? ['deriveKey', 'deriveBits'] : ['sign', 'verify']; if (hasAnyNotIn(keyUsages, allowedUsages)) { throw lazyDOMException(`Unsupported key usage for ${name} key`, 'SyntaxError'); } let keyObject; if (format === 'spki') { // Import public key const keyData = bufferLikeToArrayBuffer(data); keyObject = KeyObject.createKeyObject('public', keyData, KFormatType.DER, KeyEncoding.SPKI); } else if (format === 'pkcs8') { // Import private key const keyData = bufferLikeToArrayBuffer(data); keyObject = KeyObject.createKeyObject('private', keyData, KFormatType.DER, KeyEncoding.PKCS8); } else if (format === 'raw') { // Raw format - public key only for Ed keys const keyData = bufferLikeToArrayBuffer(data); const handle = NitroModules.createHybridObject('KeyObjectHandle'); // For raw Ed keys, we need to create them differently // Raw public keys are just the key bytes handle.init(1, keyData); // 1 = public key type keyObject = new PublicKeyObject(handle); } else { throw lazyDOMException(`Unsupported format for ${name} import: ${format}`, 'NotSupportedError'); } return new CryptoKey(keyObject, { name }, keyUsages, extractable); } function mldsaImportKey(format, data, algorithm, extractable, keyUsages) { const { name } = algorithm; // Validate usages if (hasAnyNotIn(keyUsages, ['sign', 'verify'])) { throw lazyDOMException(`Unsupported key usage for ${name} key`, 'SyntaxError'); } let keyObject; if (format === 'spki') { // Import public key const keyData = bufferLikeToArrayBuffer(data); keyObject = KeyObject.createKeyObject('public', keyData, KFormatType.DER, KeyEncoding.SPKI); } else if (format === 'pkcs8') { // Import private key const keyData = bufferLikeToArrayBuffer(data); keyObject = KeyObject.createKeyObject('private', keyData, KFormatType.DER, KeyEncoding.PKCS8); } else { throw lazyDOMException(`Unsupported format for ${name} import: ${format}`, 'NotSupportedError'); } return new CryptoKey(keyObject, { name }, keyUsages, extractable); } const exportKeySpki = async key => { 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': if (key.type === 'public') { // Export ML-DSA key in SPKI DER format 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 => { 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': if (key.type === 'private') { // Export ML-DSA key in PKCS8 DER format 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`); }; const exportKeyRaw = key => { switch (key.algorithm.name) { case 'ECDSA': // Fall through case 'ECDH': if (key.type === 'public') { return ecExportKey(key, KWebCryptoKeyFormat.kWebCryptoKeyFormatRaw); } break; case 'Ed25519': // Fall through case 'Ed448': // Fall through case 'X25519': // Fall through case 'X448': if (key.type === 'public') { // Export raw public key const exported = key.keyObject.handle.exportKey(); return bufferLikeToArrayBuffer(exported); } break; case 'AES-CTR': // Fall through case 'AES-CBC': // Fall through case 'AES-GCM': // Fall through case 'AES-KW': // Fall through case 'ChaCha20-Poly1305': // Fall through case 'HMAC': { const exported = key.keyObject.export(); // Convert Buffer to ArrayBuffer return exported.buffer.slice(exported.byteOffset, exported.byteOffset + exported.byteLength); } } throw lazyDOMException(`Unable to export a raw ${key.algorithm.name} ${key.type} key`, 'InvalidAccessError'); }; const exportKeyJWK = key => { 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 'ECDSA': // Fall through case 'ECDH': jwk.crv ||= key.algorithm.namedCurve; return jwk; case 'AES-CTR': // Fall through case 'AES-CBC': // Fall through case 'AES-GCM': // Fall through case 'AES-KW': // 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'); }; const importGenericSecretKey = async ({ name, length }, format, keyData, extractable, keyUsages) => { if (extractable) { throw new Error(`${name} keys are not extractable`); } if (hasAnyNotIn(keyUsages, ['deriveKey', 'deriveBits'])) { throw new Error(`Unsupported key usage for a ${name} key`); } switch (format) { case 'raw': { if (hasAnyNotIn(keyUsages, ['deriveKey', 'deriveBits'])) { throw new Error(`Unsupported key usage for a ${name} key`); } const checkLength = typeof keyData === 'string' || SBuffer.isBuffer(keyData) ? keyData.length * 8 : keyData.byteLength * 8; if (length !== undefined && length !== checkLength) { throw new Error('Invalid key length'); } const keyObject = createSecretKey(keyData); return new CryptoKey(keyObject, { name }, keyUsages, false); } } throw new Error(`Unable to import ${name} key with format ${format}`); }; const hkdfImportKey = async (format, keyData, algorithm, extractable, keyUsages) => { const { name } = algorithm; if (hasAnyNotIn(keyUsages, ['deriveKey', 'deriveBits'])) { throw new Error(`Unsupported key usage for a ${name} key`); } switch (format) { case 'raw': { const keyObject = createSecretKey(keyData); return new CryptoKey(keyObject, { name }, keyUsages, extractable); } default: throw new Error(`Unable to import ${name} key with format ${format}`); } }; const checkCryptoKeyPairUsages = pair => { 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'); }; // Type guard to check if result is CryptoKeyPair export function isCryptoKeyPair(result) { return 'publicKey' in result && 'privateKey' in result; } function hmacSignVerify(key, data, signature) { // 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); } // Verify operation - compare computed HMAC with provided signature const sigBytes = new Uint8Array(bufferLikeToArrayBuffer(signature)); const computedBytes = new Uint8Array(computed.buffer, computed.byteOffset, computed.byteLength); if (computedBytes.length !== sigBytes.length) { return false; } // Constant-time comparison to prevent timing attacks let result = 0; for (let i = 0; i < computedBytes.length; i++) { result |= computedBytes[i] ^ sigBytes[i]; } return result === 0; } function rsaSignVerify(key, data, padding, signature, saltLength) { // 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, data, signature) { 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(); // 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, data, signature) { 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, key, data, signature) => { const usage = signature === undefined ? 'sign' : 'verify'; algorithm = normalizeAlgorithm(algorithm, usage); if (!key.usages.includes(usage) || algorithm.name !== key.algorithm.name) { 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': return mldsaSignVerify(key, data, signature); } throw lazyDOMException(`Unrecognized algorithm name '${algorithm.name}' for '${usage}'`, 'NotSupportedError'); }; const cipherOrWrap = async (mode, algorithm, key, data, op) => { if (key.algorithm.name !== algorithm.name || !key.usages.includes(op)) { throw lazyDOMException('The requested operation is not valid for the provided key', 'InvalidAccessError'); } 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': return aesCipher(mode, key, data, algorithm); case 'AES-KW': return aesKwCipher(mode, key, data); case 'ChaCha20-Poly1305': return chaCha20Poly1305Cipher(mode, key, data, algorithm); } }; export class Subtle { async decrypt(algorithm, key, data) { const normalizedAlgorithm = normalizeAlgorithm(algorithm, 'decrypt'); return cipherOrWrap(CipherOrWrapMode.kWebCryptoCipherDecrypt, normalizedAlgorithm, key, bufferLikeToArrayBuffer(data), 'decrypt'); } async digest(algorithm, data) { const normalizedAlgorithm = normalizeAlgorithm(algorithm, 'digest'); return asyncDigest(normalizedAlgorithm, data); } async deriveBits(algorithm, baseKey, length) { // Allow either deriveBits OR deriveKey usage (WebCrypto spec allows both) if (!baseKey.keyUsages.includes('deriveBits') && !baseKey.keyUsages.includes('deriveKey')) { throw new Error('baseKey does not have deriveBits or deriveKey usage'); } if (baseKey.algorithm.name !== algorithm.name) throw new Error('Key algorithm mismatch'); switch (algorithm.name) { case 'PBKDF2': return pbkdf2DeriveBits(algorithm, baseKey, length); case 'X25519': // Fall through case 'X448': return xDeriveBits(algorithm, baseKey, length); case 'HKDF': return hkdfDeriveBits(algorithm, baseKey, length); } throw new Error(`'subtle.deriveBits()' for ${algorithm.name} is not implemented.`); } async deriveKey(algorithm, baseKey, derivedKeyAlgorithm, extractable, keyUsages) { // Validate baseKey usage if (!baseKey.usages.includes('deriveKey') && !baseKey.usages.includes('deriveBits')) { throw lazyDOMException('baseKey does not have deriveKey or deriveBits usage', 'InvalidAccessError'); } // Calculate required key length const length = getKeyLength(derivedKeyAlgorithm); // Step 1: Derive bits let derivedBits; if (baseKey.algorithm.name !== algorithm.name) throw new Error('Key algorithm mismatch'); switch (algorithm.name) { case 'PBKDF2': derivedBits = await pbkdf2DeriveBits(algorithm, baseKey, length); break; case 'X25519': // Fall through case 'X448': derivedBits = await xDeriveBits(algorithm, baseKey, length); break; case 'HKDF': derivedBits = hkdfDeriveBits(algorithm, baseKey, length); break; default: throw new Error(`'subtle.deriveKey()' for ${algorithm.name} is not implemented.`); } // Step 2: Import as key return this.importKey('raw', derivedBits, derivedKeyAlgorithm, extractable, keyUsages); } async encrypt(algorithm, key, data) { const normalizedAlgorithm = normalizeAlgorithm(algorithm, 'encrypt'); return cipherOrWrap(CipherOrWrapMode.kWebCryptoCipherEncrypt, normalizedAlgorithm, key, bufferLikeToArrayBuffer(data), 'encrypt'); } async exportKey(format, key) { if (!key.extractable) throw new Error('key is not extractable'); switch (format) { case 'spki': return await exportKeySpki(key); case 'pkcs8': return await exportKeyPkcs8(key); case 'jwk': return exportKeyJWK(key); case 'raw': return exportKeyRaw(key); } } async wrapKey(format, key, wrappingKey, wrapAlgorithm) { // Validate wrappingKey usage if (!wrappingKey.usages.includes('wrapKey')) { throw lazyDOMException('wrappingKey does not have wrapKey usage', 'InvalidAccessError'); } // Step 1: Export the key const exported = await this.exportKey(format, key); // Step 2: Convert to ArrayBuffer if JWK let keyData; 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 (wrapAlgorithm.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; } // Step 3: Encrypt the exported key return cipherOrWrap(CipherOrWrapMode.kWebCryptoCipherEncrypt, wrapAlgorithm, wrappingKey, keyData, 'wrapKey'); } async unwrapKey(format, wrappedKey, unwrappingKey, unwrapAlgorithm, unwrappedKeyAlgorithm, extractable, keyUsages) { // Validate unwrappingKey usage if (!unwrappingKey.usages.includes('unwrapKey')) { throw lazyDOMException('unwrappingKey does not have unwrapKey usage', 'InvalidAccessError'); } // Step 1: Decrypt the wrapped key const decrypted = await cipherOrWrap(CipherOrWrapMode.kWebCryptoCipherDecrypt, unwrapAlgorithm, unwrappingKey, bufferLikeToArrayBuffer(wrappedKey), 'unwrapKey'); // Step 2: Convert to appropriate format let keyData; if (format === 'jwk') { const buffer = SBuffer.from(decrypted); // For AES-KW, the data may be padded - find the null terminator let jwkString; if (unwrapAlgorithm.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); } else { keyData = decrypted; } // Step 3: Import the key return this.importKey(format, keyData, unwrappedKeyAlgorithm, extractable, keyUsages); } async generateKey(algorithm, extractable, keyUsages) { algorithm = normalizeAlgorithm(algorithm, 'generateKey'); let result; 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); break; case 'AES-CTR': // Fall through case 'AES-CBC': // Fall through case 'AES-GCM': // Fall through case 'AES-KW': result = await aesGenerateKey(algorithm, extractable, keyUsages); break; case 'ChaCha20-Poly1305': { const length = algorithm.length ?? 256; if (length !== 256) { throw lazyDOMException('ChaCha20-Poly1305 only supports 256-bit keys', 'NotSupportedError'); } result = await aesGenerateKey({ name: 'ChaCha20-Poly1305', length: 256 }, extractable, keyUsages); break; } case 'HMAC': result = await hmacGenerateKey(algorithm, extractable, keyUsages); break; case 'Ed25519': // Fall through case 'Ed448': result = await ed_generateKeyPairWebCrypto(algorithm.name.toLowerCase(), extractable, keyUsages); checkCryptoKeyPairUsages(result); break; case 'ML-DSA-44': // Fall through case 'ML-DSA-65': // Fall through case 'ML-DSA-87': result = await mldsa_generateKeyPairWebCrypto(algorithm.name, extractable, keyUsages); checkCryptoKeyPairUsages(result); break; case 'X25519': // Fall through case 'X448': result = await x_generateKeyPairWebCrypto(algorithm.name.toLowerCase(), extractable, keyUsages); checkCryptoKeyPairUsages(result); break; default: throw new Error(`'subtle.generateKey()' is not implemented for ${algorithm.name}. Unrecognized algorithm name`); } return result; } async importKey(format, data, algorithm, extractable, keyUsages) { const normalizedAlgorithm = normalizeAlgorithm(algorithm, 'importKey'); let result; switch (normalizedAlgorithm.name) { case 'RSASSA-PKCS1-v1_5': // Fall through case 'RSA-PSS': // Fall through case 'RSA-OAEP': result = rsaImportKey(format, data, normalizedAlgorithm, extractable, keyUsages); break; case 'ECDSA': // Fall through case 'ECDH': result = ecImportKey(format, data, normalizedAlgorithm, extractable, keyUsages); break; case 'HMAC': result = await hmacImportKey(normalizedAlgorithm, format, data, extractable, keyUsages); break; case 'AES-CTR': // Fall through case 'AES-CBC': // Fall through case 'AES-GCM': // Fall through case 'AES-KW': // Fall through case 'ChaCha20-Poly1305': result = await aesImportKey(normalizedAlgorithm, format, data, extractable, keyUsages); break; case 'PBKDF2': result = await importGenericSecretKey(normalizedAlgorithm, format, data, extractable, keyUsages); break; case 'HKDF': result = await hkdfImportKey(format, data, normalizedAlgorithm, extractable, keyUsages); break; case 'X25519': // Fall through case 'X448': // Fall through case 'Ed25519': // Fall through case 'Ed448': result = edImportKey(format, data, normalizedAlgorithm, extractable, keyUsages); break; case 'ML-DSA-44': // Fall through case 'ML-DSA-65': // Fall through case 'ML-DSA-87': result = mldsaImportKey(format, data, 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, key, data) { const normalizedAlgorithm = normalizeAlgorithm(algorithm, 'sign'); if (normalizedAlgorithm.name === 'HMAC') { // Validate key usage if (!key.usages.includes('sign')) { throw lazyDOMException('Key does not have sign usage', 'InvalidAccessError'); } // Get hash algorithm from key or algorithm params // Hash can be either a string or an object with name property // eslint-disable-next-line @typescript-eslint/no-explicit-any const alg = normalizedAlgorithm; // eslint-disable-next-line @typescript-eslint/no-explicit-any const keyAlg = key.algorithm; let hashAlgorithm = 'SHA-256'; if (typeof alg.hash === 'string') { hashAlgorithm = alg.hash; } else if (alg.hash?.name) { hashAlgorithm = alg.hash.name; } else if (typeof keyAlg.hash === 'string') { hashAlgorithm = keyAlg.hash; } else if (keyAlg.hash?.name) { hashAlgorithm = keyAlg.hash.name; } // Create HMAC and sign const keyData = key.keyObject.export(); const hmac = createHmac(hashAlgorithm, keyData); hmac.update(bufferLikeToArrayBuffer(data)); return bufferLikeToArrayBuffer(hmac.digest()); } return signVerify(normalizedAlgorithm, key, data); } async verify(algorithm, key, signature, data) { const normalizedAlgorithm = normalizeAlgorithm(algorithm, 'verify'); if (normalizedAlgorithm.name === 'HMAC') { // Validate key usage if (!key.usages.includes('verify')) { throw lazyDOMException('Key does not have verify usage', 'InvalidAccessError'); } // Get hash algorithm // Hash can be either a string or an object with name property // eslint-disable-next-line @typescript-eslint/no-explicit-any const alg = normalizedAlgorithm; // eslint-disable-next-line @typescript-eslint/no-explicit-any const keyAlg = key.algorithm; let hashAlgorithm = 'SHA-256';