From 1e8a6b94b1dcf98a2ae4de97b3e98e7b3e4e8f7f Mon Sep 17 00:00:00 2001 From: Luca Casonato Date: Mon, 24 Jun 2024 11:47:12 +0200 Subject: [PATCH] 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 --- Cargo.lock | 40 +++ ext/node/Cargo.toml | 3 + ext/node/ops/crypto/digest.rs | 294 ++++++++++++++---- ext/node/ops/crypto/mod.rs | 73 ++--- ext/node/polyfills/internal/crypto/hash.ts | 240 ++++++++------ tests/integration/node_unit_tests.rs | 1 + tests/node_compat/config.jsonc | 8 +- tests/node_compat/runner/TODO.md | 1 - tests/node_compat/test.ts | 16 +- tests/node_compat/test/fixtures/sample.png | Bin 0 -> 9416 bytes .../test/parallel/test-crypto-hash.js | 285 +++++++++++++++++ tests/unit_node/crypto/crypto_hash_test.ts | 26 +- tests/unit_node/crypto/crypto_misc_test.ts | 18 ++ 13 files changed, 766 insertions(+), 239 deletions(-) create mode 100644 tests/node_compat/test/fixtures/sample.png create mode 100644 tests/node_compat/test/parallel/test-crypto-hash.js create mode 100644 tests/unit_node/crypto/crypto_misc_test.ts diff --git a/Cargo.lock b/Cargo.lock index d35c101ccd..c65d3314db 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -480,6 +480,15 @@ dependencies = [ "wyz", ] +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + [[package]] name = "block" version = "0.1.6" @@ -1653,6 +1662,7 @@ dependencies = [ "aead-gcm-stream", "aes", "async-trait", + "blake2", "brotli", "bytes", "cbc", @@ -1706,8 +1716,10 @@ dependencies = [ "serde", "sha1", "sha2", + "sha3", "signature", "simd-json", + "sm3", "spki", "tokio", "url", @@ -3666,6 +3678,15 @@ dependencies = [ "signature", ] +[[package]] +name = "keccak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" +dependencies = [ + "cpufeatures", +] + [[package]] name = "khronos-egl" version = "6.0.0" @@ -5795,6 +5816,16 @@ dependencies = [ "digest", ] +[[package]] +name = "sha3" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +dependencies = [ + "digest", + "keccak", +] + [[package]] name = "shell-escape" version = "0.1.5" @@ -5910,6 +5941,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "sm3" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebb9a3b702d0a7e33bc4d85a14456633d2b165c2ad839c5fd9a8417c1ab15860" +dependencies = [ + "digest", +] + [[package]] name = "smallvec" version = "1.13.2" diff --git a/ext/node/Cargo.toml b/ext/node/Cargo.toml index 2a6fd758dd..8b5895bc7b 100644 --- a/ext/node/Cargo.toml +++ b/ext/node/Cargo.toml @@ -17,6 +17,7 @@ path = "lib.rs" aead-gcm-stream = "0.1" aes.workspace = true async-trait.workspace = true +blake2 = "0.10.6" brotli.workspace = true bytes.workspace = true cbc.workspace = true @@ -70,8 +71,10 @@ sec1 = "0.7" serde = "1.0.149" sha1.workspace = true sha2.workspace = true +sha3 = "0.10.8" signature.workspace = true simd-json = "0.13.4" +sm3 = "0.4.2" spki.workspace = true tokio.workspace = true url.workspace = true diff --git a/ext/node/ops/crypto/digest.rs b/ext/node/ops/crypto/digest.rs index 25bb0ab981..588ea7425d 100644 --- a/ext/node/ops/crypto/digest.rs +++ b/ext/node/ops/crypto/digest.rs @@ -1,107 +1,293 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. -use deno_core::error::type_error; +use deno_core::error::generic_error; use deno_core::error::AnyError; -use deno_core::Resource; +use deno_core::GcResource; use digest::Digest; use digest::DynDigest; -use std::borrow::Cow; +use digest::ExtendableOutput; +use digest::Update; use std::cell::RefCell; use std::rc::Rc; +pub struct Hasher { + pub hash: Rc>>, +} + +impl GcResource for Hasher {} + +impl Hasher { + pub fn new( + algorithm: &str, + output_length: Option, + ) -> Result { + let hash = Hash::new(algorithm, output_length)?; + + Ok(Self { + hash: Rc::new(RefCell::new(Some(hash))), + }) + } + + pub fn update(&self, data: &[u8]) -> bool { + if let Some(hash) = self.hash.borrow_mut().as_mut() { + hash.update(data); + true + } else { + false + } + } + + pub fn digest(&self) -> Option> { + let hash = self.hash.borrow_mut().take()?; + Some(hash.digest_and_drop()) + } + + pub fn clone_inner( + &self, + output_length: Option, + ) -> Result, AnyError> { + let hash = self.hash.borrow(); + let Some(hash) = hash.as_ref() else { + return Ok(None); + }; + let hash = hash.clone_hash(output_length)?; + Ok(Some(Self { + hash: Rc::new(RefCell::new(Some(hash))), + })) + } +} + pub enum Hash { + Blake2b512(Box), + Blake2s256(Box), + Md4(Box), Md5(Box), + Ripemd160(Box), + Sha1(Box), + Sha224(Box), Sha256(Box), Sha384(Box), Sha512(Box), -} + Sha512_224(Box), + Sha512_256(Box), -pub struct Context { - pub hash: Rc>, -} + Sha3_224(Box), + Sha3_256(Box), + Sha3_384(Box), + Sha3_512(Box), -impl Context { - pub fn new(algorithm: &str) -> Result { - Ok(Self { - hash: Rc::new(RefCell::new(Hash::new(algorithm)?)), - }) - } + Sm3(Box), - pub fn update(&self, data: &[u8]) { - self.hash.borrow_mut().update(data); - } - - pub fn digest(self) -> Result, AnyError> { - let hash = Rc::try_unwrap(self.hash) - .map_err(|_| type_error("Hash context is already in use"))?; - - let hash = hash.into_inner(); - Ok(hash.digest_and_drop()) - } -} - -impl Clone for Context { - fn clone(&self) -> Self { - Self { - hash: Rc::new(RefCell::new(self.hash.borrow().clone())), - } - } -} - -impl Resource for Context { - fn name(&self) -> Cow { - "cryptoDigest".into() - } + Shake128(Box, /* output_length: */ Option), + Shake256(Box, /* output_length: */ Option), } use Hash::*; impl Hash { - pub fn new(algorithm_name: &str) -> Result { - Ok(match algorithm_name { + pub fn new( + algorithm_name: &str, + output_length: Option, + ) -> Result { + match algorithm_name { + "shake128" => return Ok(Shake128(Default::default(), output_length)), + "shake256" => return Ok(Shake256(Default::default(), output_length)), + _ => {} + } + + let algorithm = match algorithm_name { + "blake2b512" => Blake2b512(Default::default()), + "blake2s256" => Blake2s256(Default::default()), + "md4" => Md4(Default::default()), "md5" => Md5(Default::default()), + "ripemd160" => Ripemd160(Default::default()), + "sha1" => Sha1(Default::default()), "sha224" => Sha224(Default::default()), "sha256" => Sha256(Default::default()), "sha384" => Sha384(Default::default()), "sha512" => Sha512(Default::default()), - _ => return Err(type_error("unsupported algorithm")), - }) + "sha512-224" => Sha512_224(Default::default()), + "sha512-256" => Sha512_256(Default::default()), + + "sha3-224" => Sha3_224(Default::default()), + "sha3-256" => Sha3_256(Default::default()), + "sha3-384" => Sha3_384(Default::default()), + "sha3-512" => Sha3_512(Default::default()), + + "sm3" => Sm3(Default::default()), + + _ => { + return Err(generic_error(format!( + "Digest method not supported: {algorithm_name}" + ))) + } + }; + if let Some(length) = output_length { + if length != algorithm.output_length() { + return Err(generic_error( + "Output length mismatch for non-extendable algorithm", + )); + } + } + Ok(algorithm) + } + + pub fn output_length(&self) -> usize { + match self { + Blake2b512(context) => context.output_size(), + Blake2s256(context) => context.output_size(), + + Md4(context) => context.output_size(), + Md5(context) => context.output_size(), + + Ripemd160(context) => context.output_size(), + + Sha1(context) => context.output_size(), + Sha224(context) => context.output_size(), + Sha256(context) => context.output_size(), + Sha384(context) => context.output_size(), + Sha512(context) => context.output_size(), + Sha512_224(context) => context.output_size(), + Sha512_256(context) => context.output_size(), + + Sha3_224(context) => context.output_size(), + Sha3_256(context) => context.output_size(), + Sha3_384(context) => context.output_size(), + Sha3_512(context) => context.output_size(), + + Sm3(context) => context.output_size(), + + Shake128(_, _) => unreachable!( + "output_length() should not be called on extendable algorithms" + ), + Shake256(_, _) => unreachable!( + "output_length() should not be called on extendable algorithms" + ), + } } pub fn update(&mut self, data: &[u8]) { match self { + Blake2b512(context) => Digest::update(&mut **context, data), + Blake2s256(context) => Digest::update(&mut **context, data), + Md4(context) => Digest::update(&mut **context, data), Md5(context) => Digest::update(&mut **context, data), + Ripemd160(context) => Digest::update(&mut **context, data), + Sha1(context) => Digest::update(&mut **context, data), Sha224(context) => Digest::update(&mut **context, data), Sha256(context) => Digest::update(&mut **context, data), Sha384(context) => Digest::update(&mut **context, data), Sha512(context) => Digest::update(&mut **context, data), + Sha512_224(context) => Digest::update(&mut **context, data), + Sha512_256(context) => Digest::update(&mut **context, data), + + Sha3_224(context) => Digest::update(&mut **context, data), + Sha3_256(context) => Digest::update(&mut **context, data), + Sha3_384(context) => Digest::update(&mut **context, data), + Sha3_512(context) => Digest::update(&mut **context, data), + + Sm3(context) => Digest::update(&mut **context, data), + + Shake128(context, _) => Update::update(&mut **context, data), + Shake256(context, _) => Update::update(&mut **context, data), }; } pub fn digest_and_drop(self) -> Box<[u8]> { match self { + Blake2b512(context) => context.finalize(), + Blake2s256(context) => context.finalize(), + Md4(context) => context.finalize(), Md5(context) => context.finalize(), + Ripemd160(context) => context.finalize(), + Sha1(context) => context.finalize(), Sha224(context) => context.finalize(), Sha256(context) => context.finalize(), Sha384(context) => context.finalize(), Sha512(context) => context.finalize(), + Sha512_224(context) => context.finalize(), + Sha512_256(context) => context.finalize(), + + Sha3_224(context) => context.finalize(), + Sha3_256(context) => context.finalize(), + Sha3_384(context) => context.finalize(), + Sha3_512(context) => context.finalize(), + + Sm3(context) => context.finalize(), + + // The default output lengths align with Node.js + Shake128(context, output_length) => { + context.finalize_boxed(output_length.unwrap_or(16)) + } + Shake256(context, output_length) => { + context.finalize_boxed(output_length.unwrap_or(32)) + } } } + pub fn clone_hash( + &self, + output_length: Option, + ) -> Result { + let hash = match self { + Shake128(context, _) => { + return Ok(Shake128(context.clone(), output_length)) + } + Shake256(context, _) => { + return Ok(Shake256(context.clone(), output_length)) + } + + Blake2b512(context) => Blake2b512(context.clone()), + Blake2s256(context) => Blake2s256(context.clone()), + + Md4(context) => Md4(context.clone()), + Md5(context) => Md5(context.clone()), + + Ripemd160(context) => Ripemd160(context.clone()), + + Sha1(context) => Sha1(context.clone()), + Sha224(context) => Sha224(context.clone()), + Sha256(context) => Sha256(context.clone()), + Sha384(context) => Sha384(context.clone()), + Sha512(context) => Sha512(context.clone()), + Sha512_224(context) => Sha512_224(context.clone()), + Sha512_256(context) => Sha512_256(context.clone()), + + Sha3_224(context) => Sha3_224(context.clone()), + Sha3_256(context) => Sha3_256(context.clone()), + Sha3_384(context) => Sha3_384(context.clone()), + Sha3_512(context) => Sha3_512(context.clone()), + + Sm3(context) => Sm3(context.clone()), + }; + + if let Some(length) = output_length { + if length != hash.output_length() { + return Err(generic_error( + "Output length mismatch for non-extendable algorithm", + )); + } + } + + Ok(hash) + } + pub fn get_hashes() -> Vec<&'static str> { vec![ + "blake2s256", + "blake2b512", "md4", "md5", "ripemd160", @@ -110,21 +296,15 @@ impl Hash { "sha256", "sha384", "sha512", + "sha512-224", + "sha512-256", + "sha3-224", + "sha3-256", + "sha3-384", + "sha3-512", + "shake128", + "shake256", + "sm3", ] } } - -impl Clone for Hash { - fn clone(&self) -> Self { - match self { - Md4(_) => Md4(Default::default()), - Md5(_) => Md5(Default::default()), - Ripemd160(_) => Ripemd160(Default::default()), - Sha1(_) => Sha1(Default::default()), - Sha224(_) => Sha224(Default::default()), - Sha256(_) => Sha256(Default::default()), - Sha384(_) => Sha384(Default::default()), - Sha512(_) => Sha512(Default::default()), - } - } -} diff --git a/ext/node/ops/crypto/mod.rs b/ext/node/ops/crypto/mod.rs index 666ce84097..8ded3420a1 100644 --- a/ext/node/ops/crypto/mod.rs +++ b/ext/node/ops/crypto/mod.rs @@ -7,7 +7,6 @@ use deno_core::serde_v8::BigInt as V8BigInt; use deno_core::unsync::spawn_blocking; use deno_core::JsBuffer; use deno_core::OpState; -use deno_core::ResourceId; use deno_core::StringOrBuffer; use deno_core::ToJsBuffer; use elliptic_curve::sec1::ToEncodedPoint; @@ -96,18 +95,13 @@ pub fn op_node_check_prime_bytes_async( }) } -#[op2(fast)] -#[smi] +#[op2] +#[cppgc] pub fn op_node_create_hash( - state: &mut OpState, #[string] algorithm: &str, -) -> u32 { - state - .resource_table - .add(match digest::Context::new(algorithm) { - Ok(context) => context, - Err(_) => return 0, - }) + output_length: Option, +) -> Result { + digest::Hasher::new(algorithm, output_length.map(|l| l as usize)) } #[op2] @@ -118,65 +112,44 @@ pub fn op_node_get_hashes() -> Vec<&'static str> { #[op2(fast)] pub fn op_node_hash_update( - state: &mut OpState, - #[smi] rid: u32, + #[cppgc] hasher: &digest::Hasher, #[buffer] data: &[u8], ) -> bool { - let context = match state.resource_table.get::(rid) { - Ok(context) => context, - _ => return false, - }; - context.update(data); - true + hasher.update(data) } #[op2(fast)] pub fn op_node_hash_update_str( - state: &mut OpState, - #[smi] rid: u32, + #[cppgc] hasher: &digest::Hasher, #[string] data: &str, ) -> bool { - let context = match state.resource_table.get::(rid) { - Ok(context) => context, - _ => return false, - }; - context.update(data.as_bytes()); - true + hasher.update(data.as_bytes()) } #[op2] -#[serde] +#[buffer] pub fn op_node_hash_digest( - state: &mut OpState, - #[smi] rid: ResourceId, -) -> Result { - let context = state.resource_table.take::(rid)?; - let context = Rc::try_unwrap(context) - .map_err(|_| type_error("Hash context is already in use"))?; - Ok(context.digest()?.into()) + #[cppgc] hasher: &digest::Hasher, +) -> Option> { + hasher.digest() } #[op2] #[string] pub fn op_node_hash_digest_hex( - state: &mut OpState, - #[smi] rid: ResourceId, -) -> Result { - let context = state.resource_table.take::(rid)?; - let context = Rc::try_unwrap(context) - .map_err(|_| type_error("Hash context is already in use"))?; - let digest = context.digest()?; - Ok(faster_hex::hex_string(&digest)) + #[cppgc] hasher: &digest::Hasher, +) -> Option { + let digest = hasher.digest()?; + Some(faster_hex::hex_string(&digest)) } -#[op2(fast)] -#[smi] +#[op2] +#[cppgc] pub fn op_node_hash_clone( - state: &mut OpState, - #[smi] rid: ResourceId, -) -> Result { - let context = state.resource_table.get::(rid)?; - Ok(state.resource_table.add(context.as_ref().clone())) + #[cppgc] hasher: &digest::Hasher, + output_length: Option, +) -> Result, AnyError> { + hasher.clone_inner(output_length.map(|l| l as usize)) } #[op2] diff --git a/ext/node/polyfills/internal/crypto/hash.ts b/ext/node/polyfills/internal/crypto/hash.ts index a1d61f953c..2e040be253 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 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 }; + +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); }; -/** - * 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; +Hash.prototype._transform = function _transform( + chunk: string | Buffer, + encoding: Encoding | "buffer", + callback: () => void, +) { + this.update(chunk, encoding); + callback(); +}; - constructor( - algorithm: string | number, - _opts?: TransformOptions, +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") ) { - 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(); - }, - }); - - if (typeof algorithm === "string") { - this.#context = op_node_create_hash( - algorithm.toLowerCase(), - ); - if (this.#context === 0) { - throw new TypeError(`Unknown hash algorithm: ${algorithm}`); - } - } else { - this.#context = algorithm; - } - - const context = this.#context; + unwrapErr(op_node_hash_update_str(this[kHandle], data)); + } else { + unwrapErr(op_node_hash_update(this[kHandle], toBuf(data, encoding))); } - copy(): Hash { - return new Hash(op_node_hash_clone(this.#context)); + 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; } - /** - * 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))); - } + const digest = op_node_hash_digest(this[kHandle]); + if (digest === null) throw new ERR_CRYPTO_HASH_FINALIZED(); - return this; - } - - /** - * 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); - } - - const digest = op_node_hash_digest(this.#context); - if (encoding === undefined) { + // 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 { diff --git a/tests/integration/node_unit_tests.rs b/tests/integration/node_unit_tests.rs index b067f3121d..d0b6d1fbd7 100644 --- a/tests/integration/node_unit_tests.rs +++ b/tests/integration/node_unit_tests.rs @@ -61,6 +61,7 @@ util::unit_test_factory!( crypto_cipher_gcm_test = crypto / crypto_cipher_gcm_test, crypto_hash_test = crypto / crypto_hash_test, crypto_key_test = crypto / crypto_key_test, + crypto_misc_test = crypto / crypto_misc_test, crypto_sign_test = crypto / crypto_sign_test, events_test, dgram_test, diff --git a/tests/node_compat/config.jsonc b/tests/node_compat/config.jsonc index 0a661c0a9a..83d91eac18 100644 --- a/tests/node_compat/config.jsonc +++ b/tests/node_compat/config.jsonc @@ -132,16 +132,17 @@ "tmpdir.js" ], "fixtures": [ - "GH-1899-output.js", "a.js", - "child-process-spawn-node.js", "child_process_should_emit_error.js", + "child-process-spawn-node.js", "echo.js", "elipses.txt", "empty.txt", "exit.js", + "GH-1899-output.js", "loop.js", "print-chars.js", + "sample.png", "x.txt" ], "fixtures/keys": ["agent1-cert.pem", "agent1-key.pem", "ca1-cert.pem"], @@ -253,6 +254,7 @@ "test-console-tty-colors.js", "test-crypto-dh-shared.js", "test-crypto-dh.js", + "test-crypto-hash.js", "test-crypto-hkdf.js", "test-crypto-hmac.js", "test-crypto-prime.js", @@ -701,8 +703,8 @@ "test-zlib-zero-windowBits.js" ], "pseudo-tty": [ - "console-dumb-tty.js", "console_colors.js", + "console-dumb-tty.js", "no_dropped_stdio.js", "no_interleaved_stdio.js", "test-tty-color-support-warning-2.js", diff --git a/tests/node_compat/runner/TODO.md b/tests/node_compat/runner/TODO.md index d5dd2fc7b6..24618dc883 100644 --- a/tests/node_compat/runner/TODO.md +++ b/tests/node_compat/runner/TODO.md @@ -464,7 +464,6 @@ NOTE: This file should not be manually edited. Please edit `tests/node_compat/co - [parallel/test-crypto-from-binary.js](https://github.com/nodejs/node/tree/v18.12.1/test/parallel/test-crypto-from-binary.js) - [parallel/test-crypto-getcipherinfo.js](https://github.com/nodejs/node/tree/v18.12.1/test/parallel/test-crypto-getcipherinfo.js) - [parallel/test-crypto-hash-stream-pipe.js](https://github.com/nodejs/node/tree/v18.12.1/test/parallel/test-crypto-hash-stream-pipe.js) -- [parallel/test-crypto-hash.js](https://github.com/nodejs/node/tree/v18.12.1/test/parallel/test-crypto-hash.js) - [parallel/test-crypto-key-objects-messageport.js](https://github.com/nodejs/node/tree/v18.12.1/test/parallel/test-crypto-key-objects-messageport.js) - [parallel/test-crypto-key-objects.js](https://github.com/nodejs/node/tree/v18.12.1/test/parallel/test-crypto-key-objects.js) - [parallel/test-crypto-keygen-async-dsa-key-object.js](https://github.com/nodejs/node/tree/v18.12.1/test/parallel/test-crypto-keygen-async-dsa-key-object.js) diff --git a/tests/node_compat/test.ts b/tests/node_compat/test.ts index 6f15f2d0b4..939fdf52a7 100644 --- a/tests/node_compat/test.ts +++ b/tests/node_compat/test.ts @@ -16,7 +16,7 @@ import { magenta } from "@std/fmt/colors.ts"; import { pooledMap } from "@std/async/pool.ts"; import { dirname, fromFileUrl, join } from "@std/path/mod.ts"; -import { fail } from "@std/assert/mod.ts"; +import { assertEquals, fail } from "@std/assert/mod.ts"; import { config, getPathsFromTestSuites, @@ -169,12 +169,14 @@ Deno.test("Node.js compatibility", async (t) => { function checkConfigTestFilesOrder(testFileLists: Array) { for (const testFileList of testFileLists) { const sortedTestList = JSON.parse(JSON.stringify(testFileList)); - sortedTestList.sort(); - if (JSON.stringify(testFileList) !== JSON.stringify(sortedTestList)) { - throw new Error( - `File names in \`config.json\` are not correct order.`, - ); - } + sortedTestList.sort((a: string, b: string) => + a.toLowerCase().localeCompare(b.toLowerCase()) + ); + assertEquals( + testFileList, + sortedTestList, + "File names in `config.json` are not correct order.", + ); } } diff --git a/tests/node_compat/test/fixtures/sample.png b/tests/node_compat/test/fixtures/sample.png new file mode 100644 index 0000000000000000000000000000000000000000..258622019038678b7c3abd376dfeedd62fe99f14 GIT binary patch literal 9416 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 V$!9S&9?O68iny#6$KVeO{{!3ra})po literal 0 HcmV?d00001 diff --git a/tests/node_compat/test/parallel/test-crypto-hash.js b/tests/node_compat/test/parallel/test-crypto-hash.js new file mode 100644 index 0000000000..18c57dab74 --- /dev/null +++ b/tests/node_compat/test/parallel/test-crypto-hash.js @@ -0,0 +1,285 @@ +// deno-fmt-ignore-file +// deno-lint-ignore-file + +// Copyright Joyent and Node contributors. All rights reserved. MIT license. +// Taken from Node 18.12.1 +// This file is automatically generated by `tests/node_compat/runner/setup.ts`. Do not modify this file manually. + +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const crypto = require('crypto'); +const fs = require('fs'); + +const fixtures = require('../common/fixtures'); + +let cryptoType; +let digest; + +// Test hashing +const a1 = crypto.createHash('sha1').update('Test123').digest('hex'); +const a2 = crypto.createHash('sha256').update('Test123').digest('base64'); +const a3 = crypto.createHash('sha512').update('Test123').digest(); // buffer +const a4 = crypto.createHash('sha1').update('Test123').digest('buffer'); + +// stream interface +let a5 = crypto.createHash('sha512'); +a5.end('Test123'); +a5 = a5.read(); + +let a6 = crypto.createHash('sha512'); +a6.write('Te'); +a6.write('st'); +a6.write('123'); +a6.end(); +a6 = a6.read(); + +let a7 = crypto.createHash('sha512'); +a7.end(); +a7 = a7.read(); + +let a8 = crypto.createHash('sha512'); +a8.write(''); +a8.end(); +a8 = a8.read(); + +if (!common.hasFipsCrypto) { + cryptoType = 'md5'; + digest = 'latin1'; + const a0 = crypto.createHash(cryptoType).update('Test123').digest(digest); + assert.strictEqual( + a0, + 'h\u00ea\u00cb\u0097\u00d8o\fF!\u00fa+\u000e\u0017\u00ca\u00bd\u008c', + `${cryptoType} with ${digest} digest failed to evaluate to expected hash` + ); +} +cryptoType = 'md5'; +digest = 'hex'; +assert.strictEqual( + a1, + '8308651804facb7b9af8ffc53a33a22d6a1c8ac2', + `${cryptoType} with ${digest} digest failed to evaluate to expected hash`); +cryptoType = 'sha256'; +digest = 'base64'; +assert.strictEqual( + a2, + '2bX1jws4GYKTlxhloUB09Z66PoJZW+y+hq5R8dnx9l4=', + `${cryptoType} with ${digest} digest failed to evaluate to expected hash`); +cryptoType = 'sha512'; +digest = 'latin1'; +assert.deepStrictEqual( + a3, + Buffer.from( + '\u00c1(4\u00f1\u0003\u001fd\u0097!O\'\u00d4C/&Qz\u00d4' + + '\u0094\u0015l\u00b8\u008dQ+\u00db\u001d\u00c4\u00b5}\u00b2' + + '\u00d6\u0092\u00a3\u00df\u00a2i\u00a1\u009b\n\n*\u000f' + + '\u00d7\u00d6\u00a2\u00a8\u0085\u00e3<\u0083\u009c\u0093' + + '\u00c2\u0006\u00da0\u00a1\u00879(G\u00ed\'', + 'latin1'), + `${cryptoType} with ${digest} digest failed to evaluate to expected hash`); +cryptoType = 'sha1'; +digest = 'hex'; +assert.deepStrictEqual( + a4, + Buffer.from('8308651804facb7b9af8ffc53a33a22d6a1c8ac2', 'hex'), + `${cryptoType} with ${digest} digest failed to evaluate to expected hash` +); + +// Stream interface should produce the same result. +assert.deepStrictEqual(a5, a3); +assert.deepStrictEqual(a6, a3); +assert.notStrictEqual(a7, undefined); +assert.notStrictEqual(a8, undefined); + +// Test multiple updates to same hash +const h1 = crypto.createHash('sha1').update('Test123').digest('hex'); +const h2 = crypto.createHash('sha1').update('Test').update('123').digest('hex'); +assert.strictEqual(h1, h2); + +// Test hashing for binary files +const fn = fixtures.path('sample.png'); +const sha1Hash = crypto.createHash('sha1'); +const fileStream = fs.createReadStream(fn); +fileStream.on('data', function(data) { + sha1Hash.update(data); +}); +fileStream.on('close', common.mustCall(function() { + // Test SHA1 of sample.png + assert.strictEqual(sha1Hash.digest('hex'), + '22723e553129a336ad96e10f6aecdf0f45e4149e'); +})); + +// Issue https://github.com/nodejs/node-v0.x-archive/issues/2227: unknown digest +// method should throw an error. +assert.throws(function() { + crypto.createHash('xyzzy'); +}, /Digest method not supported/); + +// Issue https://github.com/nodejs/node/issues/9819: throwing encoding used to +// segfault. +assert.throws( + () => crypto.createHash('sha256').digest({ + toString: () => { throw new Error('boom'); }, + }), + { + name: 'Error', + message: 'boom' + }); + +// Issue https://github.com/nodejs/node/issues/25487: error message for invalid +// arg type to update method should include all possible types +assert.throws( + () => crypto.createHash('sha256').update(), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + }); + +// Default UTF-8 encoding +const hutf8 = crypto.createHash('sha512').update('УТФ-8 text').digest('hex'); +assert.strictEqual( + hutf8, + '4b21bbd1a68e690a730ddcb5a8bc94ead9879ffe82580767ad7ec6fa8ba2dea6' + + '43a821af66afa9a45b6a78c712fecf0e56dc7f43aef4bcfc8eb5b4d8dca6ea5b'); + +assert.notStrictEqual( + hutf8, + crypto.createHash('sha512').update('УТФ-8 text', 'latin1').digest('hex')); + +const h3 = crypto.createHash('sha256'); +h3.digest(); + +assert.throws( + () => h3.digest(), + { + code: 'ERR_CRYPTO_HASH_FINALIZED', + name: 'Error' + }); + +assert.throws( + () => h3.update('foo'), + { + code: 'ERR_CRYPTO_HASH_FINALIZED', + name: 'Error' + }); + +assert.strictEqual( + crypto.createHash('sha256').update('test').digest('ucs2'), + crypto.createHash('sha256').update('test').digest().toString('ucs2')); + +assert.throws( + () => crypto.createHash(), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + message: 'The "algorithm" argument must be of type string. ' + + 'Received undefined' + } +); + +{ + const Hash = crypto.Hash; + const instance = crypto.Hash('sha256'); + assert(instance instanceof Hash, 'Hash is expected to return a new instance' + + ' when called without `new`'); +} + +// Test XOF hash functions and the outputLength option. +{ + // Default outputLengths. + assert.strictEqual(crypto.createHash('shake128').digest('hex'), + '7f9c2ba4e88f827d616045507605853e'); + assert.strictEqual(crypto.createHash('shake128', null).digest('hex'), + '7f9c2ba4e88f827d616045507605853e'); + assert.strictEqual(crypto.createHash('shake256').digest('hex'), + '46b9dd2b0ba88d13233b3feb743eeb24' + + '3fcd52ea62b81b82b50c27646ed5762f'); + assert.strictEqual(crypto.createHash('shake256', { outputLength: 0 }) + .copy() // Default outputLength. + .digest('hex'), + '46b9dd2b0ba88d13233b3feb743eeb24' + + '3fcd52ea62b81b82b50c27646ed5762f'); + + // Short outputLengths. + assert.strictEqual(crypto.createHash('shake128', { outputLength: 0 }) + .digest('hex'), + ''); + assert.strictEqual(crypto.createHash('shake128', { outputLength: 5 }) + .copy({ outputLength: 0 }) + .digest('hex'), + ''); + assert.strictEqual(crypto.createHash('shake128', { outputLength: 5 }) + .digest('hex'), + '7f9c2ba4e8'); + assert.strictEqual(crypto.createHash('shake128', { outputLength: 0 }) + .copy({ outputLength: 5 }) + .digest('hex'), + '7f9c2ba4e8'); + assert.strictEqual(crypto.createHash('shake128', { outputLength: 15 }) + .digest('hex'), + '7f9c2ba4e88f827d61604550760585'); + assert.strictEqual(crypto.createHash('shake256', { outputLength: 16 }) + .digest('hex'), + '46b9dd2b0ba88d13233b3feb743eeb24'); + + // Large outputLengths. + assert.strictEqual(crypto.createHash('shake128', { outputLength: 128 }) + .digest('hex'), + '7f9c2ba4e88f827d616045507605853e' + + 'd73b8093f6efbc88eb1a6eacfa66ef26' + + '3cb1eea988004b93103cfb0aeefd2a68' + + '6e01fa4a58e8a3639ca8a1e3f9ae57e2' + + '35b8cc873c23dc62b8d260169afa2f75' + + 'ab916a58d974918835d25e6a435085b2' + + 'badfd6dfaac359a5efbb7bcc4b59d538' + + 'df9a04302e10c8bc1cbf1a0b3a5120ea'); + const superLongHash = crypto.createHash('shake256', { + outputLength: 1024 * 1024 + }).update('The message is shorter than the hash!') + .digest('hex'); + assert.strictEqual(superLongHash.length, 2 * 1024 * 1024); + assert.ok(superLongHash.endsWith('193414035ddba77bf7bba97981e656ec')); + assert.ok(superLongHash.startsWith('a2a28dbc49cfd6e5d6ceea3d03e77748')); + + // Non-XOF hash functions should accept valid outputLength options as well. + assert.strictEqual(crypto.createHash('sha224', { outputLength: 28 }) + .digest('hex'), + 'd14a028c2a3a2bc9476102bb288234c4' + + '15a2b01f828ea62ac5b3e42f'); + + // Passing invalid sizes should throw during creation. + assert.throws(() => { + crypto.createHash('sha256', { outputLength: 28 }); + }, { + code: 'ERR_OSSL_EVP_NOT_XOF_OR_INVALID_LENGTH' + }); + + for (const outputLength of [null, {}, 'foo', false]) { + assert.throws(() => crypto.createHash('sha256', { outputLength }), + { code: 'ERR_INVALID_ARG_TYPE' }); + } + + for (const outputLength of [-1, .5, Infinity, 2 ** 90]) { + assert.throws(() => crypto.createHash('sha256', { outputLength }), + { code: 'ERR_OUT_OF_RANGE' }); + } +} + +{ + const h = crypto.createHash('sha512'); + h.digest(); + assert.throws(() => h.copy(), { code: 'ERR_CRYPTO_HASH_FINALIZED' }); + assert.throws(() => h.digest(), { code: 'ERR_CRYPTO_HASH_FINALIZED' }); +} + +{ + const a = crypto.createHash('sha512').update('abc'); + const b = a.copy(); + const c = b.copy().update('def'); + const d = crypto.createHash('sha512').update('abcdef'); + assert.strictEqual(a.digest('hex'), b.digest('hex')); + assert.strictEqual(c.digest('hex'), d.digest('hex')); +} diff --git a/tests/unit_node/crypto/crypto_hash_test.ts b/tests/unit_node/crypto/crypto_hash_test.ts index 74223067e2..96bc1d51ba 100644 --- a/tests/unit_node/crypto/crypto_hash_test.ts +++ b/tests/unit_node/crypto/crypto_hash_test.ts @@ -1,11 +1,5 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. -import { - createHash, - createHmac, - getHashes, - randomFillSync, - randomUUID, -} from "node:crypto"; +import { createHash, createHmac, getHashes } from "node:crypto"; import { Buffer } from "node:buffer"; import { Readable } from "node:stream"; import { assert, assertEquals } from "@std/assert/mod.ts"; @@ -123,17 +117,13 @@ Deno.test("[node/crypto.getHashes]", () => { } }); -Deno.test("[node/crypto.getRandomUUID] works the same way as Web Crypto API", () => { - assertEquals(randomUUID().length, crypto.randomUUID().length); - assertEquals(typeof randomUUID(), typeof crypto.randomUUID()); +Deno.test("[node/crypto.hash] supports buffer args", () => { + const buffer = Buffer.from("abc"); + const d = createHash("sha1").update(buffer).digest("hex"); + assertEquals(d, "a9993e364706816aba3e25717850c26c9cd0d89d"); }); -Deno.test("[node/crypto.randomFillSync] supported arguments", () => { - const buf = new Uint8Array(10); - - assert(randomFillSync(buf)); - assert(randomFillSync(buf, 0)); - // @ts-ignore: arraybuffer arguments are valid. - assert(randomFillSync(buf.buffer)); - assert(randomFillSync(new DataView(buf.buffer))); +Deno.test("[node/crypto.hash] does not leak", () => { + const hasher = createHash("sha1"); + hasher.update("abc"); }); diff --git a/tests/unit_node/crypto/crypto_misc_test.ts b/tests/unit_node/crypto/crypto_misc_test.ts new file mode 100644 index 0000000000..8132f2e99c --- /dev/null +++ b/tests/unit_node/crypto/crypto_misc_test.ts @@ -0,0 +1,18 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +import { randomFillSync, randomUUID } from "node:crypto"; +import { assert, assertEquals } from "../../unit/test_util.ts"; + +Deno.test("[node/crypto.getRandomUUID] works the same way as Web Crypto API", () => { + assertEquals(randomUUID().length, crypto.randomUUID().length); + assertEquals(typeof randomUUID(), typeof crypto.randomUUID()); +}); + +Deno.test("[node/crypto.randomFillSync] supported arguments", () => { + const buf = new Uint8Array(10); + + assert(randomFillSync(buf)); + assert(randomFillSync(buf, 0)); + // @ts-ignore: arraybuffer arguments are valid. + assert(randomFillSync(buf.buffer)); + assert(randomFillSync(new DataView(buf.buffer))); +});