diff options
author | Luca Casonato <hello@lcas.dev> | 2024-06-24 11:47:12 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-06-24 11:47:12 +0200 |
commit | 1e8a6b94b1dcf98a2ae4de97b3e98e7b3e4e8f7f (patch) | |
tree | 89ae2bc343dea6bf17ca9d512ea80b51540347ca /ext/node/polyfills/internal/crypto | |
parent | ff535061077d2b67e20154a7dfefe8ca92502c5a (diff) |
fix(ext/node): rewrite crypto.Hash (#24302)
Changes in this PR:
- Added new fixed size hash algorithms (blake2b512, blake2s256,
sha512-224, sha512-256, sha3-224, sha3-256, sha3-384, sha3-512, sm3)
- Added variable size hash algorithms (the concept), with the algorithms
shake128 and shake256
- Use cppgc instead of resources for the hasher
- Enable Node's crypto.Hash tests and fix found bugs
Diffstat (limited to 'ext/node/polyfills/internal/crypto')
-rw-r--r-- | ext/node/polyfills/internal/crypto/hash.ts | 232 |
1 files changed, 133 insertions, 99 deletions
diff --git a/ext/node/polyfills/internal/crypto/hash.ts b/ext/node/polyfills/internal/crypto/hash.ts index a1d61f953..2e040be25 100644 --- a/ext/node/polyfills/internal/crypto/hash.ts +++ b/ext/node/polyfills/internal/crypto/hash.ts @@ -13,8 +13,8 @@ import { op_node_hash_update, op_node_hash_update_str, } from "ext:core/ops"; +import { primordials } from "ext:core/mod.js"; -import { TextEncoder } from "ext:deno_web/08_text_encoding.js"; import { Buffer } from "node:buffer"; import { Transform } from "node:stream"; import { @@ -22,7 +22,11 @@ import { forgivingBase64UrlEncode as encodeToBase64Url, } from "ext:deno_web/00_infra.js"; import type { TransformOptions } from "ext:deno_node/_stream.d.ts"; -import { validateString } from "ext:deno_node/internal/validators.mjs"; +import { + validateEncoding, + validateString, + validateUint32, +} from "ext:deno_node/internal/validators.mjs"; import type { BinaryToTextEncoding, Encoding, @@ -32,119 +36,148 @@ 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 { isArrayBufferView } from "ext:deno_node/internal/util/types.ts"; + +const { ReflectApply, ObjectSetPrototypeOf } = primordials; -// TODO(@littledivy): Use Result<T, E> instead of boolean when -// https://bugs.chromium.org/p/v8/issues/detail?id=13600 is fixed. function unwrapErr(ok: boolean) { - if (!ok) { - throw new Error("Context is not initialized"); - } + if (!ok) throw new ERR_CRYPTO_HASH_FINALIZED(); } -const coerceToBytes = (data: string | BufferSource): Uint8Array => { - if (data instanceof Uint8Array) { - return data; - } else if (typeof data === "string") { - // This assumes UTF-8, which may not be correct. - return new TextEncoder().encode(data); - } else if (ArrayBuffer.isView(data)) { - return new Uint8Array(data.buffer, data.byteOffset, data.byteLength); - } else if (data instanceof ArrayBuffer) { - return new Uint8Array(data); - } else { - throw new TypeError("expected data to be string | BufferSource"); - } -}; +declare const __hasher: unique symbol; +type Hasher = { __hasher: typeof __hasher }; -/** - * The Hash class is a utility for creating hash digests of data. It can be used in one of two ways: - * - * - As a stream that is both readable and writable, where data is written to produce a computed hash digest on the readable side, or - * - Using the hash.update() and hash.digest() methods to produce the computed hash. - * - * The crypto.createHash() method is used to create Hash instances. Hash objects are not to be created directly using the new keyword. - */ -export class Hash extends Transform { - #context: number; +const kHandle = Symbol("kHandle"); - constructor( - algorithm: string | number, - _opts?: TransformOptions, - ) { - super({ - transform(chunk: string, _encoding: string, callback: () => void) { - op_node_hash_update(context, coerceToBytes(chunk)); - callback(); - }, - flush(callback: () => void) { - this.push(this.digest(undefined)); - callback(); - }, - }); +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"); + } - if (typeof algorithm === "string") { - this.#context = op_node_create_hash( - algorithm.toLowerCase(), + 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", ); - if (this.#context === 0) { - throw new TypeError(`Unknown hash algorithm: ${algorithm}`); - } } else { - this.#context = algorithm; + throw err; } + } + + if (this[kHandle] === null) throw new ERR_CRYPTO_HASH_FINALIZED(); + + ReflectApply(LazyTransform, this, [options]); +} - const context = this.#context; +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, + ); } - copy(): Hash { - return new Hash(op_node_hash_clone(this.#context)); + 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))); } - /** - * Updates the hash content with the given data. - */ - update(data: string | ArrayBuffer, _encoding?: string): this { - if (typeof data === "string") { - unwrapErr(op_node_hash_update_str(this.#context, data)); - } else { - unwrapErr(op_node_hash_update(this.#context, coerceToBytes(data))); - } + return this; +}; - return this; - } +Hash.prototype.digest = function digest(outputEncoding: Encoding | "buffer") { + outputEncoding = outputEncoding || getDefaultEncoding(); + outputEncoding = `${outputEncoding}`; - /** - * Calculates the digest of all of the data. - * - * If encoding is provided a string will be returned; otherwise a Buffer is returned. - * - * Supported encodings are currently 'hex', 'binary', 'base64', 'base64url'. - */ - digest(encoding?: string): Buffer | string { - if (encoding === "hex") { - return op_node_hash_digest_hex(this.#context); - } + 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.#context); - if (encoding === undefined) { + 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); - } - - // TODO(@littedivy): Fast paths for below encodings. - switch (encoding) { - case "binary": - return String.fromCharCode(...digest); - case "base64": - return encodeToBase64(digest); - case "base64url": - return encodeToBase64Url(digest); - case "buffer": - return Buffer.from(digest); - default: - return Buffer.from(digest).toString(encoding); - } + default: + return Buffer.from(digest).toString(outputEncoding); } -} +}; export function Hmac( hmac: string, @@ -171,7 +204,7 @@ class HmacImpl extends Transform { super({ transform(chunk: string, encoding: string, callback: () => void) { // deno-lint-ignore no-explicit-any - self.update(coerceToBytes(chunk), encoding as any); + self.update(Buffer.from(chunk), encoding as any); callback(); }, flush(callback: () => void) { @@ -219,9 +252,10 @@ class HmacImpl extends Transform { digest(encoding?: BinaryToTextEncoding): Buffer | string { const result = this.#hash.digest(); - return new Hash(this.#algorithm).update(this.#opad).update(result).digest( - encoding, - ); + return new Hash(this.#algorithm).update(this.#opad).update(result) + .digest( + encoding, + ); } update(data: string | ArrayBuffer, inputEncoding?: Encoding): this { |