react-native-quick-crypto
Version:
A fast implementation of Node's `crypto` module written in C/C++ JSI
1,791 lines (1,589 loc) • 59.3 kB
text/typescript
/* eslint-disable @typescript-eslint/no-unused-vars */
import { Buffer as SBuffer } from 'safe-buffer';
import type {
SubtleAlgorithm,
KeyUsage,
BinaryLike,
BufferLike,
JWK,
AnyAlgorithm,
ImportFormat,
AesKeyGenParams,
EncryptDecryptParams,
Operation,
AesCtrParams,
AesCbcParams,
AesGcmParams,
RsaOaepParams,
ChaCha20Poly1305Params,
} from './utils';
import { KFormatType, KeyEncoding } from './utils';
import {
CryptoKey,
KeyObject,
PublicKeyObject,
PrivateKeyObject,
SecretKeyObject,
} from './keys';
import type { CryptoKeyPair } from './utils/types';
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 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 } 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, type MlDsaVariant } from './mldsa';
import { hkdfDeriveBits, type HkdfAlgorithm } 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
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));
}
function normalizeAlgorithm(
algorithm: SubtleAlgorithm | AnyAlgorithm,
_operation: Operation,
): SubtleAlgorithm {
if (typeof algorithm === 'string') {
return { name: algorithm };
}
return algorithm as SubtleAlgorithm;
}
function getAlgorithmName(name: string, length: number): string {
return `${name}${length}`;
}
// Placeholder implementations for missing functions
function ecExportKey(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 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);
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;
}
async function aesGcmCipher(
mode: CipherOrWrapMode,
key: CryptoKey,
data: ArrayBuffer,
algorithm: AesGcmParams,
): Promise<ArrayBuffer> {
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 as { length: number }).length;
const cipherType = `aes-${keyLength}-gcm`;
// 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.iv),
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 (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: 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);
}
function rsaImportKey(
format: ImportFormat,
data: BufferLike | JWK,
algorithm: SubtleAlgorithm,
extractable: boolean,
keyUsages: KeyUsage[],
): CryptoKey {
const { name } = algorithm;
// Validate usages
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}`);
}
if (hasAnyNotIn(keyUsages, checkSet)) {
throw new Error(`Unsupported key usage for ${name}`);
}
let keyObject: KeyObject;
if (format === 'jwk') {
const jwk = data as JWK;
// Validate JWK
if (jwk.kty !== 'RSA') {
throw new Error('Invalid JWK format for RSA key');
}
const handle =
NitroModules.createHybridObject<KeyObjectHandle>('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 as BufferLike);
keyObject = KeyObject.createKeyObject(
'public',
keyData,
KFormatType.DER,
KeyEncoding.SPKI,
);
} else if (format === 'pkcs8') {
const keyData = bufferLikeToArrayBuffer(data as BufferLike);
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 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<CryptoKey> {
// Validate usages
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;
// 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>('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 as BinaryLike);
} 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: 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<CryptoKey> {
const { name, length } = algorithm;
// Validate usages
const validUsages: KeyUsage[] = [
'encrypt',
'decrypt',
'wrapKey',
'unwrapKey',
];
if (hasAnyNotIn(keyUsages, validUsages)) {
throw new Error(`Unsupported key usage for ${name}`);
}
let keyObject: KeyObject;
let actualLength: number;
if (format === 'jwk') {
const jwk = data as JWK;
// Validate JWK
if (jwk.kty !== 'oct') {
throw new Error('Invalid JWK format for AES key');
}
const handle =
NitroModules.createHybridObject<KeyObjectHandle>('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 as BufferLike);
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: ImportFormat,
data: BufferLike,
algorithm: SubtleAlgorithm,
extractable: boolean,
keyUsages: KeyUsage[],
): CryptoKey {
const { name } = algorithm;
// Validate usages
const isX = name === 'X25519' || name === 'X448';
const allowedUsages: KeyUsage[] = isX
? ['deriveKey', 'deriveBits']
: ['sign', 'verify'];
if (hasAnyNotIn(keyUsages, allowedUsages)) {
throw lazyDOMException(
`Unsupported key usage for ${name} key`,
'SyntaxError',
);
}
let keyObject: 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>('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: ImportFormat,
data: BufferLike,
algorithm: SubtleAlgorithm,
extractable: boolean,
keyUsages: KeyUsage[],
): CryptoKey {
const { name } = algorithm;
// Validate usages
if (hasAnyNotIn(keyUsages, ['sign', 'verify'])) {
throw lazyDOMException(
`Unsupported key usage for ${name} key`,
'SyntaxError',
);
}
let keyObject: 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: CryptoKey,
): Promise<ArrayBuffer | unknown> => {
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: CryptoKey,
): Promise<ArrayBuffer | unknown> => {
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: CryptoKey): ArrayBuffer | unknown => {
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: 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 '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 }: SubtleAlgorithm,
format: ImportFormat,
keyData: BufferLike | BinaryLike,
extractable: boolean,
keyUsages: KeyUsage[],
): Promise<CryptoKey> => {
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 as BinaryLike);
return new CryptoKey(keyObject, { name }, keyUsages, false);
}
}
throw new Error(`Unable to import ${name} key with format ${format}`);
};
const hkdfImportKey = async (
format: ImportFormat,
keyData: BufferLike | BinaryLike,
algorithm: SubtleAlgorithm,
extractable: boolean,
keyUsages: KeyUsage[],
): Promise<CryptoKey> => {
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 as BinaryLike);
return new CryptoKey(keyObject, { name }, keyUsages, extractable);
}
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',
);
};
// 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,
);
}
// 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: 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 (!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: CipherOrWrapMode,
algorithm: EncryptDecryptParams,
key: CryptoKey,
data: ArrayBuffer,
op: Operation,
): Promise<ArrayBuffer> => {
if (
key.algorithm.name !== algorithm.name ||
!key.usages.includes(op as KeyUsage)
) {
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 as ChaCha20Poly1305Params,
);
}
};
export class Subtle {
async decrypt(
algorithm: EncryptDecryptParams,
key: CryptoKey,
data: BufferLike,
): Promise<ArrayBuffer> {
const normalizedAlgorithm = normalizeAlgorithm(algorithm, 'decrypt');
return cipherOrWrap(
CipherOrWrapMode.kWebCryptoCipherDecrypt,
normalizedAlgorithm as EncryptDecryptParams,
key,
bufferLikeToArrayBuffer(data),
'decrypt',
);
}
async digest(
algorithm: SubtleAlgorithm | AnyAlgorithm,
data: BufferLike,
): Promise<ArrayBuffer> {
const normalizedAlgorithm = normalizeAlgorithm(
algorithm,
'digest' as Operation,
);
return asyncDigest(normalizedAlgorithm, data);
}
async deriveBits(
algorithm: SubtleAlgorithm,
baseKey: CryptoKey,
length: number,
): Promise<ArrayBuffer> {
// 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 as unknown as HkdfAlgorithm,
baseKey,
length,
);
}
throw new Error(
`'subtle.deriveBits()' for ${algorithm.name} is not implemented.`,
);
}
async deriveKey(
algorithm: SubtleAlgorithm,
baseKey: CryptoKey,
derivedKeyAlgorithm: SubtleAlgorithm,
extractable: boolean,
keyUsages: KeyUsage[],
): Promise<CryptoKey> {
// 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: ArrayBuffer;
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 as unknown as HkdfAlgorithm,
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: EncryptDecryptParams,
key: CryptoKey,
data: BufferLike,
): Promise<ArrayBuffer> {
const normalizedAlgorithm = normalizeAlgorithm(algorithm, 'encrypt');
return cipherOrWrap(
CipherOrWrapMode.kWebCryptoCipherEncrypt,
normalizedAlgorithm as EncryptDecryptParams,
key,
bufferLikeToArrayBuffer(data),
'encrypt',
);
}
async exportKey(
format: ImportFormat,
key: CryptoKey,
): Promise<ArrayBuffer | JWK> {
if (!key.extractable) throw new Error('key is not extractable');
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':
return exportKeyRaw(key) as ArrayBuffer;
}
}
async wrapKey(
format: ImportFormat,
key: CryptoKey,
wrappingKey: CryptoKey,
wrapAlgorithm: EncryptDecryptParams,
): Promise<ArrayBuffer> {
// 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: 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 (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 as ArrayBuffer;
}
// Step 3: Encrypt the exported key
return cipherOrWrap(
CipherOrWrapMode.kWebCryptoCipherEncrypt,
wrapAlgorithm,
wrappingKey,
keyData,
'wrapKey',
);
}
async unwrapKey(
format: ImportFormat,
wrappedKey: BufferLike,
unwrappingKey: CryptoKey,
unwrapAlgorithm: EncryptDecryptParams,
unwrappedKeyAlgorithm: SubtleAlgorithm | AnyAlgorithm,
extractable: boolean,
keyUsages: KeyUsage[],
): Promise<CryptoKey> {
// 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: 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 (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) 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<CryptoKey | CryptoKeyPair> {
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