// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. // This module is browser compatible. /** * Extensions to the * {@link https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API | Web Crypto API} * supporting additional encryption APIs, but also delegating to the built-in * APIs when possible. * * Provides additional digest algorithms that are not part of the WebCrypto * standard as well as a `subtle.digest` and `subtle.digestSync` methods. * * The {@linkcode KeyStack} export implements the {@linkcode KeyRing} interface * for managing rotatable keys for signing data to prevent tampering, like with * HTTP cookies. * * ## Supported algorithms * * Here is a list of supported algorithms. If the algorithm name in WebCrypto * and Wasm/Rust is the same, this library prefers to use the implementation * provided by WebCrypto. * * Length-adjustable algorithms support the * {@linkcode DigestAlgorithmObject.length} option. * * WebCrypto: * - `SHA-384` * - `SHA-256` (length-extendable) * - `SHA-512` (length-extendable) * * Wasm/Rust: * - `BLAKE2B` * - `BLAKE2B-128` * - `BLAKE2B-160` * - `BLAKE2B-224` * - `BLAKE2B-256` * - `BLAKE2B-384` * - `BLAKE2S` * - `BLAKE3` (length-adjustable) * - `KECCAK-224` * - `KECCAK-256` * - `KECCAK-384` * - `KECCAK-512` * - `SHA-384` * - `SHA3-224` * - `SHA3-256` * - `SHA3-384` * - `SHA3-512` * - `SHAKE128` (length-adjustable) * - `SHAKE256` (length-adjustable) * - `TIGER` * - `RIPEMD-160` (length-extendable) * - `SHA-224` (length-extendable) * - `SHA-256` (length-extendable) * - `SHA-512` (length-extendable) * - `MD4` (length-extendable and collidable) * - `MD5` (length-extendable and collidable) * - `SHA-1` (length-extendable and collidable) * - `FNV32` (non-cryptographic) * - `FNV32A` (non-cryptographic) * - `FNV64` (non-cryptographic) * - `FNV64A` (non-cryptographic) * * @example * ```ts * import { crypto } from "@std/crypto"; * * // This will delegate to the runtime's WebCrypto implementation. * console.log( * new Uint8Array( * await crypto.subtle.digest( * "SHA-384", * new TextEncoder().encode("hello world"), * ), * ), * ); * * // This will use a bundled Wasm/Rust implementation. * console.log( * new Uint8Array( * await crypto.subtle.digest( * "BLAKE3", * new TextEncoder().encode("hello world"), * ), * ), * ); * ``` * * @example Convert hash to a string * * ```ts * import { * crypto, * } from "@std/crypto"; * import { encodeHex } from "@std/encoding/hex" * import { encodeBase64 } from "@std/encoding/base64" * * const hash = await crypto.subtle.digest( * "SHA-384", * new TextEncoder().encode("You hear that Mr. Anderson?"), * ); * * // Hex encoding * console.log(encodeHex(hash)); * * // Or with base64 encoding * console.log(encodeBase64(hash)); * ``` * * @module */ import { DIGEST_ALGORITHM_NAMES, type DigestAlgorithmName, instantiateWasm, } from "./_wasm/mod.ts"; export { DIGEST_ALGORITHM_NAMES, type DigestAlgorithmName }; /** Digest algorithms supported by WebCrypto. */ const WEB_CRYPTO_DIGEST_ALGORITHM_NAMES = [ "SHA-384", "SHA-256", "SHA-512", // insecure (length-extendable and collidable): "SHA-1", ] as const; /** * A copy of the global WebCrypto interface, with methods bound so they're * safe to re-export. */ const webCrypto = ((crypto) => ({ getRandomValues: crypto.getRandomValues?.bind(crypto), randomUUID: crypto.randomUUID?.bind(crypto), subtle: { decrypt: crypto.subtle?.decrypt?.bind(crypto.subtle), deriveBits: crypto.subtle?.deriveBits?.bind(crypto.subtle), deriveKey: crypto.subtle?.deriveKey?.bind(crypto.subtle), digest: crypto.subtle?.digest?.bind(crypto.subtle), encrypt: crypto.subtle?.encrypt?.bind(crypto.subtle), exportKey: crypto.subtle?.exportKey?.bind(crypto.subtle), generateKey: crypto.subtle?.generateKey?.bind(crypto.subtle), importKey: crypto.subtle?.importKey?.bind(crypto.subtle), sign: crypto.subtle?.sign?.bind(crypto.subtle), unwrapKey: crypto.subtle?.unwrapKey?.bind(crypto.subtle), verify: crypto.subtle?.verify?.bind(crypto.subtle), wrapKey: crypto.subtle?.wrapKey?.bind(crypto.subtle), }, }))(globalThis.crypto); function toUint8Array(data: unknown): Uint8Array | undefined { if (data instanceof Uint8Array) { return data; } else if (ArrayBuffer.isView(data)) { return new Uint8Array(data.buffer, data.byteOffset, data.byteLength); } else if (data instanceof ArrayBuffer) { return new Uint8Array(data); } return undefined; } /** Extensions to the web standard `SubtleCrypto` interface. */ export interface StdSubtleCrypto extends SubtleCrypto { /** * Returns a new `Promise` object that will digest `data` using the specified * `AlgorithmIdentifier`. */ digest( algorithm: DigestAlgorithm, data: BufferSource | AsyncIterable | Iterable, ): Promise; /** * Returns a ArrayBuffer with the result of digesting `data` using the * specified `AlgorithmIdentifier`. */ digestSync( algorithm: DigestAlgorithm, data: BufferSource | Iterable, ): ArrayBuffer; } /** Extensions to the Web {@linkcode Crypto} interface. */ export interface StdCrypto extends Crypto { /** Extension to the {@linkcode crypto.SubtleCrypto} interface. */ readonly subtle: StdSubtleCrypto; } /** * An wrapper for WebCrypto adding support for additional non-standard * algorithms, but delegating to the runtime WebCrypto implementation whenever * possible. */ const stdCrypto: StdCrypto = ((x) => x)({ ...webCrypto, subtle: { ...webCrypto.subtle, /** * Polyfills stream support until the Web Crypto API does so: * @see {@link https://github.com/wintercg/proposal-webcrypto-streams} */ async digest( algorithm: DigestAlgorithm, data: BufferSource | AsyncIterable | Iterable, ): Promise { const { name, length } = normalizeAlgorithm(algorithm); assertValidDigestLength(length); // We delegate to WebCrypto whenever possible, if ( // if the algorithm is supported by the WebCrypto standard, (WEB_CRYPTO_DIGEST_ALGORITHM_NAMES as readonly string[]).includes( name, ) && // and the data is a single buffer, isBufferSource(data) ) { return await webCrypto.subtle.digest(algorithm, data); } else if (DIGEST_ALGORITHM_NAMES.includes(name as DigestAlgorithmName)) { if (isBufferSource(data)) { // Otherwise, we use our bundled Wasm implementation via digestSync // if it supports the algorithm. return stdCrypto.subtle.digestSync(algorithm, data); } else if (isIterable(data)) { return stdCrypto.subtle.digestSync( algorithm, data as Iterable, ); } else if (isAsyncIterable(data)) { const wasmCrypto = instantiateWasm(); const context = new wasmCrypto.DigestContext(name); for await (const chunk of data as AsyncIterable) { const chunkBytes = toUint8Array(chunk); if (!chunkBytes) { throw new TypeError( "Cannot digest the data: A chunk is not ArrayBuffer nor ArrayBufferView", ); } context.update(chunkBytes); } return context.digestAndDrop(length).buffer; } else { throw new TypeError( "data must be a BufferSource or [Async]Iterable", ); } } // (TypeScript type definitions prohibit this case.) If they're trying // to call an algorithm we don't recognize, pass it along to WebCrypto // in case it's a non-standard algorithm supported by the the runtime // they're using. return await webCrypto.subtle.digest(algorithm, data as BufferSource); }, digestSync( algorithm: DigestAlgorithm, data: BufferSource | Iterable, ): ArrayBuffer { const { name, length } = normalizeAlgorithm(algorithm); assertValidDigestLength(length); const wasmCrypto = instantiateWasm(); if (isBufferSource(data)) { const bytes = toUint8Array(data)!; return wasmCrypto.digest(name, bytes, length).buffer; } if (isIterable(data)) { const context = new wasmCrypto.DigestContext(name); for (const chunk of data) { const chunkBytes = toUint8Array(chunk); if (!chunkBytes) { throw new TypeError( "Cannot digest the data: A chunk is not ArrayBuffer nor ArrayBufferView", ); } context.update(chunkBytes); } return context.digestAndDrop(length).buffer; } throw new TypeError( "data must be a BufferSource or Iterable", ); }, }, }); /* * The largest digest length the current Wasm implementation can support. This * is the value of `isize::MAX` on 32-bit platforms like Wasm, which is the * maximum allowed capacity of a Rust `Vec`. */ const MAX_DIGEST_LENGTH = 0x7FFF_FFFF; /** * Asserts that a number is a valid length for a digest, which must be an * integer that fits in a Rust `Vec`, or be undefined. */ function assertValidDigestLength(value?: number) { if ( value !== undefined && (value < 0 || value > MAX_DIGEST_LENGTH || !Number.isInteger(value)) ) { throw new RangeError( `length must be an integer between 0 and ${MAX_DIGEST_LENGTH}, inclusive`, ); } } /** Extended digest algorithm objects. */ export type DigestAlgorithmObject = { name: DigestAlgorithmName; length?: number; }; /** * Extended digest algorithms accepted by {@linkcode stdCrypto.subtle.digest}. * * The `length` option will be ignored for * {@link https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest#algorithm | Web Standard algorithms}. */ export type DigestAlgorithm = DigestAlgorithmName | DigestAlgorithmObject; function normalizeAlgorithm(algorithm: DigestAlgorithm) { return ((typeof algorithm === "string") ? { name: algorithm.toUpperCase() } : { ...algorithm, name: algorithm.name.toUpperCase(), }) as DigestAlgorithmObject; } function isBufferSource(obj: unknown): obj is BufferSource { return obj instanceof ArrayBuffer || ArrayBuffer.isView(obj); } function isIterable(obj: unknown): obj is Iterable { return typeof (obj as Iterable)[Symbol.iterator] === "function"; } function isAsyncIterable(obj: unknown): obj is AsyncIterable { return typeof (obj as AsyncIterable)[Symbol.asyncIterator] === "function"; } export { stdCrypto as crypto };