Skip to content
Merged
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
16 changes: 16 additions & 0 deletions deps/ncrypto/ncrypto.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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 {};
Copy link
Member

Choose a reason for hiding this comment

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

@codebytere ... can I ask you to take a look at this from a Whether-This-Will-Work-In-BoringSSL perspective?


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) {}
Expand Down
5 changes: 5 additions & 0 deletions deps/ncrypto/ncrypto.h
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
15 changes: 12 additions & 3 deletions doc/api/crypto.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
returned digest. **Default:** `'hex'`.
* `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
Expand All @@ -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
Expand Down
33 changes: 25 additions & 8 deletions lib/internal/crypto/hash.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ const {
const {
validateEncoding,
validateString,
validateObject,
validateUint32,
} = require('internal/validators');

Expand Down Expand Up @@ -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.
Expand All @@ -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 = {
Expand Down
70 changes: 59 additions & 11 deletions src/crypto/crypto_hash.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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]] {
Expand All @@ -230,21 +231,68 @@ void Hash::OneShotDigest(const FunctionCallbackInfo<Value>& args) {

enum encoding output_enc = ParseEncoding(isolate, args[4], args[5], HEX);

DataPointer output = ([&] {
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;
if (args[3]->IsString()) {
Utf8Value utf8(isolate, args[3]);
ncrypto::Buffer<const unsigned char> buf{
buf = {
.data = reinterpret_cast<const unsigned char*>(utf8.out()),
.len = utf8.length(),
};
return ncrypto::hashDigest(buf, md);
} else {
ArrayBufferViewContents<unsigned char> input(args[3]);
buf = {
.data = reinterpret_cast<const unsigned char*>(input.data()),
.len = input.length(),
};
}

if (is_xof) {
return ncrypto::xofHashDigest(buf, md, output_length);
}

ArrayBufferViewContents<unsigned char> input(args[3]);
ncrypto::Buffer<const unsigned char> buf{
.data = reinterpret_cast<const unsigned char*>(input.data()),
.len = input.length(),
};
return ncrypto::hashDigest(buf, md);
})();

Expand Down
18 changes: 18 additions & 0 deletions test/parallel/test-crypto-default-shake-lengths-oneshot.js
Original file line number Diff line number Diff line change
@@ -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 changes: 122 additions & 0 deletions test/parallel/test-crypto-oneshot-hash-xof.js
Original file line number Diff line number Diff line change
@@ -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' });
}
2 changes: 1 addition & 1 deletion test/parallel/test-crypto-oneshot-hash.js
Original file line number Diff line number Diff line change
Expand Up @@ -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' });
});

Expand Down
Loading