Skip to content

Commit 1c4fe6d

Browse files
Aditi-1400panva
andauthored
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: #57312 Co-authored-by: Filip Skokan <[email protected]> PR-URL: #58121 Fixes: #57312 Reviewed-By: Joyee Cheung <[email protected]> Reviewed-By: Filip Skokan <[email protected]> Reviewed-By: Rafael Gonzaga <[email protected]>
1 parent c7eff61 commit 1c4fe6d

File tree

8 files changed

+258
-23
lines changed

8 files changed

+258
-23
lines changed

deps/ncrypto/ncrypto.cc

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4191,6 +4191,22 @@ DataPointer hashDigest(const Buffer<const unsigned char>& buf,
41914191
return data.resize(result_size);
41924192
}
41934193

4194+
DataPointer xofHashDigest(const Buffer<const unsigned char>& buf,
4195+
const EVP_MD* md,
4196+
size_t output_length) {
4197+
if (md == nullptr) return {};
4198+
4199+
EVPMDCtxPointer ctx = EVPMDCtxPointer::New();
4200+
if (!ctx) return {};
4201+
if (ctx.digestInit(md) != 1) {
4202+
return {};
4203+
}
4204+
if (ctx.digestUpdate(reinterpret_cast<const Buffer<const void>&>(buf)) != 1) {
4205+
return {};
4206+
}
4207+
return ctx.digestFinal(output_length);
4208+
}
4209+
41944210
// ============================================================================
41954211

41964212
X509Name::X509Name() : name_(nullptr), total_(0) {}

deps/ncrypto/ncrypto.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,8 +278,13 @@ class Digest final {
278278
const EVP_MD* md_ = nullptr;
279279
};
280280

281+
// Computes a fixed-length digest.
281282
DataPointer hashDigest(const Buffer<const unsigned char>& data,
282283
const EVP_MD* md);
284+
// Computes a variable-length digest for XOF algorithms (e.g. SHAKE128).
285+
DataPointer xofHashDigest(const Buffer<const unsigned char>& data,
286+
const EVP_MD* md,
287+
size_t length);
283288

