import { Array, ArrayBuffer, ArrayBufferPrototypeGetResizable as resizable, ArrayFrom, getter, Object, ObjectCreate, ObjectDefineProperty as define, ObjectFreeze as freeze, ReflectApply as apply, Set, SetPrototypeHas as has, String, SymbolIterator, TypedArrayPrototypeGetBuffer as typedBuffer, TypedArrayPrototypeGetByteLength as byteLength, TypedArrayPrototypeGetByteOffset as byteOffset, TypedArrayPrototypeGetTag as tag, TypeError, Uint8Array, Uint8ArrayPrototypeSet as setBytes, uncurryThis, WeakMap, WeakMapPrototypeSet as set, } from "ext:core/primordials"; import { op_crypto_derive_key as deriveKeyBytes, op_crypto_digest as digestBytes, op_crypto_key_clone as cloneKey, op_crypto_key_deserialize as deserializeKey, op_crypto_key_dispose as disposeKey, op_crypto_key_export as exportKeyBytes, op_crypto_key_export_jwk as exportKeyJwk, op_crypto_key_generate as generateKeyBytes, op_crypto_key_import as importKeyBytes, op_crypto_key_import_asymmetric as importAsymmetric, op_crypto_key_import_jwk as importJwk, op_crypto_key_pair_generate as generateKeyPairBytes, op_crypto_key_serialize as serializeKey, op_crypto_random as randomBytes, op_crypto_run as runCrypto, op_crypto_unwrap_key as unwrapKeyBytes, op_crypto_uuid as uuid, op_crypto_wrap_key as wrapKeyBytes, } from "ext:core/ops"; import { installInterface } from "ext:web/interface.js"; import { createException } from "ext:web/events.js"; import { array, dictionary, required, state, string } from "ext:web/idl.js"; import { registerSerializable, registerWire } from "ext:web/serialization.js"; import type { CryptoBuffer, CryptoKeyFormat, CryptoKeyType, CryptoKeyUsage, CryptoOperation, CryptoView, } from "./types.js"; type KeyMeta = { name: string; length?: number; hash?: string; usages: readonly string[]; publicUsages?: readonly string[]; extractable: boolean; type: CryptoKeyType; curve?: string; exponent?: CryptoBuffer; }; type KeyState = { id: number; meta: KeyMeta & { publicUsages: readonly string[] }; algorithm: Record; }; type AlgorithmRecord = Record; type RunParams = { hash?: string; iv?: CryptoBuffer; counter?: CryptoBuffer; additionalData?: CryptoBuffer; tagLength?: number; iterations?: number; salt?: CryptoBuffer; info?: CryptoBuffer; peerId?: number; }; type CryptoStates = { subtle: SubtleCrypto }; const cryptoStates = new WeakMap(), subtleStates = new WeakMap(), keyStates = new WeakMap(); const charCodeAt = uncurryThis(String.prototype.charCodeAt); const fromCharCode = String.fromCharCode; const ceil = Math.ceil; const subarray = uncurryThis(Uint8Array.prototype.subarray); const registerFinalizer = uncurryThis(FinalizationRegistry.prototype.register); type Secret = { readonly __secret: unique symbol }; const secret = ObjectCreate(null) as Secret; function exception(message: string, name: string): Error { return createException({ message, name }); } const isView = ArrayBuffer.isView; const getArrayBufferLength = getter(ArrayBuffer.prototype, "byteLength"); const dataViewBuffer = getter(DataView.prototype, "buffer"); const dataViewOffset = getter(DataView.prototype, "byteOffset"); const dataViewLength = getter(DataView.prototype, "byteLength"); const arrayPush: (array: T[], ...items: T[]) => number = uncurryThis( Array.prototype.push, ); const arrayIncludes: ( array: readonly T[], value: unknown, fromIndex?: number, ) => boolean = uncurryThis(Array.prototype.includes); function arraySlice(value: readonly T[]): T[] { const copy: T[] = array(); for (let i = 0; i < value.length; i++) copy[i] = value[i]; return copy; } const arraySome: ( array: readonly T[], callback: (value: T, index: number, array: readonly T[]) => unknown, ) => boolean = uncurryThis(Array.prototype.some); const arrayFilter: ( array: readonly T[], callback: (value: T, index: number, array: readonly T[]) => unknown, ) => T[] = uncurryThis(Array.prototype.filter); const stringSlice = uncurryThis(String.prototype.slice); const stringIndexOf = uncurryThis(String.prototype.indexOf); const stringIncludes = uncurryThis(String.prototype.includes); const startsWith = uncurryThis(String.prototype.startsWith); const Number = globalThis.Number; const numberIsInteger = Number.isInteger, numberIsFinite = Number.isFinite, trunc = Math.trunc; const Boolean = globalThis.Boolean; const integerTags = new Set([ "Int8Array", "Uint8Array", "Uint8ClampedArray", "Int16Array", "Uint16Array", "Int32Array", "Uint32Array", "BigInt64Array", "BigUint64Array", ]); const digestNames = new Set(["SHA-1", "SHA-256", "SHA-384", "SHA-512"]); const aesNames = new Set(["AES-CBC", "AES-CTR", "AES-GCM", "AES-KW"]); const asymmetricNames = new Set([ "RSA-OAEP", "RSA-PSS", "RSASSA-PKCS1-V1_5", "ECDSA", "ECDH", "ED25519", "X25519", ]); const keyUsages = new Set([ "encrypt", "decrypt", "sign", "verify", "deriveKey", "deriveBits", "wrapKey", "unwrapKey", ]); const assign = Object.assign; const empty = new Uint8Array(0); const own = (value: ArrayLike): CryptoBuffer => new Uint8Array(value); function peerKeyId(algorithm: AlgorithmRecord, baseKey: KeyState): number { const publicKey = algorithm.public; if (publicKey === undefined) { throw new TypeError("ECDH public key is required"); } const peer = keyState(publicKey); if ( peer.meta.type !== "public" || peer.meta.name !== baseKey.meta.name || peer.meta.curve !== baseKey.meta.curve ) { throw exception( "ECDH public key does not match the base key", "InvalidAccessError", ); } return peer.id; } const keyFinalizer = new FinalizationRegistry((id) => disposeKey(id)); function sourceBytes(value: unknown): CryptoBuffer { if (isView(value)) { let buffer; try { buffer = dataViewBuffer(value); } catch {} try { if (buffer !== undefined) { if (getArrayBufferLength(buffer) === 0) return own(empty); return own( new Uint8Array(buffer, dataViewOffset(value), dataViewLength(value)), ); } getArrayBufferLength(typedBuffer(value)); const length = byteLength(value); if (length === 0) return own(empty); return own(new Uint8Array(typedBuffer(value), byteOffset(value), length)); } catch { throw new TypeError("Expected an attached BufferSource"); } } try { const length = getArrayBufferLength(value); return length === 0 ? own(empty) : own(new Uint8Array(value as ArrayBuffer, 0, length)); } catch { throw new TypeError("Expected a non-shared BufferSource"); } } function algorithm(value: unknown): string { let name; if ( value !== null && (typeof value === "object" || typeof value === "function") ) { name = dictionary(value).name; if (name === undefined) throw new TypeError("Algorithm.name is required"); } else name = value; const input = string(name); let result = ""; for (let i = 0; i < input.length; i++) { const code = charCodeAt(input, i); result += fromCharCode(code >= 97 && code <= 122 ? code - 32 : code); } return result; } function usageList(value: unknown, name: string): CryptoKeyUsage[] { if ( value === null || value === undefined || typeof (value as { [SymbolIterator]?: unknown })[SymbolIterator] !== "function" ) throw new TypeError("Expected an iterable of key usages"); const sequence = ArrayFrom(value as Iterable); const usages = array(); for (let i = 0; i < sequence.length; i++) { const usage = string(sequence[i]); if (!has(keyUsages, usage)) throw new TypeError("Invalid key usage"); if (!arrayIncludes(usages, usage)) arrayPush(usages, usage); } const allowed = name === "HMAC" || name === "ECDSA" || name === "ED25519" || name === "RSA-PSS" || name === "RSASSA-PKCS1-V1_5" ? ["sign", "verify"] : name === "PBKDF2" || name === "HKDF" || name === "ECDH" || name === "X25519" ? ["deriveBits", "deriveKey"] : name === "RSA-OAEP" ? ["encrypt", "decrypt", "wrapKey", "unwrapKey"] : name === "AES-KW" ? ["wrapKey", "unwrapKey"] : ["encrypt", "decrypt", "wrapKey", "unwrapKey"]; if (arraySome(usages, (usage) => !arrayIncludes(allowed, usage))) { throw exception( "The requested key usages are not valid for this algorithm", "SyntaxError", ); } return usages as CryptoKeyUsage[]; } function hashName(value: unknown): string { const name = algorithm(value); if (!has(digestNames, name)) { throw exception("Unsupported hash algorithm", "NotSupportedError"); } return name; } function hmacLength(hash: string): number { return hash === "SHA-384" || hash === "SHA-512" ? 1024 : 512; } function metadata( name: string, length: number | undefined, hash: string | undefined, usages: readonly string[], extractable: boolean, type: CryptoKeyType = "secret", curve?: string, exponent?: CryptoBuffer | null, ): KeyMeta { return { name, length, hash, usages: freeze(arraySlice(usages)), extractable, type, curve, exponent: exponent ? own(exponent) : undefined, }; } function makeKey(id: number, meta: KeyMeta): CryptoKey { meta.usages = freeze(arraySlice(meta.usages)); meta.publicUsages = freeze(ArrayFrom(meta.usages)); const value = ObjectCreate(CryptoKey.prototype); const internal = { id, meta: meta as KeyState["meta"], algorithm: freshAlgorithm(meta), }; set(keyStates, value, internal); registerFinalizer(keyFinalizer, value, id); return value; } registerSerializable(keyStates, (internal) => makeKey(cloneKey(internal.id), { name: internal.meta.name, length: internal.meta!.length, hash: internal.meta.hash, curve: internal.meta.curve, exponent: internal.meta.exponent, usages: arraySlice(internal.meta.usages), extractable: internal.meta.extractable, type: internal.meta.type, })); registerWire({ name: "CryptoKey", states: keyStates, encode: ( internal: KeyState, ): [number, KeyMeta] => [serializeKey(internal.id), internal.meta], decode: (packet: [number, KeyMeta]) => makeKey(deserializeKey(packet[0]), packet[1]), }); function keyState(value: unknown): KeyState { return state(keyStates, value); } function freshAlgorithm(meta: KeyMeta): Record { const result: Record = ObjectCreate(null); result.name = meta.name === "ED25519" ? "Ed25519" : meta.name === "RSASSA-PKCS1-V1_5" ? "RSASSA-PKCS1-v1_5" : meta.name; if (meta!.length !== undefined) { if ( meta.name === "RSA-OAEP" || meta.name === "RSA-PSS" || meta.name === "RSASSA-PKCS1-V1_5" ) result.modulusLength = meta!.length; else if (has(aesNames, meta.name) || meta.name === "HMAC") { result.length = meta!.length; } } if (meta.curve !== undefined) result.namedCurve = meta.curve; if (meta.exponent !== undefined) result.publicExponent = own(meta.exponent); if (meta.hash !== undefined) { const hash: { name: string } = ObjectCreate(null); hash.name = meta.hash; result.hash = hash; } return result; } function operationError(error: unknown, fallback: string): Error { const message = (error as { message?: string } | null | undefined)?.message ?? "The cryptographic operation failed"; const codes = [ "NotSupportedError", "InvalidAccessError", "DataError", "SyntaxError", "OperationError", ]; for (let i = 0; i < codes.length; i++) { const code = codes[i]; if (startsWith(message, `${code}: `)) { return exception(stringSlice(message, code.length + 2), code); } } const name = stringIncludes(message, "JWK") || message === "Invalid CryptoKey length" ? "DataError" : stringIncludes(message, "usage") && !stringIncludes(message, "does not permit") || stringIncludes(message, "must be non-extractable") || message === "Invalid asymmetric key" ? "SyntaxError" : stringIncludes(message, "does not permit") || stringIncludes(message, "not extractable") ? "InvalidAccessError" : fallback; return exception(message, name); } async function settled( promise: Promise, fallback = "OperationError", ): Promise { try { return await promise; } catch (error) { throw operationError(error, fallback); } } async function settledBytes( promise: Promise, ): Promise { return typedBuffer(await settled(promise)) as ArrayBuffer; } function emptyRun( operation: CryptoOperation, id: number, input: CryptoBuffer = empty, params: RunParams | undefined = undefined, ) { params = assign(ObjectCreate(null), params) as RunParams; return runCrypto([ operation, id, params.hash ?? "", params.iv ?? empty, params.counter ?? empty, params.additionalData ?? empty, params.tagLength ?? 128, params.iterations ?? 0, params.salt ?? empty, params.info ?? empty, input, params.peerId ?? 0, ]); } function pssSaltLength(value: unknown): number { const number = +(value as number); if (!numberIsFinite(number)) { throw new TypeError("Invalid RSA-PSS saltLength"); } const integer = trunc(number); if (integer < 0 || integer > 0xffffffff) { throw new TypeError("Invalid RSA-PSS saltLength"); } return integer; } function algorithmParams( value: unknown, forKey = false, ): AlgorithmRecord & { dictionary: AlgorithmRecord | undefined; name: string; length?: number; hash?: string; exponent?: CryptoBuffer; saltLength?: number; curve?: string; } { const dictionaryValue = typeof value === "string" ? undefined : dictionary(value) as AlgorithmRecord; const name = algorithm(value); const result: AlgorithmRecord & { dictionary: AlgorithmRecord | undefined; name: string; length?: number; hash?: string; exponent?: CryptoBuffer; saltLength?: number; curve?: string; } = { dictionary: dictionaryValue, name }; if (startsWith(name, "AES-")) { if (forKey) result.length = dictionaryValue?.length as number | undefined; } else if (name === "HMAC") { if (forKey) { const hash = dictionaryValue?.hash; result.hash = hash === undefined ? undefined : hashName(hash); result.length = dictionaryValue?.length as number | undefined; } } else if (name === "PBKDF2" || name === "HKDF") { } else if ( name === "RSA-OAEP" || name === "RSA-PSS" || name === "RSASSA-PKCS1-V1_5" ) { if (forKey) { const hash = dictionaryValue?.hash; result.hash = hash === undefined ? undefined : hashName(hash); result.length = dictionaryValue?.modulusLength as number | undefined; const exponent = dictionaryValue?.publicExponent; result.exponent = exponent === undefined ? undefined : sourceBytes(exponent); if (!result.hash) { throw new TypeError("RSA key algorithms require a hash"); } } else if (name === "RSA-PSS") { result.saltLength = pssSaltLength(dictionaryValue?.saltLength); } } else if (name === "ECDSA") { if (forKey) { result.curve = dictionaryValue?.namedCurve as string | undefined; } else { const hash = dictionaryValue?.hash; result.hash = hash === undefined ? undefined : hashName(hash); } } else if (name === "ECDH") { if (forKey) { result.curve = dictionaryValue?.namedCurve as string | undefined; } } else if (!has(asymmetricNames, name)) { throw exception( "The requested algorithm is not supported", "NotSupportedError", ); } return result; } function checkKeyOperation( key: unknown, usage: string, algorithmName: string, ): KeyState { const internal = keyState(key); if ( internal.meta.name !== algorithmName || !arrayIncludes(internal.meta.usages, usage) ) { throw exception( "The CryptoKey does not permit this operation", "InvalidAccessError", ); } return internal; } function normalizeInputAlgorithm( value: unknown, ): { name: string; params: RunParams } { const a: AlgorithmRecord | undefined = typeof value === "string" ? undefined : dictionary(value) as AlgorithmRecord, name = algorithm(value), params = ObjectCreate(null); if (name === "AES-CBC") { params.iv = sourceBytes(a!.iv); if (params.iv.length !== 16) { throw exception("AES-CBC requires a 16-byte IV", "OperationError"); } } else if (name === "AES-CTR") { params.counter = sourceBytes(a!.counter); if (params.counter.length !== 16) { throw exception("AES-CTR requires a 16-byte counter", "OperationError"); } const bits = Number(a!.length); if (!numberIsInteger(bits) || bits < 1 || bits > 128) { throw exception("Invalid AES-CTR counter length", "OperationError"); } params.tagLength = bits; } else if (name === "AES-GCM") { params.iv = sourceBytes(a!.iv); if (!params.iv.length) { throw exception("AES-GCM IV must not be empty", "OperationError"); } params.additionalData = a!.additionalData === undefined ? empty : sourceBytes(a!.additionalData); params.tagLength = a!.tagLength === undefined ? 128 : Number(a!.tagLength); if (!arrayIncludes([32, 64, 96, 104, 112, 120, 128], params.tagLength)) { throw exception("Invalid AES-GCM tag length", "OperationError"); } } else if (name === "RSA-OAEP") { params.iv = a?.label === undefined ? empty : sourceBytes(a.label); } else if (name === "RSA-PSS") { params.tagLength = pssSaltLength(a?.saltLength); } else if (name !== "AES-KW") { throw exception( "The requested algorithm is not yet implemented", "NotSupportedError", ); } return { name, params }; } function keyFormat(value: unknown): CryptoKeyFormat { const format = string(value); if (!arrayIncludes(["raw", "jwk", "spki", "pkcs8"], format)) { throw new TypeError("Invalid key format"); } return format as CryptoKeyFormat; } const jwkMembers = [ "alg", "crv", "d", "dp", "dq", "e", "ext", "k", "key_ops", "kty", "n", "oth", "p", "q", "qi", "use", "x", "y", ]; function jwkSequence( value: unknown, convert: (value: unknown) => unknown, ): unknown[] { const iterator = (value as { [SymbolIterator]?: unknown } | null | undefined) ?.[SymbolIterator]; if (typeof iterator !== "function") { throw new TypeError("Expected an iterable JWK sequence"); } const iterable: Iterable = ObjectCreate(null); iterable[SymbolIterator] = () => apply(iterator, value, []) as Iterator; return arraySlice(ArrayFrom(iterable, convert)); } function jwkDictionary(value: unknown): Record { value = dictionary(value); const result: Record = ObjectCreate(null); for (let index = 0; index < jwkMembers.length; index++) { const name = jwkMembers[index]!, member = (value as Record)[name]; if (member === undefined) continue; if (name === "ext") result[name] = Boolean(member); else if (name === "key_ops") result[name] = jwkSequence(member, string); else if (name === "oth") { result[name] = jwkSequence(member, (value) => { value = dictionary(value); const info: Record = ObjectCreate(null), members = ["d", "r", "t"]; for (let index = 0; index < members.length; index++) { const name = members[index]!, member = (value as Record)[name]; if (member !== undefined) info[name] = string(member); } return info; }); } else result[name] = string(member); } return result; } class CryptoKey { constructor() { throw new TypeError("CryptoKey cannot be constructed directly"); } get type() { return keyState(this).meta.type; } get extractable() { return keyState(this).meta.extractable; } get algorithm() { return keyState(this).algorithm; } get usages() { return keyState(this).meta.publicUsages; } } class SubtleCrypto { constructor(token: typeof secret) { if (token !== secret) throw new TypeError("Illegal constructor"); set(subtleStates, this, true); } async digest(algorithmIdentifier: unknown, data: unknown) { required(arguments.length, 2); state(subtleStates, this); const name = algorithm(algorithmIdentifier); if (!has(digestNames, name)) { throw exception( "The digest algorithm is not supported", "NotSupportedError", ); } return settledBytes(digestBytes([name, sourceBytes(data)])); } async generateKey( algorithmIdentifier: unknown, extractable: unknown, usages: unknown, ) { required(arguments.length, 3); state(subtleStates, this); const p = algorithmParams(algorithmIdentifier, true); if (p.name === "HMAC" && !p.hash!) { throw new TypeError("HMAC requires a hash algorithm"); } const keyUsage = usageList(usages, p.name), isExtractable = Boolean(extractable); if (has(asymmetricNames, p.name)) { let length = p.length ?? 0, curve = p.curve ?? "", exponent = p.exponent ?? empty; if (startsWith(p.name, "RSA-") || startsWith(p.name, "RSASSA-")) { if ( !numberIsInteger(length) || length < 512 || length > 8192 || !exponent?.length ) throw exception("Invalid RSA key parameters", "DataError"); if ( exponent[exponent.length - 1] % 2 === 0 || (exponent.length === 1 && exponent[0] < 3) ) throw exception("Invalid RSA public exponent", "DataError"); } else if (p.name === "ECDSA" || p.name === "ECDH") { if (!arrayIncludes(["P-256", "P-384", "P-521"], curve)) { throw exception("Unsupported elliptic curve", "NotSupportedError"); } length = curve === "P-256" ? 256 : curve === "P-384" ? 384 : 521; } else { length = 256; } const result = await settled( generateKeyPairBytes([ p.name, p.hash ?? "", length, exponent, curve, keyUsage, isExtractable, ]), ); const publicId = result[0], privateId = result[1], actualLength = result[2], actualExponent = result[3]; const publicUsages = p.name === "RSA-OAEP" ? arrayFilter(keyUsage, (u) => u === "encrypt" || u === "wrapKey") : p.name === "ECDH" || p.name === "X25519" ? [] : arrayFilter(keyUsage, (u) => u === "verify"); const privateUsages = p.name === "RSA-OAEP" ? arrayFilter(keyUsage, (u) => u === "decrypt" || u === "unwrapKey") : p.name === "ECDH" || p.name === "X25519" ? keyUsage : arrayFilter(keyUsage, (u) => u === "sign"); return { publicKey: makeKey( publicId, metadata( p.name, actualLength, p.hash, publicUsages, true, "public", curve || undefined, actualExponent, ), ), privateKey: makeKey( privateId, metadata( p.name, actualLength, p.hash, privateUsages, isExtractable, "private", curve || undefined, actualExponent, ), ), }; } if (!has(aesNames, p.name) && p.name !== "HMAC") { throw exception( "Key generation is not supported for this algorithm", "NotSupportedError", ); } let length: number | undefined = p.length; if (p.name === "HMAC" && length === undefined) length = hmacLength(p.hash!); if ( !numberIsInteger(length) || (startsWith(p.name, "AES-") && !arrayIncludes([128, 192, 256], length!)) || (p.name === "HMAC" && (length! < 1 || length! > 65536)) ) { throw exception("Invalid key length", "OperationError"); } const hash = p.hash ?? ""; const id = await settled( generateKeyBytes([p.name, length!, hash, keyUsage, isExtractable]), ); return makeKey( id, metadata(p.name, length, hash || undefined, keyUsage, isExtractable), ); } async importKey( format: unknown, keyData: unknown, algorithmIdentifier: unknown, extractable: unknown, usages: unknown, ) { required(arguments.length, 5); state(subtleStates, this); const formatName = keyFormat(format); const jwk = formatName === "jwk" ? jwkDictionary(keyData) : undefined; const p = algorithmParams(algorithmIdentifier, true), keyUsage = usageList(usages, p.name), isExtractable = Boolean(extractable); if (p.name === "HMAC" && !p.hash!) { throw new TypeError("HMAC requires a hash algorithm"); } if (has(asymmetricNames, p.name)) { if (formatName === "pkcs8" && keyUsage.length === 0) { throw exception("Private key usages must not be empty", "SyntaxError"); } if (formatName === "jwk") { const result = await settled( importJwk([ jwk!, p.name, p.hash ?? "", p.curve ?? "", keyUsage, isExtractable, p.name === "HMAC" ? p.length ?? 0 : 0, ]), ); return makeKey( result.id, metadata( p.name, result.length, p.hash, keyUsage, isExtractable, result.type, p.curve, result.exponent, ), ); } const data = sourceBytes(keyData); const result = await settled( importAsymmetric([ formatName, data, p.name, p.hash ?? "", p.curve ?? "", keyUsage, isExtractable, ]), ); return makeKey( result.id, metadata( p.name, result.length, p.hash, keyUsage, isExtractable, result.type, p.curve, result.exponent, ), ); } if (formatName !== "raw" && formatName !== "jwk") { throw exception("Key format is not supported", "NotSupportedError"); } let raw = formatName === "raw" ? sourceBytes(keyData) : undefined; let length = raw ? raw.length * 8 : undefined; if (raw && p.name === "HMAC") { length = p.length ?? length; if ( !numberIsInteger(length) || length! <= 0 || length! > raw.length * 8 || length! <= raw.length * 8 - 8 ) { throw exception("Invalid HMAC key length", "DataError"); } raw = own(subarray(raw, 0, ceil(length! / 8))); if (length! % 8 !== 0) raw[raw.length - 1] &= 0xff << (8 - length! % 8); } if ( raw && startsWith(p.name, "AES-") && !arrayIncludes([128, 192, 256], length) ) throw exception("Invalid AES key length", "DataError"); if (raw && p.name === "HMAC" && !raw.length) { throw exception("Invalid HMAC key", "DataError"); } const hash = p.hash ?? ""; if (formatName === "raw") { const id = await settled( importKeyBytes([ p.name, raw!, length ?? 0, hash, keyUsage, isExtractable, ]), ); return makeKey( id, metadata(p.name, length, hash || undefined, keyUsage, isExtractable), ); } const idAndLength = await settled( importJwk([ jwk!, p.name, hash, p.curve ?? "", keyUsage, isExtractable, p.name === "HMAC" ? p.length ?? 0 : 0, ]), ); const { id, length: jwkLength } = idAndLength; return makeKey( id, metadata(p.name, jwkLength, hash || undefined, keyUsage, isExtractable), ); } async exportKey(format: unknown, key: unknown) { required(arguments.length, 2); state(subtleStates, this); const internal = keyState(key), formatName = keyFormat(format); if (!internal.meta.extractable) { throw exception("CryptoKey is not extractable", "InvalidAccessError"); } if ( formatName === "raw" || formatName === "spki" || formatName === "pkcs8" ) return settledBytes(exportKeyBytes([internal.id, formatName])); return settled(exportKeyJwk(internal.id)); } async encrypt(algorithmIdentifier: unknown, key: unknown, data: unknown) { required(arguments.length, 3); state(subtleStates, this); const p = normalizeInputAlgorithm(algorithmIdentifier), k = checkKeyOperation(key, "encrypt", p.name); if (p.name === "AES-KW") { throw exception( "AES-KW is only available through wrapKey and unwrapKey", "InvalidAccessError", ); } p.params.hash = k.meta.hash ?? ""; return settledBytes(emptyRun("encrypt", k.id, sourceBytes(data), p.params)); } async decrypt(algorithmIdentifier: unknown, key: unknown, data: unknown) { required(arguments.length, 3); state(subtleStates, this); const p = normalizeInputAlgorithm(algorithmIdentifier), k = checkKeyOperation(key, "decrypt", p.name); if (p.name === "AES-KW") { throw exception( "AES-KW is only available through wrapKey and unwrapKey", "InvalidAccessError", ); } p.params.hash = k.meta.hash ?? ""; return settledBytes(emptyRun("decrypt", k.id, sourceBytes(data), p.params)); } async sign(algorithmIdentifier: unknown, key: unknown, data: unknown) { required(arguments.length, 3); state(subtleStates, this); const p = algorithmParams(algorithmIdentifier), k = checkKeyOperation(key, "sign", p.name); if ( !arrayIncludes([ "HMAC", "RSA-PSS", "RSASSA-PKCS1-V1_5", "ECDSA", "ED25519", ], p.name) ) { throw exception( "The requested signing algorithm is not supported", "NotSupportedError", ); } if (p.name === "ECDSA" && !p.hash!) { throw new TypeError("ECDSA requires a hash"); } if (k.meta.hash && p.hash && p.hash !== k.meta.hash) { throw exception( "Operation hash does not match the key", "InvalidAccessError", ); } return settledBytes( emptyRun("sign", k.id, sourceBytes(data), { hash: k.meta.hash ?? p.hash ?? "", tagLength: p.name === "RSA-PSS" ? p.saltLength : 0, }), ); } async verify( algorithmIdentifier: unknown, key: unknown, signature: unknown, data: unknown, ) { required(arguments.length, 4); state(subtleStates, this); const p = algorithmParams(algorithmIdentifier), k = checkKeyOperation(key, "verify", p.name); if ( !arrayIncludes([ "HMAC", "RSA-PSS", "RSASSA-PKCS1-V1_5", "ECDSA", "ED25519", ], p.name) ) { throw exception( "The requested signing algorithm is not supported", "NotSupportedError", ); } if (p.name === "ECDSA" && !p.hash!) { throw new TypeError("ECDSA requires a hash"); } if (k.meta.hash && p.hash && p.hash !== k.meta.hash) { throw exception( "Operation hash does not match the key", "InvalidAccessError", ); } const sig = sourceBytes(signature), result = await settled( emptyRun("verify", k.id, sourceBytes(data), { hash: k.meta.hash ?? p.hash ?? "", tagLength: p.name === "RSA-PSS" ? p.saltLength : 0, additionalData: sig, }), ); return result[0] === 1; } async deriveBits( algorithmIdentifier: unknown, baseKey: unknown, length: unknown = null, ) { required(arguments.length, 2); state(subtleStates, this); const p = dictionary(algorithmIdentifier), name = algorithm(p), k = checkKeyOperation(baseKey, "deriveBits", name); if (name === "ECDH" || name === "X25519") { const maximum = ceil(k.meta!.length! / 8) * 8; const bitLength = length == null ? maximum : Number(length); if ( !numberIsInteger(bitLength) || bitLength < 0 || bitLength > maximum ) throw exception("Invalid derived bit length", "OperationError"); return settledBytes( emptyRun("deriveBits", k.id, empty, { peerId: peerKeyId(p, k), tagLength: bitLength, }), ); } if (name !== "PBKDF2" && name !== "HKDF") { throw exception( "The requested derivation algorithm is not yet implemented", "NotSupportedError", ); } const bitLength = Number(length); if ( length === null || !numberIsInteger(bitLength) || bitLength < 0 || bitLength % 8 ) { throw exception("Invalid derived bit length", "OperationError"); } const params = { hash: hashName(p.hash!), salt: sourceBytes(p.salt), info: name === "HKDF" ? sourceBytes(p.info) : empty, iterations: name === "PBKDF2" ? Number(p.iterations) : 0, tagLength: bitLength, }; if ( name === "PBKDF2" && (!numberIsInteger(params.iterations) || params.iterations <= 0) ) throw exception("Invalid PBKDF2 iteration count", "OperationError"); return settledBytes(emptyRun("deriveBits", k.id, empty, params)); } async deriveKey( algorithmIdentifier: unknown, baseKey: unknown, derivedKeyType: unknown, extractable: unknown, usages: unknown, ) { required(arguments.length, 5); state(subtleStates, this); const source = dictionary(algorithmIdentifier), sourceName = algorithm(source); const base = checkKeyOperation(baseKey, "deriveKey", sourceName); if ( sourceName !== "PBKDF2" && sourceName !== "HKDF" && sourceName !== "ECDH" && sourceName !== "X25519" ) { throw exception( "The requested derivation algorithm is not yet implemented", "NotSupportedError", ); } const target = algorithmParams(derivedKeyType, true); if (!has(aesNames, target.name) && target.name !== "HMAC") { throw exception( "Derived key algorithm is not supported", "NotSupportedError", ); } const length = target.length ?? (target.name === "HMAC" ? hmacLength(target.hash!) : undefined); if ( !numberIsInteger(length) || (startsWith(target.name, "AES-") && !arrayIncludes([128, 192, 256], length!)) || (target.name === "HMAC" && (length! < 1 || length! > 65536)) ) throw exception("Invalid derived key length", "OperationError"); const keyUsage = usageList(usages, target.name), isExtractable = Boolean(extractable); const params = sourceName === "ECDH" || sourceName === "X25519" ? { hash: "", salt: empty, info: empty, iterations: 0 } : { hash: hashName(source.hash), salt: sourceBytes(source.salt), info: sourceName === "HKDF" ? sourceBytes(source.info) : empty, iterations: sourceName === "PBKDF2" ? Number(source.iterations) : 0, }; if ( sourceName === "PBKDF2" && (!numberIsInteger(params.iterations) || params.iterations <= 0) ) throw exception("Invalid PBKDF2 iteration count", "OperationError"); const targetHash = target.hash ?? ""; const peerId = sourceName === "ECDH" || sourceName === "X25519" ? peerKeyId(source, base) : 0; const id = await settled( deriveKeyBytes([ base.id, params.hash, params.salt, params.iterations, params.info, length!, target.name, targetHash, keyUsage, isExtractable, peerId, ]), ); return makeKey( id, metadata( target.name, length, targetHash || undefined, keyUsage, isExtractable, ), ); } async wrapKey( format: unknown, key: unknown, wrappingKey: unknown, wrapAlgorithm: unknown, ) { required(arguments.length, 4); state(subtleStates, this); if (!keyState(key).meta.extractable) { throw exception("CryptoKey is not extractable", "InvalidAccessError"); } const p = normalizeInputAlgorithm(wrapAlgorithm), wrapping = checkKeyOperation(wrappingKey, "wrapKey", p.name); if ( !arrayIncludes( ["AES-KW", "AES-CBC", "AES-CTR", "AES-GCM", "RSA-OAEP"], p.name, ) ) { throw exception( "The wrapping algorithm is not supported", "NotSupportedError", ); } p.params.hash = wrapping.meta.hash ?? ""; const formatName = keyFormat(format); return settledBytes( wrapKeyBytes([ wrapping.id, keyState(key).id, formatName, p.name, p.params.hash, p.params.iv ?? empty, p.params.counter ?? empty, p.params.additionalData ?? empty, p.params.tagLength ?? 128, ]), ); } async unwrapKey( format: unknown, wrappedKey: unknown, unwrappingKey: unknown, unwrapAlgorithm: unknown, unwrappedKeyAlgorithm: unknown, extractable: unknown, usages: unknown, ) { required(arguments.length, 7); state(subtleStates, this); const p = normalizeInputAlgorithm(unwrapAlgorithm), wrapping = checkKeyOperation(unwrappingKey, "unwrapKey", p.name); if ( !arrayIncludes( ["AES-KW", "AES-CBC", "AES-CTR", "AES-GCM", "RSA-OAEP"], p.name, ) ) { throw exception( "The unwrapping algorithm is not supported", "NotSupportedError", ); } p.params.hash = wrapping.meta.hash ?? ""; const formatName = keyFormat(format); const target = algorithmParams(unwrappedKeyAlgorithm, true), keyUsage = usageList(usages, target.name); const result = await settled( unwrapKeyBytes([ wrapping.id, formatName, sourceBytes(wrappedKey), target.name, target.hash ?? "", target.curve ?? "", keyUsage, Boolean(extractable), p.params.iv ?? empty, p.params.counter ?? empty, p.params.additionalData ?? empty, p.params.tagLength ?? 128, target.length ?? 0, ]), ); return makeKey( result.id, metadata( target.name, result.length, target.hash, keyUsage, Boolean(extractable), result.type, target.curve, result.exponent, ), ); } } class Crypto { constructor(token: typeof secret) { if (token !== secret) throw new TypeError("Illegal constructor"); set(cryptoStates, this, { subtle: new SubtleCrypto(secret) }); } get subtle() { return state(cryptoStates, this).subtle; } getRandomValues(array: T): T { required(arguments.length, 1); state(cryptoStates, this); if (!isView(array)) throw new TypeError("Expected an ArrayBufferView"); let source; try { source = typedBuffer(array); } catch { source = dataViewBuffer(array); } try { getArrayBufferLength(source); } catch { throw new TypeError("Shared buffers are not allowed"); } if (!has(integerTags, tag(array))) { throw exception("Expected an integer TypedArray", "TypeMismatchError"); } const length = byteLength(array); if (length > 65536) { throw exception( "The requested length exceeds 65536 bytes", "QuotaExceededError", ); } if (length === 0) return array; if (resizable(source)) { const generated = new Uint8Array(length); randomBytes(generated); setBytes(new Uint8Array(source, byteOffset(array), length), generated); } else { randomBytes(array); } return array; } randomUUID() { state(cryptoStates, this); return uuid(null); } } export type { Crypto, CryptoKey, SubtleCrypto }; installInterface("CryptoKey", CryptoKey); installInterface("SubtleCrypto", SubtleCrypto); installInterface("Crypto", Crypto); const crypto = new Crypto(secret); define(globalThis, "crypto", { value: crypto, writable: true, configurable: true, });