Skip to content

Commit b9379c5

Browse files
committed
src: support outputLength in XOF hash functions
Support `outputLength` option in crypto.hash() for XOF hash functions to align with the behaviour of crypto.createHash() API
1 parent f77a96c commit b9379c5

File tree

7 files changed

+184
-17
lines changed

7 files changed

+184
-17
lines changed

deps/ncrypto/ncrypto.cc

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4191,6 +4191,31 @@ 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 (EVP_DigestInit_ex(ctx, md, nullptr) != 1) {
4202+
return {};
4203+
}
4204+
if (EVP_DigestUpdate(ctx, buf.data, buf.len) != 1) {
4205+
return {};
4206+
}
4207+
auto data = DataPointer::Alloc(output_length);
4208+
if (!data) {
4209+
return {};
4210+
}
4211+
if (!EVP_DigestFinalXOF(
4212+
ctx, reinterpret_cast<unsigned char*>(data.get()), output_length)) {
4213+
return {};
4214+
}
4215+
4216+
return data;
4217+
}
4218+
41944219
// ============================================================================
41954220

41964221
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: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4203,7 +4203,7 @@ 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:
@@ -4219,8 +4219,12 @@ added:
42194219
input encoding is desired for a string input, user could encode the string
42204220
into a `TypedArray` using either `TextEncoder` or `Buffer.from()` and passing
42214221
the encoded `TypedArray` into this API instead.
4222-
* `outputEncoding` {string|undefined} [Encoding][encoding] used to encode the
4223-
returned digest. **Default:** `'hex'`.
4222+
* `options` {Object|string}
4223+
* `outputEncoding` {string|undefined} [Encoding][encoding] used to encode the
4224+
returned digest. **Default:** `'hex'`.
4225+
* `outputLength` {number|undefined} For XOF hash functions such as 'shake256',
4226+
the outputLength option can be used to specify the desired output length in bytes.
4227+
**Default:** `undefined`
42244228
* Returns: {string|Buffer}
42254229

42264230
A utility for creating one-shot hash digests of data. It can be faster than

lib/internal/crypto/hash.js

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -193,11 +193,24 @@ async function asyncDigest(algorithm, data) {
193193
throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError');
194194
}
195195

196-
function hash(algorithm, input, outputEncoding = 'hex') {
196+
function hash(algorithm, input, options = {}) {
197197
validateString(algorithm, 'algorithm');
198198
if (typeof input !== 'string' && !isArrayBufferView(input)) {
199199
throw new ERR_INVALID_ARG_TYPE('input', ['Buffer', 'TypedArray', 'DataView', 'string'], input);
200200
}
201+
let outputEncoding = 'hex';
202+
let outputLength;
203+
204+
if (options !== null && typeof options !== 'function') {
205+
if (typeof options === 'string') {
206+
outputEncoding = options;
207+
} else if (typeof options === 'object') {
208+
({ outputEncoding = 'hex', outputLength } = options);
209+
} else {
210+
throw new ERR_INVALID_ARG_TYPE('options', ['object', 'string'], options);
211+
}
212+
}
213+
201214
let normalized = outputEncoding;
202215
// Fast case: if it's 'hex', we don't need to validate it further.
203216
if (outputEncoding !== 'hex') {
@@ -213,8 +226,14 @@ function hash(algorithm, input, outputEncoding = 'hex') {
213226
}
214227
}
215228
}
229+
if (outputLength !== undefined) {
230+
validateUint32(outputLength, 'outputLength');
231+
if (outputLength === 0) {
232+
return normalized === 'buffer' ? Buffer.alloc(0) : '';
233+
}
234+
}
216235
return oneShotDigest(algorithm, getCachedHashId(algorithm), getHashCache(),
217-
input, normalized, encodingsMap[normalized]);
236+
input, normalized, encodingsMap[normalized], outputLength);
218237
}
219238

