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: https://github.com/nodejs/node/issues/28757 PR-URL: https://github.com/nodejs/node/pull/28805 Reviewed-By: Anna Henningsen <anna@addaleax.net> Reviewed-By: Sam Roberts <vieuxtech@gmail.com> Reviewed-By: Rich Trott <rtrott@gmail.com>
This commit is contained in:
parent
64e4b0c0ac
commit
31d9b2f14f
@ -1785,6 +1785,10 @@ and description of each available elliptic curve.
|
|||||||
### crypto.createHash(algorithm[, options])
|
### crypto.createHash(algorithm[, options])
|
||||||
<!-- YAML
|
<!-- YAML
|
||||||
added: v0.1.92
|
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}
|
* `algorithm` {string}
|
||||||
* `options` {Object} [`stream.transform` options][]
|
* `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
|
Creates and returns a `Hash` object that can be used to generate hash digests
|
||||||
using the given `algorithm`. Optional `options` argument controls stream
|
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
|
The `algorithm` is dependent on the available algorithms supported by the
|
||||||
version of OpenSSL on the platform. Examples are `'sha256'`, `'sha512'`, etc.
|
version of OpenSSL on the platform. Examples are `'sha256'`, `'sha512'`, etc.
|
||||||
|
@ -25,7 +25,7 @@ const {
|
|||||||
ERR_CRYPTO_HASH_UPDATE_FAILED,
|
ERR_CRYPTO_HASH_UPDATE_FAILED,
|
||||||
ERR_INVALID_ARG_TYPE
|
ERR_INVALID_ARG_TYPE
|
||||||
} = require('internal/errors').codes;
|
} = require('internal/errors').codes;
|
||||||
const { validateString } = require('internal/validators');
|
const { validateString, validateUint32 } = require('internal/validators');
|
||||||
const { normalizeEncoding } = require('internal/util');
|
const { normalizeEncoding } = require('internal/util');
|
||||||
const { isArrayBufferView } = require('internal/util/types');
|
const { isArrayBufferView } = require('internal/util/types');
|
||||||
const LazyTransform = require('internal/streams/lazy_transform');
|
const LazyTransform = require('internal/streams/lazy_transform');
|
||||||
@ -36,7 +36,10 @@ function Hash(algorithm, options) {
|
|||||||
if (!(this instanceof Hash))
|
if (!(this instanceof Hash))
|
||||||
return new Hash(algorithm, options);
|
return new Hash(algorithm, options);
|
||||||
validateString(algorithm, 'algorithm');
|
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] = {
|
this[kState] = {
|
||||||
[kFinalized]: false
|
[kFinalized]: false
|
||||||
};
|
};
|
||||||
|
@ -4569,15 +4569,21 @@ void Hash::New(const FunctionCallbackInfo<Value>& args) {
|
|||||||
|
|
||||||
const node::Utf8Value hash_type(env->isolate(), args[0]);
|
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());
|
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(),
|
return ThrowCryptoError(env, ERR_get_error(),
|
||||||
"Digest method not supported");
|
"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);
|
const EVP_MD* md = EVP_get_digestbyname(hash_type);
|
||||||
if (md == nullptr)
|
if (md == nullptr)
|
||||||
return false;
|
return false;
|
||||||
@ -4586,6 +4592,18 @@ bool Hash::HashInit(const char* hash_type) {
|
|||||||
mdctx_.reset();
|
mdctx_.reset();
|
||||||
return false;
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -4634,13 +4652,40 @@ void Hash::HashDigest(const FunctionCallbackInfo<Value>& args) {
|
|||||||
encoding = ParseEncoding(env->isolate(), args[0], BUFFER);
|
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
|
// Some hash algorithms such as SHA3 do not support calling
|
||||||
// EVP_DigestFinal_ex more than once, however, Hash._flush
|
// EVP_DigestFinal_ex more than once, however, Hash._flush
|
||||||
// and Hash.digest can both be used to retrieve the digest,
|
// and Hash.digest can both be used to retrieve the digest,
|
||||||
// so we need to cache it.
|
// so we need to cache it.
|
||||||
// See https://github.com/nodejs/node/issues/28245.
|
// 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;
|
Local<Value> error;
|
||||||
|
@ -585,7 +585,7 @@ class Hash : public BaseObject {
|
|||||||
SET_MEMORY_INFO_NAME(Hash)
|
SET_MEMORY_INFO_NAME(Hash)
|
||||||
SET_SELF_SIZE(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);
|
bool HashUpdate(const char* data, int len);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
@ -596,18 +596,21 @@ class Hash : public BaseObject {
|
|||||||
Hash(Environment* env, v8::Local<v8::Object> wrap)
|
Hash(Environment* env, v8::Local<v8::Object> wrap)
|
||||||
: BaseObject(env, wrap),
|
: BaseObject(env, wrap),
|
||||||
mdctx_(nullptr),
|
mdctx_(nullptr),
|
||||||
md_len_(0) {
|
has_md_(false),
|
||||||
|
md_value_(nullptr) {
|
||||||
MakeWeak();
|
MakeWeak();
|
||||||
}
|
}
|
||||||
|
|
||||||
~Hash() override {
|
~Hash() override {
|
||||||
OPENSSL_cleanse(md_value_, md_len_);
|
if (md_value_ != nullptr)
|
||||||
|
OPENSSL_clear_free(md_value_, md_len_);
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
EVPMDPointer mdctx_;
|
EVPMDPointer mdctx_;
|
||||||
unsigned char md_value_[EVP_MAX_MD_SIZE];
|
bool has_md_;
|
||||||
unsigned int md_len_;
|
unsigned int md_len_;
|
||||||
|
unsigned char* md_value_;
|
||||||
};
|
};
|
||||||
|
|
||||||
class SignBase : public BaseObject {
|
class SignBase : public BaseObject {
|
||||||
|
@ -185,3 +185,69 @@ common.expectsError(
|
|||||||
assert(instance instanceof Hash, 'Hash is expected to return a new instance' +
|
assert(instance instanceof Hash, 'Hash is expected to return a new instance' +
|
||||||
' when called without `new`');
|
' 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' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user