diff --git a/doc/api/crypto.md b/doc/api/crypto.md index 96d99cbbe76dec..1f77c536a330f4 100644 --- a/doc/api/crypto.md +++ b/doc/api/crypto.md @@ -1785,6 +1785,10 @@ and description of each available elliptic curve. ### crypto.createHash(algorithm[, options]) * `algorithm` {string} * `options` {Object} [`stream.transform` options][] @@ -1792,7 +1796,8 @@ added: v0.1.92 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. diff --git a/lib/internal/crypto/hash.js b/lib/internal/crypto/hash.js index a58164802124d3..667624dce08cd8 100644 --- a/lib/internal/crypto/hash.js +++ b/lib/internal/crypto/hash.js @@ -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'); @@ -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 }; diff --git a/src/node_crypto.cc b/src/node_crypto.cc index 590c6d1c374c08..bc61082f24f165 100644 --- a/src/node_crypto.cc +++ b/src/node_crypto.cc @@ -4569,15 +4569,21 @@ void Hash::New(const FunctionCallbackInfo& args) { const node::Utf8Value hash_type(env->isolate(), args[0]); + Maybe xof_md_len = Nothing(); + if (!args[1]->IsUndefined()) { + CHECK(args[1]->IsUint32()); + xof_md_len = Just(args[1].As()->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 xof_md_len) { const EVP_MD* md = EVP_get_digestbyname(hash_type); if (md == nullptr) return false; @@ -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; } @@ -4634,13 +4652,40 @@ void Hash::HashDigest(const FunctionCallbackInfo& 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(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 error; diff --git a/src/node_crypto.h b/src/node_crypto.h index 3e337eaddbe490..07ca412e8f7fc6 100644 --- a/src/node_crypto.h +++ b/src/node_crypto.h @@ -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 xof_md_len); bool HashUpdate(const char* data, int len); protected: @@ -596,18 +596,21 @@ class Hash : public BaseObject { Hash(Environment* env, v8::Local wrap) : BaseObject(env, wrap), mdctx_(nullptr), - md_len_(0) { + 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 { diff --git a/test/parallel/test-crypto-hash.js b/test/parallel/test-crypto-hash.js index de15f00bc918aa..7f185146bc8b6e 100644 --- a/test/parallel/test-crypto-hash.js +++ b/test/parallel/test-crypto-hash.js @@ -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' }); + } +}