220239
module.exports = {

src/crypto/crypto_hash.cc

Lines changed: 30 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,39 @@ 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+
DataPointer output = ([&]() -> DataPointer {
235+
Utf8Value utf8(isolate, args[3]);
236+
ncrypto::Buffer<const unsigned char> buf;
234237
if (args[3]->IsString()) {
235-
Utf8Value utf8(isolate, args[3]);
236-
ncrypto::Buffer<const unsigned char> buf{
238+
buf = {
237239
.data = reinterpret_cast<const unsigned char*>(utf8.out()),
238240
.len = utf8.length(),
239241
};
240-
return ncrypto::hashDigest(buf, md);
242+
} else {
243+
ArrayBufferViewContents<unsigned char> input(args[3]);
244+
buf = {
245+
.data = reinterpret_cast<const unsigned char*>(input.data()),
246+
.len = input.length(),
247+
};
241248
}
242249

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-
};
250+
if (!args[6]->IsUndefined()) {
251+
bool isXOF = (EVP_MD_flags(md) & EVP_MD_FLAG_XOF) != 0;
252+
int output_length = args[6].As<Uint32>()->Value();
253+
if (isXOF) {
254+
return ncrypto::xofHashDigest(buf, md, output_length);
255+
}
256+
if (output_length != EVP_MD_get_size(md)) {
257+
Utf8Value method(isolate, args[0]);
258+
std::string message = "Output length " + std::to_string(output_length) +
259+
" is invalid for ";
260+
message += method.ToString() + ", which does not support XOF";
261+
ThrowCryptoError(env, ERR_get_error(), message.c_str());
262+
return {};
263+
}
264+
// Does not support XOF but output length matches expected digest size.
265+
// Compute digest as usual.
266+
}
248267
return ncrypto::hashDigest(buf, md);
249268
})();
250269

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
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', '', 'hex'),
15+
crypto.createHash('shake128').update('').digest('hex')
16+
);
17+
18+
assert.strictEqual(
19+
crypto.hash('shake256', '', 'hex'),
20+
crypto.createHash('shake256').update('').digest('hex')
21+
);
22+
23+
// Short outputLengths.
24+
assert.strictEqual(crypto.hash('shake128', '', { encoding: 'hex', outputLength: 0 }),
25+
crypto.createHash('shake128', { outputLength: 0 })
26+
.update('').digest('hex'));
27+
28+
assert.strictEqual(
29+
crypto.hash('shake128', '', { outputEncoding: 'hex', outputLength: 5 }),
30+
crypto.createHash('shake128', { outputLength: 5 }).update('').digest('hex')
31+
);
32+
// Check length
33+
assert.strictEqual(
34+
crypto.hash('shake128', '', { outputEncoding: 'hex', outputLength: 5 }).length,
35+
crypto.createHash('shake128', { outputLength: 5 }).update('').digest('hex')
36+
.length
37+
);
38+
39+
assert.strictEqual(
40+
crypto.hash('shake128', '', { outputEncoding: 'hex', outputLength: 15 }),
41+
crypto.createHash('shake128', { outputLength: 15 }).update('').digest('hex')
42+
);
43+
// Check length
44+
assert.strictEqual(
45+
crypto.hash('shake128', '', { outputEncoding: 'hex', outputLength: 15 }).length,
46+
crypto.createHash('shake128', { outputLength: 15 }).update('').digest('hex')
47+
.length
48+
);
49+
50+
assert.strictEqual(
51+
crypto.hash('shake256', '', { outputEncoding: 'hex', outputLength: 16 }),
52+
crypto.createHash('shake256', { outputLength: 16 }).update('').digest('hex')
53+
);
54+
// Check length
55+
assert.strictEqual(
56+
crypto.hash('shake256', '', { outputEncoding: 'hex', outputLength: 16 }).length,
57+
crypto.createHash('shake256', { outputLength: 16 }).update('').digest('hex')
58+
.length
59+
);
60+
61+
// Large outputLengths.
62+
assert.strictEqual(
63+
crypto.hash('shake128', '', { outputEncoding: 'hex', outputLength: 128 }),
64+
crypto
65+
.createHash('shake128', { outputLength: 128 }).update('')
66+
.digest('hex')
67+
);
68+
// Check length without encoding
69+
assert.strictEqual(
70+
crypto.hash('shake128', '', { outputLength: 128 }).length,
71+
crypto
72+
.createHash('shake128', { outputLength: 128 }).update('')
73+
.digest('hex').length
74+
);
75+
assert.strictEqual(
76+
crypto.hash('shake256', '', { outputLength: 128 }),
77+
crypto
78+
.createHash('shake256', { outputLength: 128 }).update('')
79+
.digest('hex')
80+
);
81+
82+
const actual = crypto.hash('shake256', 'The message is shorter than the hash!', { outputLength: 1024 * 1024 });
83+
const expected = crypto
84+
.createHash('shake256', {
85+
outputLength: 1024 * 1024,
86+
})
87+
.update('The message is shorter than the hash!')
88+
.digest('hex');
89+
assert.strictEqual(actual, expected);
90+
91+
// Non-XOF hash functions should accept valid outputLength options as well.
92+
assert.strictEqual(crypto.hash('sha224', '', 'hex', 28),
93+
'd14a028c2a3a2bc9476102bb288234c4' +
94+
'15a2b01f828ea62ac5b3e42f');
95+
}

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

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

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

0 commit comments

Comments
 (0)