Skip to content

Commit aeab00b

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 4454d09 commit aeab00b

File tree

7 files changed

+182
-17
lines changed

7 files changed

+182
-17
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<constunsignedchar>& 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<unsignedchar*>(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: 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<constunsignedchar>& data,
282283
const EVP_MD* md);
284+
// Computes a variable-length digest for XOF algorithms (e.g. SHAKE128).
285+
DataPointer xofHashDigest(const Buffer<constunsignedchar>& data,
286+
const EVP_MD* md,
287+
size_t length);
283288

284289
classCipherfinal{
285290
public:

‎doc/api/crypto.md‎

Lines changed: 7 additions & 3 deletions
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[, options])`
41914191

41924192
<!-- YAML
41934193
added:
@@ -4203,8 +4203,12 @@ added:
42034203
input encoding is desired for a string input, user could encode the string
42044204
into a `TypedArray` using either `TextEncoder` or `Buffer.from()` and passing
42054205
the encoded `TypedArray` into this API instead.
4206-
*`outputEncoding`{string|undefined} [Encoding][encoding] used to encode the
4207-
returned digest. **Default:**`'hex'`.
4206+
*`options`{Object|string}
4207+
*`outputEncoding`{string|undefined} [Encoding][encoding] used to encode the
4208+
returned digest. **Default:**`'hex'`.
4209+
*`outputLength`{number|undefined} For XOF hash functions such as 'shake256',
4210+
the outputLength option can be used to specify the desired output length in bytes.
4211+
**Default:**`undefined`
42084212
* Returns:{string|Buffer}
42094213

42104214
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
throwlazyDOMException('Unrecognized algorithm name','NotSupportedError');
194194
}
195195

196-
functionhash(algorithm,input,outputEncoding='hex'){
196+
functionhash(algorithm,input,options={}){
197197
validateString(algorithm,'algorithm');
198198
if(typeofinput!=='string'&&!isArrayBufferView(input)){
199199
thrownewERR_INVALID_ARG_TYPE('input',['Buffer','TypedArray','DataView','string'],input);
200200
}
201+
letoutputEncoding='hex';
202+
letoutputLength;
203+
204+
if(options!==null&&typeofoptions!=='function'){
205+
if(typeofoptions==='string'){
206+
outputEncoding=options;
207+
}elseif(typeofoptions==='object'){
208+
({ outputEncoding ='hex', outputLength }=options);
209+
}else{
210+
thrownewERR_INVALID_ARG_TYPE('options',['object','string'],options);
211+
}
212+
}
213+
201214
letnormalized=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+
returnnormalized==='buffer' ? Buffer.alloc(0) : '';
233+
}
234+
}
216235
returnoneShotDigest(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: 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
voidHash::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<constunsignedchar> buf;
234237
if (args[3]->IsString()){
235-
Utf8Value utf8(isolate, args[3]);
236-
ncrypto::Buffer<constunsignedchar> buf{
238+
buf ={
237239
.data = reinterpret_cast<constunsignedchar*>(utf8.out()),
238240
.len = utf8.length(),
239241
};
240-
returnncrypto::hashDigest(buf, md);
242+
} else{
243+
ArrayBufferViewContents<unsignedchar> input(args[3]);
244+
buf ={
245+
.data = reinterpret_cast<constunsignedchar*>(input.data()),
246+
.len = input.length(),
247+
};
241248
}
242249

243-
ArrayBufferViewContents<unsignedchar> input(args[3]);
244-
ncrypto::Buffer<constunsignedchar> buf{
245-
.data = reinterpret_cast<constunsignedchar*>(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+
returnncrypto::xofHashDigest(buf, md, output_length);
255+
} elseif (!isXOF && output_length != EVP_MD_get_size(md)){
256+
ThrowCryptoError(env, ERR_get_error(), "Invalid length or not XOF");
257+
return{};
258+
}
259+
}
248260
returnncrypto::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+
constcommon=require('../common');
4+
5+
if(!common.hasCrypto)common.skip('missing crypto');
6+
7+
constassert=require('assert');
8+
constcrypto=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+
constactual=crypto.hash('shake256','The message is shorter than the hash!',{outputLength: 1024*1024});
83+
constexpected=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)