284289
class Cipher final {
285290
public:

doc/api/crypto.md

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4203,12 +4203,16 @@ A convenient alias for [`crypto.webcrypto.getRandomValues()`][]. This
42034203
implementation is not compliant with the Web Crypto spec, to write
42044204
web-compatible code use [`crypto.webcrypto.getRandomValues()`][] instead.
42054205

4206-
### `crypto.hash(algorithm, data[, outputEncoding])`
4206+
### `crypto.hash(algorithm, data[, options])`
42074207

42084208
<!-- YAML
42094209
added:
42104210
- v21.7.0
42114211
- v20.12.0
4212+
changes:
4213+
- version: REPLACEME
4214+
pr-url: https://github.com/nodejs/node/pull/58121
4215+
description: The `outputLength` option was added for XOF hash functions.
42124216
-->
42134217

42144218
> Stability: 1.2 - Release candidate
@@ -4219,8 +4223,11 @@ added:
42194223
input encoding is desired for a string input, user could encode the string
42204224
into a `TypedArray` using either `TextEncoder` or `Buffer.from()` and passing
42214225
the encoded `TypedArray` into this API instead.
4222-
* `outputEncoding` {string|undefined} [Encoding][encoding] used to encode the
4223-
returned digest. **Default:** `'hex'`.
4226+
* `options` {Object|string}
4227+
* `outputEncoding` {string} [Encoding][encoding] used to encode the
4228+
returned digest. **Default:** `'hex'`.
4229+
* `outputLength` {number} For XOF hash functions such as 'shake256',
4230+
the outputLength option can be used to specify the desired output length in bytes.
42244231
* Returns: {string|Buffer}
42254232

42264233
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.
42334240
On recent releases of OpenSSL, `openssl list -digest-algorithms` will
42344241
display the available digest algorithms.
42354242

4243+
If `options` is a string, then it specifies the `outputEncoding`.
4244+
42364245
Example:
42374246

42384247
```cjs

lib/internal/crypto/hash.js

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ const {
5454
const {
5555
validateEncoding,
5656
validateString,
57+
validateObject,
5758
validateUint32,
5859
} = require('internal/validators');
5960

@@ -218,14 +219,27 @@ async function asyncDigest(algorithm, data) {
218219
throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError');
219220
}
220221

221-
function hash(algorithm, input, outputEncoding = 'hex') {
222+
function hash(algorithm, input, options) {
222223
validateString(algorithm, 'algorithm');
223224
if (typeof input !== 'string' && !isArrayBufferView(input)) {
224225
throw new ERR_INVALID_ARG_TYPE('input', ['Buffer', 'TypedArray', 'DataView', 'string'], input);
225226
}
227+
let outputEncoding;
228+
let outputLength;
229+
230+
if (typeof options === 'string') {
231+
outputEncoding = options;
232+
} else if (options !== undefined) {
233+
validateObject(options, 'options');
234+
outputLength = options.outputLength;
235+
outputEncoding = options.outputEncoding;
236+
}
237+
238+
outputEncoding ??= 'hex';
239+
226240
let normalized = outputEncoding;
227241
// Fast case: if it's 'hex', we don't need to validate it further.
228-
if (outputEncoding !== 'hex') {
242+
if (normalized !== 'hex') {
229243
validateString(outputEncoding, 'outputEncoding');
230244
normalized = normalizeEncoding(outputEncoding);
231245
// If the encoding is invalid, normalizeEncoding() returns undefined.
@@ -238,14 +252,17 @@ function hash(algorithm, input, outputEncoding = 'hex') {
238252
}
239253
}
240254
}
241-
// TODO: ideally we have to ship https://github.com/nodejs/node/pull/58121 so
242-
// that a proper DEP0198 deprecation can be done here as well.
243-
const normalizedAlgorithm = normalizeAlgorithm(algorithm);
244-
if (normalizedAlgorithm === 'shake128' || normalizedAlgorithm === 'shake256') {
245-
return new Hash(algorithm).update(input).digest(normalized);
255+
256+
if (outputLength !== undefined) {
257+
validateUint32(outputLength, 'outputLength');
258+
}
259+
260+
if (outputLength === undefined) {
261+
maybeEmitDeprecationWarning(algorithm);
246262
}
263+
247264
return oneShotDigest(algorithm, getCachedHashId(algorithm), getHashCache(),
248-
input, normalized, encodingsMap[normalized]);
265+
input, normalized, encodingsMap[normalized], outputLength);
249266
}
250267

251268
module.exports = {

src/crypto/crypto_hash.cc

Lines changed: 59 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -208,17 +208,18 @@ const EVP_MD* GetDigestImplementation(Environment* env,
208208
}
209209

210210
// crypto.digest(algorithm, algorithmId, algorithmCache,
211-
// input, outputEncoding, outputEncodingId)
211+
// input, outputEncoding, outputEncodingId, outputLength)
212212
void Hash::OneShotDigest(const FunctionCallbackInfo<Value>& args) {
213213
Environment* env = Environment::GetCurrent(args);
214214
Isolate* isolate = env->isolate();
215-
CHECK_EQ(args.Length(), 6);
215+
CHECK_EQ(args.Length(), 7);
216216
CHECK(args[0]->IsString()); // algorithm
217217
CHECK(args[1]->IsInt32()); // algorithmId
218218
CHECK(args[2]->IsObject()); // algorithmCache
219219
CHECK(args[3]->IsString() || args[3]->IsArrayBufferView()); // input
220220
CHECK(args[4]->IsString()); // outputEncoding
221221
CHECK(args[5]->IsUint32() || args[5]->IsUndefined()); // outputEncodingId
222+
CHECK(args[6]->IsUint32() || args[6]->IsUndefined()); // outputLength
222223

223224
const EVP_MD* md = GetDigestImplementation(env, args[0], args[1], args[2]);
224225
if (md == nullptr) [[unlikely]] {
@@ -230,21 +231,68 @@ void Hash::OneShotDigest(const FunctionCallbackInfo<Value>& args) {
230231

231232
enum encoding output_enc = ParseEncoding(isolate, args[4], args[5], HEX);
232233

233-
DataPointer output = ([&] {
234+
bool is_xof = (EVP_MD_flags(md) & EVP_MD_FLAG_XOF) != 0;
235+
int output_length = EVP_MD_size(md);
236+
237+
// This is to cause hash() to fail when an incorrect
238+
// outputLength option was passed for a non-XOF hash function.
239+
if (!is_xof && !args[6]->IsUndefined()) {
240+
output_length = args[6].As<Uint32>()->Value();
241+
if (output_length != EVP_MD_size(md)) {
242+
Utf8Value method(isolate, args[0]);
243+
std::string message =
244+
"Output length " + std::to_string(output_length) + " is invalid for ";
245+
message += method.ToString() + ", which does not support XOF";
246+
return ThrowCryptoError(env, ERR_get_error(), message.c_str());
247+
}
248+
} else if (is_xof) {
249+
if (!args[6]->IsUndefined()) {
250+
output_length = args[6].As<Uint32>()->Value();
251+
} else if (output_length == 0) {
252+
// This is to handle OpenSSL 3.4's breaking change in SHAKE128/256
253+
// default lengths
254+
const char* name = OBJ_nid2sn(EVP_MD_type(md));
255+
if (name != nullptr) {
256+
if (strcmp(name, "SHAKE128") == 0) {
257+
output_length = 16;
258+
} else if (strcmp(name, "SHAKE256") == 0) {
259+
output_length = 32;
260+
}
261+
}
262+
}
263+
}
264+
265+
if (output_length == 0) {
266+
if (output_enc == BUFFER) {
267+
Local<v8::ArrayBuffer> ab = v8::ArrayBuffer::New(isolate, 0);
268+
args.GetReturnValue().Set(
269+
Buffer::New(isolate, ab, 0, 0).ToLocalChecked());
270+
} else {
271+
args.GetReturnValue().Set(v8::String::Empty(isolate));
272+
}
273+
return;
274+
}
275+
276+
DataPointer output = ([&]() -> DataPointer {
277+
Utf8Value utf8(isolate, args[3]);
278+
ncrypto::Buffer<const unsigned char> buf;
234279
if (args[3]->IsString()) {
235-
Utf8Value utf8(isolate, args[3]);
236-
ncrypto::Buffer<const unsigned char> buf{
280+
buf = {
237281
.data = reinterpret_cast<const unsigned char*>(utf8.out()),
238282
.len = utf8.length(),
239283
};
240-
return ncrypto::hashDigest(buf, md);
284+
} else {
285+
ArrayBufferViewContents<unsigned char> input(args[3]);
286+
buf = {
287+
.data = reinterpret_cast<const unsigned char*>(input.data()),
288+
.len = input.length(),
289+
};
290+
}
291+
292+
if (is_xof) {
293+
return ncrypto::xofHashDigest(buf, md, output_length);
241294
}
242295

243-
ArrayBufferViewContents<unsigned char> input(args[3]);
244-
ncrypto::Buffer<const unsigned char> buf{
245-
.data = reinterpret_cast<const unsigned char*>(input.data()),
246-
.len = input.length(),
247-
};
248296
return ncrypto::hashDigest(buf, md);
249297
})();
250298

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Flags: --pending-deprecation
2+
'use strict';
3+
4+
const common = require('../common');
5+
if (!common.hasCrypto)
6+
common.skip('missing crypto');
7+
8+
const { hash } = require('crypto');
9+
10+
common.expectWarning({
11+
DeprecationWarning: {
12+
DEP0198: 'Creating SHAKE128/256 digests without an explicit options.outputLength is deprecated.',
13+
}
14+
});
15+
16+
{
17+
hash('shake128', 'test');
18+
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
'use strict';
2+
// This tests crypto.hash() works.
3+
const common = require('../common');
4+
5+
if (!common.hasCrypto) common.skip('missing crypto');
6+
7+
const assert = require('assert');
8+
const crypto = require('crypto');
9+
10+
// Test XOF hash functions and the outputLength option.
11+
{
12+
// Default outputLengths.
13+
assert.strictEqual(
14+
crypto.hash('shake128', ''),
15+
'7f9c2ba4e88f827d616045507605853e'
16+
);
17+
18+
assert.strictEqual(
19+
crypto.hash('shake256', ''),
20+
'46b9dd2b0ba88d13233b3feb743eeb243fcd52ea62b81b82b50c27646ed5762f'
21+
);
22+
23+
// outputEncoding as an option.
24+
assert.strictEqual(
25+
crypto.hash('shake128', '', { outputEncoding: 'base64url' }),
26+
'f5wrpOiPgn1hYEVQdgWFPg'
27+
);
28+
29+
assert.strictEqual(
30+
crypto.hash('shake256', '', { outputEncoding: 'base64url' }),
31+
'RrndKwuojRMjOz_rdD7rJD_NUupiuBuCtQwnZG7Vdi8'
32+
);
33+
34+
assert.deepStrictEqual(
35+
crypto.hash('shake128', '', { outputEncoding: 'buffer' }),
36+
Buffer.from('f5wrpOiPgn1hYEVQdgWFPg', 'base64url')
37+
);
38+
39+
assert.deepStrictEqual(
40+
crypto.hash('shake256', '', { outputEncoding: 'buffer' }),
41+
Buffer.from('RrndKwuojRMjOz_rdD7rJD_NUupiuBuCtQwnZG7Vdi8', 'base64url')
42+
);
43+
44+
// Short outputLengths.
45+
assert.strictEqual(crypto.hash('shake128', '', { outputLength: 0 }), '');
46+
assert.deepStrictEqual(crypto.hash('shake128', '', { outputEncoding: 'buffer', outputLength: 0 }),
47+
Buffer.alloc(0));
48+
49+
assert.strictEqual(
50+
crypto.hash('shake128', '', { outputLength: 5 }),
51+
crypto.createHash('shake128', { outputLength: 5 }).update('').digest('hex')
52+
);
53+
// Check length
54+
assert.strictEqual(
55+
crypto.hash('shake128', '', { outputLength: 5 }).length,
56+
crypto.createHash('shake128', { outputLength: 5 }).update('').digest('hex')
57+
.length
58+
);
59+
60+
assert.strictEqual(
61+
crypto.hash('shake128', '', { outputLength: 15 }),
62+
crypto.createHash('shake128', { outputLength: 15 }).update('').digest('hex')
63+
);
64+
// Check length
65+
assert.strictEqual(
66+
crypto.hash('shake128', '', { outputLength: 15 }).length,
67+
crypto.createHash('shake128', { outputLength: 15 }).update('').digest('hex')
68+
.length
69+
);
70+
71+
assert.strictEqual(
72+
crypto.hash('shake256', '', { outputLength: 16 }),
73+
crypto.createHash('shake256', { outputLength: 16 }).update('').digest('hex')
74+
);
75+
// Check length
76+
assert.strictEqual(
77+
crypto.hash('shake256', '', { outputLength: 16 }).length,
78+
crypto.createHash('shake256', { outputLength: 16 }).update('').digest('hex')
79+
.length
80+
);
81+
82+
// Large outputLengths.
83+
assert.strictEqual(
84+
crypto.hash('shake128', '', { outputLength: 128 }),
85+
crypto
86+
.createHash('shake128', { outputLength: 128 }).update('')
87+
.digest('hex')
88+
);
89+
// Check length without encoding
90+
assert.strictEqual(
91+
crypto.hash('shake128', '', { outputLength: 128 }).length,
92+
crypto
93+
.createHash('shake128', { outputLength: 128 }).update('')
94+
.digest('hex').length
95+
);
96+
assert.strictEqual(
97+
crypto.hash('shake256', '', { outputLength: 128 }),
98+
crypto
99+
.createHash('shake256', { outputLength: 128 }).update('')
100+
.digest('hex')
101+
);
102+
103+
const actual = crypto.hash('shake256', 'The message is shorter than the hash!', { outputLength: 1024 * 1024 });
104+
const expected = crypto
105+
.createHash('shake256', {
106+
outputLength: 1024 * 1024,
107+
})
108+
.update('The message is shorter than the hash!')
109+
.digest('hex');
110+
assert.strictEqual(actual, expected);
111+
112+
// Non-XOF hash functions should accept valid outputLength options as well.
113+
assert.strictEqual(crypto.hash('sha224', '', { outputLength: 28 }),
114+
'd14a028c2a3a2bc9476102bb288234c4' +
115+
'15a2b01f828ea62ac5b3e42f');
116+
117+
// Non-XOF hash functions should fail when outputLength isn't their actual outputLength
118+
assert.throws(() => crypto.hash('sha224', '', { outputLength: 32 }),
119+
{ message: 'Output length 32 is invalid for sha224, which does not support XOF' });
120+
assert.throws(() => crypto.hash('sha224', '', { outputLength: 0 }),
121+
{ message: 'Output length 0 is invalid for sha224, which does not support XOF' });
122+
}

test/parallel/test-crypto-oneshot-hash.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ const fs = require('fs');
1919
assert.throws(() => { crypto.hash('sha1', invalid); }, { code: 'ERR_INVALID_ARG_TYPE' });
2020
});
2121

22-
[null, true, 1, () => {}, {}].forEach((invalid) => {
22+
[0, 1, NaN, true, Symbol(0)].forEach((invalid) => {
2323
assert.throws(() => { crypto.hash('sha1', 'test', invalid); }, { code: 'ERR_INVALID_ARG_TYPE' });
2424
});
2525

0 commit comments

Comments
 (0)