Skip to content

Commit d0594f4

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 9cc0195 commit d0594f4

File tree

6 files changed

+162
-14
lines changed

6 files changed

+162
-14
lines changed

deps/ncrypto/ncrypto.cc

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

4193+
DataPointer xofHashDigest(const Buffer<const unsigned char>& buf,
4194+
const EVP_MD* md,
4195+
size_t output_length) {
4196+
if (md == nullptr) return {};
4197+
4198+
EVP_MD_CTX* ctx = EVP_MD_CTX_new();
4199+
if (!ctx) return {};
4200+
if (EVP_DigestInit_ex(ctx, md, nullptr) != 1) {
4201+
EVP_MD_CTX_free(ctx);
4202+
return {};
4203+
}
4204+
if (EVP_DigestUpdate(ctx, buf.data, buf.len) != 1) {
4205+
EVP_MD_CTX_free(ctx);
4206+
return {};
4207+
}
4208+
auto data = DataPointer::Alloc(output_length);
4209+
if (!data) {
4210+
EVP_MD_CTX_free(ctx);
4211+
return {};
4212+
}
4213+
if (!EVP_DigestFinalXOF(
4214+
ctx, reinterpret_cast<unsigned char*>(data.get()), output_length)) {
4215+
EVP_MD_CTX_free(ctx);
4216+
return {};
4217+
}
4218+
4219+
EVP_MD_CTX_free(ctx);
4220+
return data;
4221+
}
4222+
41934223
// ============================================================================
41944224

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

deps/ncrypto/ncrypto.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,9 @@ class Digest final {
280280

281281
DataPointer hashDigest(const Buffer<const unsigned char>& data,
282282
const EVP_MD* md);
283+
DataPointer xofHashDigest(const Buffer<const unsigned char>& data,
284+
const EVP_MD* md,
285+
size_t length);
283286

284287
class Cipher final {
285288
public:

doc/api/crypto.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4187,7 +4187,7 @@ A convenient alias for [`crypto.webcrypto.getRandomValues()`][]. This
41874187
implementation is not compliant with the Web Crypto spec, to write
41884188
web-compatible code use [`crypto.webcrypto.getRandomValues()`][] instead.
41894189

4190-
### `crypto.hash(algorithm, data[, outputEncoding])`
4190+
### `crypto.hash(algorithm, data[, outputEncoding, outputLength])`
41914191

41924192
<!-- YAML
41934193
added:
@@ -4205,6 +4205,8 @@ added:
42054205
the encoded `TypedArray` into this API instead.
42064206
* `outputEncoding` {string|undefined} [Encoding][encoding] used to encode the
42074207
returned digest. **Default:** `'hex'`.
4208+
* `outputLength` {number|undefined} For XOF hash functions such as 'shake256',
4209+
the outputLength option can be used to specify the desired output length in bytes.
42084210
* Returns: {string|Buffer}
42094211

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

lib/internal/crypto/hash.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ 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, outputEncoding = 'hex', outputLength = undefined) {
197197
validateString(algorithm, 'algorithm');
198198
if (typeof input !== 'string' && !isArrayBufferView(input)) {
199199
throw new ERR_INVALID_ARG_TYPE('input', ['Buffer', 'TypedArray', 'DataView', 'string'], input);
@@ -213,8 +213,14 @@ function hash(algorithm, input, outputEncoding = 'hex') {
213213
}
214214
}
215215
}
216+
if (outputLength !== undefined) {
217+
validateUint32(outputLength, 'outputLength');
218+
if (outputLength === 0) {
219+
return normalized === 'buffer' ? Buffer.alloc(0) : '';
220+
}
221+
}
216222
return oneShotDigest(algorithm, getCachedHashId(algorithm), getHashCache(),
217-
input, normalized, encodingsMap[normalized]);
223+
input, normalized, encodingsMap[normalized], outputLength);
218224
}
219225

220226
module.exports = {

src/crypto/crypto_hash.cc

Lines changed: 23 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,32 @@ 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+
} else if (!isXOF && output_length != EVP_MD_get_size(md)) {
256+
ThrowCryptoError(env, ERR_get_error(), "Invalid length or not XOF");
257+
return {};
258+
}
259+
}
248260
return ncrypto::hashDigest(buf, md);
249261
})();
250262

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', '', 'hex', 0),
25+
crypto.createHash('shake128', { outputLength: 0 })
26+
.update('').digest('hex'));
27+
28+
assert.strictEqual(
29+
crypto.hash('shake128', '', 'hex', 5),
30+
crypto.createHash('shake128', { outputLength: 5 }).update('').digest('hex')
31+
);
32+
// Check length
33+
assert.strictEqual(
34+
crypto.hash('shake128', '', 'hex', 5).length,
35+
crypto.createHash('shake128', { outputLength: 5 }).update('').digest('hex')
36+
.length
37+
);
38+
39+
assert.strictEqual(
40+
crypto.hash('shake128', '', 'hex', 15),
41+
crypto.createHash('shake128', { outputLength: 15 }).update('').digest('hex')
42+
);
43+
// Check length
44+
assert.strictEqual(
45+
crypto.hash('shake128', '', 'hex', 15).length,
46+
crypto.createHash('shake128', { outputLength: 15 }).update('').digest('hex')
47+
.length
48+
);
49+
50+
assert.strictEqual(
51+
crypto.hash('shake256', '', 'hex', 16),
52+
crypto.createHash('shake256', { outputLength: 16 }).update('').digest('hex')
53+
);
54+
// Check length
55+
assert.strictEqual(
56+
crypto.hash('shake256', '', 'hex', 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', '', 'hex', 128),
64+
crypto
65+
.createHash('shake128', { outputLength: 128 }).update('')
66+
.digest('hex')
67+
);
68+
// Check length
69+
assert.strictEqual(
70+
crypto.hash('shake128', '', 'hex', 128).length,
71+
crypto
72+
.createHash('shake128', { outputLength: 128 }).update('')
73+
.digest('hex').length
74+
);
75+
assert.strictEqual(
76+
crypto.hash('shake256', '', 'hex', 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!', 'hex', 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+
}

0 commit comments

Comments
 (0)