mirror of
https://github.com/nodejs/node.git
synced 2025-08-15 13:48:44 +02:00
crypto: support outputLength option in crypto.hash for XOF functions
Support `outputLength` option in crypto.hash() for XOF hash functions to align with the behaviour of crypto.createHash() API closes: https://github.com/nodejs/node/issues/57312 Co-authored-by: Filip Skokan <panva.ip@gmail.com> PR-URL: https://github.com/nodejs/node/pull/58121 Fixes: https://github.com/nodejs/node/issues/57312 Reviewed-By: Joyee Cheung <joyeec9h3@gmail.com> Reviewed-By: Filip Skokan <panva.ip@gmail.com> Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com>
This commit is contained in:
parent
c7eff619c2
commit
1c4fe6d795
8 changed files with 258 additions and 23 deletions
16
deps/ncrypto/ncrypto.cc
vendored
16
deps/ncrypto/ncrypto.cc
vendored
|
@ -4191,6 +4191,22 @@ DataPointer hashDigest(const Buffer<const unsigned char>& buf,
|
|||
return data.resize(result_size);
|
||||
}
|
||||
|
||||
DataPointer xofHashDigest(const Buffer<const unsigned char>& buf,
|
||||
const EVP_MD* md,
|
||||
size_t output_length) {
|
||||
if (md == nullptr) return {};
|
||||
|
||||
EVPMDCtxPointer ctx = EVPMDCtxPointer::New();
|
||||
if (!ctx) return {};
|
||||
if (ctx.digestInit(md) != 1) {
|
||||
return {};
|
||||
}
|
||||
if (ctx.digestUpdate(reinterpret_cast<const Buffer<const void>&>(buf)) != 1) {
|
||||
return {};
|
||||
}
|
||||
return ctx.digestFinal(output_length);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
X509Name::X509Name() : name_(nullptr), total_(0) {}
|
||||
|
|
5
deps/ncrypto/ncrypto.h
vendored
5
deps/ncrypto/ncrypto.h
vendored
|
@ -278,8 +278,13 @@ class Digest final {
|
|||
const EVP_MD* md_ = nullptr;
|
||||
};
|
||||
|
||||
// Computes a fixed-length digest.
|
||||
DataPointer hashDigest(const Buffer<const unsigned char>& data,
|
||||
const EVP_MD* md);
|
||||
// Computes a variable-length digest for XOF algorithms (e.g. SHAKE128).
|
||||
DataPointer xofHashDigest(const Buffer<const unsigned char>& data,
|
||||
const EVP_MD* md,
|
||||
size_t length);
|
||||
|
||||
class Cipher final {
|
||||
public:
|
||||
|
|
|
@ -4203,12 +4203,16 @@ A convenient alias for [`crypto.webcrypto.getRandomValues()`][]. This
|
|||
implementation is not compliant with the Web Crypto spec, to write
|
||||
web-compatible code use [`crypto.webcrypto.getRandomValues()`][] instead.
|
||||
|
||||
### `crypto.hash(algorithm, data[, outputEncoding])`
|
||||
### `crypto.hash(algorithm, data[, options])`
|
||||
|
||||
<!-- YAML
|
||||
added:
|
||||
- v21.7.0
|
||||
- v20.12.0
|
||||
changes:
|
||||
- version: REPLACEME
|
||||
pr-url: https://github.com/nodejs/node/pull/58121
|
||||
description: The `outputLength` option was added for XOF hash functions.
|
||||
-->
|
||||
|
||||
> Stability: 1.2 - Release candidate
|
||||
|
@ -4219,8 +4223,11 @@ added:
|
|||
input encoding is desired for a string input, user could encode the string
|
||||
into a `TypedArray` using either `TextEncoder` or `Buffer.from()` and passing
|
||||
the encoded `TypedArray` into this API instead.
|
||||
* `outputEncoding` {string|undefined} [Encoding][encoding] used to encode the
|
||||
* `options` {Object|string}
|
||||
* `outputEncoding` {string} [Encoding][encoding] used to encode the
|
||||
returned digest. **Default:** `'hex'`.
|
||||
* `outputLength` {number} For XOF hash functions such as 'shake256',
|
||||
the outputLength option can be used to specify the desired output length in bytes.
|
||||
* Returns: {string|Buffer}
|
||||
|
||||
A utility for creating one-shot hash digests of data. It can be faster than
|
||||
|
@ -4233,6 +4240,8 @@ version of OpenSSL on the platform. Examples are `'sha256'`, `'sha512'`, etc.
|
|||
On recent releases of OpenSSL, `openssl list -digest-algorithms` will
|
||||
display the available digest algorithms.
|
||||
|
||||
If `options` is a string, then it specifies the `outputEncoding`.
|
||||
|
||||
Example:
|
||||
|
||||
```cjs
|
||||
|
|
|
@ -54,6 +54,7 @@ const {
|
|||
const {
|
||||
validateEncoding,
|
||||
validateString,
|
||||
validateObject,
|
||||
validateUint32,
|
||||
} = require('internal/validators');
|
||||
|
||||
|
@ -218,14 +219,27 @@ async function asyncDigest(algorithm, data) {
|
|||
throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError');
|
||||
}
|
||||
|
||||
function hash(algorithm, input, outputEncoding = 'hex') {
|
||||
function hash(algorithm, input, options) {
|
||||
validateString(algorithm, 'algorithm');
|
||||
if (typeof input !== 'string' && !isArrayBufferView(input)) {
|
||||
throw new ERR_INVALID_ARG_TYPE('input', ['Buffer', 'TypedArray', 'DataView', 'string'], input);
|
||||
}
|
||||
let outputEncoding;
|
||||
let outputLength;
|
||||
|
||||
if (typeof options === 'string') {
|
||||
outputEncoding = options;
|
||||
} else if (options !== undefined) {
|
||||
validateObject(options, 'options');
|
||||
outputLength = options.outputLength;
|
||||
outputEncoding = options.outputEncoding;
|
||||
}
|
||||
|
||||
outputEncoding ??= 'hex';
|
||||
|
||||
let normalized = outputEncoding;
|
||||
// Fast case: if it's 'hex', we don't need to validate it further.
|
||||
if (outputEncoding !== 'hex') {
|
||||
if (normalized !== 'hex') {
|
||||
validateString(outputEncoding, 'outputEncoding');
|
||||
normalized = normalizeEncoding(outputEncoding);
|
||||
// If the encoding is invalid, normalizeEncoding() returns undefined.
|
||||
|
@ -238,14 +252,17 @@ function hash(algorithm, input, outputEncoding = 'hex') {
|
|||
}
|
||||
}
|
||||
}
|
||||
// TODO: ideally we have to ship https://github.com/nodejs/node/pull/58121 so
|
||||
// that a proper DEP0198 deprecation can be done here as well.
|
||||
const normalizedAlgorithm = normalizeAlgorithm(algorithm);
|
||||
if (normalizedAlgorithm === 'shake128' || normalizedAlgorithm === 'shake256') {
|
||||
return new Hash(algorithm).update(input).digest(normalized);
|
||||
|
||||
if (outputLength !== undefined) {
|
||||
validateUint32(outputLength, 'outputLength');
|
||||
}
|
||||
|
||||
if (outputLength === undefined) {
|
||||
maybeEmitDeprecationWarning(algorithm);
|
||||
}
|
||||
|
||||
return oneShotDigest(algorithm, getCachedHashId(algorithm), getHashCache(),
|
||||
input, normalized, encodingsMap[normalized]);
|
||||
input, normalized, encodingsMap[normalized], outputLength);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
|
|
@ -208,17 +208,18 @@ const EVP_MD* GetDigestImplementation(Environment* env,
|
|||
}
|
||||
|
||||
// crypto.digest(algorithm, algorithmId, algorithmCache,
|
||||
// input, outputEncoding, outputEncodingId)
|
||||
// input, outputEncoding, outputEncodingId, outputLength)
|
||||
void Hash::OneShotDigest(const FunctionCallbackInfo<Value>& args) {
|
||||
Environment* env = Environment::GetCurrent(args);
|
||||
Isolate* isolate = env->isolate();
|
||||
CHECK_EQ(args.Length(), 6);
|
||||
CHECK_EQ(args.Length(), 7);
|
||||
CHECK(args[0]->IsString()); // algorithm
|
||||
CHECK(args[1]->IsInt32()); // algorithmId
|
||||
CHECK(args[2]->IsObject()); // algorithmCache
|
||||
CHECK(args[3]->IsString() || args[3]->IsArrayBufferView()); // input
|
||||
CHECK(args[4]->IsString()); // outputEncoding
|
||||
CHECK(args[5]->IsUint32() || args[5]->IsUndefined()); // outputEncodingId
|
||||
CHECK(args[6]->IsUint32() || args[6]->IsUndefined()); // outputLength
|
||||
|
||||
const EVP_MD* md = GetDigestImplementation(env, args[0], args[1], args[2]);
|
||||
if (md == nullptr) [[unlikely]] {
|
||||
|
@ -230,21 +231,68 @@ void Hash::OneShotDigest(const FunctionCallbackInfo<Value>& args) {
|
|||
|
||||
enum encoding output_enc = ParseEncoding(isolate, args[4], args[5], HEX);
|
||||
|
||||
DataPointer output = ([&] {
|
||||
if (args[3]->IsString()) {
|
||||
bool is_xof = (EVP_MD_flags(md) & EVP_MD_FLAG_XOF) != 0;
|
||||
int output_length = EVP_MD_size(md);
|
||||
|
||||
// This is to cause hash() to fail when an incorrect
|
||||
// outputLength option was passed for a non-XOF hash function.
|
||||
if (!is_xof && !args[6]->IsUndefined()) {
|
||||
output_length = args[6].As<Uint32>()->Value();
|
||||
if (output_length != EVP_MD_size(md)) {
|
||||
Utf8Value method(isolate, args[0]);
|
||||
std::string message =
|
||||
"Output length " + std::to_string(output_length) + " is invalid for ";
|
||||
message += method.ToString() + ", which does not support XOF";
|
||||
return ThrowCryptoError(env, ERR_get_error(), message.c_str());
|
||||
}
|
||||
} else if (is_xof) {
|
||||
if (!args[6]->IsUndefined()) {
|
||||
output_length = args[6].As<Uint32>()->Value();
|
||||
} else if (output_length == 0) {
|
||||
// This is to handle OpenSSL 3.4's breaking change in SHAKE128/256
|
||||
// default lengths
|
||||
const char* name = OBJ_nid2sn(EVP_MD_type(md));
|
||||
if (name != nullptr) {
|
||||
if (strcmp(name, "SHAKE128") == 0) {
|
||||
output_length = 16;
|
||||
} else if (strcmp(name, "SHAKE256") == 0) {
|
||||
output_length = 32;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (output_length == 0) {
|
||||
if (output_enc == BUFFER) {
|
||||
Local<v8::ArrayBuffer> ab = v8::ArrayBuffer::New(isolate, 0);
|
||||
args.GetReturnValue().Set(
|
||||
Buffer::New(isolate, ab, 0, 0).ToLocalChecked());
|
||||
} else {
|
||||
args.GetReturnValue().Set(v8::String::Empty(isolate));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
DataPointer output = ([&]() -> DataPointer {
|
||||
Utf8Value utf8(isolate, args[3]);
|
||||
ncrypto::Buffer<const unsigned char> buf{
|
||||
ncrypto::Buffer<const unsigned char> buf;
|
||||
if (args[3]->IsString()) {
|
||||
buf = {
|
||||
.data = reinterpret_cast<const unsigned char*>(utf8.out()),
|
||||
.len = utf8.length(),
|
||||
};
|
||||
return ncrypto::hashDigest(buf, md);
|
||||
}
|
||||
|
||||
} else {
|
||||
ArrayBufferViewContents<unsigned char> input(args[3]);
|
||||
ncrypto::Buffer<const unsigned char> buf{
|
||||
buf = {
|
||||
.data = reinterpret_cast<const unsigned char*>(input.data()),
|
||||
.len = input.length(),
|
||||
};
|
||||
}
|
||||
|
||||
if (is_xof) {
|
||||
return ncrypto::xofHashDigest(buf, md, output_length);
|
||||
}
|
||||
|
||||
return ncrypto::hashDigest(buf, md);
|
||||
})();
|
||||
|
||||
|
|
18
test/parallel/test-crypto-default-shake-lengths-oneshot.js
Normal file
18
test/parallel/test-crypto-default-shake-lengths-oneshot.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
// Flags: --pending-deprecation
|
||||
'use strict';
|
||||
|
||||
const common = require('../common');
|
||||
if (!common.hasCrypto)
|
||||
common.skip('missing crypto');
|
||||
|
||||
const { hash } = require('crypto');
|
||||
|
||||
common.expectWarning({
|
||||
DeprecationWarning: {
|
||||
DEP0198: 'Creating SHAKE128/256 digests without an explicit options.outputLength is deprecated.',
|
||||
}
|
||||
});
|
||||
|
||||
{
|
||||
hash('shake128', 'test');
|
||||
}
|
122
test/parallel/test-crypto-oneshot-hash-xof.js
Normal file
122
test/parallel/test-crypto-oneshot-hash-xof.js
Normal file
|
@ -0,0 +1,122 @@
|
|||
'use strict';
|
||||
// This tests crypto.hash() works.
|
||||
const common = require('../common');
|
||||
|
||||
if (!common.hasCrypto) common.skip('missing crypto');
|
||||
|
||||
const assert = require('assert');
|
||||
const crypto = require('crypto');
|
||||
|
||||
// Test XOF hash functions and the outputLength option.
|
||||
{
|
||||
// Default outputLengths.
|
||||
assert.strictEqual(
|
||||
crypto.hash('shake128', ''),
|
||||
'7f9c2ba4e88f827d616045507605853e'
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
crypto.hash('shake256', ''),
|
||||
'46b9dd2b0ba88d13233b3feb743eeb243fcd52ea62b81b82b50c27646ed5762f'
|
||||
);
|
||||
|
||||
// outputEncoding as an option.
|
||||
assert.strictEqual(
|
||||
crypto.hash('shake128', '', { outputEncoding: 'base64url' }),
|
||||
'f5wrpOiPgn1hYEVQdgWFPg'
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
crypto.hash('shake256', '', { outputEncoding: 'base64url' }),
|
||||
'RrndKwuojRMjOz_rdD7rJD_NUupiuBuCtQwnZG7Vdi8'
|
||||
);
|
||||
|
||||
assert.deepStrictEqual(
|
||||
crypto.hash('shake128', '', { outputEncoding: 'buffer' }),
|
||||
Buffer.from('f5wrpOiPgn1hYEVQdgWFPg', 'base64url')
|
||||
);
|
||||
|
||||
assert.deepStrictEqual(
|
||||
crypto.hash('shake256', '', { outputEncoding: 'buffer' }),
|
||||
Buffer.from('RrndKwuojRMjOz_rdD7rJD_NUupiuBuCtQwnZG7Vdi8', 'base64url')
|
||||
);
|
||||
|
||||
// Short outputLengths.
|
||||
assert.strictEqual(crypto.hash('shake128', '', { outputLength: 0 }), '');
|
||||
assert.deepStrictEqual(crypto.hash('shake128', '', { outputEncoding: 'buffer', outputLength: 0 }),
|
||||
Buffer.alloc(0));
|
||||
|
||||
assert.strictEqual(
|
||||
crypto.hash('shake128', '', { outputLength: 5 }),
|
||||
crypto.createHash('shake128', { outputLength: 5 }).update('').digest('hex')
|
||||
);
|
||||
// Check length
|
||||
assert.strictEqual(
|
||||
crypto.hash('shake128', '', { outputLength: 5 }).length,
|
||||
crypto.createHash('shake128', { outputLength: 5 }).update('').digest('hex')
|
||||
.length
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
crypto.hash('shake128', '', { outputLength: 15 }),
|
||||
crypto.createHash('shake128', { outputLength: 15 }).update('').digest('hex')
|
||||
);
|
||||
// Check length
|
||||
assert.strictEqual(
|
||||
crypto.hash('shake128', '', { outputLength: 15 }).length,
|
||||
crypto.createHash('shake128', { outputLength: 15 }).update('').digest('hex')
|
||||
.length
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
crypto.hash('shake256', '', { outputLength: 16 }),
|
||||
crypto.createHash('shake256', { outputLength: 16 }).update('').digest('hex')
|
||||
);
|
||||
// Check length
|
||||
assert.strictEqual(
|
||||
crypto.hash('shake256', '', { outputLength: 16 }).length,
|
||||
crypto.createHash('shake256', { outputLength: 16 }).update('').digest('hex')
|
||||
.length
|
||||
);
|
||||
|
||||
// Large outputLengths.
|
||||
assert.strictEqual(
|
||||
crypto.hash('shake128', '', { outputLength: 128 }),
|
||||
crypto
|
||||
.createHash('shake128', { outputLength: 128 }).update('')
|
||||
.digest('hex')
|
||||
);
|
||||
// Check length without encoding
|
||||
assert.strictEqual(
|
||||
crypto.hash('shake128', '', { outputLength: 128 }).length,
|
||||
crypto
|
||||
.createHash('shake128', { outputLength: 128 }).update('')
|
||||
.digest('hex').length
|
||||
);
|
||||
assert.strictEqual(
|
||||
crypto.hash('shake256', '', { outputLength: 128 }),
|
||||
crypto
|
||||
.createHash('shake256', { outputLength: 128 }).update('')
|
||||
.digest('hex')
|
||||
);
|
||||
|
||||
const actual = crypto.hash('shake256', 'The message is shorter than the hash!', { outputLength: 1024 * 1024 });
|
||||
const expected = crypto
|
||||
.createHash('shake256', {
|
||||
outputLength: 1024 * 1024,
|
||||
})
|
||||
.update('The message is shorter than the hash!')
|
||||
.digest('hex');
|
||||
assert.strictEqual(actual, expected);
|
||||
|
||||
// Non-XOF hash functions should accept valid outputLength options as well.
|
||||
assert.strictEqual(crypto.hash('sha224', '', { outputLength: 28 }),
|
||||
'd14a028c2a3a2bc9476102bb288234c4' +
|
||||
'15a2b01f828ea62ac5b3e42f');
|
||||
|
||||
// Non-XOF hash functions should fail when outputLength isn't their actual outputLength
|
||||
assert.throws(() => crypto.hash('sha224', '', { outputLength: 32 }),
|
||||
{ message: 'Output length 32 is invalid for sha224, which does not support XOF' });
|
||||
assert.throws(() => crypto.hash('sha224', '', { outputLength: 0 }),
|
||||
{ message: 'Output length 0 is invalid for sha224, which does not support XOF' });
|
||||
}
|
|
@ -19,7 +19,7 @@ const fs = require('fs');
|
|||
assert.throws(() => { crypto.hash('sha1', invalid); }, { code: 'ERR_INVALID_ARG_TYPE' });
|
||||
});
|
||||
|
||||
[null, true, 1, () => {}, {}].forEach((invalid) => {
|
||||
[0, 1, NaN, true, Symbol(0)].forEach((invalid) => {
|
||||
assert.throws(() => { crypto.hash('sha1', 'test', invalid); }, { code: 'ERR_INVALID_ARG_TYPE' });
|
||||
});
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue