crypto: add argon2() and argon2Sync() methods

Co-authored-by: Filip Skokan <panva.ip@gmail.com>
Co-authored-by: James M Snell <jasnell@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/50353
Reviewed-By: Ethan Arrowood <ethan@arrowood.dev>
Reviewed-By: Filip Skokan <panva.ip@gmail.com>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
This commit is contained in:
Ranieri Althoff 2025-08-19 21:30:38 +02:00 committed by GitHub
parent ef58be6f0c
commit bdcab711b8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 983 additions and 19 deletions

View File

@ -81,6 +81,9 @@ class Benchmark {
if (typeof value === 'number') {
if (key === 'dur' || key === 'duration') {
value = 0.05;
} else if (key === 'memory') {
// minimum Argon2 memcost with 1 lane is 8
value = 8;
} else if (value > 1) {
value = 1;
}

View File

@ -0,0 +1,53 @@
'use strict';
const common = require('../common.js');
const { hasOpenSSL } = require('../../test/common/crypto.js');
const assert = require('node:assert');
const {
argon2,
argon2Sync,
randomBytes,
} = require('node:crypto');
if (!hasOpenSSL(3, 2)) {
console.log('Skipping: Argon2 requires OpenSSL >= 3.2');
process.exit(0);
}
const bench = common.createBenchmark(main, {
mode: ['sync', 'async'],
algorithm: ['argon2d', 'argon2i', 'argon2id'],
passes: [1, 3],
parallelism: [2, 4, 8],
memory: [2 ** 11, 2 ** 16, 2 ** 21],
n: [50],
});
function measureSync(n, algorithm, message, nonce, options) {
bench.start();
for (let i = 0; i < n; ++i)
argon2Sync(algorithm, { ...options, message, nonce, tagLength: 64 });
bench.end(n);
}
function measureAsync(n, algorithm, message, nonce, options) {
let remaining = n;
function done(err) {
assert.ifError(err);
if (--remaining === 0)
bench.end(n);
}
bench.start();
for (let i = 0; i < n; ++i)
argon2(algorithm, { ...options, message, nonce, tagLength: 64 }, done);
}
function main({ n, mode, algorithm, ...options }) {
// Message, nonce, secret, associated data & tag length do not affect performance
const message = randomBytes(32);
const nonce = randomBytes(16);
if (mode === 'sync')
measureSync(n, algorithm, message, nonce, options);
else
measureAsync(n, algorithm, message, nonce, options);
}

View File

@ -10,7 +10,12 @@
#include <algorithm>
#include <cstring>
#if OPENSSL_VERSION_MAJOR >= 3
#include <openssl/core_names.h>
#include <openssl/params.h>
#include <openssl/provider.h>
#if OPENSSL_VERSION_NUMBER >= 0x30200000L
#include <openssl/thread.h>
#endif
#endif
#if OPENSSL_WITH_PQC
struct PQCMapping {
@ -1868,6 +1873,102 @@ DataPointer pbkdf2(const Digest& md,
return {};
}
#if OPENSSL_VERSION_NUMBER >= 0x30200000L
#ifndef OPENSSL_NO_ARGON2
DataPointer argon2(const Buffer<const char>& pass,
const Buffer<const unsigned char>& salt,
uint32_t lanes,
size_t length,
uint32_t memcost,
uint32_t iter,
uint32_t version,
const Buffer<const unsigned char>& secret,
const Buffer<const unsigned char>& ad,
Argon2Type type) {
ClearErrorOnReturn clearErrorOnReturn;
std::string_view algorithm;
switch (type) {
case Argon2Type::ARGON2I:
algorithm = "ARGON2I";
break;
case Argon2Type::ARGON2D:
algorithm = "ARGON2D";
break;
case Argon2Type::ARGON2ID:
algorithm = "ARGON2ID";
break;
default:
// Invalid Argon2 type
return {};
}
// creates a new library context to avoid locking when running concurrently
auto ctx = DeleteFnPtr<OSSL_LIB_CTX, OSSL_LIB_CTX_free>{OSSL_LIB_CTX_new()};
if (!ctx) {
return {};
}
// required if threads > 1
if (lanes > 1 && OSSL_set_max_threads(ctx.get(), lanes) != 1) {
return {};
}
auto kdf = DeleteFnPtr<EVP_KDF, EVP_KDF_free>{
EVP_KDF_fetch(ctx.get(), algorithm.data(), nullptr)};
if (!kdf) {
return {};
}
auto kctx =
DeleteFnPtr<EVP_KDF_CTX, EVP_KDF_CTX_free>{EVP_KDF_CTX_new(kdf.get())};
if (!kctx) {
return {};
}
std::vector<OSSL_PARAM> params;
params.reserve(9);
params.push_back(OSSL_PARAM_construct_octet_string(
OSSL_KDF_PARAM_PASSWORD,
const_cast<char*>(pass.len > 0 ? pass.data : ""),
pass.len));
params.push_back(OSSL_PARAM_construct_octet_string(
OSSL_KDF_PARAM_SALT, const_cast<unsigned char*>(salt.data), salt.len));
params.push_back(OSSL_PARAM_construct_uint32(OSSL_KDF_PARAM_THREADS, &lanes));
params.push_back(
OSSL_PARAM_construct_uint32(OSSL_KDF_PARAM_ARGON2_LANES, &lanes));
params.push_back(
OSSL_PARAM_construct_uint32(OSSL_KDF_PARAM_ARGON2_MEMCOST, &memcost));
params.push_back(OSSL_PARAM_construct_uint32(OSSL_KDF_PARAM_ITER, &iter));
if (ad.len != 0) {
params.push_back(OSSL_PARAM_construct_octet_string(
OSSL_KDF_PARAM_ARGON2_AD, const_cast<unsigned char*>(ad.data), ad.len));
}
if (secret.len != 0) {
params.push_back(OSSL_PARAM_construct_octet_string(
OSSL_KDF_PARAM_SECRET,
const_cast<unsigned char*>(secret.data),
secret.len));
}
params.push_back(OSSL_PARAM_construct_end());
auto dp = DataPointer::Alloc(length);
if (dp && EVP_KDF_derive(kctx.get(),
reinterpret_cast<unsigned char*>(dp.get()),
length,
params.data()) == 1) {
return dp;
}
return {};
}
#endif
#endif
// ============================================================================
EVPKeyPointer::PrivateKeyEncodingConfig::PrivateKeyEncodingConfig(

View File

@ -1557,6 +1557,23 @@ DataPointer pbkdf2(const Digest& md,
uint32_t iterations,
size_t length);
#if OPENSSL_VERSION_NUMBER >= 0x30200000L
#ifndef OPENSSL_NO_ARGON2
enum class Argon2Type { ARGON2D, ARGON2I, ARGON2ID };
DataPointer argon2(const Buffer<const char>& pass,
const Buffer<const unsigned char>& salt,
uint32_t lanes,
size_t length,
uint32_t memcost,
uint32_t iter,
uint32_t version,
const Buffer<const unsigned char>& secret,
const Buffer<const unsigned char>& ad,
Argon2Type type);
#endif
#endif
// ============================================================================
// Version metadata
#define NCRYPTO_VERSION "0.0.1"

View File

@ -2970,6 +2970,171 @@ Does not perform any other validation checks on the certificate.
## `node:crypto` module methods and properties
### `crypto.argon2(algorithm, parameters, callback)`
<!-- YAML
added: REPLACEME
-->
> Stability: 1.2 - Release candidate
* `algorithm` {string} Variant of Argon2, one of `"argon2d"`, `"argon2i"` or `"argon2id"`.
* `parameters` {Object}
* `message` {string|ArrayBuffer|Buffer|TypedArray|DataView} REQUIRED, this is the password for password
hashing applications of Argon2.
* `nonce` {string|ArrayBuffer|Buffer|TypedArray|DataView} REQUIRED, must be at
least 8 bytes long. This is the salt for password hashing applications of Argon2.
* `parallelism` {number} REQUIRED, degree of parallelism determines how many computational chains (lanes)
can be run. Must be greater than 1 and less than `2**24-1`.
* `tagLength` {number} REQUIRED, the length of the key to generate. Must be greater than 4 and
less than `2**32-1`.
* `memory` {number} REQUIRED, memory cost in 1KiB blocks. Must be greater than
`8 * parallelism` and less than `2**32-1`. The actual number of blocks is rounded
down to the nearest multiple of `4 * parallelism`.
* `passes` {number} REQUIRED, number of passes (iterations). Must be greater than 1 and less
than `2**32-1`.
* `secret` {string|ArrayBuffer|Buffer|TypedArray|DataView|undefined} OPTIONAL, Random additional input,
similar to the salt, that should **NOT** be stored with the derived key. This is known as pepper in
password hashing applications. If used, must have a length not greater than `2**32-1` bytes.
* `associatedData` {string|ArrayBuffer|Buffer|TypedArray|DataView|undefined} OPTIONAL, Additional data to
be added to the hash, functionally equivalent to salt or secret, but meant for
non-random data. If used, must have a length not greater than `2**32-1` bytes.
* `callback` {Function}
* `err` {Error}
* `derivedKey` {Buffer}
Provides an asynchronous [Argon2][] implementation. Argon2 is a password-based
key derivation function that is designed to be expensive computationally and
memory-wise in order to make brute-force attacks unrewarding.
The `nonce` should be as unique as possible. It is recommended that a nonce is
random and at least 16 bytes long. See [NIST SP 800-132][] for details.
When passing strings for `message`, `nonce`, `secret` or `associatedData`, please
consider [caveats when using strings as inputs to cryptographic APIs][].
The `callback` function is called with two arguments: `err` and `derivedKey`.
`err` is an exception object when key derivation fails, otherwise `err` is
`null`. `derivedKey` is passed to the callback as a [`Buffer`][].
An exception is thrown when any of the input arguments specify invalid values
or types.
```mjs
const { argon2, randomBytes } = await import('node:crypto');
const parameters = {
message: 'password',
nonce: randomBytes(16),
parallelism: 4,
tagLength: 64,
memory: 65536,
passes: 3,
};
argon2('argon2id', parameters, (err, derivedKey) => {
if (err) throw err;
console.log(derivedKey.toString('hex')); // 'af91dad...9520f15'
});
```
```cjs
const { argon2, randomBytes } = require('node:crypto');
const parameters = {
message: 'password',
nonce: randomBytes(16),
parallelism: 4,
tagLength: 64,
memory: 65536,
passes: 3,
};
argon2('argon2id', parameters, (err, derivedKey) => {
if (err) throw err;
console.log(derivedKey.toString('hex')); // 'af91dad...9520f15'
});
```
### `crypto.argon2Sync(algorithm, parameters)`
<!-- YAML
added: REPLACEME
-->
> Stability: 1.2 - Release candidate
* `algorithm` {string} Variant of Argon2, one of `"argon2d"`, `"argon2i"` or `"argon2id"`.
* `parameters` {Object}
* `message` {string|ArrayBuffer|Buffer|TypedArray|DataView} REQUIRED, this is the password for password
hashing applications of Argon2.
* `nonce` {string|ArrayBuffer|Buffer|TypedArray|DataView} REQUIRED, must be at
least 8 bytes long. This is the salt for password hashing applications of Argon2.
* `parallelism` {number} REQUIRED, degree of parallelism determines how many computational chains (lanes)
can be run. Must be greater than 1 and less than `2**24-1`.
* `tagLength` {number} REQUIRED, the length of the key to generate. Must be greater than 4 and
less than `2**32-1`.
* `memory` {number} REQUIRED, memory cost in 1KiB blocks. Must be greater than
`8 * parallelism` and less than `2**32-1`. The actual number of blocks is rounded
down to the nearest multiple of `4 * parallelism`.
* `passes` {number} REQUIRED, number of passes (iterations). Must be greater than 1 and less
than `2**32-1`.
* `secret` {string|ArrayBuffer|Buffer|TypedArray|DataView|undefined} OPTIONAL, Random additional input,
similar to the salt, that should **NOT** be stored with the derived key. This is known as pepper in
password hashing applications. If used, must have a length not greater than `2**32-1` bytes.
* `associatedData` {string|ArrayBuffer|Buffer|TypedArray|DataView|undefined} OPTIONAL, Additional data to
be added to the hash, functionally equivalent to salt or secret, but meant for
non-random data. If used, must have a length not greater than `2**32-1` bytes.
* Returns: {Buffer}
Provides a synchronous [Argon2][] implementation. Argon2 is a password-based
key derivation function that is designed to be expensive computationally and
memory-wise in order to make brute-force attacks unrewarding.
The `nonce` should be as unique as possible. It is recommended that a nonce is
random and at least 16 bytes long. See [NIST SP 800-132][] for details.
When passing strings for `message`, `nonce`, `secret` or `associatedData`, please
consider [caveats when using strings as inputs to cryptographic APIs][].
An exception is thrown when key derivation fails, otherwise the derived key is
returned as a [`Buffer`][].
An exception is thrown when any of the input arguments specify invalid values
or types.
```mjs
const { argon2Sync, randomBytes } = await import('node:crypto');
const parameters = {
message: 'password',
nonce: randomBytes(16),
parallelism: 4,
tagLength: 64,
memory: 65536,
passes: 3,
};
const derivedKey = argon2Sync('argon2id', parameters);
console.log(derivedKey.toString('hex')); // 'af91dad...9520f15'
```
```cjs
const { argon2Sync, randomBytes } = require('node:crypto');
const parameters = {
message: 'password',
nonce: randomBytes(16),
parallelism: 4,
tagLength: 64,
memory: 65536,
passes: 3,
};
const derivedKey = argon2Sync('argon2id', parameters);
console.log(derivedKey.toString('hex')); // 'af91dad...9520f15'
```
### `crypto.checkPrime(candidate[, options], callback)`
<!-- YAML
@ -6284,6 +6449,7 @@ See the [list of SSL OP Flags][] for details.
[`verify.verify()`]: #verifyverifyobject-signature-signatureencoding
[`x509.fingerprint256`]: #x509fingerprint256
[`x509.verify(publicKey)`]: #x509verifypublickey
[argon2]: https://www.rfc-editor.org/rfc/rfc9106.html
[asymmetric key types]: #asymmetric-key-types
[caveats when using strings as inputs to cryptographic APIs]: #using-strings-as-inputs-to-cryptographic-apis
[certificate object]: tls.md#certificate-object

View File

@ -826,6 +826,12 @@ when an error occurs (and is caught) during the creation of the
context, for example, when the allocation fails or the maximum call stack
size is reached when the context is created.
<a id="ERR_CRYPTO_ARGON2_NOT_SUPPORTED"></a>
### `ERR_CRYPTO_ARGON2_NOT_SUPPORTED`
Argon2 is not supported by the current version of OpenSSL being used.
<a id="ERR_CRYPTO_CUSTOM_ENGINE_NOT_SUPPORTED"></a>
### `ERR_CRYPTO_CUSTOM_ENGINE_NOT_SUPPORTED`

View File

@ -57,6 +57,10 @@ const {
randomInt,
randomUUID,
} = require('internal/crypto/random');
const {
argon2,
argon2Sync,
} = require('internal/crypto/argon2');
const {
pbkdf2,
pbkdf2Sync,
@ -171,6 +175,8 @@ function createVerify(algorithm, options) {
module.exports = {
// Methods
argon2,
argon2Sync,
checkPrime,
checkPrimeSync,
createCipheriv,

View File

@ -0,0 +1,185 @@
'use strict';
const {
FunctionPrototypeCall,
MathPow,
Uint8Array,
} = primordials;
const { Buffer } = require('buffer');
const {
Argon2Job,
kCryptoJobAsync,
kCryptoJobSync,
kTypeArgon2d,
kTypeArgon2i,
kTypeArgon2id,
} = internalBinding('crypto');
const { getArrayBufferOrView } = require('internal/crypto/util');
const {
validateString,
validateFunction,
validateInteger,
validateObject,
validateOneOf,
validateUint32,
} = require('internal/validators');
const {
codes: {
ERR_CRYPTO_ARGON2_NOT_SUPPORTED,
},
} = require('internal/errors');
/**
* @param {'argon2d' | 'argon2i' | 'argon2id'} algorithm
* @param {object} parameters
* @param {ArrayBufferLike} parameters.message
* @param {ArrayBufferLike} parameters.nonce
* @param {number} parameters.parallelism
* @param {number} parameters.tagLength
* @param {number} parameters.memory
* @param {number} parameters.passes
* @param {ArrayBufferLike} [parameters.secret]
* @param {ArrayBufferLike} [parameters.associatedData]
* @param {Function} callback
*/
function argon2(algorithm, parameters, callback) {
parameters = check(algorithm, parameters);
validateFunction(callback, 'callback');
const job = new Argon2Job(
kCryptoJobAsync,
parameters.message,
parameters.nonce,
parameters.parallelism,
parameters.tagLength,
parameters.memory,
parameters.passes,
parameters.secret,
parameters.associatedData,
parameters.type);
job.ondone = (error, result) => {
if (error !== undefined)
return FunctionPrototypeCall(callback, job, error);
const buf = Buffer.from(result);
return FunctionPrototypeCall(callback, job, null, buf);
};
job.run();
}
/**
* @param {'argon2d' | 'argon2i' | 'argon2id'} algorithm
* @param {object} parameters
* @param {ArrayBufferLike} parameters.message
* @param {ArrayBufferLike} parameters.nonce
* @param {number} parameters.parallelism
* @param {number} parameters.tagLength
* @param {number} parameters.memory
* @param {number} parameters.passes
* @param {ArrayBufferLike} [parameters.secret]
* @param {ArrayBufferLike} [parameters.associatedData]
* @returns {Buffer}
*/
function argon2Sync(algorithm, parameters) {
parameters = check(algorithm, parameters);
const job = new Argon2Job(
kCryptoJobSync,
parameters.message,
parameters.nonce,
parameters.parallelism,
parameters.tagLength,
parameters.memory,
parameters.passes,
parameters.secret,
parameters.associatedData,
parameters.type);
const { 0: err, 1: result } = job.run();
if (err !== undefined)
throw err;
return Buffer.from(result);
}
/**
* @param {'argon2d' | 'argon2i' | 'argon2id'} algorithm
* @param {object} parameters
* @param {ArrayBufferLike} parameters.message
* @param {ArrayBufferLike} parameters.nonce
* @param {number} parameters.parallelism
* @param {number} parameters.tagLength
* @param {number} parameters.memory
* @param {number} parameters.passes
* @param {ArrayBufferLike} [parameters.secret]
* @param {ArrayBufferLike} [parameters.associatedData]
* @returns {object}
*/
function check(algorithm, parameters) {
if (Argon2Job === undefined)
throw new ERR_CRYPTO_ARGON2_NOT_SUPPORTED();
validateString(algorithm, 'algorithm');
validateOneOf(algorithm, 'algorithm', ['argon2d', 'argon2i', 'argon2id']);
let type;
switch (algorithm) {
case 'argon2d':
type = kTypeArgon2d;
break;
case 'argon2i':
type = kTypeArgon2i;
break;
case 'argon2id':
type = kTypeArgon2id;
break;
default: // unreachable
throw new ERR_CRYPTO_ARGON2_NOT_SUPPORTED();
}
validateObject(parameters, 'parameters');
const { parallelism, tagLength, memory, passes } = parameters;
const MAX_POSITIVE_UINT_32 = MathPow(2, 32) - 1;
const message = getArrayBufferOrView(parameters.message, 'parameters.message');
validateInteger(message.byteLength, 'parameters.message.byteLength', 0, MAX_POSITIVE_UINT_32);
const nonce = getArrayBufferOrView(parameters.nonce, 'parameters.nonce');
validateInteger(nonce.byteLength, 'parameters.nonce.byteLength', 8, MAX_POSITIVE_UINT_32);
validateInteger(parallelism, 'parameters.parallelism', 1, MathPow(2, 24) - 1);
validateInteger(tagLength, 'parameters.tagLength', 4, MAX_POSITIVE_UINT_32);
validateInteger(memory, 'parameters.memory', 8 * parallelism, MAX_POSITIVE_UINT_32);
validateUint32(passes, 'parameters.passes', true);
let secret;
if (parameters.secret === undefined) {
secret = new Uint8Array(0);
} else {
secret = getArrayBufferOrView(parameters.secret);
validateInteger(secret.byteLength, 'parameters.secret.byteLength', 0, MAX_POSITIVE_UINT_32);
}
let associatedData;
if (parameters.associatedData === undefined) {
associatedData = new Uint8Array(0);
} else {
associatedData = getArrayBufferOrView(parameters.associatedData);
validateInteger(associatedData.byteLength, 'parameters.associatedData.byteLength', 0, MAX_POSITIVE_UINT_32);
}
return { message, nonce, secret, associatedData, tagLength, passes, parallelism, memory, type };
}
module.exports = {
argon2,
argon2Sync,
};

View File

@ -1163,6 +1163,7 @@ E('ERR_CONSOLE_WRITABLE_STREAM',
'Console expects a writable stream instance for %s', TypeError);
E('ERR_CONSTRUCT_CALL_REQUIRED', 'Class constructor %s cannot be invoked without `new`', TypeError);
E('ERR_CONTEXT_NOT_INITIALIZED', 'context used is not initialized', Error);
E('ERR_CRYPTO_ARGON2_NOT_SUPPORTED', 'Argon2 algorithm not supported', Error);
E('ERR_CRYPTO_CUSTOM_ENGINE_NOT_SUPPORTED',
'Custom engines not supported by this OpenSSL', Error);
E('ERR_CRYPTO_ECDH_INVALID_FORMAT', 'Invalid ECDH format: %s', TypeError);

View File

@ -332,6 +332,7 @@
],
'node_crypto_sources': [
'src/crypto/crypto_aes.cc',
'src/crypto/crypto_argon2.cc',
'src/crypto/crypto_bio.cc',
'src/crypto/crypto_chacha20_poly1305.cc',
'src/crypto/crypto_common.cc',
@ -357,6 +358,7 @@
'src/crypto/crypto_scrypt.cc',
'src/crypto/crypto_tls.cc',
'src/crypto/crypto_x509.cc',
'src/crypto/crypto_argon2.h',
'src/crypto/crypto_bio.h',
'src/crypto/crypto_clienthello-inl.h',
'src/crypto/crypto_dh.h',
@ -978,11 +980,11 @@
'variables': {
'mkssldef_flags': [
# Categories to export.
'-CAES,BF,BIO,DES,DH,DSA,EC,ECDH,ECDSA,ENGINE,EVP,HMAC,MD4,MD5,'
'PSK,RC2,RC4,RSA,SHA,SHA0,SHA1,SHA256,SHA512,SOCK,STDIO,TLSEXT,'
'UI,FP_API,TLS1_METHOD,TLS1_1_METHOD,TLS1_2_METHOD,SCRYPT,OCSP,'
'NEXTPROTONEG,RMD160,CAST,DEPRECATEDIN_1_1_0,DEPRECATEDIN_1_2_0,'
'DEPRECATEDIN_3_0',
'-CAES,ARGON2,BF,BIO,DES,DH,DSA,EC,ECDH,ECDSA,ENGINE,EVP,HMAC,'
'MD4,MD5,PSK,RC2,RC4,RSA,SHA,SHA0,SHA1,SHA256,SHA512,SOCK,STDIO,'
'TLSEXT,UI,FP_API,TLS1_METHOD,TLS1_1_METHOD,TLS1_2_METHOD,'
'SCRYPT,OCSP,NEXTPROTONEG,RMD160,CAST,DEPRECATEDIN_1_1_0,'
'DEPRECATEDIN_1_2_0,DEPRECATEDIN_3_0',
# Defines.
'-DWIN32',
# Symbols to filter from the export list.

View File

@ -92,6 +92,7 @@ namespace node {
V(KEYPAIRGENREQUEST) \
V(KEYGENREQUEST) \
V(KEYEXPORTREQUEST) \
V(ARGON2REQUEST) \
V(CIPHERREQUEST) \
V(DERIVEBITSREQUEST) \
V(HASHREQUEST) \

View File

@ -33,6 +33,7 @@ following table:
| File (\*.h/\*.cc) | Description |
| -------------------- | -------------------------------------------------------------------------- |
| `crypto_aes` | AES Cipher support. |
| `crypto_argon2` | Argon2 key / bit generation implementation. |
| `crypto_cipher` | General Encryption/Decryption utilities. |
| `crypto_clienthello` | TLS/SSL client hello parser implementation. Used during SSL/TLS handshake. |
| `crypto_context` | Implementation of the `SecureContext` object. |

172
src/crypto/crypto_argon2.cc Normal file
View File

@ -0,0 +1,172 @@
#include "crypto/crypto_argon2.h"
#include "async_wrap-inl.h"
#include "threadpoolwork-inl.h"
#if OPENSSL_VERSION_NUMBER >= 0x30200000L
#ifndef OPENSSL_NO_ARGON2
#include <openssl/core_names.h>
namespace node::crypto {
using v8::FunctionCallbackInfo;
using v8::JustVoid;
using v8::Local;
using v8::Maybe;
using v8::MaybeLocal;
using v8::Nothing;
using v8::Object;
using v8::Uint32;
using v8::Value;
Argon2Config::Argon2Config(Argon2Config&& other) noexcept
: mode{other.mode},
pass{std::move(other.pass)},
salt{std::move(other.salt)},
secret{std::move(other.secret)},
ad{std::move(other.ad)},
type{other.type},
iter{other.iter},
lanes{other.lanes},
memcost{other.memcost},
keylen{other.keylen} {}
Argon2Config& Argon2Config::operator=(Argon2Config&& other) noexcept {
if (&other == this) return *this;
this->~Argon2Config();
return *new (this) Argon2Config(std::move(other));
}
void Argon2Config::MemoryInfo(MemoryTracker* tracker) const {
if (mode == kCryptoJobAsync) {
tracker->TrackFieldWithSize("pass", pass.size());
tracker->TrackFieldWithSize("salt", salt.size());
tracker->TrackFieldWithSize("secret", secret.size());
tracker->TrackFieldWithSize("ad", ad.size());
}
}
MaybeLocal<Value> Argon2Traits::EncodeOutput(Environment* env,
const Argon2Config& config,
ByteSource* out) {
return out->ToArrayBuffer(env);
}
Maybe<void> Argon2Traits::AdditionalConfig(
CryptoJobMode mode,
const FunctionCallbackInfo<Value>& args,
unsigned int offset,
Argon2Config* config) {
Environment* env = Environment::GetCurrent(args);
config->mode = mode;
ArrayBufferOrViewContents<char> pass(args[offset]);
ArrayBufferOrViewContents<char> salt(args[offset + 1]);
ArrayBufferOrViewContents<char> secret(args[offset + 6]);
ArrayBufferOrViewContents<char> ad(args[offset + 7]);
if (!pass.CheckSizeInt32()) [[unlikely]] {
THROW_ERR_OUT_OF_RANGE(env, "pass is too large");
return Nothing<void>();
}
if (!salt.CheckSizeInt32()) [[unlikely]] {
THROW_ERR_OUT_OF_RANGE(env, "salt is too large");
return Nothing<void>();
}
if (!secret.CheckSizeInt32()) [[unlikely]] {
THROW_ERR_OUT_OF_RANGE(env, "secret is too large");
return Nothing<void>();
}
if (!ad.CheckSizeInt32()) [[unlikely]] {
THROW_ERR_OUT_OF_RANGE(env, "ad is too large");
return Nothing<void>();
}
const bool isAsync = mode == kCryptoJobAsync;
config->pass = isAsync ? pass.ToCopy() : pass.ToByteSource();
config->salt = isAsync ? salt.ToCopy() : salt.ToByteSource();
config->secret = isAsync ? secret.ToCopy() : secret.ToByteSource();
config->ad = isAsync ? ad.ToCopy() : ad.ToByteSource();
CHECK(args[offset + 2]->IsUint32()); // lanes
CHECK(args[offset + 3]->IsUint32()); // keylen
CHECK(args[offset + 4]->IsUint32()); // memcost
CHECK(args[offset + 5]->IsUint32()); // iter
CHECK(args[offset + 8]->IsUint32()); // type
config->lanes = args[offset + 2].As<Uint32>()->Value();
config->keylen = args[offset + 3].As<Uint32>()->Value();
config->memcost = args[offset + 4].As<Uint32>()->Value();
config->iter = args[offset + 5].As<Uint32>()->Value();
config->type =
static_cast<ncrypto::Argon2Type>(args[offset + 8].As<Uint32>()->Value());
if (!ncrypto::argon2(config->pass,
config->salt,
config->lanes,
config->keylen,
config->memcost,
config->iter,
config->version,
config->secret,
config->ad,
config->type)) {
THROW_ERR_CRYPTO_INVALID_ARGON2_PARAMS(env);
return Nothing<void>();
}
return JustVoid();
}
bool Argon2Traits::DeriveBits(Environment* env,
const Argon2Config& config,
ByteSource* out,
CryptoJobMode mode) {
// If the config.length is zero-length, just return an empty buffer.
// It's useless, yes, but allowed via the API.
if (config.keylen == 0) {
*out = ByteSource();
return true;
}
// Both the pass and salt may be zero-length at this point
auto dp = ncrypto::argon2(config.pass,
config.salt,
config.lanes,
config.keylen,
config.memcost,
config.iter,
config.version,
config.secret,
config.ad,
config.type);
if (!dp) return false;
DCHECK(!dp.isSecure());
*out = ByteSource::Allocated(dp.release());
return true;
}
static constexpr auto kTypeArgon2d = ncrypto::Argon2Type::ARGON2D;
static constexpr auto kTypeArgon2i = ncrypto::Argon2Type::ARGON2I;
static constexpr auto kTypeArgon2id = ncrypto::Argon2Type::ARGON2ID;
void Argon2::Initialize(Environment* env, Local<Object> target) {
Argon2Job::Initialize(env, target);
NODE_DEFINE_CONSTANT(target, kTypeArgon2d);
NODE_DEFINE_CONSTANT(target, kTypeArgon2i);
NODE_DEFINE_CONSTANT(target, kTypeArgon2id);
}
void Argon2::RegisterExternalReferences(ExternalReferenceRegistry* registry) {
Argon2Job::RegisterExternalReferences(registry);
}
} // namespace node::crypto
#endif
#endif

View File

@ -0,0 +1,86 @@
#ifndef SRC_CRYPTO_CRYPTO_ARGON2_H_
#define SRC_CRYPTO_CRYPTO_ARGON2_H_
#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
#include "crypto/crypto_util.h"
namespace node::crypto {
#if !defined(OPENSSL_NO_ARGON2) && OPENSSL_VERSION_NUMBER >= 0x30200000L
// Argon2 is a password-based key derivation algorithm
// defined in https://datatracker.ietf.org/doc/html/rfc9106
// It takes as input a password, a salt value, and a
// handful of additional parameters that control the
// cost of the operation. In this case, the higher
// the cost, the better the result. The length parameter
// defines the number of bytes that are generated.
// The salt must be as random as possible and should be
// at least 16 bytes in length.
struct Argon2Config final : public MemoryRetainer {
CryptoJobMode mode;
ByteSource pass;
ByteSource salt;
ByteSource secret;
ByteSource ad;
ncrypto::Argon2Type type;
uint32_t iter;
uint32_t lanes;
uint32_t memcost;
uint32_t version = 0x13;
uint32_t keylen;
Argon2Config() = default;
explicit Argon2Config(Argon2Config&& other) noexcept;
Argon2Config& operator=(Argon2Config&& other) noexcept;
void MemoryInfo(MemoryTracker* tracker) const override;
SET_MEMORY_INFO_NAME(Argon2Config)
SET_SELF_SIZE(Argon2Config)
};
struct Argon2Traits final {
using AdditionalParameters = Argon2Config;
static constexpr const char* JobName = "Argon2Job";
static constexpr AsyncWrap::ProviderType Provider =
AsyncWrap::PROVIDER_ARGON2REQUEST;
static v8::Maybe<void> AdditionalConfig(
CryptoJobMode mode,
const v8::FunctionCallbackInfo<v8::Value>& args,
unsigned int offset,
Argon2Config* config);
static bool DeriveBits(Environment* env,
const Argon2Config& config,
ByteSource* out,
CryptoJobMode mode);
static v8::MaybeLocal<v8::Value> EncodeOutput(Environment* env,
const Argon2Config& config,
ByteSource* out);
};
using Argon2Job = DeriveBitsJob<Argon2Traits>;
namespace Argon2 {
void Initialize(Environment* env, v8::Local<v8::Object> target);
void RegisterExternalReferences(ExternalReferenceRegistry* registry);
} // namespace Argon2
#else
// If there is no Argon2 support, Argon2Job becomes a non-op
struct Argon2Job {
static void Initialize(Environment* env, v8::Local<v8::Object> target) {}
};
#endif
} // namespace node::crypto
#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
#endif // SRC_CRYPTO_CRYPTO_ARGON2_H_

View File

@ -60,6 +60,12 @@ namespace crypto {
V(Verify) \
V(X509Certificate)
#if !defined(OPENSSL_NO_ARGON2) && OPENSSL_VERSION_NUMBER >= 0x30200000L
#define ARGON2_NAMESPACE_LIST(V) V(Argon2)
#else
#define ARGON2_NAMESPACE_LIST(V)
#endif // !OPENSSL_NO_ARGON2 && OpenSSL >= 3.2
#ifdef OPENSSL_NO_SCRYPT
#define SCRYPT_NAMESPACE_LIST(V)
#else
@ -68,6 +74,7 @@ namespace crypto {
#define CRYPTO_NAMESPACE_LIST(V) \
CRYPTO_NAMESPACE_LIST_BASE(V) \
ARGON2_NAMESPACE_LIST(V) \
SCRYPT_NAMESPACE_LIST(V)
void Initialize(Local<Object> target,

View File

@ -29,6 +29,7 @@
// remains for convenience for any code that still imports it. New
// code should include the relevant src/crypto headers directly.
#include "crypto/crypto_aes.h"
#include "crypto/crypto_argon2.h"
#include "crypto/crypto_bio.h"
#include "crypto/crypto_chacha20_poly1305.h"
#include "crypto/crypto_cipher.h"

View File

@ -50,6 +50,7 @@ void OOMErrorHandler(const char* location, const v8::OOMDetails& details);
V(ERR_CONSTRUCT_CALL_INVALID, TypeError) \
V(ERR_CRYPTO_CUSTOM_ENGINE_NOT_SUPPORTED, Error) \
V(ERR_CRYPTO_INITIALIZATION_FAILED, Error) \
V(ERR_CRYPTO_INVALID_ARGON2_PARAMS, TypeError) \
V(ERR_CRYPTO_INVALID_AUTH_TAG, TypeError) \
V(ERR_CRYPTO_INVALID_COUNTER, TypeError) \
V(ERR_CRYPTO_INVALID_CURVE, TypeError) \
@ -184,6 +185,7 @@ ERRORS_WITH_CODE(V)
V(ERR_CONSTRUCT_CALL_INVALID, "Constructor cannot be called") \
V(ERR_CONSTRUCT_CALL_REQUIRED, "Cannot call constructor without `new`") \
V(ERR_CRYPTO_INITIALIZATION_FAILED, "Initialization failed") \
V(ERR_CRYPTO_INVALID_ARGON2_PARAMS, "Invalid Argon2 params") \
V(ERR_CRYPTO_INVALID_AUTH_TAG, "Invalid authentication tag") \
V(ERR_CRYPTO_INVALID_COUNTER, "Invalid counter") \
V(ERR_CRYPTO_INVALID_CURVE, "Invalid EC curve name") \

View File

@ -0,0 +1,14 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const { hasOpenSSL } = require('../common/crypto');
if (hasOpenSSL(3, 2))
common.skip('requires OpenSSL < 3.2');
const assert = require('node:assert');
const crypto = require('node:crypto');
assert.throws(() => crypto.argon2(), { code: 'ERR_CRYPTO_ARGON2_NOT_SUPPORTED' });

View File

@ -0,0 +1,139 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const { hasOpenSSL } = require('../common/crypto');
if (!hasOpenSSL(3, 2))
common.skip('requires OpenSSL >= 3.2');
const assert = require('node:assert');
const crypto = require('node:crypto');
function runArgon2(algorithm, options) {
const syncResult = crypto.argon2Sync(algorithm, options);
crypto.argon2(algorithm, options,
common.mustSucceed((asyncResult) => {
assert.deepStrictEqual(asyncResult, syncResult);
}));
return syncResult;
}
const message = Buffer.alloc(32, 0x01);
const nonce = Buffer.alloc(16, 0x02);
const secret = Buffer.alloc(8, 0x03);
const associatedData = Buffer.alloc(12, 0x04);
const defaults = { message, nonce, parallelism: 1, tagLength: 64, memory: 8, passes: 3 };
const good = [
// Test vectors from RFC 9106 https://www.rfc-editor.org/rfc/rfc9106.html#name-test-vectors
// and OpenSSL 3.2 https://github.com/openssl/openssl/blob/6dfa998f7ea150f9c6d4e4727cf6d5c82a68a8da/test/recipes/30-test_evp_data/evpkdf_argon2.txt
//
// OpenSSL defaults are:
// - outlen: 64
// - passes: 3
// - parallelism: 1
// - memory: 8
// https://github.com/openssl/openssl/blob/6dfa998f7ea150f9c6d4e4727cf6d5c82a68a8da/providers/implementations/kdfs/argon2.c#L77-L82
[
'argon2d',
{ secret, associatedData, parallelism: 4, tagLength: 32, memory: 32 },
'512b391b6f1162975371d30919734294f868e3be3984f3c1a13a4db9fabe4acb',
],
[
'argon2i',
{ secret, associatedData, parallelism: 4, tagLength: 32, memory: 32 },
'c814d9d1dc7f37aa13f0d77f2494bda1c8de6b016dd388d29952a4c4672b6ce8',
],
[
'argon2id',
{ secret, associatedData, parallelism: 4, tagLength: 32, memory: 32 },
'0d640df58d78766c08c037a34a8b53c9d01ef0452d75b65eb52520e96b01e659',
],
[
'argon2d',
{ message: '1234567890', nonce: 'saltsalt' },
'd16ad773b1c6400d3193bc3e66271603e9de72bace20af3f89c236f5434cdec9' +
'9072ddfc6b9c77ea9f386c0e8d7cb0c37cec6ec3277a22c92d5be58ef67c7eaa',
],
[
'argon2id',
{ message: '', parallelism: 4, tagLength: 32, memory: 32 },
'0a34f1abde67086c82e785eaf17c68382259a264f4e61b91cd2763cb75ac189a',
],
[
'argon2d',
{ message: '1234567890', nonce: 'saltsalt', parallelism: 2, memory: 65536 },
'5ca0ab135de1241454840172696c301c7b8fd99a788cd11cf9699044cadf7fca' +
'0a6e3762cb3043a71adf6553db3fd7925101b0ccf8868b098492a4addb2486bc',
],
[
'argon2i',
{ parallelism: 4, tagLength: 32, memory: 32 },
'a9a7510e6db4d588ba3414cd0e094d480d683f97b9ccb612a544fe8ef65ba8e0',
],
[
'argon2id',
{ parallelism: 4, tagLength: 32, memory: 32 },
'03aab965c12001c9d7d0d2de33192c0494b684bb148196d73c1df1acaf6d0c2e',
],
[
'argon2d',
{ message: '1234567890', nonce: 'saltsalt', parallelism: 2, tagLength: 128, memory: 65536 },
'a86c83a19f0b234ecba8c275d16d059153f961e4c39ec9b1be98b3e73d791789' +
'363682443ad594334048634e91c493affed0bc29fd329a0e553c00149d6db19a' +
'f4e4a354aec14dbd575d78ba87d4a4bc4746666e7a4e6ee1572bbffc2eba308a' +
'2d825cb7b41fde3a95d5cff0dfa2d0fdd636b32aea8b4a3c532742d330bd1b90',
],
];
// Test vectors that should fail.
const bad = [
['argon2id', { nonce: nonce.subarray(0, 7) }, 'parameters.nonce.byteLength'], // nonce.byteLength < 8
['argon2id', { tagLength: 3 }, 'parameters.tagLength'], // tagLength < 4
['argon2id', { tagLength: 2 ** 32 }, 'parameters.tagLength'], // tagLength > 2^(32)-1
['argon2id', { passes: 0 }, 'parameters.passes'], // passes < 2
['argon2id', { passes: 2 ** 32 }, 'parameters.passes'], // passes > 2^(32)-1
['argon2id', { parallelism: 0 }, 'parameters.parallelism'], // parallelism < 1
['argon2id', { parallelism: 2 ** 24 }, 'parameters.parallelism'], // Parallelism > 2^(24)-1
['argon2id', { parallelism: 4, memory: 16 }, 'parameters.memory'], // Memory < 8 * parallelism
['argon2id', { memory: 2 ** 32 }, 'parameters.memory'], // memory > 2^(32)-1
];
for (const [algorithm, overrides, expected] of good) {
const parameters = { ...defaults, ...overrides };
const actual = runArgon2(algorithm, parameters);
assert.strictEqual(actual.toString('hex'), expected);
}
for (const [algorithm, overrides, param] of bad) {
const expected = {
code: 'ERR_OUT_OF_RANGE',
message: new RegExp(`The value of "${param}" is out of range`),
};
const parameters = { ...defaults, ...overrides };
assert.throws(() => crypto.argon2(algorithm, parameters, () => {}), expected);
assert.throws(() => crypto.argon2Sync(algorithm, parameters), expected);
}
for (const key of Object.keys(defaults)) {
const expected = {
code: 'ERR_INVALID_ARG_TYPE',
message: new RegExp(`"parameters\\.${key}"`),
};
const parameters = { ...defaults };
delete parameters[key];
assert.throws(() => crypto.argon2('argon2id', parameters, () => {}), expected);
assert.throws(() => crypto.argon2Sync('argon2id', parameters), expected);
}
{
const expected = { code: 'ERR_INVALID_ARG_TYPE' };
assert.throws(() => crypto.argon2(), expected);
assert.throws(() => crypto.argon2('argon2id', null), expected);
assert.throws(() => crypto.argon2('argon2id', defaults, null), expected);
assert.throws(() => crypto.argon2('argon2id', defaults, {}), expected);
}

View File

@ -46,6 +46,7 @@ const { getSystemErrorName } = require('util');
delete providers.MESSAGEPORT;
delete providers.WORKER;
// TODO(danbev): Test for these
delete providers.ARGON2REQUEST;
delete providers.JSUDPWRAP;
delete providers.KEYPAIRGENREQUEST;
delete providers.KEYGENREQUEST;