Skip to content

Commit 31d9b2f

Browse files
tniessenTrott
authored andcommitted
crypto: add outputLength option to crypto.createHash
This change adds an outputLength option to crypto.createHash which allows users to produce variable-length hash values using XOF hash functons. Fixes: #28757 PR-URL: #28805 Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: Sam Roberts <[email protected]> Reviewed-By: Rich Trott <[email protected]>
1 parent 64e4b0c commit 31d9b2f

File tree

5 files changed

+133
-11
lines changed

5 files changed

+133
-11
lines changed

Diff for: doc/api/crypto.md

+6-1
Original file line numberDiff line numberDiff line change
@@ -1785,14 +1785,19 @@ and description of each available elliptic curve.
17851785
### crypto.createHash(algorithm[, options])
17861786
<!-- YAML
17871787
added: v0.1.92
1788+
changes:
1789+
- version: REPLACEME
1790+
pr-url: https://github.com/nodejs/node/pull/28805
1791+
description: The `outputLength` option was added for XOF hash functions.
17881792
-->
17891793
* `algorithm` {string}
17901794
* `options` {Object} [`stream.transform` options][]
17911795
* Returns: {Hash}
17921796

17931797
Creates and returns a `Hash` object that can be used to generate hash digests
17941798
using the given `algorithm`. Optional `options` argument controls stream
1795-
behavior.
1799+
behavior. For XOF hash functions such as `'shake256'`, the `outputLength` option
1800+
can be used to specify the desired output length in bytes.
17961801

17971802
The `algorithm` is dependent on the available algorithms supported by the
17981803
version of OpenSSL on the platform. Examples are `'sha256'`, `'sha512'`, etc.

Diff for: lib/internal/crypto/hash.js

