diff --git a/benchmark/common.js b/benchmark/common.js index 0d3dbef24d..0bb3fa3dfa 100644 --- a/benchmark/common.js +++ b/benchmark/common.js @@ -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; } diff --git a/benchmark/crypto/argon2.js b/benchmark/crypto/argon2.js new file mode 100644 index 0000000000..ce6a824233 --- /dev/null +++ b/benchmark/crypto/argon2.js @@ -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); +} diff --git a/deps/ncrypto/ncrypto.cc b/deps/ncrypto/ncrypto.cc index c6ef072e05..d451a20c28 100644 --- a/deps/ncrypto/ncrypto.cc +++ b/deps/ncrypto/ncrypto.cc @@ -10,7 +10,12 @@ #include #include #if OPENSSL_VERSION_MAJOR >= 3 +#include +#include #include +#if OPENSSL_VERSION_NUMBER >= 0x30200000L +#include +#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& pass, + const Buffer& salt, + uint32_t lanes, + size_t length, + uint32_t memcost, + uint32_t iter, + uint32_t version, + const Buffer& secret, + const Buffer& 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_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_fetch(ctx.get(), algorithm.data(), nullptr)}; + if (!kdf) { + return {}; + } + + auto kctx = + DeleteFnPtr{EVP_KDF_CTX_new(kdf.get())}; + if (!kctx) { + return {}; + } + + std::vector params; + params.reserve(9); + + params.push_back(OSSL_PARAM_construct_octet_string( + OSSL_KDF_PARAM_PASSWORD, + const_cast(pass.len > 0 ? pass.data : ""), + pass.len)); + params.push_back(OSSL_PARAM_construct_octet_string( + OSSL_KDF_PARAM_SALT, const_cast(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(ad.data), ad.len)); + } + + if (secret.len != 0) { + params.push_back(OSSL_PARAM_construct_octet_string( + OSSL_KDF_PARAM_SECRET, + const_cast(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(dp.get()), + length, + params.data()) == 1) { + return dp; + } + + return {}; +} +#endif +#endif + // ============================================================================ EVPKeyPointer::PrivateKeyEncodingConfig::PrivateKeyEncodingConfig( diff --git a/deps/ncrypto/ncrypto.h b/deps/ncrypto/ncrypto.h index 42f9cf6aa2..b2af688de3 100644 --- a/deps/ncrypto/ncrypto.h +++ b/deps/ncrypto/ncrypto.h @@ -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& pass, + const Buffer& salt, + uint32_t lanes, + size_t length, + uint32_t memcost, + uint32_t iter, + uint32_t version, + const Buffer& secret, + const Buffer& ad, + Argon2Type type); +#endif +#endif + // ============================================================================ // Version metadata #define NCRYPTO_VERSION "0.0.1" diff --git a/doc/api/crypto.md b/doc/api/crypto.md index a95a2a4173..85c8af86e1 100644 --- a/doc/api/crypto.md +++ b/doc/api/crypto.md @@ -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)` + + + +> 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)` + + + +> 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)`