Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
crypto: add argon2() and argon2Sync() methods
Co-authored-by: Filip Skokan <[email protected]>
Co-authored-by: James M Snell <[email protected]>
  • Loading branch information
3 people committed Aug 19, 2025
commit f05b81e6e172aa40e0b42ccb19f54e169158d30c
3 changes: 3 additions & 0 deletions benchmark/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
47 changes: 47 additions & 0 deletions benchmark/crypto/argon2.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
'use strict';

const common = require('../common.js');
const assert = require('node:assert');
const {
argon2,
argon2Sync,
randomBytes,
} = require('node:crypto');

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);
}
99 changes: 99 additions & 0 deletions deps/ncrypto/ncrypto.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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_MINOR >= 2
#include <openssl/thread.h>
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 you confirm if there's any guards needed here for boring?

#endif
#endif
#if OPENSSL_WITH_PQC
struct PQCMapping {
Expand Down Expand Up @@ -1868,6 +1873,100 @@ DataPointer pbkdf2(const Digest& md,
return {};
}

#if OPENSSL_VERSION_PREREQ(3, 2)
#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.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(
Expand Down
17 changes: 17 additions & 0 deletions deps/ncrypto/ncrypto.h
Original file line number Diff line number Diff line change
Expand Up @@ -1557,6 +1557,23 @@ DataPointer pbkdf2(const Digest& md,
uint32_t iterations,
size_t length);

#if OPENSSL_VERSION_PREREQ(3, 2)
#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"
Expand Down
166 changes: 166 additions & 0 deletions doc/api/crypto.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Copy link
Contributor Author

@ranisalt ranisalt Aug 13, 2025

Choose a reason for hiding this comment

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

I just found out about WICG/webcrypto-modern-algos and their specification for Argon2Params uses secretValue instead of secret

Is that something we want to follow, so that the implementations are closer?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Also, this implementation so far ignores the version param, assuming it will never be changed. v1.3 has been released and not changed since 2016 (v1.0 was released in 2015)

Copy link
Member

Choose a reason for hiding this comment

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

node:crypto doesn't need to be aligned on either of these.

Copy link
Member

@panva panva Aug 14, 2025

Choose a reason for hiding this comment

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

And i've already got the Argon2 Web Cryptography part queued up to go when this one and #59365 land.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Cool, that looks a lot like the generic KDF proposal that was mentioned very early in the history of this PR.

If you need a second pair of hands for anything let me know, I'm eager to participate 😄

This PR is otherwise complete, I believe.

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
Expand Down Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions doc/api/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
Loading