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
text/typescript
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) {