// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. // Copyright Joyent, Inc. and Node.js contributors. All rights reserved. MIT license. // TODO(petamoriken): enable prefer-primordials for node polyfills // deno-lint-ignore-file prefer-primordials import { op_node_create_hash, op_node_export_secret_key, op_node_get_hashes, op_node_hash_clone, op_node_hash_digest, op_node_hash_digest_hex, op_node_hash_update, op_node_hash_update_str, } from "ext:core/ops"; import { primordials } from "ext:core/mod.js"; import { Buffer } from "node:buffer"; import { Transform } from "node:stream"; import { forgivingBase64Encode as encodeToBase64, forgivingBase64UrlEncode as encodeToBase64Url, } from "ext:deno_web/00_infra.js"; import type { TransformOptions } from "ext:deno_node/_stream.d.ts"; import { validateEncoding, validateString, validateUint32, } from "ext:deno_node/internal/validators.mjs"; import type { BinaryToTextEncoding, Encoding, } from "ext:deno_node/internal/crypto/types.ts"; import { KeyObject, prepareSecretKey, } from "ext:deno_node/internal/crypto/keys.ts"; import { ERR_CRYPTO_HASH_FINALIZED, ERR_INVALID_ARG_TYPE, NodeError, } from "ext:deno_node/internal/errors.ts"; import LazyTransform from "ext:deno_node/internal/streams/lazy_transform.mjs"; import { getDefaultEncoding, toBuf, } from "ext:deno_node/internal/crypto/util.ts"; import { isAnyArrayBuffer, isArrayBufferView, } from "ext:deno_node/internal/util/types.ts"; const { ReflectApply, ObjectSetPrototypeOf } = primordials; function unwrapErr(ok: boolean) { if (!ok) throw new ERR_CRYPTO_HASH_FINALIZED(); } declare const __hasher: unique symbol; type Hasher = { __hasher: typeof __hasher }; const kHandle = Symbol("kHandle"); export function Hash( this: Hash, algorithm: string | Hasher, options?: { outputLength?: number }, ): Hash { if (!(this instanceof Hash)) { return new Hash(algorithm, options); } if (!(typeof algorithm === "object")) { validateString(algorithm, "algorithm"); } const xofLen = typeof options === "object" && options !== null ? options.outputLength : undefined; if (xofLen !== undefined) { validateUint32(xofLen, "options.outputLength"); } try { this[kHandle] = typeof algorithm === "object" ? op_node_hash_clone(algorithm, xofLen) : op_node_create_hash(algorithm.toLowerCase(), xofLen); } catch (err) { // TODO(lucacasonato): don't do this if (err.message === "Output length mismatch for non-extendable algorithm") { throw new NodeError( "ERR_OSSL_EVP_NOT_XOF_OR_INVALID_LENGTH", "Invalid XOF digest length", ); } else { throw err; } } if (this[kHandle] === null) throw new ERR_CRYPTO_HASH_FINALIZED(); ReflectApply(LazyTransform, this, [options]); } interface Hash { [kHandle]: object; } ObjectSetPrototypeOf(Hash.prototype, LazyTransform.prototype); ObjectSetPrototypeOf(Hash, LazyTransform); Hash.prototype.copy = function copy(options?: { outputLength: number }) { return new Hash(this[kHandle], options); }; Hash.prototype._transform = function _transform( chunk: string | Buffer, encoding: Encoding | "buffer", callback: () => void, ) { this.update(chunk, encoding); callback(); }; Hash.prototype._flush = function _flush(callback: () => void) { this.push(this.digest()); callback(); }; Hash.prototype.update = function update( data: string | Buffer, encoding: Encoding | "buffer", ) { encoding = encoding || getDefaultEncoding(); if (typeof data === "string") { validateEncoding(data, encoding); } else if (!isArrayBufferView(data)) { throw new ERR_INVALID_ARG_TYPE( "data", ["string", "Buffer", "TypedArray", "DataView"], data, ); } if ( typeof data === "string" && (encoding === "utf8" || encoding === "buffer") ) { unwrapErr(op_node_hash_update_str(this[kHandle], data)); } else { unwrapErr(op_node_hash_update(this[kHandle], toBuf(data, encoding))); } return this; }; Hash.prototype.digest = function digest(outputEncoding: Encoding | "buffer") { outputEncoding = outputEncoding || getDefaultEncoding(); outputEncoding = `${outputEncoding}`; if (outputEncoding === "hex") { const result = op_node_hash_digest_hex(this[kHandle]); if (result === null) throw new ERR_CRYPTO_HASH_FINALIZED(); return result; } const digest = op_node_hash_digest(this[kHandle]); if (digest === null) throw new ERR_CRYPTO_HASH_FINALIZED(); // TODO(@littedivy): Fast paths for below encodings. switch (outputEncoding) { case "binary": return String.fromCharCode(...digest); case "base64": return encodeToBase64(digest); case "base64url": return encodeToBase64Url(digest); case undefined: case "buffer": return Buffer.from(digest); default: return Buffer.from(digest).toString(outputEncoding); } }; export function Hmac( hmac: string, key: string | ArrayBuffer | KeyObject, options?: TransformOptions, ): Hmac { return new HmacImpl(hmac, key, options); } type Hmac = HmacImpl; class HmacImpl extends Transform { #ipad: Uint8Array; #opad: Uint8Array; #ZEROES = Buffer.alloc(128); #algorithm: string; #hash: Hash; constructor( hmac: string, key: string | ArrayBuffer | KeyObject, options?: TransformOptions, ) { super({ transform(chunk: string, encoding: string, callback: () => void) { // deno-lint-ignore no-explicit-any self.update(Buffer.from(chunk), encoding as any); callback(); }, flush(callback: () => void) { this.push(self.digest()); callback(); }, }); // deno-lint-ignore no-this-alias const self = this; validateString(hmac, "hmac"); key = prepareSecretKey(key, options?.encoding); let keyData; if (isArrayBufferView(key)) { keyData = key; } else if (isAnyArrayBuffer(key)) { keyData = new Uint8Array(key); } else { keyData = op_node_export_secret_key(key); } const alg = hmac.toLowerCase(); this.#algorithm = alg; const blockSize = (alg === "sha512" || alg === "sha384") ? 128 : 64; const keySize = keyData.length; let bufKey: Buffer; if (keySize > blockSize) { const hash = new Hash(alg, options); bufKey = hash.update(keyData).digest() as Buffer; } else { bufKey = Buffer.concat([keyData, this.#ZEROES], blockSize); } this.#ipad = Buffer.allocUnsafe(blockSize); this.#opad = Buffer.allocUnsafe(blockSize); for (let i = 0; i < blockSize; i++) { this.#ipad[i] = bufKey[i] ^ 0x36; this.#opad[i] = bufKey[i] ^ 0x5C; } this.#hash = new Hash(alg); this.#hash.update(this.#ipad); } digest(): Buffer; digest(encoding: BinaryToTextEncoding): string; digest(encoding?: BinaryToTextEncoding): Buffer | string { const result = this.#hash.digest(); return new Hash(this.#algorithm).update(this.#opad).update(result) .digest( encoding, ); } update(data: string | ArrayBuffer, inputEncoding?: Encoding): this { this.#hash.update(data, inputEncoding); return this; } } Hmac.prototype = HmacImpl.prototype; /** * Creates and returns a Hash object that can be used to generate hash digests * using the given `algorithm`. Optional `options` argument controls stream behavior. */ export function createHash(algorithm: string, opts?: TransformOptions) { return new Hash(algorithm, opts); } /** * Get the list of implemented hash algorithms. * @returns Array of hash algorithm names. */ export function getHashes() { return op_node_get_hashes(); } export default { Hash, Hmac, createHash, };