crypto: support outputLength option in crypto.hash for XOF functions

Support `outputLength` option in crypto.hash() for XOF hash
functions to align with the behaviour of crypto.createHash()
API

closes: https://github.com/nodejs/node/issues/57312

Co-authored-by: Filip Skokan <panva.ip@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/58121
Fixes: https://github.com/nodejs/node/issues/57312
Reviewed-By: Joyee Cheung <joyeec9h3@gmail.com>
Reviewed-By: Filip Skokan <panva.ip@gmail.com>
Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com>
This commit is contained in:
Aditi 2025-07-08 19:09:00 +05:30 committed by RafaelGSS
parent 3afb15b715
commit 22b60e8a57
No known key found for this signature in database
GPG key ID: 8BEAB4DFCF555EF4
8 changed files with 258 additions and 23 deletions

View file

@ -4191,6 +4191,22 @@ DataPointer hashDigest(const Buffer<const unsigned char>& buf,
return data.resize(result_size);
}
DataPointer xofHashDigest(const Buffer<const unsigned char>& buf,
const EVP_MD* md,
size_t output_length) {
if (md == nullptr) return {};
EVPMDCtxPointer ctx = EVPMDCtxPointer::New();
if (!ctx) return {};
if (ctx.digestInit(md) != 1) {
return {};
}
if (ctx.digestUpdate(reinterpret_cast<const Buffer<const void>&>(buf)) != 1) {
return {};
}
return ctx.digestFinal(output_length);
}
// ============================================================================
X509Name::X509Name() : name_(nullptr), total_(0) {}

View file

