Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

crypto: add outputLength option to crypto.createHash #28805

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion doc/api/crypto.md
Original file line number Diff line number Diff line change
Expand Up @@ -1785,14 +1785,19 @@ and description of each available elliptic curve.
### crypto.createHash(algorithm[, options])
<!-- YAML
added: v0.1.92
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/28805
description: The `outputLength` option was added for XOF hash functions.
-->
* `algorithm` {string}
* `options` {Object} [`stream.transform` options][]
* Returns: {Hash}

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

The `algorithm` is dependent on the available algorithms supported by the
version of OpenSSL on the platform. Examples are `'sha256'`, `'sha512'`, etc.
Expand Down
7 changes: 5 additions & 2 deletions lib/internal/crypto/hash.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const {
ERR_CRYPTO_HASH_UPDATE_FAILED,
ERR_INVALID_ARG_TYPE
} = require('internal/errors').codes;
const { validateString } = require('internal/validators');
const { validateString, validateUint32 } = require('internal/validators');
const { normalizeEncoding } = require('internal/util');
const { isArrayBufferView } = require('internal/util/types');
const LazyTransform = require('internal/streams/lazy_transform');
Expand All @@ -36,7 +36,10 @@ function Hash(algorithm, options) {
if (!(this instanceof Hash))
return new Hash(algorithm, options);
validateString(algorithm, 'algorithm');
this[kHandle] = new _Hash(algorithm);
const xofLen = typeof options === 'object' ? options.outputLength : undefined;
if (xofLen !== undefined)
validateUint32(xofLen, 'options.outputLength');
this[kHandle] = new _Hash(algorithm, xofLen);
this[kState] = {
[kFinalized]: false
};
Expand Down
53 changes: 49 additions & 4 deletions src/node_crypto.cc
Original file line number Diff line number Diff line change
Expand Up @@ -4569,15 +4569,21 @@ void Hash::New(const FunctionCallbackInfo<Value>& args) {

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

Maybe<unsigned int> xof_md_len = Nothing<unsigned int>();
if (!args[1]->IsUndefined()) {
CHECK(args[1]->IsUint32());
xof_md_len = Just<unsigned int>(args[1].As<Uint32>()->Value());
}

Hash* hash = new Hash(env, args.This());
if (!hash->HashInit(*hash_type)) {
if (!hash->HashInit(*hash_type, xof_md_len)) {
return ThrowCryptoError(env, ERR_get_error(),
"Digest method not supported");
}
}


bool Hash::HashInit(const char* hash_type) {
bool Hash::HashInit(const char* hash_type, Maybe<unsigned int> xof_md_len) {
const EVP_MD* md = EVP_get_digestbyname(hash_type);
if (md == nullptr)
return false;
Expand All @@ -4586,6 +4592,18 @@ bool Hash::HashInit(const char* hash_type) {
mdctx_.reset();
return false;
}

md_len_ = EVP_MD_size(md);
if (xof_md_len.IsJust() && xof_md_len.FromJust() != md_len_) {
// This is a little hack to cause createHash to fail when an incorrect
// hashSize option was passed for a non-XOF hash function.
if ((EVP_MD_meth_get_flags(md) & EVP_MD_FLAG_XOF) == 0) {
EVPerr(EVP_F_EVP_DIGESTFINALXOF, EVP_R_NOT_XOF_OR_INVALID_LENGTH);
return false;
}
md_len_ = xof_md_len.FromJust();
}

return true;
}

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

if (hash->md_len_ == 0) {
// TODO(tniessen): SHA3_squeeze does not work for zero-length outputs on all
// platforms and will cause a segmentation fault if called. This workaround
// causes hash.digest() to correctly return an empty buffer / string.
// See https://github.com/openssl/openssl/issues/9431.
if (!hash->has_md_ && hash->md_len_ == 0) {
hash->has_md_ = true;
}

if (!hash->has_md_) {
// Some hash algorithms such as SHA3 do not support calling
// EVP_DigestFinal_ex more than once, however, Hash._flush
// and Hash.digest can both be used to retrieve the digest,
// so we need to cache it.
// See https://github.com/nodejs/node/issues/28245.
EVP_DigestFinal_ex(hash->mdctx_.get(), hash->md_value_, &hash->md_len_);

hash->md_value_ = MallocOpenSSL<unsigned char>(hash->md_len_);

size_t default_len = EVP_MD_CTX_size(hash->mdctx_.get());
int ret;
if (hash->md_len_ == default_len) {
ret = EVP_DigestFinal_ex(hash->mdctx_.get(), hash->md_value_,
&hash->md_len_);
} else {
ret = EVP_DigestFinalXOF(hash->mdctx_.get(), hash->md_value_,
hash->md_len_);
}

if (ret != 1) {
OPENSSL_free(hash->md_value_);
hash->md_value_ = nullptr;
return ThrowCryptoError(env, ERR_get_error());
}

hash->has_md_ = true;
}

Local<Value> error;
Expand Down
11 changes: 7 additions & 4 deletions src/node_crypto.h
Original file line number Diff line number Diff line change
Expand Up @@ -585,7 +585,7 @@ class Hash : public BaseObject {
SET_MEMORY_INFO_NAME(Hash)
SET_SELF_SIZE(Hash)

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

protected:
Expand All @@ -596,18 +596,21 @@ class Hash : public BaseObject {
Hash(Environment* env, v8::Local<v8::Object> wrap)
: BaseObject(env, wrap),
mdctx_(nullptr),
md_len_(0) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you intend to remove this line? Now md_len_ will have an undefined value, probably non-zero.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It shouldn't matter, it will be set in the JS constructor before any other calls are made.

has_md_(false),
md_value_(nullptr) {
MakeWeak();
}

~Hash() override {
OPENSSL_cleanse(md_value_, md_len_);
if (md_value_ != nullptr)
OPENSSL_clear_free(md_value_, md_len_);
}

private:
EVPMDPointer mdctx_;
unsigned char md_value_[EVP_MAX_MD_SIZE];
bool has_md_;
unsigned int md_len_;
unsigned char* md_value_;
};

class SignBase : public BaseObject {
Expand Down
66 changes: 66 additions & 0 deletions test/parallel/test-crypto-hash.js
Original file line number Diff line number Diff line change
Expand Up @@ -185,3 +185,69 @@ common.expectsError(
assert(instance instanceof Hash, 'Hash is expected to return a new instance' +
' when called without `new`');
}

// Test XOF hash functions and the outputLength option.
{
// Default outputLengths.
assert.strictEqual(crypto.createHash('shake128').digest('hex'),
'7f9c2ba4e88f827d616045507605853e');
assert.strictEqual(crypto.createHash('shake256').digest('hex'),
'46b9dd2b0ba88d13233b3feb743eeb24' +
'3fcd52ea62b81b82b50c27646ed5762f');

// Short outputLengths.
assert.strictEqual(crypto.createHash('shake128', { outputLength: 0 })
.digest('hex'),
'');
assert.strictEqual(crypto.createHash('shake128', { outputLength: 5 })
.digest('hex'),
'7f9c2ba4e8');
assert.strictEqual(crypto.createHash('shake128', { outputLength: 15 })
.digest('hex'),
'7f9c2ba4e88f827d61604550760585');
assert.strictEqual(crypto.createHash('shake256', { outputLength: 16 })
.digest('hex'),
'46b9dd2b0ba88d13233b3feb743eeb24');

// Large outputLengths.
assert.strictEqual(crypto.createHash('shake128', { outputLength: 128 })
.digest('hex'),
'7f9c2ba4e88f827d616045507605853e' +
'd73b8093f6efbc88eb1a6eacfa66ef26' +
'3cb1eea988004b93103cfb0aeefd2a68' +
'6e01fa4a58e8a3639ca8a1e3f9ae57e2' +
'35b8cc873c23dc62b8d260169afa2f75' +
'ab916a58d974918835d25e6a435085b2' +
'badfd6dfaac359a5efbb7bcc4b59d538' +
'df9a04302e10c8bc1cbf1a0b3a5120ea');
const superLongHash = crypto.createHash('shake256', {
outputLength: 1024 * 1024
}).update('The message is shorter than the hash!')
.digest('hex');
assert.strictEqual(superLongHash.length, 2 * 1024 * 1024);
assert.ok(superLongHash.endsWith('193414035ddba77bf7bba97981e656ec'));
assert.ok(superLongHash.startsWith('a2a28dbc49cfd6e5d6ceea3d03e77748'));

// Non-XOF hash functions should accept valid outputLength options as well.
assert.strictEqual(crypto.createHash('sha224', { outputLength: 28 })
.digest('hex'),
'd14a028c2a3a2bc9476102bb288234c4' +
'15a2b01f828ea62ac5b3e42f');

// Passing invalid sizes should throw during creation.
common.expectsError(() => {
crypto.createHash('sha256', { outputLength: 28 });
}, {
code: 'ERR_OSSL_EVP_NOT_XOF_OR_INVALID_LENGTH'
});

for (const outputLength of [null, {}, 'foo', false]) {
common.expectsError(() => crypto.createHash('sha256', { outputLength }),
{ code: 'ERR_INVALID_ARG_TYPE' });
}

for (const outputLength of [-1, .5, Infinity, 2 ** 90]) {
common.expectsError(() => crypto.createHash('sha256', { outputLength }),
{ code: 'ERR_OUT_OF_RANGE' });
}
}