Skip to content
Open
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
14 changes: 12 additions & 2 deletions doc/api/crypto.md
Original file line number Diff line number Diff line change
Expand Up @@ -3771,6 +3771,10 @@ and description of each available elliptic curve.
<!-- YAML
added: v0.1.92
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/64000
description: The `outputLength` option is now required for XOF
hash functions without default output lengths.
- version: v12.8.0
pr-url: https://github.com/nodejs/node/pull/28805
description: The `outputLength` option was added for XOF hash functions.
Expand All @@ -3783,7 +3787,8 @@ changes:
Creates and returns a `Hash` object that can be used to generate hash digests
using the given `algorithm`. Optional `options` argument controls stream
behavior. For XOF hash functions such as `'shake256'`, the `outputLength` option
can be used to specify the desired output length in bytes.
specifies the desired output length in bytes. It is required for XOF hash
functions without a default output length.

The `algorithm` is dependent on the available algorithms supported by the
version of OpenSSL on the platform. Examples are `'sha256'`, `'sha512'`, etc.
Expand Down Expand Up @@ -4889,6 +4894,10 @@ added:
- v21.7.0
- v20.12.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/64000
description: The `outputLength` option is now required for XOF
hash functions without default output lengths.
- version:
- v25.5.0
- v24.13.1
Expand All @@ -4909,7 +4918,8 @@ changes:
* `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.
specifies the desired output length in bytes. This option is required for
XOF hash functions without a default output length.
* Returns: {string|Buffer}

A utility for creating one-shot hash digests of data. It can be faster than
Expand Down
8 changes: 6 additions & 2 deletions doc/api/deprecations.md
Original file line number Diff line number Diff line change
Expand Up @@ -4351,6 +4351,9 @@ npx codemod@latest @nodejs/types-is-native-error

<!-- YAML
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/64000
description: End-of-Life.
- version: v25.0.0
pr-url: https://github.com/nodejs/node/pull/59008
description: Runtime deprecation.
Expand All @@ -4362,9 +4365,10 @@ changes:
description: Documentation-only deprecation with support for `--pending-deprecation`.
-->

Type: Runtime
Type: End-of-Life

Creating SHAKE-128 and SHAKE-256 digests without an explicit `options.outputLength` is deprecated.
Creating SHAKE-128 and SHAKE-256 digests without an explicit
`options.outputLength` is no longer supported.

### DEP0199: `require('node:_http_*')`

Expand Down
27 changes: 0 additions & 27 deletions lib/internal/crypto/hash.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
const {
FunctionPrototypeCall,
ObjectSetPrototypeOf,
StringPrototypeReplace,
StringPrototypeToLowerCase,
Symbol,
} = primordials;
Expand Down Expand Up @@ -68,25 +67,6 @@ const LazyTransform = require('internal/streams/lazy_transform');
const kState = Symbol('kState');
const kFinalized = Symbol('kFinalized');

/**
* @param {string} name
* @returns {string}
*/
function normalizeAlgorithm(name) {
return StringPrototypeReplace(StringPrototypeToLowerCase(name), '-', '');
}

const maybeEmitDeprecationWarning = getDeprecationWarningEmitter(
'DEP0198',
'Creating SHAKE128/256 digests without an explicit options.outputLength is deprecated.',
undefined,
false,
(algorithm) => {
const normalized = normalizeAlgorithm(algorithm);
return normalized === 'shake128' || normalized === 'shake256';
},
);

const emitHmacDigestDeprecation = getDeprecationWarningEmitter(
'DEP0206',
'Calling Hmac.digest() more than once is deprecated.',
Expand All @@ -112,9 +92,6 @@ function Hash(algorithm, options) {
this[kState] = {
[kFinalized]: false,
};
if (!isCopy && xofLen === undefined) {
maybeEmitDeprecationWarning(algorithm);
}
FunctionPrototypeCall(LazyTransform, this, options);
}

Expand Down Expand Up @@ -299,10 +276,6 @@ function hash(algorithm, input, options) {
outputLength += 0;
}

if (outputLength === undefined) {
maybeEmitDeprecationWarning(algorithm);
}

return oneShotDigest(algorithm, getCachedHashId(algorithm), getHashCache(),
input, normalized, encodingsMap[normalized], outputLength);
}
Expand Down
78 changes: 48 additions & 30 deletions src/crypto/crypto_hash.cc
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,39 @@ const EVP_MD* GetDigestImplementation(Environment* env,
#endif
}

void MarkInvalidXofLength() {
EVPerr(EVP_F_EVP_DIGESTFINALXOF, EVP_R_NOT_XOF_OR_INVALID_LENGTH);
}

// DEP0198 EOL requires XOFs without an OpenSSL-defined default output length
// to fail when outputLength is omitted. OpenSSL 3.4 and later report a digest
// size of 0 for such XOFs, including SHAKE, which had weak historical defaults
// before OpenSSL 3.4. For older OpenSSL versions, identify those resolved
// EVP_MD values explicitly to keep the missing-outputLength error
// version-independent.
#if !OPENSSL_VERSION_PREREQ(3, 4)
bool IsShakeDigest(const EVP_MD* md) {
#if OPENSSL_VERSION_MAJOR >= 3
return EVP_MD_is_a(md, "SHAKE128") || EVP_MD_is_a(md, "SHAKE256");
#else
const char* name = OBJ_nid2sn(EVP_MD_type(md));
return name != nullptr &&
(strcmp(name, "SHAKE128") == 0 || strcmp(name, "SHAKE256") == 0);
#endif
}
#endif

bool ShouldRejectMissingXofLength(const EVP_MD* md, size_t default_length) {
if (default_length == 0) return true;

#if !OPENSSL_VERSION_PREREQ(3, 4)
return IsShakeDigest(md);
#else
static_cast<void>(md);
return false;
#endif
}

// crypto.digest(algorithm, algorithmId, algorithmCache,
// input, outputEncoding, outputEncodingId, outputLength)
void Hash::OneShotDigest(const FunctionCallbackInfo<Value>& args) {
Expand Down Expand Up @@ -275,18 +308,10 @@ void Hash::OneShotDigest(const FunctionCallbackInfo<Value>& args) {
} 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
// TODO(@panva): remove this behaviour when DEP0198 is End-Of-Life
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;
}
}
} else if (ShouldRejectMissingXofLength(md, output_length)) {
MarkInvalidXofLength();
return ThrowCryptoError(
env, ERR_get_error(), "Digest method not supported");
}
}

Expand Down Expand Up @@ -369,6 +394,12 @@ void Hash::RegisterExternalReferences(ExternalReferenceRegistry* registry) {
void Hash::New(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);

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());
}

const Hash* orig = nullptr;
const EVP_MD* md = nullptr;
if (args[0]->IsObject()) {
Expand All @@ -379,12 +410,6 @@ void Hash::New(const FunctionCallbackInfo<Value>& args) {
md = GetDigestImplementation(env, args[0], args[2], args[3]);
}

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 (md == nullptr || !hash->HashInit(md, xof_md_len)) {
return ThrowCryptoError(env, ERR_get_error(),
Expand All @@ -405,18 +430,11 @@ bool Hash::HashInit(const EVP_MD* md, Maybe<unsigned int> xof_md_len) {

md_len_ = mdctx_.getDigestSize();

// This is to handle OpenSSL 3.4's breaking change in SHAKE128/256
// default lengths
// TODO(@panva): remove this behaviour when DEP0198 is End-Of-Life
if (mdctx_.hasXofFlag() && !xof_md_len.IsJust() && md_len_ == 0) {
const char* name = OBJ_nid2sn(EVP_MD_type(md));
if (name != nullptr) {
if (strcmp(name, "SHAKE128") == 0) {
md_len_ = 16;
} else if (strcmp(name, "SHAKE256") == 0) {
md_len_ = 32;
}
}
if (mdctx_.hasXofFlag() && !xof_md_len.IsJust() &&
ShouldRejectMissingXofLength(md, md_len_)) {
MarkInvalidXofLength();
mdctx_.reset();
return false;
}

if (xof_md_len.IsJust() && xof_md_len.FromJust() != md_len_) {
Expand Down
37 changes: 27 additions & 10 deletions test/parallel/test-crypto-default-shake-lengths-oneshot.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,34 @@ const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');

if (process.features.openssl_is_boringssl)
common.skip('not supported by BoringSSL');
const {
getHashes,
hash,
} = require('crypto');

const { hash } = require('crypto');
if (!getHashes().includes('shake128'))
common.skip('unsupported shake128 test');

common.expectWarning({
DeprecationWarning: {
DEP0198: 'Creating SHAKE128/256 digests without an explicit options.outputLength is deprecated.',
}
});
const assert = require('assert');

{
hash('shake128', 'test');
const invalidXofLength = {
code: 'ERR_OSSL_EVP_NOT_XOF_OR_INVALID_LENGTH',
name: 'Error',
message: /not XOF or invalid length/,
};

const shakeAlgorithms = [
'shake128',
'SHAKE128',
'shake256',
'SHAKE256',
];

for (const algorithm of shakeAlgorithms) {
assert.throws(() => hash(algorithm, 'test'), invalidXofLength);
assert.throws(() => hash(algorithm, 'test', 'hex'), invalidXofLength);
assert.throws(
() => hash(algorithm, 'test', { outputEncoding: 'buffer' }),
invalidXofLength);
assert.throws(() => hash(algorithm, 'test', {}), invalidXofLength);
}
42 changes: 31 additions & 11 deletions test/parallel/test-crypto-default-shake-lengths.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,39 @@ const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');

const crypto = require('crypto');
if (!crypto.getHashes().includes('shake128')) {
const {
createHash,
getHashes,
} = require('crypto');

if (!getHashes().includes('shake128'))
common.skip('unsupported shake128 test');
}

const { createHash } = require('crypto');
const assert = require('assert');

const invalidXofLength = {
code: 'ERR_OSSL_EVP_NOT_XOF_OR_INVALID_LENGTH',
name: 'Error',
message: /not XOF or invalid length/,
};

common.expectWarning({
DeprecationWarning: {
DEP0198: 'Creating SHAKE128/256 digests without an explicit options.outputLength is deprecated.',
}
});
const shakeAlgorithms = [
'shake128',
'SHAKE128',
'shake256',
'SHAKE256',
];

{
createHash('shake128').update('test').digest();
for (const algorithm of shakeAlgorithms) {
assert.throws(() => createHash(algorithm), invalidXofLength);
assert.throws(() => createHash(algorithm, null), invalidXofLength);
assert.throws(() => createHash(algorithm, {}), invalidXofLength);
}

const shake128 = createHash('shake128', { outputLength: 5 });
const shake128Copy = shake128.copy({ outputLength: 5 });

assert.throws(() => shake128.copy(), invalidXofLength);
assert.throws(() => shake128.copy(null), invalidXofLength);
assert.throws(() => shake128.copy({}), invalidXofLength);
assert.throws(() => shake128Copy.copy(), invalidXofLength);
31 changes: 20 additions & 11 deletions test/parallel/test-crypto-hash.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,6 @@ common.expectWarning({
DeprecationWarning: [
['crypto.Hash constructor is deprecated.',
'DEP0179'],
...(process.features.openssl_is_boringssl ? [] : [[
'Creating SHAKE128/256 digests without an explicit options.outputLength is deprecated.',
'DEP0198',
]]),
]
});

Expand Down Expand Up @@ -194,16 +190,29 @@ assert.throws(

// Test XOF hash functions and the outputLength option.
if (!process.features.openssl_is_boringssl) {
// Default outputLengths.
assert.strictEqual(crypto.createHash('shake128').digest('hex'),
'7f9c2ba4e88f827d616045507605853e');
assert.strictEqual(crypto.createHash('shake128', null).digest('hex'),
const invalidXofLength = {
code: 'ERR_OSSL_EVP_NOT_XOF_OR_INVALID_LENGTH',
name: 'Error',
message: /not XOF or invalid length/,
};

assert.throws(() => crypto.createHash('shake128'), invalidXofLength);
assert.throws(() => crypto.createHash('shake128', null), invalidXofLength);
assert.throws(() => crypto.createHash('shake256'), invalidXofLength);
assert.throws(() => crypto.createHash('shake256', {}), invalidXofLength);

assert.strictEqual(crypto.createHash('shake128', { outputLength: 16 })
.digest('hex'),
'7f9c2ba4e88f827d616045507605853e');
assert.strictEqual(crypto.createHash('shake256').digest('hex'),
assert.strictEqual(crypto.createHash('shake256', { outputLength: 32 })
.digest('hex'),
'46b9dd2b0ba88d13233b3feb743eeb24' +
'3fcd52ea62b81b82b50c27646ed5762f');
assert.strictEqual(crypto.createHash('shake256', { outputLength: 0 })
.copy() // Default outputLength.
const shake256 = crypto.createHash('shake256', { outputLength: 0 });
assert.throws(() => shake256.copy(), invalidXofLength);
assert.throws(() => shake256.copy(null), invalidXofLength);
assert.throws(() => shake256.copy({}), invalidXofLength);
assert.strictEqual(shake256.copy({ outputLength: 32 })
.digest('hex'),
'46b9dd2b0ba88d13233b3feb743eeb24' +
'3fcd52ea62b81b82b50c27646ed5762f');
Expand Down
Loading
Loading