+5-2
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ const {
2525
ERR_CRYPTO_HASH_UPDATE_FAILED,
2626
ERR_INVALID_ARG_TYPE
2727
} = require('internal/errors').codes;
28-
const { validateString } = require('internal/validators');
28+
const { validateString, validateUint32 } = require('internal/validators');
2929
const { normalizeEncoding } = require('internal/util');
3030
const { isArrayBufferView } = require('internal/util/types');
3131
const LazyTransform = require('internal/streams/lazy_transform');
@@ -36,7 +36,10 @@ function Hash(algorithm, options) {
3636
if (!(this instanceof Hash))
3737
return new Hash(algorithm, options);
3838
validateString(algorithm, 'algorithm');
39-
this[kHandle] = new _Hash(algorithm);
39+
const xofLen = typeof options === 'object' ? options.outputLength : undefined;
40+
if (xofLen !== undefined)
41+
validateUint32(xofLen, 'options.outputLength');
42+
this[kHandle] = new _Hash(algorithm, xofLen);
4043
this[kState] = {
4144
[kFinalized]: false
4245
};

Diff for: src/node_crypto.cc

+49-4
Original file line numberDiff line numberDiff line change
@@ -4569,15 +4569,21 @@ void Hash::New(const FunctionCallbackInfo<Value>& args) {
45694569

45704570
const node::Utf8Value hash_type(env->isolate(), args[0]);
45714571

4572+
Maybe<unsigned int> xof_md_len = Nothing<unsigned int>();
4573+
if (!args[1]->IsUndefined()) {
4574+
CHECK(args[1]->IsUint32());
4575+
xof_md_len = Just<unsigned int>(args[1].As<Uint32>()->Value());
4576+
}
4577+
45724578
Hash* hash = new Hash(env, args.This());
4573-
if (!hash->HashInit(*hash_type)) {
4579+
if (!hash->HashInit(*hash_type, xof_md_len)) {
45744580
return ThrowCryptoError(env, ERR_get_error(),
45754581
"Digest method not supported");
45764582
}
45774583
}
45784584

45794585

4580-
bool Hash::HashInit(const char* hash_type) {
4586+
bool Hash::HashInit(const char* hash_type, Maybe<unsigned int> xof_md_len) {
45814587
const EVP_MD* md = EVP_get_digestbyname(hash_type);
45824588
if (md == nullptr)
45834589
return false;
@@ -4586,6 +4592,18 @@ bool Hash::HashInit(const char* hash_type) {
45864592
mdctx_.reset();
45874593
return false;
45884594
}
4595+
4596+
md_len_ = EVP_MD_size(md);
4597+
if (xof_md_len.IsJust() && xof_md_len.FromJust() != md_len_) {
4598+
// This is a little hack to cause createHash to fail when an incorrect
4599+
// hashSize option was passed for a non-XOF hash function.
4600+
if ((EVP_MD_meth_get_flags(md) & EVP_MD_FLAG_XOF) == 0) {
4601+
EVPerr(EVP_F_EVP_DIGESTFINALXOF, EVP_R_NOT_XOF_OR_INVALID_LENGTH);
4602+
return false;
4603+
}
4604+
md_len_ = xof_md_len.FromJust();
4605+
}
4606+
45894607
return true;
45904608
}
45914609

@@ -4634,13 +4652,40 @@ void Hash::HashDigest(const FunctionCallbackInfo<Value>& args) {
46344652
encoding = ParseEncoding(env->isolate(), args[0], BUFFER);
46354653
}
46364654

4637-
if (hash->md_len_ == 0) {
4655+
// TODO(tniessen): SHA3_squeeze does not work for zero-length outputs on all
4656+
// platforms and will cause a segmentation fault if called. This workaround
4657+
// causes hash.digest() to correctly return an empty buffer / string.
4658+
// See https://github.com/openssl/openssl/issues/9431.
4659+
if (!hash->has_md_ && hash->md_len_ == 0) {
4660+
hash->has_md_ = true;
4661+
}
4662+
4663+
if (!hash->has_md_) {
46384664
// Some hash algorithms such as SHA3 do not support calling
46394665
// EVP_DigestFinal_ex more than once, however, Hash._flush
46404666
// and Hash.digest can both be used to retrieve the digest,
46414667
// so we need to cache it.
46424668
// See https://github.com/nodejs/node/issues/28245.
4643-
EVP_DigestFinal_ex(hash->mdctx_.get(), hash->md_value_, &hash->md_len_);
4669+
4670+
hash->md_value_ = MallocOpenSSL<unsigned char>(hash->md_len_);
4671+
4672+
size_t default_len = EVP_MD_CTX_size(hash->mdctx_.get());
4673+
int ret;
4674+
if (hash->md_len_ == default_len) {
4675+
ret = EVP_DigestFinal_ex(hash->mdctx_.get(), hash->md_value_,
4676+
&hash->md_len_);
4677+
} else {
4678+
ret = EVP_DigestFinalXOF(hash->mdctx_.get(), hash->md_value_,
4679+
hash->md_len_);
4680+
}
4681+
4682+
if (ret != 1) {
4683+
OPENSSL_free(hash->md_value_);
4684+
hash->md_value_ = nullptr;
4685+
return ThrowCryptoError(env, ERR_get_error());
4686+
}
4687+
4688+
hash->has_md_ = true;
46444689
}
46454690

46464691
Local<Value> error;

Diff for: src/node_crypto.h

+7-4
Original file line numberDiff line numberDiff line change
@@ -585,7 +585,7 @@ class Hash : public BaseObject {
585585
SET_MEMORY_INFO_NAME(Hash)
586586
SET_SELF_SIZE(Hash)
587587

588-
bool HashInit(const char* hash_type);
588+
bool HashInit(const char* hash_type, v8::Maybe<unsigned int> xof_md_len);
589589
bool HashUpdate(const char* data, int len);
590590

591591
protected:
@@ -596,18 +596,21 @@ class Hash : public BaseObject {
596596
Hash(Environment* env, v8::Local<v8::Object> wrap)
597597
: BaseObject(env, wrap),
598598
mdctx_(nullptr),
599-
md_len_(0) {
599+
has_md_(false),
600+
md_value_(nullptr) {
600601
MakeWeak();
601602
}
602603

603604
~Hash() override {
604-
OPENSSL_cleanse(md_value_, md_len_);
605+
if (md_value_ != nullptr)
606+
OPENSSL_clear_free(md_value_, md_len_);
605607
}
606608

607609
private:
608610
EVPMDPointer mdctx_;
609-
unsigned char md_value_[EVP_MAX_MD_SIZE];
611+
bool has_md_;
610612
unsigned int md_len_;
613+
unsigned char* md_value_;
611614
};
612615

613616
class SignBase : public BaseObject {

Diff for: test/parallel/test-crypto-hash.js

+66
Original file line numberDiff line numberDiff line change
@@ -185,3 +185,69 @@ common.expectsError(
185185
assert(instance instanceof Hash, 'Hash is expected to return a new instance' +
186186
' when called without `new`');
187187
}
188+
189+
// Test XOF hash functions and the outputLength option.
190+
{
191+
// Default outputLengths.
192+
assert.strictEqual(crypto.createHash('shake128').digest('hex'),
193+
'7f9c2ba4e88f827d616045507605853e');
194+
assert.strictEqual(crypto.createHash('shake256').digest('hex'),
195+
'46b9dd2b0ba88d13233b3feb743eeb24' +
196+
'3fcd52ea62b81b82b50c27646ed5762f');
197+
198+
// Short outputLengths.
199+
assert.strictEqual(crypto.createHash('shake128', { outputLength: 0 })
200+
.digest('hex'),
201+
'');
202+
assert.strictEqual(crypto.createHash('shake128', { outputLength: 5 })
203+
.digest('hex'),
204+
'7f9c2ba4e8');
205+
assert.strictEqual(crypto.createHash('shake128', { outputLength: 15 })
206+
.digest('hex'),
207+
'7f9c2ba4e88f827d61604550760585');
208+
assert.strictEqual(crypto.createHash('shake256', { outputLength: 16 })
209+
.digest('hex'),
210+
'46b9dd2b0ba88d13233b3feb743eeb24');
211+
212+
// Large outputLengths.
213+
assert.strictEqual(crypto.createHash('shake128', { outputLength: 128 })
214+
.digest('hex'),
215+
'7f9c2ba4e88f827d616045507605853e' +
216+
'd73b8093f6efbc88eb1a6eacfa66ef26' +
217+
'3cb1eea988004b93103cfb0aeefd2a68' +
218+
'6e01fa4a58e8a3639ca8a1e3f9ae57e2' +
219+
'35b8cc873c23dc62b8d260169afa2f75' +
220+
'ab916a58d974918835d25e6a435085b2' +
221+
'badfd6dfaac359a5efbb7bcc4b59d538' +
222+
'df9a04302e10c8bc1cbf1a0b3a5120ea');
223+
const superLongHash = crypto.createHash('shake256', {
224+
outputLength: 1024 * 1024
225+
}).update('The message is shorter than the hash!')
226+
.digest('hex');
227+
assert.strictEqual(superLongHash.length, 2 * 1024 * 1024);
228+
assert.ok(superLongHash.endsWith('193414035ddba77bf7bba97981e656ec'));
229+
assert.ok(superLongHash.startsWith('a2a28dbc49cfd6e5d6ceea3d03e77748'));
230+
231+
// Non-XOF hash functions should accept valid outputLength options as well.
232+
assert.strictEqual(crypto.createHash('sha224', { outputLength: 28 })
233+
.digest('hex'),
234+
'd14a028c2a3a2bc9476102bb288234c4' +
235+
'15a2b01f828ea62ac5b3e42f');
236+
237+
// Passing invalid sizes should throw during creation.
238+
common.expectsError(() => {
239+
crypto.createHash('sha256', { outputLength: 28 });
240+
}, {
241+
code: 'ERR_OSSL_EVP_NOT_XOF_OR_INVALID_LENGTH'
242+
});
243+
244+
for (const outputLength of [null, {}, 'foo', false]) {
245+
common.expectsError(() => crypto.createHash('sha256', { outputLength }),
246+
{ code: 'ERR_INVALID_ARG_TYPE' });
247+
}
248+
249+
for (const outputLength of [-1, .5, Infinity, 2 ** 90]) {
250+
common.expectsError(() => crypto.createHash('sha256', { outputLength }),
251+
{ code: 'ERR_OUT_OF_RANGE' });
252+
}
253+
}

0 commit comments

Comments
 (0)