Skip to content

Commit cf81cfa

Browse files
ranisaltpanvajasnell
committed
crypto: add argon2() and argon2Sync() methods
Co-authored-by: Filip Skokan <[email protected]> Co-authored-by: James M Snell <[email protected]>
1 parent f7a2ba7 commit cf81cfa

File tree

20 files changed

+969
-19
lines changed

20 files changed

+969
-19
lines changed

benchmark/common.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,9 @@ class Benchmark {
8181
if (typeof value === 'number') {
8282
if (key === 'dur' || key === 'duration') {
8383
value = 0.05;
84+
} else if (key === 'memory') {
85+
// minimum Argon2 memcost with 1 lane is 8
86+
value = 8;
8487
} else if (value > 1) {
8588
value = 1;
8689
}

benchmark/crypto/argon2.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
'use strict';
2+
3+
const common = require('../common.js');
4+
const assert = require('node:assert');
5+
const {
6+
argon2,
7+
argon2Sync,
8+
randomBytes,
9+
} = require('node:crypto');
10+
11+
const bench = common.createBenchmark(main, {
12+
mode: ['sync', 'async'],
13+
algorithm: ['argon2d', 'argon2i', 'argon2id'],
14+
passes: [1, 3],
15+
parallelism: [2, 4, 8],
16+
memory: [2 ** 11, 2 ** 16, 2 ** 21],
17+
n: [50],
18+
});
19+
20+
function measureSync(n, algorithm, message, nonce, options) {
21+
bench.start();
22+
for (let i = 0; i < n; ++i)
23+
argon2Sync(algorithm, { ...options, message, nonce, tagLength: 64 });
24+
bench.end(n);
25+
}
26+
27+
function measureAsync(n, algorithm, message, nonce, options) {
28+
let remaining = n;
29+
function done(err) {
30+
assert.ifError(err);
31+
if (--remaining === 0)
32+
bench.end(n);
33+
}
34+
bench.start();
35+
for (let i = 0; i < n; ++i)
36+
argon2(algorithm, { ...options, message, nonce, tagLength: 64 }, done);
37+
}
38+
39+
function main({ n, mode, algorithm, ...options }) {
40+
// Message, nonce, secret, associated data & tag length do not affect performance
41+
const message = randomBytes(32);
42+
const nonce = randomBytes(16);
43+
if (mode === 'sync')
44+
measureSync(n, algorithm, message, nonce, options);
45+
else
46+
measureAsync(n, algorithm, message, nonce, options);
47+
}

deps/ncrypto/ncrypto.cc

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,12 @@
1010
#include <algorithm>
1111
#include <cstring>
1212
#if OPENSSL_VERSION_MAJOR >= 3
13+
#include <openssl/core_names.h>
14+
#include <openssl/params.h>
1315
#include <openssl/provider.h>
16+
#if OPENSSL_VERSION_MINOR >= 2
17+
#include <openssl/thread.h>
18+
#endif
1419
#endif
1520

1621
// EVP_PKEY_CTX_set_dsa_paramgen_q_bits was added in OpenSSL 1.1.1e.
@@ -1852,6 +1857,100 @@ DataPointer pbkdf2(const Digest& md,
18521857
return {};
18531858
}
18541859

1860+
#if OPENSSL_VERSION_PREREQ(3, 2)
1861+
#ifndef OPENSSL_NO_ARGON2
1862+
DataPointer argon2(const Buffer<const char>& pass,
1863+
const Buffer<const unsigned char>& salt,
1864+
uint32_t lanes,
1865+
size_t length,
1866+
uint32_t memcost,
1867+
uint32_t iter,
1868+
uint32_t version,
1869+
const Buffer<const unsigned char>& secret,
1870+
const Buffer<const unsigned char>& ad,
1871+
Argon2Type type) {
1872+
ClearErrorOnReturn clearErrorOnReturn;
1873+
1874+
std::string_view algorithm;
1875+
switch (type) {
1876+
case Argon2Type::ARGON2I:
1877+
algorithm = "ARGON2I";
1878+
break;
1879+
case Argon2Type::ARGON2D:
1880+
algorithm = "ARGON2D";
1881+
break;
1882+
case Argon2Type::ARGON2ID:
1883+
algorithm = "ARGON2ID";
1884+
break;
1885+
default:
1886+
// Invalid Argon2 type
1887+
return {};
1888+
}
1889+
1890+
// creates a new library context to avoid locking when running concurrently
1891+
auto ctx = DeleteFnPtr<OSSL_LIB_CTX, OSSL_LIB_CTX_free>{OSSL_LIB_CTX_new()};
1892+
if (!ctx) {
1893+
return {};
1894+
}
1895+
1896+
// required if threads > 1
1897+
if (lanes > 1 && OSSL_set_max_threads(ctx.get(), lanes) != 1) {
1898+
return {};
1899+
}
1900+
1901+
auto kdf = DeleteFnPtr<EVP_KDF, EVP_KDF_free>{
1902+
EVP_KDF_fetch(ctx.get(), algorithm.data(), nullptr)};
1903+
if (!kdf) {
1904+
return {};
1905+
}
1906+
1907+
auto kctx =
1908+
DeleteFnPtr<EVP_KDF_CTX, EVP_KDF_CTX_free>{EVP_KDF_CTX_new(kdf.get())};
1909+
if (!kctx) {
1910+
return {};
1911+
}
1912+
1913+
std::vector<OSSL_PARAM> params;
1914+
params.reserve(9);
1915+
1916+
params.push_back(OSSL_PARAM_construct_octet_string(
1917+
OSSL_KDF_PARAM_PASSWORD, const_cast<char*>(pass.data), pass.len));
1918+
params.push_back(OSSL_PARAM_construct_octet_string(
1919+
OSSL_KDF_PARAM_SALT, const_cast<unsigned char*>(salt.data), salt.len));
1920+
params.push_back(OSSL_PARAM_construct_uint32(OSSL_KDF_PARAM_THREADS, &lanes));
1921+
params.push_back(
1922+
OSSL_PARAM_construct_uint32(OSSL_KDF_PARAM_ARGON2_LANES, &lanes));
1923+
params.push_back(
1924+
OSSL_PARAM_construct_uint32(OSSL_KDF_PARAM_ARGON2_MEMCOST, &memcost));
1925+
params.push_back(OSSL_PARAM_construct_uint32(OSSL_KDF_PARAM_ITER, &iter));
1926+
1927+
if (ad.len != 0) {
1928+
params.push_back(OSSL_PARAM_construct_octet_string(
1929+
OSSL_KDF_PARAM_ARGON2_AD, const_cast<unsigned char*>(ad.data), ad.len));
1930+
}
1931+
1932+
if (secret.len != 0) {
1933+
params.push_back(OSSL_PARAM_construct_octet_string(
1934+
OSSL_KDF_PARAM_SECRET,
1935+
const_cast<unsigned char*>(secret.data),
1936+
secret.len));
1937+
}
1938+
1939+
params.push_back(OSSL_PARAM_construct_end());
1940+
1941+
auto dp = DataPointer::Alloc(length);
1942+
if (dp && EVP_KDF_derive(kctx.get(),
1943+
reinterpret_cast<unsigned char*>(dp.get()),
1944+
length,
1945+
params.data()) == 1) {
1946+
return dp;
1947+
}
1948+
1949+
return {};
1950+
}
1951+
#endif
1952+
#endif
1953+
18551954
// ============================================================================
18561955

18571956
EVPKeyPointer::PrivateKeyEncodingConfig::PrivateKeyEncodingConfig(

deps/ncrypto/ncrypto.h

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1553,6 +1553,23 @@ DataPointer pbkdf2(const Digest& md,
15531553
uint32_t iterations,
15541554
size_t length);
15551555

1556+
#if OPENSSL_VERSION_PREREQ(3, 2)
1557+
#ifndef OPENSSL_NO_ARGON2
1558+
enum class Argon2Type { ARGON2D, ARGON2I, ARGON2ID };
1559+
1560+
DataPointer argon2(const Buffer<const char>& pass,
1561+
const Buffer<const unsigned char>& salt,
1562+
uint32_t lanes,
1563+
size_t length,
1564+
uint32_t memcost,
1565+
uint32_t iter,
1566+
uint32_t version,
1567+
const Buffer<const unsigned char>& secret,
1568+
const Buffer<const unsigned char>& ad,
1569+
Argon2Type type);
1570+
#endif
1571+
#endif
1572+
15561573
// ============================================================================
15571574
// Version metadata
15581575
#define NCRYPTO_VERSION "0.0.1"

doc/api/crypto.md

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2958,6 +2958,171 @@ Does not perform any other validation checks on the certificate.
29582958

29592959
## `node:crypto` module methods and properties
29602960

2961+
### `crypto.argon2(algorithm, parameters, callback)`
2962+
2963+
<!-- YAML
2964+
added: REPLACEME
2965+
-->
2966+
2967+
> Stability: 1.2 - Release candidate
2968+
2969+
* `algorithm` {string} Variant of Argon2, one of `"argon2d"`, `"argon2i"` or `"argon2id"`.
2970+
* `parameters` {Object}
2971+
* `message` {string|ArrayBuffer|Buffer|TypedArray|DataView} REQUIRED, this is the password for password
2972+
hashing applications of Argon2.
2973+
* `nonce` {string|ArrayBuffer|Buffer|TypedArray|DataView} REQUIRED, must be at
2974+
least 8 bytes long. This is the salt for password hashing applications of Argon2.
2975+
* `parallelism` {number} REQUIRED, degree of parallelism determines how many computational chains (lanes)
2976+
can be run. Must be greater than 1 and less than `2**24-1`.
2977+
* `tagLength` {number} REQUIRED, the length of the key to generate. Must be greater than 4 and
2978+
less than `2**32-1`.
2979+
* `memory` {number} REQUIRED, memory cost in 1KiB blocks. Must be greater than
2980+
`8 * parallelism` and less than `2**32-1`. The actual number of blocks is rounded
2981+
down to the nearest multiple of `4 * parallelism`.
2982+
* `passes` {number} REQUIRED, number of passes (iterations). Must be greater than 1 and less
2983+
than `2**32-1`.
2984+
* `secret` {string|ArrayBuffer|Buffer|TypedArray|DataView|undefined} OPTIONAL, Random additional input,
2985+
similar to the salt, that should **NOT** be stored with the derived key. This is known as pepper in
2986+
password hashing applications. If used, must have a length not greater than `2**32-1` bytes.
2987+
* `associatedData` {string|ArrayBuffer|Buffer|TypedArray|DataView|undefined} OPTIONAL, Additional data to
2988+
be added to the hash, functionally equivalent to salt or secret, but meant for
2989+
non-random data. If used, must have a length not greater than `2**32-1` bytes.
2990+
* `callback` {Function}
2991+
* `err` {Error}
2992+
* `derivedKey` {Buffer}
2993+
2994+
Provides an asynchronous [Argon2][] implementation. Argon2 is a password-based
2995+
key derivation function that is designed to be expensive computationally and
2996+
memory-wise in order to make brute-force attacks unrewarding.
2997+
2998+
The `nonce` should be as unique as possible. It is recommended that a nonce is
2999+
random and at least 16 bytes long. See [NIST SP 800-132][] for details.
3000+
3001+
When passing strings for `message`, `nonce`, `secret` or `associatedData`, please
3002+
consider [caveats when using strings as inputs to cryptographic APIs][].
3003+
3004+
The `callback` function is called with two arguments: `err` and `derivedKey`.
3005+
`err` is an exception object when key derivation fails, otherwise `err` is
3006+
`null`. `derivedKey` is passed to the callback as a [`Buffer`][].
3007+
3008+
An exception is thrown when any of the input arguments specify invalid values
3009+
or types.
3010+
3011+
```mjs
3012+
const { argon2, randomBytes } = await import('node:crypto');
3013+
3014+
const parameters = {
3015+
message: 'password',
3016+
nonce: randomBytes(16),
3017+
parallelism: 4,
3018+
tagLength: 64,
3019+
memory: 65536,
3020+
passes: 3,
3021+
};
3022+
3023+
argon2('argon2id', parameters, (err, derivedKey) => {
3024+
if (err) throw err;
3025+
console.log(derivedKey.toString('hex')); // 'af91dad...9520f15'
3026+
});
3027+
```
3028+
3029+
```cjs
3030+
const { argon2, randomBytes } = require('node:crypto');
3031+
3032+
const parameters = {
3033+
message: 'password',
3034+
nonce: randomBytes(16),
3035+
parallelism: 4,
3036+
tagLength: 64,
3037+
memory: 65536,
3038+
passes: 3,
3039+
};
3040+
3041+
argon2('argon2id', parameters, (err, derivedKey) => {
3042+
if (err) throw err;
3043+
console.log(derivedKey.toString('hex')); // 'af91dad...9520f15'
3044+
});
3045+
```
3046+
3047+
### `crypto.argon2Sync(algorithm, parameters)`
3048+
3049+
<!-- YAML
3050+
added: REPLACEME
3051+
-->
3052+
3053+
> Stability: 1.2 - Release candidate
3054+
3055+
* `algorithm` {string} Variant of Argon2, one of `"argon2d"`, `"argon2i"` or `"argon2id"`.
3056+
* `parameters` {Object}
3057+
* `message` {string|ArrayBuffer|Buffer|TypedArray|DataView} REQUIRED, this is the password for password
3058+
hashing applications of Argon2.
3059+
* `nonce` {string|ArrayBuffer|Buffer|TypedArray|DataView} REQUIRED, must be at
3060+
least 8 bytes long. This is the salt for password hashing applications of Argon2.
3061+
* `parallelism` {number} REQUIRED, degree of parallelism determines how many computational chains (lanes)
3062+
can be run. Must be greater than 1 and less than `2**24-1`.
3063+
* `tagLength` {number} REQUIRED, the length of the key to generate. Must be greater than 4 and
3064+
less than `2**32-1`.
3065+
* `memory` {number} REQUIRED, memory cost in 1KiB blocks. Must be greater than
3066+
`8 * parallelism` and less than `2**32-1`. The actual number of blocks is rounded
3067+
down to the nearest multiple of `4 * parallelism`.
3068+
* `passes` {number} REQUIRED, number of passes (iterations). Must be greater than 1 and less
3069+
than `2**32-1`.
3070+
* `secret` {string|ArrayBuffer|Buffer|TypedArray|DataView|undefined} OPTIONAL, Random additional input,
3071+
similar to the salt, that should **NOT** be stored with the derived key. This is known as pepper in
3072+
password hashing applications. If used, must have a length not greater than `2**32-1` bytes.
3073+
* `associatedData` {string|ArrayBuffer|Buffer|TypedArray|DataView|undefined} OPTIONAL, Additional data to
3074+
be added to the hash, functionally equivalent to salt or secret, but meant for
3075+
non-random data. If used, must have a length not greater than `2**32-1` bytes.
3076+
* Returns: {Buffer}
3077+
3078+
Provides a synchronous [Argon2][] implementation. Argon2 is a password-based
3079+
key derivation function that is designed to be expensive computationally and
3080+
memory-wise in order to make brute-force attacks unrewarding.
3081+
3082+
The `nonce` should be as unique as possible. It is recommended that a nonce is
3083+
random and at least 16 bytes long. See [NIST SP 800-132][] for details.
3084+
3085+
When passing strings for `message`, `nonce`, `secret` or `associatedData`, please
3086+
consider [caveats when using strings as inputs to cryptographic APIs][].
3087+
3088+
An exception is thrown when key derivation fails, otherwise the derived key is
3089+
returned as a [`Buffer`][].
3090+
3091+
An exception is thrown when any of the input arguments specify invalid values
3092+
or types.
3093+
3094+
```mjs
3095+
const { argon2Sync, randomBytes } = await import('node:crypto');
3096+
3097+
const parameters = {
3098+
message: 'password',
3099+
nonce: randomBytes(16),
3100+
parallelism: 4,
3101+
tagLength: 64,
3102+
memory: 65536,
3103+
passes: 3,
3104+
};
3105+
3106+
const derivedKey = argon2Sync('argon2id', parameters);
3107+
console.log(derivedKey.toString('hex')); // 'af91dad...9520f15'
3108+
```
3109+
3110+
```cjs
3111+
const { argon2Sync, randomBytes } = require('node:crypto');
3112+
3113+
const parameters = {
3114+
message: 'password',
3115+
nonce: randomBytes(16),
3116+
parallelism: 4,
3117+
tagLength: 64,
3118+
memory: 65536,
3119+
passes: 3,
3120+
};
3121+
3122+
const derivedKey = argon2Sync('argon2id', parameters);
3123+
console.log(derivedKey.toString('hex')); // 'af91dad...9520f15'
3124+
```
3125+
29613126
### `crypto.checkPrime(candidate[, options], callback)`
29623127

29633128
<!-- YAML
@@ -6268,6 +6433,7 @@ See the [list of SSL OP Flags][] for details.
62686433
[`verify.verify()`]: #verifyverifyobject-signature-signatureencoding
62696434
[`x509.fingerprint256`]: #x509fingerprint256
62706435
[`x509.verify(publicKey)`]: #x509verifypublickey
6436+
[argon2]: https://www.rfc-editor.org/rfc/rfc9106.html
62716437
[caveats when using strings as inputs to cryptographic APIs]: #using-strings-as-inputs-to-cryptographic-apis
62726438
[certificate object]: tls.md#certificate-object
62736439
[encoding]: buffer.md#buffers-and-character-encodings

doc/api/errors.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -826,6 +826,12 @@ when an error occurs (and is caught) during the creation of the
826826
context, for example, when the allocation fails or the maximum call stack
827827
size is reached when the context is created.
828828

829+
<a id="ERR_CRYPTO_ARGON2_NOT_SUPPORTED"></a>
830+
831+
### `ERR_CRYPTO_ARGON2_NOT_SUPPORTED`
832+
833+
Argon2 is not supported by the current version of OpenSSL being used.
834+
829835
<a id="ERR_CRYPTO_CUSTOM_ENGINE_NOT_SUPPORTED"></a>
830836

831837
### `ERR_CRYPTO_CUSTOM_ENGINE_NOT_SUPPORTED`

0 commit comments

Comments
 (0)