@ -278,8 +278,13 @@ class Digest final {
const EVP_MD* md_ = nullptr;
};
// Computes a fixed-length digest.
DataPointer hashDigest(const Buffer<const unsigned char>& data,
const EVP_MD* md);
// Computes a variable-length digest for XOF algorithms (e.g. SHAKE128).
DataPointer xofHashDigest(const Buffer<const unsigned char>& data,
const EVP_MD* md,
size_t length);
class Cipher final {
public:

View file

@ -4203,12 +4203,16 @@ A convenient alias for [`crypto.webcrypto.getRandomValues()`][]. This
implementation is not compliant with the Web Crypto spec, to write
web-compatible code use [`crypto.webcrypto.getRandomValues()`][] instead.
### `crypto.hash(algorithm, data[, outputEncoding])`
### `crypto.hash(algorithm, data[, options])`
<!-- YAML
added:
- v21.7.0
- v20.12.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/58121
description: The `outputLength` option was added for XOF hash functions.
-->
> Stability: 1.2 - Release candidate
@ -4219,8 +4223,11 @@ added:
input encoding is desired for a string input, user could encode the string
into a `TypedArray` using either `TextEncoder` or `Buffer.from()` and passing
the encoded `TypedArray` into this API instead.
* `outputEncoding` {string|undefined} [Encoding][encoding] used to encode the
returned digest. **Default:** `'hex'`.
* `options` {Object|string}
* `outputEncoding` {string} [Encoding][encoding] used to encode the
returned digest. **Default:** `'hex'`.
* `outputLength` {number} For XOF hash functions such as 'shake256',
the outputLength option can be used to specify the desired output length in bytes.
* Returns: {string|Buffer}
A utility for creating one-shot hash digests of data. It can be faster than
@ -4233,6 +4240,8 @@ version of OpenSSL on the platform. Examples are `'sha256'`, `'sha512'`, etc.
On recent releases of OpenSSL, `openssl list -digest-algorithms` will
display the available digest algorithms.
If `options` is a string, then it specifies the `outputEncoding`.
Example:
```cjs

View file

@ -54,6 +54,7 @@ const {
const {
validateEncoding,
validateString,
validateObject,
validateUint32,
} = require('internal/validators');
@ -218,14 +219,27 @@ async function asyncDigest(algorithm, data) {
throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError');
}
function hash(algorithm, input, outputEncoding = 'hex') {
function hash(algorithm, input, options) {
validateString(algorithm, 'algorithm');
if (typeof input !== 'string' && !isArrayBufferView(input)) {
throw new ERR_INVALID_ARG_TYPE('input', ['Buffer', 'TypedArray', 'DataView', 'string'], input);
}
let outputEncoding;
let outputLength;
if (typeof options === 'string') {
outputEncoding = options;
} else if (options !== undefined) {
validateObject(options, 'options');
outputLength = options.outputLength;
outputEncoding = options.outputEncoding;
}
outputEncoding ??= 'hex';
let normalized = outputEncoding;
// Fast case: if it's 'hex', we don't need to validate it further.
if (outputEncoding !== 'hex') {
if (normalized !== 'hex') {
validateString(outputEncoding, 'outputEncoding');
normalized = normalizeEncoding(outputEncoding);
// If the encoding is invalid, normalizeEncoding() returns undefined.
@ -238,14 +252,17 @@ function hash(algorithm, input, outputEncoding = 'hex') {
}
}
}
// TODO: ideally we have to ship https://github.com/nodejs/node/pull/58121 so
// that a proper DEP0198 deprecation can be done here as well.
const normalizedAlgorithm = normalizeAlgorithm(algorithm);
if (normalizedAlgorithm === 'shake128' || normalizedAlgorithm === 'shake256') {
return new Hash(algorithm).update(input).digest(normalized);
if (outputLength !== undefined) {
validateUint32(outputLength, 'outputLength');
}
if (outputLength === undefined) {
maybeEmitDeprecationWarning(algorithm);
}
return oneShotDigest(algorithm, getCachedHashId(algorithm), getHashCache(),
input, normalized, encodingsMap[normalized]);
input, normalized, encodingsMap[normalized], outputLength);
}
module.exports = {

View file

@ -208,17 +208,18 @@ const EVP_MD* GetDigestImplementation(Environment* env,
}
// crypto.digest(algorithm, algorithmId, algorithmCache,
// input, outputEncoding, outputEncodingId)
// input, outputEncoding, outputEncodingId, outputLength)
void Hash::OneShotDigest(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
Isolate* isolate = env->isolate();
CHECK_EQ(args.Length(), 6);
CHECK_EQ(args.Length(), 7);
CHECK(args[0]->IsString()); // algorithm
CHECK(args[1]->IsInt32()); // algorithmId
CHECK(args[2]->IsObject()); // algorithmCache
CHECK(args[3]->IsString() || args[3]->IsArrayBufferView()); // input
CHECK(args[4]->IsString()); // outputEncoding
CHECK(args[5]->IsUint32() || args[5]->IsUndefined()); // outputEncodingId
CHECK(args[6]->IsUint32() || args[6]->IsUndefined()); // outputLength
const EVP_MD* md = GetDigestImplementation(env, args[0], args[1], args[2]);
if (md == nullptr) [[unlikely]] {
@ -230,21 +231,68 @@ void Hash::OneShotDigest(const FunctionCallbackInfo<Value>& args) {
enum encoding output_enc = ParseEncoding(isolate, args[4], args[5], HEX);
DataPointer output = ([&] {
bool is_xof = (EVP_MD_flags(md) & EVP_MD_FLAG_XOF) != 0;
int output_length = EVP_MD_size(md);
// This is to cause hash() to fail when an incorrect
// outputLength option was passed for a non-XOF hash function.
if (!is_xof && !args[6]->IsUndefined()) {
output_length = args[6].As<Uint32>()->Value();
if (output_length != EVP_MD_size(md)) {
Utf8Value method(isolate, args[0]);
std::string message =
"Output length " + std::to_string(output_length) + " is invalid for ";
message += method.ToString() + ", which does not support XOF";
return ThrowCryptoError(env, ERR_get_error(), message.c_str());
}
} else if (is_xof) {
if (!args[6]->IsUndefined()) {
output_length = args[6].As<Uint32>()->Value();
} else if (output_length == 0) {
// This is to handle OpenSSL 3.4's breaking change in SHAKE128/256
// default lengths
const char* name = OBJ_nid2sn(EVP_MD_type(md));
if (name != nullptr) {
if (strcmp(name, "SHAKE128") == 0) {
output_length = 16;
} else if (strcmp(name, "SHAKE256") == 0) {
output_length = 32;
}
}
}
}
if (output_length == 0) {
if (output_enc == BUFFER) {
Local<v8::ArrayBuffer> ab = v8::ArrayBuffer::New(isolate, 0);
args.GetReturnValue().Set(
Buffer::New(isolate, ab, 0, 0).ToLocalChecked());
} else {
args.GetReturnValue().Set(v8::String::Empty(isolate));
}
return;
}
DataPointer output = ([&]() -> DataPointer {
Utf8Value utf8(isolate, args[3]);
ncrypto::Buffer<const unsigned char> buf;
if (args[3]->IsString()) {
Utf8Value utf8(isolate, args[3]);
ncrypto::Buffer<const unsigned char> buf{
buf = {
.data = reinterpret_cast<const unsigned char*>(utf8.out()),
.len = utf8.length(),
};
return ncrypto::hashDigest(buf, md);
} else {
ArrayBufferViewContents<unsigned char> input(args[3]);
buf = {
.data = reinterpret_cast<const unsigned char*>(input.data()),
.len = input.length(),
};
}
if (is_xof) {
return ncrypto::xofHashDigest(buf, md, output_length);
}
ArrayBufferViewContents<unsigned char> input(args[3]);
ncrypto::Buffer<const unsigned char> buf{
.data = reinterpret_cast<const unsigned char*>(input.data()),
.len = input.length(),
};
return ncrypto::hashDigest(buf, md);
})();

View file

@ -0,0 +1,18 @@
// Flags: --pending-deprecation
'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const { hash } = require('crypto');
common.expectWarning({
DeprecationWarning: {
DEP0198: 'Creating SHAKE128/256 digests without an explicit options.outputLength is deprecated.',
}
});
{
hash('shake128', 'test');
}

View file

@ -0,0 +1,122 @@
'use strict';
// This tests crypto.hash() works.
const common = require('../common');
if (!common.hasCrypto) common.skip('missing crypto');
const assert = require('assert');
const crypto = require('crypto');
// Test XOF hash functions and the outputLength option.
{
// Default outputLengths.
assert.strictEqual(
crypto.hash('shake128', ''),
'7f9c2ba4e88f827d616045507605853e'
);
assert.strictEqual(
crypto.hash('shake256', ''),
'46b9dd2b0ba88d13233b3feb743eeb243fcd52ea62b81b82b50c27646ed5762f'
);
// outputEncoding as an option.
assert.strictEqual(
crypto.hash('shake128', '', { outputEncoding: 'base64url' }),
'f5wrpOiPgn1hYEVQdgWFPg'
);
assert.strictEqual(
crypto.hash('shake256', '', { outputEncoding: 'base64url' }),
'RrndKwuojRMjOz_rdD7rJD_NUupiuBuCtQwnZG7Vdi8'
);
assert.deepStrictEqual(
crypto.hash('shake128', '', { outputEncoding: 'buffer' }),
Buffer.from('f5wrpOiPgn1hYEVQdgWFPg', 'base64url')
);
assert.deepStrictEqual(
crypto.hash('shake256', '', { outputEncoding: 'buffer' }),
Buffer.from('RrndKwuojRMjOz_rdD7rJD_NUupiuBuCtQwnZG7Vdi8', 'base64url')
);
// Short outputLengths.
assert.strictEqual(crypto.hash('shake128', '', { outputLength: 0 }), '');
assert.deepStrictEqual(crypto.hash('shake128', '', { outputEncoding: 'buffer', outputLength: 0 }),
Buffer.alloc(0));
assert.strictEqual(
crypto.hash('shake128', '', { outputLength: 5 }),
crypto.createHash('shake128', { outputLength: 5 }).update('').digest('hex')
);
// Check length
assert.strictEqual(
crypto.hash('shake128', '', { outputLength: 5 }).length,
crypto.createHash('shake128', { outputLength: 5 }).update('').digest('hex')
.length
);
assert.strictEqual(
crypto.hash('shake128', '', { outputLength: 15 }),
crypto.createHash('shake128', { outputLength: 15 }).update('').digest('hex')
);
// Check length
assert.strictEqual(
crypto.hash('shake128', '', { outputLength: 15 }).length,
crypto.createHash('shake128', { outputLength: 15 }).update('').digest('hex')
.length
);
assert.strictEqual(
crypto.hash('shake256', '', { outputLength: 16 }),
crypto.createHash('shake256', { outputLength: 16 }).update('').digest('hex')
);
// Check length
assert.strictEqual(
crypto.hash('shake256', '', { outputLength: 16 }).length,
crypto.createHash('shake256', { outputLength: 16 }).update('').digest('hex')
.length
);
// Large outputLengths.
assert.strictEqual(
crypto.hash('shake128', '', { outputLength: 128 }),
crypto
.createHash('shake128', { outputLength: 128 }).update('')
.digest('hex')
);
// Check length without encoding
assert.strictEqual(
crypto.hash('shake128', '', { outputLength: 128 }).length,
crypto
.createHash('shake128', { outputLength: 128 }).update('')
.digest('hex').length
);
assert.strictEqual(
crypto.hash('shake256', '', { outputLength: 128 }),
crypto
.createHash('shake256', { outputLength: 128 }).update('')
.digest('hex')
);
const actual = crypto.hash('shake256', 'The message is shorter than the hash!', { outputLength: 1024 * 1024 });
const expected = crypto
.createHash('shake256', {
outputLength: 1024 * 1024,
})
.update('The message is shorter than the hash!')
.digest('hex');
assert.strictEqual(actual, expected);
// Non-XOF hash functions should accept valid outputLength options as well.
assert.strictEqual(crypto.hash('sha224', '', { outputLength: 28 }),
'd14a028c2a3a2bc9476102bb288234c4' +
'15a2b01f828ea62ac5b3e42f');
// Non-XOF hash functions should fail when outputLength isn't their actual outputLength
assert.throws(() => crypto.hash('sha224', '', { outputLength: 32 }),
{ message: 'Output length 32 is invalid for sha224, which does not support XOF' });
assert.throws(() => crypto.hash('sha224', '', { outputLength: 0 }),
{ message: 'Output length 0 is invalid for sha224, which does not support XOF' });
}

View file

@ -19,7 +19,7 @@ const fs = require('fs');
assert.throws(() => { crypto.hash('sha1', invalid); }, { code: 'ERR_INVALID_ARG_TYPE' });
});
[null, true, 1, () => {}, {}].forEach((invalid) => {
[0, 1, NaN, true, Symbol(0)].forEach((invalid) => {
assert.throws(() => { crypto.hash('sha1', 'test', invalid); }, { code: 'ERR_INVALID_ARG_TYPE' });
});