Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit b7c6ad5

Browse filesBrowse files
tniessentargos
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 <anna@addaleax.net> Reviewed-By: Sam Roberts <vieuxtech@gmail.com> Reviewed-By: Rich Trott <rtrott@gmail.com>
1 parent d3426ee commit b7c6ad5
Copy full SHA for b7c6ad5

File tree

Expand file treeCollapse file tree

5 files changed

+133
-11
lines changed
Open diff view settings
Filter options
Expand file treeCollapse file tree

5 files changed

+133
-11
lines changed
Open diff view settings
Collapse file

‎doc/api/crypto.md‎

Copy file name to clipboardExpand all lines: doc/api/crypto.md
+6-1Lines changed: 6 additions & 1 deletion
  • Display the source diff
  • Display the rich diff
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.
Collapse file

‎lib/internal/crypto/hash.js‎

Copy file name to clipboardExpand all lines: lib/internal/crypto/hash.js
+5-2Lines changed: 5 additions & 2 deletions
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
};
Collapse file

‎src/node_crypto.cc‎

Copy file name to clipboardExpand all lines: src/node_crypto.cc
+49-4Lines changed: 49 additions & 4 deletions
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;
Collapse file

‎src/node_crypto.h‎

Copy file name to clipboardExpand all lines: src/node_crypto.h
+7-4Lines changed: 7 additions & 4 deletions
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 {
Collapse file

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

Copy file name to clipboardExpand all lines: test/parallel/test-crypto-hash.js
+66Lines changed: 66 additions & 0 deletions
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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.