mirror of
https://github.com/zebrajr/node.git
synced 2025-12-06 12:20:27 +01:00
crypto: support ML-KEM, DHKEM, and RSASVE key encapsulation mechanisms
PR-URL: https://github.com/nodejs/node/pull/59491 Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com> Reviewed-By: Tobias Nießen <tniessen@tnie.de> Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com>
This commit is contained in:
parent
70690be494
commit
f8d68d30ae
140
benchmark/crypto/kem.js
Normal file
140
benchmark/crypto/kem.js
Normal file
|
|
@ -0,0 +1,140 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const common = require('../common.js');
|
||||||
|
const { hasOpenSSL } = require('../../test/common/crypto.js');
|
||||||
|
const crypto = require('crypto');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const fixtures_keydir = path.resolve(__dirname, '../../test/fixtures/keys/');
|
||||||
|
|
||||||
|
function readKey(name) {
|
||||||
|
return fs.readFileSync(`${fixtures_keydir}/${name}.pem`, 'utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyFixtures = {};
|
||||||
|
|
||||||
|
if (hasOpenSSL(3, 5)) {
|
||||||
|
keyFixtures['ml-kem-512'] = readKey('ml_kem_512_private');
|
||||||
|
keyFixtures['ml-kem-768'] = readKey('ml_kem_768_private');
|
||||||
|
keyFixtures['ml-kem-1024'] = readKey('ml_kem_1024_private');
|
||||||
|
}
|
||||||
|
if (hasOpenSSL(3, 2)) {
|
||||||
|
keyFixtures['p-256'] = readKey('ec_p256_private');
|
||||||
|
keyFixtures['p-384'] = readKey('ec_p384_private');
|
||||||
|
keyFixtures['p-521'] = readKey('ec_p521_private');
|
||||||
|
keyFixtures.x25519 = readKey('x25519_private');
|
||||||
|
keyFixtures.x448 = readKey('x448_private');
|
||||||
|
}
|
||||||
|
if (hasOpenSSL(3, 0)) {
|
||||||
|
keyFixtures.rsa = readKey('rsa_private_2048');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(keyFixtures).length === 0) {
|
||||||
|
console.log('no supported key types available for this OpenSSL version');
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const bench = common.createBenchmark(main, {
|
||||||
|
keyType: Object.keys(keyFixtures),
|
||||||
|
mode: ['sync', 'async', 'async-parallel'],
|
||||||
|
keyFormat: ['keyObject', 'keyObject.unique'],
|
||||||
|
op: ['encapsulate', 'decapsulate'],
|
||||||
|
n: [1e3],
|
||||||
|
}, {
|
||||||
|
combinationFilter(p) {
|
||||||
|
// "keyObject.unique" allows to compare the result with "keyObject" to
|
||||||
|
// assess whether mutexes over the key material impact the operation
|
||||||
|
return p.keyFormat !== 'keyObject.unique' ||
|
||||||
|
(p.keyFormat === 'keyObject.unique' && p.mode === 'async-parallel');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function measureSync(n, op, privateKey, keys, ciphertexts) {
|
||||||
|
bench.start();
|
||||||
|
for (let i = 0; i < n; ++i) {
|
||||||
|
const key = privateKey || keys[i];
|
||||||
|
if (op === 'encapsulate') {
|
||||||
|
crypto.encapsulate(key);
|
||||||
|
} else {
|
||||||
|
crypto.decapsulate(key, ciphertexts[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bench.end(n);
|
||||||
|
}
|
||||||
|
|
||||||
|
function measureAsync(n, op, privateKey, keys, ciphertexts) {
|
||||||
|
let remaining = n;
|
||||||
|
function done() {
|
||||||
|
if (--remaining === 0)
|
||||||
|
bench.end(n);
|
||||||
|
else
|
||||||
|
one();
|
||||||
|
}
|
||||||
|
|
||||||
|
function one() {
|
||||||
|
const key = privateKey || keys[n - remaining];
|
||||||
|
if (op === 'encapsulate') {
|
||||||
|
crypto.encapsulate(key, done);
|
||||||
|
} else {
|
||||||
|
crypto.decapsulate(key, ciphertexts[n - remaining], done);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bench.start();
|
||||||
|
one();
|
||||||
|
}
|
||||||
|
|
||||||
|
function measureAsyncParallel(n, op, privateKey, keys, ciphertexts) {
|
||||||
|
let remaining = n;
|
||||||
|
function done() {
|
||||||
|
if (--remaining === 0)
|
||||||
|
bench.end(n);
|
||||||
|
}
|
||||||
|
bench.start();
|
||||||
|
for (let i = 0; i < n; ++i) {
|
||||||
|
const key = privateKey || keys[i];
|
||||||
|
if (op === 'encapsulate') {
|
||||||
|
crypto.encapsulate(key, done);
|
||||||
|
} else {
|
||||||
|
crypto.decapsulate(key, ciphertexts[i], done);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function main({ n, mode, keyFormat, keyType, op }) {
|
||||||
|
const pems = [...Buffer.alloc(n)].map(() => keyFixtures[keyType]);
|
||||||
|
const keyObjects = pems.map(crypto.createPrivateKey);
|
||||||
|
|
||||||
|
let privateKey, keys, ciphertexts;
|
||||||
|
|
||||||
|
switch (keyFormat) {
|
||||||
|
case 'keyObject':
|
||||||
|
privateKey = keyObjects[0];
|
||||||
|
break;
|
||||||
|
case 'keyObject.unique':
|
||||||
|
keys = keyObjects;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error('not implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pre-generate ciphertexts for decapsulate operations
|
||||||
|
if (op === 'decapsulate') {
|
||||||
|
if (privateKey) {
|
||||||
|
ciphertexts = [...Buffer.alloc(n)].map(() => crypto.encapsulate(privateKey).ciphertext);
|
||||||
|
} else {
|
||||||
|
ciphertexts = keys.map((key) => crypto.encapsulate(key).ciphertext);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (mode) {
|
||||||
|
case 'sync':
|
||||||
|
measureSync(n, op, privateKey, keys, ciphertexts);
|
||||||
|
break;
|
||||||
|
case 'async':
|
||||||
|
measureAsync(n, op, privateKey, keys, ciphertexts);
|
||||||
|
break;
|
||||||
|
case 'async-parallel':
|
||||||
|
measureAsyncParallel(n, op, privateKey, keys, ciphertexts);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
121
deps/ncrypto/ncrypto.cc
vendored
121
deps/ncrypto/ncrypto.cc
vendored
|
|
@ -4510,4 +4510,125 @@ const Digest Digest::FromName(const char* name) {
|
||||||
return ncrypto::getDigestByName(name);
|
return ncrypto::getDigestByName(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// KEM Implementation
|
||||||
|
#if OPENSSL_VERSION_MAJOR >= 3
|
||||||
|
#if !OPENSSL_VERSION_PREREQ(3, 5)
|
||||||
|
bool KEM::SetOperationParameter(EVP_PKEY_CTX* ctx, const EVPKeyPointer& key) {
|
||||||
|
const char* operation = nullptr;
|
||||||
|
|
||||||
|
switch (EVP_PKEY_id(key.get())) {
|
||||||
|
case EVP_PKEY_RSA:
|
||||||
|
operation = OSSL_KEM_PARAM_OPERATION_RSASVE;
|
||||||
|
break;
|
||||||
|
#if OPENSSL_VERSION_PREREQ(3, 2)
|
||||||
|
case EVP_PKEY_EC:
|
||||||
|
case EVP_PKEY_X25519:
|
||||||
|
case EVP_PKEY_X448:
|
||||||
|
operation = OSSL_KEM_PARAM_OPERATION_DHKEM;
|
||||||
|
break;
|
||||||
|
#endif
|
||||||
|
default:
|
||||||
|
unreachable();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (operation != nullptr) {
|
||||||
|
OSSL_PARAM params[] = {
|
||||||
|
OSSL_PARAM_utf8_string(
|
||||||
|
OSSL_KEM_PARAM_OPERATION, const_cast<char*>(operation), 0),
|
||||||
|
OSSL_PARAM_END};
|
||||||
|
|
||||||
|
if (EVP_PKEY_CTX_set_params(ctx, params) <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
std::optional<KEM::EncapsulateResult> KEM::Encapsulate(
|
||||||
|
const EVPKeyPointer& public_key) {
|
||||||
|
ClearErrorOnReturn clear_error_on_return;
|
||||||
|
|
||||||
|
auto ctx = public_key.newCtx();
|
||||||
|
if (!ctx) return std::nullopt;
|
||||||
|
|
||||||
|
if (EVP_PKEY_encapsulate_init(ctx.get(), nullptr) <= 0) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
#if !OPENSSL_VERSION_PREREQ(3, 5)
|
||||||
|
if (!SetOperationParameter(ctx.get(), public_key)) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Determine output buffer sizes
|
||||||
|
size_t ciphertext_len = 0;
|
||||||
|
size_t shared_key_len = 0;
|
||||||
|
|
||||||
|
if (EVP_PKEY_encapsulate(
|
||||||
|
ctx.get(), nullptr, &ciphertext_len, nullptr, &shared_key_len) <= 0) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ciphertext = DataPointer::Alloc(ciphertext_len);
|
||||||
|
auto shared_key = DataPointer::Alloc(shared_key_len);
|
||||||
|
if (!ciphertext || !shared_key) return std::nullopt;
|
||||||
|
|
||||||
|
if (EVP_PKEY_encapsulate(ctx.get(),
|
||||||
|
static_cast<unsigned char*>(ciphertext.get()),
|
||||||
|
&ciphertext_len,
|
||||||
|
static_cast<unsigned char*>(shared_key.get()),
|
||||||
|
&shared_key_len) <= 0) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
return EncapsulateResult(std::move(ciphertext), std::move(shared_key));
|
||||||
|
}
|
||||||
|
|
||||||
|
DataPointer KEM::Decapsulate(const EVPKeyPointer& private_key,
|
||||||
|
const Buffer<const void>& ciphertext) {
|
||||||
|
ClearErrorOnReturn clear_error_on_return;
|
||||||
|
|
||||||
|
auto ctx = private_key.newCtx();
|
||||||
|
if (!ctx) return {};
|
||||||
|
|
||||||
|
if (EVP_PKEY_decapsulate_init(ctx.get(), nullptr) <= 0) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
#if !OPENSSL_VERSION_PREREQ(3, 5)
|
||||||
|
if (!SetOperationParameter(ctx.get(), private_key)) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// First pass: determine shared secret size
|
||||||
|
size_t shared_key_len = 0;
|
||||||
|
if (EVP_PKEY_decapsulate(ctx.get(),
|
||||||
|
nullptr,
|
||||||
|
&shared_key_len,
|
||||||
|
static_cast<const unsigned char*>(ciphertext.data),
|
||||||
|
ciphertext.len) <= 0) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
auto shared_key = DataPointer::Alloc(shared_key_len);
|
||||||
|
if (!shared_key) return {};
|
||||||
|
|
||||||
|
if (EVP_PKEY_decapsulate(ctx.get(),
|
||||||
|
static_cast<unsigned char*>(shared_key.get()),
|
||||||
|
&shared_key_len,
|
||||||
|
static_cast<const unsigned char*>(ciphertext.data),
|
||||||
|
ciphertext.len) <= 0) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return shared_key;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // OPENSSL_VERSION_MAJOR >= 3
|
||||||
|
|
||||||
} // namespace ncrypto
|
} // namespace ncrypto
|
||||||
|
|
|
||||||
34
deps/ncrypto/ncrypto.h
vendored
34
deps/ncrypto/ncrypto.h
vendored
|
|
@ -1574,6 +1574,40 @@ DataPointer argon2(const Buffer<const char>& pass,
|
||||||
#endif
|
#endif
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// KEM (Key Encapsulation Mechanism)
|
||||||
|
#if OPENSSL_VERSION_MAJOR >= 3
|
||||||
|
|
||||||
|
class KEM final {
|
||||||
|
public:
|
||||||
|
struct EncapsulateResult {
|
||||||
|
DataPointer ciphertext;
|
||||||
|
DataPointer shared_key;
|
||||||
|
|
||||||
|
EncapsulateResult() = default;
|
||||||
|
EncapsulateResult(DataPointer ct, DataPointer sk)
|
||||||
|
: ciphertext(std::move(ct)), shared_key(std::move(sk)) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Encapsulate a shared secret using KEM with a public key.
|
||||||
|
// Returns both the ciphertext and shared secret.
|
||||||
|
static std::optional<EncapsulateResult> Encapsulate(
|
||||||
|
const EVPKeyPointer& public_key);
|
||||||
|
|
||||||
|
// Decapsulate a shared secret using KEM with a private key and ciphertext.
|
||||||
|
// Returns the shared secret.
|
||||||
|
static DataPointer Decapsulate(const EVPKeyPointer& private_key,
|
||||||
|
const Buffer<const void>& ciphertext);
|
||||||
|
|
||||||
|
private:
|
||||||
|
#if !OPENSSL_VERSION_PREREQ(3, 5)
|
||||||
|
static bool SetOperationParameter(EVP_PKEY_CTX* ctx,
|
||||||
|
const EVPKeyPointer& key);
|
||||||
|
#endif
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // OPENSSL_VERSION_MAJOR >= 3
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Version metadata
|
// Version metadata
|
||||||
#define NCRYPTO_VERSION "0.0.1"
|
#define NCRYPTO_VERSION "0.0.1"
|
||||||
|
|
|
||||||
|
|
@ -3741,6 +3741,40 @@ the corresponding digest algorithm. This does not work for all signature
|
||||||
algorithms, such as `'ecdsa-with-SHA256'`, so it is best to always use digest
|
algorithms, such as `'ecdsa-with-SHA256'`, so it is best to always use digest
|
||||||
algorithm names.
|
algorithm names.
|
||||||
|
|
||||||
|
### `crypto.decapsulate(key, ciphertext[, callback])`
|
||||||
|
|
||||||
|
<!-- YAML
|
||||||
|
added: REPLACEME
|
||||||
|
-->
|
||||||
|
|
||||||
|
> Stability: 1.2 - Release candidate
|
||||||
|
|
||||||
|
* `key` {Object|string|ArrayBuffer|Buffer|TypedArray|DataView|KeyObject} Private Key
|
||||||
|
* `ciphertext` {ArrayBuffer|Buffer|TypedArray|DataView}
|
||||||
|
* `callback` {Function}
|
||||||
|
* `err` {Error}
|
||||||
|
* `sharedKey` {Buffer}
|
||||||
|
* Returns: {Buffer} if the `callback` function is not provided.
|
||||||
|
|
||||||
|
<!--lint enable maximum-line-length remark-lint-->
|
||||||
|
|
||||||
|
Key decapsulation using a KEM algorithm with a private key.
|
||||||
|
|
||||||
|
Supported key types and their KEM algorithms are:
|
||||||
|
|
||||||
|
* `'rsa'`[^openssl30] RSA Secret Value Encapsulation
|
||||||
|
* `'ec'`[^openssl32] DHKEM(P-256, HKDF-SHA256), DHKEM(P-384, HKDF-SHA256), DHKEM(P-521, HKDF-SHA256)
|
||||||
|
* `'x25519'`[^openssl32] DHKEM(X25519, HKDF-SHA256)
|
||||||
|
* `'x448'`[^openssl32] DHKEM(X448, HKDF-SHA512)
|
||||||
|
* `'ml-kem-512'`[^openssl35] ML-KEM
|
||||||
|
* `'ml-kem-768'`[^openssl35] ML-KEM
|
||||||
|
* `'ml-kem-1024'`[^openssl35] ML-KEM
|
||||||
|
|
||||||
|
If `key` is not a [`KeyObject`][], this function behaves as if `key` had been
|
||||||
|
passed to [`crypto.createPrivateKey()`][].
|
||||||
|
|
||||||
|
If the `callback` function is provided this function uses libuv's threadpool.
|
||||||
|
|
||||||
### `crypto.diffieHellman(options[, callback])`
|
### `crypto.diffieHellman(options[, callback])`
|
||||||
|
|
||||||
<!-- YAML
|
<!-- YAML
|
||||||
|
|
@ -3767,6 +3801,43 @@ ECDH operation.
|
||||||
|
|
||||||
If the `callback` function is provided this function uses libuv's threadpool.
|
If the `callback` function is provided this function uses libuv's threadpool.
|
||||||
|
|
||||||
|
### `crypto.encapsulate(key[, callback])`
|
||||||
|
|
||||||
|
<!-- YAML
|
||||||
|
added: REPLACEME
|
||||||
|
-->
|
||||||
|
|
||||||
|
> Stability: 1.2 - Release candidate
|
||||||
|
|
||||||
|
* `key` {Object|string|ArrayBuffer|Buffer|TypedArray|DataView|KeyObject} Public Key
|
||||||
|
* `callback` {Function}
|
||||||
|
* `err` {Error}
|
||||||
|
* `result` {Object}
|
||||||
|
* `sharedKey` {Buffer}
|
||||||
|
* `ciphertext` {Buffer}
|
||||||
|
* Returns: {Object} if the `callback` function is not provided.
|
||||||
|
* `sharedKey` {Buffer}
|
||||||
|
* `ciphertext` {Buffer}
|
||||||
|
|
||||||
|
<!--lint enable maximum-line-length remark-lint-->
|
||||||
|
|
||||||
|
Key encapsulation using a KEM algorithm with a public key.
|
||||||
|
|
||||||
|
Supported key types and their KEM algorithms are:
|
||||||
|
|
||||||
|
* `'rsa'`[^openssl30] RSA Secret Value Encapsulation
|
||||||
|
* `'ec'`[^openssl32] DHKEM(P-256, HKDF-SHA256), DHKEM(P-384, HKDF-SHA256), DHKEM(P-521, HKDF-SHA256)
|
||||||
|
* `'x25519'`[^openssl32] DHKEM(X25519, HKDF-SHA256)
|
||||||
|
* `'x448'`[^openssl32] DHKEM(X448, HKDF-SHA512)
|
||||||
|
* `'ml-kem-512'`[^openssl35] ML-KEM
|
||||||
|
* `'ml-kem-768'`[^openssl35] ML-KEM
|
||||||
|
* `'ml-kem-1024'`[^openssl35] ML-KEM
|
||||||
|
|
||||||
|
If `key` is not a [`KeyObject`][], this function behaves as if `key` had been
|
||||||
|
passed to [`crypto.createPublicKey()`][].
|
||||||
|
|
||||||
|
If the `callback` function is provided this function uses libuv's threadpool.
|
||||||
|
|
||||||
### `crypto.fips`
|
### `crypto.fips`
|
||||||
|
|
||||||
<!-- YAML
|
<!-- YAML
|
||||||
|
|
@ -6366,6 +6437,10 @@ See the [list of SSL OP Flags][] for details.
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
[^openssl30]: Requires OpenSSL >= 3.0
|
||||||
|
|
||||||
|
[^openssl32]: Requires OpenSSL >= 3.2
|
||||||
|
|
||||||
[^openssl35]: Requires OpenSSL >= 3.5
|
[^openssl35]: Requires OpenSSL >= 3.5
|
||||||
|
|
||||||
[AEAD algorithms]: https://en.wikipedia.org/wiki/Authenticated_encryption
|
[AEAD algorithms]: https://en.wikipedia.org/wiki/Authenticated_encryption
|
||||||
|
|
|
||||||
|
|
@ -1065,6 +1065,17 @@ Key's Elliptic Curve is not registered for use in the
|
||||||
Key's Asymmetric Key Type is not registered for use in the
|
Key's Asymmetric Key Type is not registered for use in the
|
||||||
[JSON Web Key Types Registry][].
|
[JSON Web Key Types Registry][].
|
||||||
|
|
||||||
|
<a id="ERR_CRYPTO_KEM_NOT_SUPPORTED"></a>
|
||||||
|
|
||||||
|
### `ERR_CRYPTO_KEM_NOT_SUPPORTED`
|
||||||
|
|
||||||
|
<!-- YAML
|
||||||
|
added: REPLACEME
|
||||||
|
-->
|
||||||
|
|
||||||
|
Attempted to use KEM operations while Node.js was not compiled with
|
||||||
|
OpenSSL with KEM support.
|
||||||
|
|
||||||
<a id="ERR_CRYPTO_OPERATION_FAILED"></a>
|
<a id="ERR_CRYPTO_OPERATION_FAILED"></a>
|
||||||
|
|
||||||
### `ERR_CRYPTO_OPERATION_FAILED`
|
### `ERR_CRYPTO_OPERATION_FAILED`
|
||||||
|
|
|
||||||
|
|
@ -122,6 +122,10 @@ const {
|
||||||
secureHeapUsed,
|
secureHeapUsed,
|
||||||
} = require('internal/crypto/util');
|
} = require('internal/crypto/util');
|
||||||
const Certificate = require('internal/crypto/certificate');
|
const Certificate = require('internal/crypto/certificate');
|
||||||
|
const {
|
||||||
|
encapsulate,
|
||||||
|
decapsulate,
|
||||||
|
} = require('internal/crypto/kem');
|
||||||
|
|
||||||
let webcrypto;
|
let webcrypto;
|
||||||
function lazyWebCrypto() {
|
function lazyWebCrypto() {
|
||||||
|
|
@ -225,6 +229,8 @@ module.exports = {
|
||||||
setFips,
|
setFips,
|
||||||
verify: verifyOneShot,
|
verify: verifyOneShot,
|
||||||
hash,
|
hash,
|
||||||
|
encapsulate,
|
||||||
|
decapsulate,
|
||||||
|
|
||||||
// Classes
|
// Classes
|
||||||
Certificate,
|
Certificate,
|
||||||
|
|
|
||||||
112
lib/internal/crypto/kem.js
Normal file
112
lib/internal/crypto/kem.js
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const {
|
||||||
|
FunctionPrototypeCall,
|
||||||
|
} = primordials;
|
||||||
|
|
||||||
|
const {
|
||||||
|
codes: {
|
||||||
|
ERR_CRYPTO_KEM_NOT_SUPPORTED,
|
||||||
|
},
|
||||||
|
} = require('internal/errors');
|
||||||
|
|
||||||
|
const {
|
||||||
|
validateFunction,
|
||||||
|
} = require('internal/validators');
|
||||||
|
|
||||||
|
const {
|
||||||
|
kCryptoJobAsync,
|
||||||
|
kCryptoJobSync,
|
||||||
|
KEMDecapsulateJob,
|
||||||
|
KEMEncapsulateJob,
|
||||||
|
} = internalBinding('crypto');
|
||||||
|
|
||||||
|
const {
|
||||||
|
preparePrivateKey,
|
||||||
|
preparePublicOrPrivateKey,
|
||||||
|
} = require('internal/crypto/keys');
|
||||||
|
|
||||||
|
const {
|
||||||
|
getArrayBufferOrView,
|
||||||
|
} = require('internal/crypto/util');
|
||||||
|
|
||||||
|
function encapsulate(key, callback) {
|
||||||
|
if (!KEMEncapsulateJob)
|
||||||
|
throw new ERR_CRYPTO_KEM_NOT_SUPPORTED();
|
||||||
|
|
||||||
|
if (callback !== undefined)
|
||||||
|
validateFunction(callback, 'callback');
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: keyData,
|
||||||
|
format: keyFormat,
|
||||||
|
type: keyType,
|
||||||
|
passphrase: keyPassphrase,
|
||||||
|
} = preparePublicOrPrivateKey(key);
|
||||||
|
|
||||||
|
const job = new KEMEncapsulateJob(
|
||||||
|
callback ? kCryptoJobAsync : kCryptoJobSync,
|
||||||
|
keyData,
|
||||||
|
keyFormat,
|
||||||
|
keyType,
|
||||||
|
keyPassphrase);
|
||||||
|
|
||||||
|
if (!callback) {
|
||||||
|
const { 0: err, 1: result } = job.run();
|
||||||
|
if (err !== undefined)
|
||||||
|
throw err;
|
||||||
|
const { 0: sharedKey, 1: ciphertext } = result;
|
||||||
|
return { sharedKey, ciphertext };
|
||||||
|
}
|
||||||
|
|
||||||
|
job.ondone = (error, result) => {
|
||||||
|
if (error) return FunctionPrototypeCall(callback, job, error);
|
||||||
|
const { 0: sharedKey, 1: ciphertext } = result;
|
||||||
|
FunctionPrototypeCall(callback, job, null, { sharedKey, ciphertext });
|
||||||
|
};
|
||||||
|
job.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
function decapsulate(key, ciphertext, callback) {
|
||||||
|
if (!KEMDecapsulateJob)
|
||||||
|
throw new ERR_CRYPTO_KEM_NOT_SUPPORTED();
|
||||||
|
|
||||||
|
if (callback !== undefined)
|
||||||
|
validateFunction(callback, 'callback');
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: keyData,
|
||||||
|
format: keyFormat,
|
||||||
|
type: keyType,
|
||||||
|
passphrase: keyPassphrase,
|
||||||
|
} = preparePrivateKey(key);
|
||||||
|
|
||||||
|
ciphertext = getArrayBufferOrView(ciphertext, 'ciphertext');
|
||||||
|
|
||||||
|
const job = new KEMDecapsulateJob(
|
||||||
|
callback ? kCryptoJobAsync : kCryptoJobSync,
|
||||||
|
keyData,
|
||||||
|
keyFormat,
|
||||||
|
keyType,
|
||||||
|
keyPassphrase,
|
||||||
|
ciphertext);
|
||||||
|
|
||||||
|
if (!callback) {
|
||||||
|
const { 0: err, 1: result } = job.run();
|
||||||
|
if (err !== undefined)
|
||||||
|
throw err;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
job.ondone = (error, result) => {
|
||||||
|
if (error) return FunctionPrototypeCall(callback, job, error);
|
||||||
|
FunctionPrototypeCall(callback, job, null, result);
|
||||||
|
};
|
||||||
|
job.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
encapsulate,
|
||||||
|
decapsulate,
|
||||||
|
};
|
||||||
|
|
@ -1184,6 +1184,7 @@ E('ERR_CRYPTO_INVALID_JWK', 'Invalid JWK data', TypeError);
|
||||||
E('ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE',
|
E('ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE',
|
||||||
'Invalid key object type %s, expected %s.', TypeError);
|
'Invalid key object type %s, expected %s.', TypeError);
|
||||||
E('ERR_CRYPTO_INVALID_STATE', 'Invalid state for operation %s', Error);
|
E('ERR_CRYPTO_INVALID_STATE', 'Invalid state for operation %s', Error);
|
||||||
|
E('ERR_CRYPTO_KEM_NOT_SUPPORTED', 'KEM is not supported', Error);
|
||||||
E('ERR_CRYPTO_PBKDF2_ERROR', 'PBKDF2 error', Error);
|
E('ERR_CRYPTO_PBKDF2_ERROR', 'PBKDF2 error', Error);
|
||||||
E('ERR_CRYPTO_SCRYPT_NOT_SUPPORTED', 'Scrypt algorithm not supported', Error);
|
E('ERR_CRYPTO_SCRYPT_NOT_SUPPORTED', 'Scrypt algorithm not supported', Error);
|
||||||
// Switch to TypeError. The current implementation does not seem right.
|
// Switch to TypeError. The current implementation does not seem right.
|
||||||
|
|
|
||||||
1
node.gyp
1
node.gyp
|
|
@ -345,6 +345,7 @@
|
||||||
'src/crypto/crypto_context.cc',
|
'src/crypto/crypto_context.cc',
|
||||||
'src/crypto/crypto_ec.cc',
|
'src/crypto/crypto_ec.cc',
|
||||||
'src/crypto/crypto_ml_dsa.cc',
|
'src/crypto/crypto_ml_dsa.cc',
|
||||||
|
'src/crypto/crypto_kem.cc',
|
||||||
'src/crypto/crypto_hmac.cc',
|
'src/crypto/crypto_hmac.cc',
|
||||||
'src/crypto/crypto_random.cc',
|
'src/crypto/crypto_random.cc',
|
||||||
'src/crypto/crypto_rsa.cc',
|
'src/crypto/crypto_rsa.cc',
|
||||||
|
|
|
||||||
262
src/crypto/crypto_kem.cc
Normal file
262
src/crypto/crypto_kem.cc
Normal file
|
|
@ -0,0 +1,262 @@
|
||||||
|
#include "crypto/crypto_kem.h"
|
||||||
|
|
||||||
|
#if OPENSSL_VERSION_MAJOR >= 3
|
||||||
|
|
||||||
|
#include "async_wrap-inl.h"
|
||||||
|
#include "base_object-inl.h"
|
||||||
|
#include "crypto/crypto_keys.h"
|
||||||
|
#include "crypto/crypto_util.h"
|
||||||
|
#include "env-inl.h"
|
||||||
|
#include "memory_tracker-inl.h"
|
||||||
|
#include "node_buffer.h"
|
||||||
|
#include "threadpoolwork-inl.h"
|
||||||
|
#include "v8.h"
|
||||||
|
|
||||||
|
namespace node {
|
||||||
|
|
||||||
|
using ncrypto::EVPKeyPointer;
|
||||||
|
using v8::Array;
|
||||||
|
using v8::FunctionCallbackInfo;
|
||||||
|
using v8::Local;
|
||||||
|
using v8::Maybe;
|
||||||
|
using v8::MaybeLocal;
|
||||||
|
using v8::Nothing;
|
||||||
|
using v8::Object;
|
||||||
|
using v8::Value;
|
||||||
|
|
||||||
|
namespace crypto {
|
||||||
|
|
||||||
|
KEMConfiguration::KEMConfiguration(KEMConfiguration&& other) noexcept
|
||||||
|
: job_mode(other.job_mode),
|
||||||
|
mode(other.mode),
|
||||||
|
key(std::move(other.key)),
|
||||||
|
ciphertext(std::move(other.ciphertext)) {}
|
||||||
|
|
||||||
|
KEMConfiguration& KEMConfiguration::operator=(
|
||||||
|
KEMConfiguration&& other) noexcept {
|
||||||
|
if (&other == this) return *this;
|
||||||
|
this->~KEMConfiguration();
|
||||||
|
return *new (this) KEMConfiguration(std::move(other));
|
||||||
|
}
|
||||||
|
|
||||||
|
void KEMConfiguration::MemoryInfo(MemoryTracker* tracker) const {
|
||||||
|
tracker->TrackField("key", key);
|
||||||
|
if (job_mode == kCryptoJobAsync) {
|
||||||
|
tracker->TrackFieldWithSize("ciphertext", ciphertext.size());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
bool DoKEMEncapsulate(Environment* env,
|
||||||
|
const EVPKeyPointer& public_key,
|
||||||
|
ByteSource* out,
|
||||||
|
CryptoJobMode mode) {
|
||||||
|
auto result = ncrypto::KEM::Encapsulate(public_key);
|
||||||
|
if (!result) {
|
||||||
|
if (mode == kCryptoJobSync) {
|
||||||
|
THROW_ERR_CRYPTO_OPERATION_FAILED(env, "Failed to perform encapsulation");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pack the result: [ciphertext_len][shared_key_len][ciphertext][shared_key]
|
||||||
|
size_t ciphertext_len = result->ciphertext.size();
|
||||||
|
size_t shared_key_len = result->shared_key.size();
|
||||||
|
size_t total_len =
|
||||||
|
sizeof(uint32_t) + sizeof(uint32_t) + ciphertext_len + shared_key_len;
|
||||||
|
|
||||||
|
auto data = ncrypto::DataPointer::Alloc(total_len);
|
||||||
|
if (!data) {
|
||||||
|
if (mode == kCryptoJobSync) {
|
||||||
|
THROW_ERR_CRYPTO_OPERATION_FAILED(env,
|
||||||
|
"Failed to allocate output buffer");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned char* ptr = static_cast<unsigned char*>(data.get());
|
||||||
|
|
||||||
|
// Write size headers
|
||||||
|
*reinterpret_cast<uint32_t*>(ptr) = static_cast<uint32_t>(ciphertext_len);
|
||||||
|
*reinterpret_cast<uint32_t*>(ptr + sizeof(uint32_t)) =
|
||||||
|
static_cast<uint32_t>(shared_key_len);
|
||||||
|
|
||||||
|
// Write ciphertext and shared key data
|
||||||
|
unsigned char* ciphertext_ptr = ptr + 2 * sizeof(uint32_t);
|
||||||
|
unsigned char* shared_key_ptr = ciphertext_ptr + ciphertext_len;
|
||||||
|
|
||||||
|
std::memcpy(ciphertext_ptr, result->ciphertext.get(), ciphertext_len);
|
||||||
|
std::memcpy(shared_key_ptr, result->shared_key.get(), shared_key_len);
|
||||||
|
|
||||||
|
*out = ByteSource::Allocated(data.release());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DoKEMDecapsulate(Environment* env,
|
||||||
|
const EVPKeyPointer& private_key,
|
||||||
|
const ByteSource& ciphertext,
|
||||||
|
ByteSource* out,
|
||||||
|
CryptoJobMode mode) {
|
||||||
|
ncrypto::Buffer<const void> ciphertext_buf{ciphertext.data(),
|
||||||
|
ciphertext.size()};
|
||||||
|
auto shared_key = ncrypto::KEM::Decapsulate(private_key, ciphertext_buf);
|
||||||
|
if (!shared_key) {
|
||||||
|
if (mode == kCryptoJobSync) {
|
||||||
|
THROW_ERR_CRYPTO_OPERATION_FAILED(env, "Failed to perform decapsulation");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
*out = ByteSource::Allocated(shared_key.release());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // anonymous namespace
|
||||||
|
|
||||||
|
// KEMEncapsulateTraits implementation
|
||||||
|
Maybe<void> KEMEncapsulateTraits::AdditionalConfig(
|
||||||
|
CryptoJobMode mode,
|
||||||
|
const FunctionCallbackInfo<Value>& args,
|
||||||
|
unsigned int offset,
|
||||||
|
KEMConfiguration* params) {
|
||||||
|
params->job_mode = mode;
|
||||||
|
params->mode = KEMMode::Encapsulate;
|
||||||
|
|
||||||
|
unsigned int key_offset = offset;
|
||||||
|
auto public_key_data =
|
||||||
|
KeyObjectData::GetPublicOrPrivateKeyFromJs(args, &key_offset);
|
||||||
|
if (!public_key_data) {
|
||||||
|
return Nothing<void>();
|
||||||
|
}
|
||||||
|
params->key = std::move(public_key_data);
|
||||||
|
|
||||||
|
return v8::JustVoid();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool KEMEncapsulateTraits::DeriveBits(Environment* env,
|
||||||
|
const KEMConfiguration& params,
|
||||||
|
ByteSource* out,
|
||||||
|
CryptoJobMode mode) {
|
||||||
|
Mutex::ScopedLock lock(params.key.mutex());
|
||||||
|
const auto& public_key = params.key.GetAsymmetricKey();
|
||||||
|
|
||||||
|
return DoKEMEncapsulate(env, public_key, out, mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
MaybeLocal<Value> KEMEncapsulateTraits::EncodeOutput(
|
||||||
|
Environment* env, const KEMConfiguration& params, ByteSource* out) {
|
||||||
|
// The output contains:
|
||||||
|
// [ciphertext_len][shared_key_len][ciphertext][shared_key]
|
||||||
|
const unsigned char* data = out->data<unsigned char>();
|
||||||
|
|
||||||
|
uint32_t ciphertext_len = *reinterpret_cast<const uint32_t*>(data);
|
||||||
|
uint32_t shared_key_len =
|
||||||
|
*reinterpret_cast<const uint32_t*>(data + sizeof(uint32_t));
|
||||||
|
|
||||||
|
const unsigned char* ciphertext_ptr = data + 2 * sizeof(uint32_t);
|
||||||
|
const unsigned char* shared_key_ptr = ciphertext_ptr + ciphertext_len;
|
||||||
|
|
||||||
|
MaybeLocal<Object> ciphertext_buf =
|
||||||
|
node::Buffer::Copy(env->isolate(),
|
||||||
|
reinterpret_cast<const char*>(ciphertext_ptr),
|
||||||
|
ciphertext_len);
|
||||||
|
|
||||||
|
MaybeLocal<Object> shared_key_buf =
|
||||||
|
node::Buffer::Copy(env->isolate(),
|
||||||
|
reinterpret_cast<const char*>(shared_key_ptr),
|
||||||
|
shared_key_len);
|
||||||
|
|
||||||
|
Local<Object> ciphertext_obj;
|
||||||
|
Local<Object> shared_key_obj;
|
||||||
|
if (!ciphertext_buf.ToLocal(&ciphertext_obj) ||
|
||||||
|
!shared_key_buf.ToLocal(&shared_key_obj)) {
|
||||||
|
return MaybeLocal<Value>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return an array [sharedKey, ciphertext].
|
||||||
|
Local<Array> result = Array::New(env->isolate(), 2);
|
||||||
|
if (result->Set(env->context(), 0, shared_key_obj).IsNothing() ||
|
||||||
|
result->Set(env->context(), 1, ciphertext_obj).IsNothing()) {
|
||||||
|
return MaybeLocal<Value>();
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// KEMDecapsulateTraits implementation
|
||||||
|
Maybe<void> KEMDecapsulateTraits::AdditionalConfig(
|
||||||
|
CryptoJobMode mode,
|
||||||
|
const FunctionCallbackInfo<Value>& args,
|
||||||
|
unsigned int offset,
|
||||||
|
KEMConfiguration* params) {
|
||||||
|
Environment* env = Environment::GetCurrent(args);
|
||||||
|
|
||||||
|
params->job_mode = mode;
|
||||||
|
params->mode = KEMMode::Decapsulate;
|
||||||
|
|
||||||
|
unsigned int key_offset = offset;
|
||||||
|
auto private_key_data =
|
||||||
|
KeyObjectData::GetPrivateKeyFromJs(args, &key_offset, true);
|
||||||
|
if (!private_key_data) {
|
||||||
|
return Nothing<void>();
|
||||||
|
}
|
||||||
|
params->key = std::move(private_key_data);
|
||||||
|
|
||||||
|
ArrayBufferOrViewContents<unsigned char> ciphertext(args[key_offset]);
|
||||||
|
if (!ciphertext.CheckSizeInt32()) {
|
||||||
|
THROW_ERR_OUT_OF_RANGE(env, "ciphertext is too big");
|
||||||
|
return Nothing<void>();
|
||||||
|
}
|
||||||
|
|
||||||
|
params->ciphertext =
|
||||||
|
mode == kCryptoJobAsync ? ciphertext.ToCopy() : ciphertext.ToByteSource();
|
||||||
|
|
||||||
|
return v8::JustVoid();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool KEMDecapsulateTraits::DeriveBits(Environment* env,
|
||||||
|
const KEMConfiguration& params,
|
||||||
|
ByteSource* out,
|
||||||
|
CryptoJobMode mode) {
|
||||||
|
Mutex::ScopedLock lock(params.key.mutex());
|
||||||
|
const auto& private_key = params.key.GetAsymmetricKey();
|
||||||
|
|
||||||
|
return DoKEMDecapsulate(env, private_key, params.ciphertext, out, mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
MaybeLocal<Value> KEMDecapsulateTraits::EncodeOutput(
|
||||||
|
Environment* env, const KEMConfiguration& params, ByteSource* out) {
|
||||||
|
return out->ToBuffer(env);
|
||||||
|
}
|
||||||
|
|
||||||
|
void InitializeKEM(Environment* env, Local<Object> target) {
|
||||||
|
KEMEncapsulateJob::Initialize(env, target);
|
||||||
|
KEMDecapsulateJob::Initialize(env, target);
|
||||||
|
|
||||||
|
constexpr int kKEMEncapsulate = static_cast<int>(KEMMode::Encapsulate);
|
||||||
|
constexpr int kKEMDecapsulate = static_cast<int>(KEMMode::Decapsulate);
|
||||||
|
|
||||||
|
NODE_DEFINE_CONSTANT(target, kKEMEncapsulate);
|
||||||
|
NODE_DEFINE_CONSTANT(target, kKEMDecapsulate);
|
||||||
|
}
|
||||||
|
|
||||||
|
void RegisterKEMExternalReferences(ExternalReferenceRegistry* registry) {
|
||||||
|
KEMEncapsulateJob::RegisterExternalReferences(registry);
|
||||||
|
KEMDecapsulateJob::RegisterExternalReferences(registry);
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace KEM {
|
||||||
|
void Initialize(Environment* env, Local<Object> target) {
|
||||||
|
InitializeKEM(env, target);
|
||||||
|
}
|
||||||
|
|
||||||
|
void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
|
||||||
|
RegisterKEMExternalReferences(registry);
|
||||||
|
}
|
||||||
|
} // namespace KEM
|
||||||
|
|
||||||
|
} // namespace crypto
|
||||||
|
} // namespace node
|
||||||
|
|
||||||
|
#endif
|
||||||
113
src/crypto/crypto_kem.h
Normal file
113
src/crypto/crypto_kem.h
Normal file
|
|
@ -0,0 +1,113 @@
|
||||||
|
#ifndef SRC_CRYPTO_CRYPTO_KEM_H_
|
||||||
|
#define SRC_CRYPTO_CRYPTO_KEM_H_
|
||||||
|
|
||||||
|
#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
|
||||||
|
|
||||||
|
#include "base_object.h"
|
||||||
|
#include "crypto/crypto_keys.h"
|
||||||
|
#include "crypto/crypto_util.h"
|
||||||
|
#include "env.h"
|
||||||
|
#include "memory_tracker.h"
|
||||||
|
#include "node_external_reference.h"
|
||||||
|
|
||||||
|
#if OPENSSL_VERSION_MAJOR >= 3
|
||||||
|
|
||||||
|
namespace node {
|
||||||
|
namespace crypto {
|
||||||
|
|
||||||
|
enum class KEMMode { Encapsulate, Decapsulate };
|
||||||
|
|
||||||
|
struct KEMConfiguration final : public MemoryRetainer {
|
||||||
|
CryptoJobMode job_mode;
|
||||||
|
KEMMode mode;
|
||||||
|
KeyObjectData key;
|
||||||
|
ByteSource ciphertext;
|
||||||
|
|
||||||
|
KEMConfiguration() = default;
|
||||||
|
explicit KEMConfiguration(KEMConfiguration&& other) noexcept;
|
||||||
|
KEMConfiguration& operator=(KEMConfiguration&& other) noexcept;
|
||||||
|
|
||||||
|
void MemoryInfo(MemoryTracker* tracker) const override;
|
||||||
|
SET_MEMORY_INFO_NAME(KEMConfiguration)
|
||||||
|
SET_SELF_SIZE(KEMConfiguration)
|
||||||
|
};
|
||||||
|
|
||||||
|
struct KEMEncapsulateTraits final {
|
||||||
|
using AdditionalParameters = KEMConfiguration;
|
||||||
|
static constexpr const char* JobName = "KEMEncapsulateJob";
|
||||||
|
|
||||||
|
static constexpr AsyncWrap::ProviderType Provider =
|
||||||
|
AsyncWrap::PROVIDER_DERIVEBITSREQUEST;
|
||||||
|
|
||||||
|
static v8::Maybe<void> AdditionalConfig(
|
||||||
|
CryptoJobMode mode,
|
||||||
|
const v8::FunctionCallbackInfo<v8::Value>& args,
|
||||||
|
unsigned int offset,
|
||||||
|
KEMConfiguration* params);
|
||||||
|
|
||||||
|
static bool DeriveBits(Environment* env,
|
||||||
|
const KEMConfiguration& params,
|
||||||
|
ByteSource* out,
|
||||||
|
CryptoJobMode mode);
|
||||||
|
|
||||||
|
static v8::MaybeLocal<v8::Value> EncodeOutput(Environment* env,
|
||||||
|
const KEMConfiguration& params,
|
||||||
|
ByteSource* out);
|
||||||
|
};
|
||||||
|
|
||||||
|
struct KEMDecapsulateTraits final {
|
||||||
|
using AdditionalParameters = KEMConfiguration;
|
||||||
|
static constexpr const char* JobName = "KEMDecapsulateJob";
|
||||||
|
|
||||||
|
static constexpr AsyncWrap::ProviderType Provider =
|
||||||
|
AsyncWrap::PROVIDER_DERIVEBITSREQUEST;
|
||||||
|
|
||||||
|
static v8::Maybe<void> AdditionalConfig(
|
||||||
|
CryptoJobMode mode,
|
||||||
|
const v8::FunctionCallbackInfo<v8::Value>& args,
|
||||||
|
unsigned int offset,
|
||||||
|
KEMConfiguration* params);
|
||||||
|
|
||||||
|
static bool DeriveBits(Environment* env,
|
||||||
|
const KEMConfiguration& params,
|
||||||
|
ByteSource* out,
|
||||||
|
CryptoJobMode mode);
|
||||||
|
|
||||||
|
static v8::MaybeLocal<v8::Value> EncodeOutput(Environment* env,
|
||||||
|
const KEMConfiguration& params,
|
||||||
|
ByteSource* out);
|
||||||
|
};
|
||||||
|
|
||||||
|
using KEMEncapsulateJob = DeriveBitsJob<KEMEncapsulateTraits>;
|
||||||
|
using KEMDecapsulateJob = DeriveBitsJob<KEMDecapsulateTraits>;
|
||||||
|
|
||||||
|
void InitializeKEM(Environment* env, v8::Local<v8::Object> target);
|
||||||
|
void RegisterKEMExternalReferences(ExternalReferenceRegistry* registry);
|
||||||
|
|
||||||
|
namespace KEM {
|
||||||
|
void Initialize(Environment* env, v8::Local<v8::Object> target);
|
||||||
|
void RegisterExternalReferences(ExternalReferenceRegistry* registry);
|
||||||
|
} // namespace KEM
|
||||||
|
|
||||||
|
} // namespace crypto
|
||||||
|
} // namespace node
|
||||||
|
|
||||||
|
#else
|
||||||
|
|
||||||
|
// Provide stub implementations when OpenSSL < 3.0
|
||||||
|
namespace node {
|
||||||
|
namespace crypto {
|
||||||
|
namespace KEM {
|
||||||
|
inline void Initialize(Environment* env, v8::Local<v8::Object> target) {
|
||||||
|
// No-op when OpenSSL < 3.0
|
||||||
|
}
|
||||||
|
inline void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
|
||||||
|
// No-op when OpenSSL < 3.0
|
||||||
|
}
|
||||||
|
} // namespace KEM
|
||||||
|
} // namespace crypto
|
||||||
|
} // namespace node
|
||||||
|
|
||||||
|
#endif
|
||||||
|
#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
|
||||||
|
#endif // SRC_CRYPTO_CRYPTO_KEM_H_
|
||||||
|
|
@ -66,6 +66,13 @@ namespace crypto {
|
||||||
#define ARGON2_NAMESPACE_LIST(V)
|
#define ARGON2_NAMESPACE_LIST(V)
|
||||||
#endif // !OPENSSL_NO_ARGON2 && OpenSSL >= 3.2
|
#endif // !OPENSSL_NO_ARGON2 && OpenSSL >= 3.2
|
||||||
|
|
||||||
|
// KEM functionality requires OpenSSL 3.0.0 or later
|
||||||
|
#if OPENSSL_VERSION_MAJOR >= 3
|
||||||
|
#define KEM_NAMESPACE_LIST(V) V(KEM)
|
||||||
|
#else
|
||||||
|
#define KEM_NAMESPACE_LIST(V)
|
||||||
|
#endif
|
||||||
|
|
||||||
#ifdef OPENSSL_NO_SCRYPT
|
#ifdef OPENSSL_NO_SCRYPT
|
||||||
#define SCRYPT_NAMESPACE_LIST(V)
|
#define SCRYPT_NAMESPACE_LIST(V)
|
||||||
#else
|
#else
|
||||||
|
|
@ -75,6 +82,7 @@ namespace crypto {
|
||||||
#define CRYPTO_NAMESPACE_LIST(V) \
|
#define CRYPTO_NAMESPACE_LIST(V) \
|
||||||
CRYPTO_NAMESPACE_LIST_BASE(V) \
|
CRYPTO_NAMESPACE_LIST_BASE(V) \
|
||||||
ARGON2_NAMESPACE_LIST(V) \
|
ARGON2_NAMESPACE_LIST(V) \
|
||||||
|
KEM_NAMESPACE_LIST(V) \
|
||||||
SCRYPT_NAMESPACE_LIST(V)
|
SCRYPT_NAMESPACE_LIST(V)
|
||||||
|
|
||||||
void Initialize(Local<Object> target,
|
void Initialize(Local<Object> target,
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,9 @@
|
||||||
#include "crypto/crypto_hash.h"
|
#include "crypto/crypto_hash.h"
|
||||||
#include "crypto/crypto_hkdf.h"
|
#include "crypto/crypto_hkdf.h"
|
||||||
#include "crypto/crypto_hmac.h"
|
#include "crypto/crypto_hmac.h"
|
||||||
|
#if OPENSSL_VERSION_MAJOR >= 3
|
||||||
|
#include "crypto/crypto_kem.h"
|
||||||
|
#endif
|
||||||
#include "crypto/crypto_keygen.h"
|
#include "crypto/crypto_keygen.h"
|
||||||
#include "crypto/crypto_keys.h"
|
#include "crypto/crypto_keys.h"
|
||||||
#include "crypto/crypto_ml_dsa.h"
|
#include "crypto/crypto_ml_dsa.h"
|
||||||
|
|
|
||||||
211
test/parallel/test-crypto-encap-decap.js
Normal file
211
test/parallel/test-crypto-encap-decap.js
Normal file
|
|
@ -0,0 +1,211 @@
|
||||||
|
'use strict';
|
||||||
|
const common = require('../common');
|
||||||
|
if (!common.hasCrypto)
|
||||||
|
common.skip('missing crypto');
|
||||||
|
|
||||||
|
const assert = require('assert');
|
||||||
|
const crypto = require('crypto');
|
||||||
|
const fixtures = require('../common/fixtures');
|
||||||
|
const { hasOpenSSL } = require('../common/crypto');
|
||||||
|
const { promisify } = require('util');
|
||||||
|
|
||||||
|
if (!hasOpenSSL(3)) {
|
||||||
|
assert.throws(() => crypto.encapsulate(), { code: 'ERR_CRYPTO_KEM_NOT_SUPPORTED' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.throws(() => crypto.encapsulate(), { code: 'ERR_INVALID_ARG_TYPE',
|
||||||
|
message: /The "key" argument must be of type/ });
|
||||||
|
assert.throws(() => crypto.decapsulate(), { code: 'ERR_INVALID_ARG_TYPE',
|
||||||
|
message: /The "key" argument must be of type/ });
|
||||||
|
|
||||||
|
const keys = {
|
||||||
|
'rsa': {
|
||||||
|
supported: hasOpenSSL(3), // RSASVE was added in 3.0
|
||||||
|
publicKey: fixtures.readKey('rsa_public_2048.pem', 'ascii'),
|
||||||
|
privateKey: fixtures.readKey('rsa_private_2048.pem', 'ascii'),
|
||||||
|
sharedSecretLength: 256,
|
||||||
|
ciphertextLength: 256,
|
||||||
|
},
|
||||||
|
'rsa-pss': {
|
||||||
|
supported: false, // Only raw RSA is supported
|
||||||
|
publicKey: fixtures.readKey('rsa_pss_public_2048.pem', 'ascii'),
|
||||||
|
privateKey: fixtures.readKey('rsa_pss_private_2048.pem', 'ascii'),
|
||||||
|
},
|
||||||
|
'p-256': {
|
||||||
|
supported: hasOpenSSL(3, 2), // DHKEM was added in 3.2
|
||||||
|
publicKey: fixtures.readKey('ec_p256_public.pem', 'ascii'),
|
||||||
|
privateKey: fixtures.readKey('ec_p256_private.pem', 'ascii'),
|
||||||
|
sharedSecretLength: 32,
|
||||||
|
ciphertextLength: 65,
|
||||||
|
},
|
||||||
|
'p-384': {
|
||||||
|
supported: hasOpenSSL(3, 2), // DHKEM was added in 3.2
|
||||||
|
publicKey: fixtures.readKey('ec_p384_public.pem', 'ascii'),
|
||||||
|
privateKey: fixtures.readKey('ec_p384_private.pem', 'ascii'),
|
||||||
|
sharedSecretLength: 48,
|
||||||
|
ciphertextLength: 97,
|
||||||
|
},
|
||||||
|
'p-521': {
|
||||||
|
supported: hasOpenSSL(3, 2), // DHKEM was added in 3.2
|
||||||
|
publicKey: fixtures.readKey('ec_p521_public.pem', 'ascii'),
|
||||||
|
privateKey: fixtures.readKey('ec_p521_private.pem', 'ascii'),
|
||||||
|
sharedSecretLength: 64,
|
||||||
|
ciphertextLength: 133,
|
||||||
|
},
|
||||||
|
'secp256k1': {
|
||||||
|
supported: false, // only P-256, P-384, and P-521 are supported
|
||||||
|
publicKey: fixtures.readKey('ec_secp256k1_public.pem', 'ascii'),
|
||||||
|
privateKey: fixtures.readKey('ec_secp256k1_private.pem', 'ascii'),
|
||||||
|
},
|
||||||
|
'x25519': {
|
||||||
|
supported: hasOpenSSL(3, 2), // DHKEM was added in 3.2
|
||||||
|
publicKey: fixtures.readKey('x25519_public.pem', 'ascii'),
|
||||||
|
privateKey: fixtures.readKey('x25519_private.pem', 'ascii'),
|
||||||
|
sharedSecretLength: 32,
|
||||||
|
ciphertextLength: 32,
|
||||||
|
},
|
||||||
|
'x448': {
|
||||||
|
supported: hasOpenSSL(3, 2), // DHKEM was added in 3.2
|
||||||
|
publicKey: fixtures.readKey('x448_public.pem', 'ascii'),
|
||||||
|
privateKey: fixtures.readKey('x448_private.pem', 'ascii'),
|
||||||
|
sharedSecretLength: 64,
|
||||||
|
ciphertextLength: 56,
|
||||||
|
},
|
||||||
|
'ml-kem-512': {
|
||||||
|
supported: hasOpenSSL(3, 5),
|
||||||
|
publicKey: fixtures.readKey('ml_kem_512_public.pem', 'ascii'),
|
||||||
|
privateKey: fixtures.readKey('ml_kem_512_private.pem', 'ascii'),
|
||||||
|
sharedSecretLength: 32,
|
||||||
|
ciphertextLength: 768,
|
||||||
|
},
|
||||||
|
'ml-kem-768': {
|
||||||
|
supported: hasOpenSSL(3, 5),
|
||||||
|
publicKey: fixtures.readKey('ml_kem_768_public.pem', 'ascii'),
|
||||||
|
privateKey: fixtures.readKey('ml_kem_768_private.pem', 'ascii'),
|
||||||
|
sharedSecretLength: 32,
|
||||||
|
ciphertextLength: 1088,
|
||||||
|
},
|
||||||
|
'ml-kem-1024': {
|
||||||
|
supported: hasOpenSSL(3, 5),
|
||||||
|
publicKey: fixtures.readKey('ml_kem_1024_public.pem', 'ascii'),
|
||||||
|
privateKey: fixtures.readKey('ml_kem_1024_private.pem', 'ascii'),
|
||||||
|
sharedSecretLength: 32,
|
||||||
|
ciphertextLength: 1568,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const [name, { supported, publicKey, privateKey, sharedSecretLength, ciphertextLength }] of Object.entries(keys)) {
|
||||||
|
if (!supported) {
|
||||||
|
assert.throws(() => crypto.encapsulate(publicKey),
|
||||||
|
{ code: /ERR_OSSL_EVP_DECODE_ERROR|ERR_CRYPTO_OPERATION_FAILED/ });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
assert.throws(() => crypto.decapsulate(privateKey, null),
|
||||||
|
{
|
||||||
|
code: 'ERR_INVALID_ARG_TYPE',
|
||||||
|
message: /instance of ArrayBuffer, Buffer, TypedArray, or DataView\. Received null/
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatKeyAs(key, params) {
|
||||||
|
return { ...params, key: key.export(params) };
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyObjects = {
|
||||||
|
publicKey: crypto.createPublicKey(publicKey),
|
||||||
|
privateKey: crypto.createPrivateKey(privateKey),
|
||||||
|
};
|
||||||
|
|
||||||
|
const keyPairs = [
|
||||||
|
keyObjects,
|
||||||
|
{ publicKey, privateKey },
|
||||||
|
{
|
||||||
|
publicKey: formatKeyAs(keyObjects.publicKey, { format: 'der', type: 'spki' }),
|
||||||
|
privateKey: formatKeyAs(keyObjects.privateKey, { format: 'der', type: 'pkcs8' })
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// TODO(@panva): ML-KEM does not have a JWK format defined yet, add once standardized
|
||||||
|
if (!keyObjects.privateKey.asymmetricKeyType.startsWith('ml')) {
|
||||||
|
keyPairs.push({
|
||||||
|
publicKey: formatKeyAs(keyObjects.publicKey, { format: 'jwk' }),
|
||||||
|
privateKey: formatKeyAs(keyObjects.privateKey, { format: 'jwk' })
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const kp of keyPairs) {
|
||||||
|
// sync
|
||||||
|
{
|
||||||
|
const { sharedKey, ciphertext } = crypto.encapsulate(kp.publicKey);
|
||||||
|
assert(Buffer.isBuffer(sharedKey));
|
||||||
|
assert.strictEqual(sharedKey.byteLength, sharedSecretLength);
|
||||||
|
assert(Buffer.isBuffer(ciphertext));
|
||||||
|
assert.strictEqual(ciphertext.byteLength, ciphertextLength);
|
||||||
|
const sharedKey2 = crypto.decapsulate(kp.privateKey, ciphertext);
|
||||||
|
assert(Buffer.isBuffer(sharedKey2));
|
||||||
|
assert.strictEqual(sharedKey2.byteLength, sharedSecretLength);
|
||||||
|
assert(sharedKey.equals(sharedKey2));
|
||||||
|
}
|
||||||
|
|
||||||
|
// async
|
||||||
|
{
|
||||||
|
crypto.encapsulate(kp.publicKey, common.mustSucceed(({ sharedKey, ciphertext }) => {
|
||||||
|
assert(Buffer.isBuffer(sharedKey));
|
||||||
|
assert.strictEqual(sharedKey.byteLength, sharedSecretLength);
|
||||||
|
assert(Buffer.isBuffer(ciphertext));
|
||||||
|
assert.strictEqual(ciphertext.byteLength, ciphertextLength);
|
||||||
|
crypto.decapsulate(kp.privateKey, ciphertext, common.mustSucceed((sharedKey2) => {
|
||||||
|
assert(Buffer.isBuffer(sharedKey2));
|
||||||
|
assert.strictEqual(sharedKey2.byteLength, sharedSecretLength);
|
||||||
|
assert(sharedKey.equals(sharedKey2));
|
||||||
|
}));
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// promisified
|
||||||
|
(async () => {
|
||||||
|
const { sharedKey, ciphertext } = await promisify(crypto.encapsulate)(kp.publicKey);
|
||||||
|
assert(Buffer.isBuffer(sharedKey));
|
||||||
|
assert.strictEqual(sharedKey.byteLength, sharedSecretLength);
|
||||||
|
assert(Buffer.isBuffer(ciphertext));
|
||||||
|
assert.strictEqual(ciphertext.byteLength, ciphertextLength);
|
||||||
|
const sharedKey2 = await promisify(crypto.decapsulate)(kp.privateKey, ciphertext);
|
||||||
|
assert(Buffer.isBuffer(sharedKey2));
|
||||||
|
assert.strictEqual(sharedKey2.byteLength, sharedSecretLength);
|
||||||
|
assert(sharedKey.equals(sharedKey2));
|
||||||
|
})().then(common.mustCall());
|
||||||
|
}
|
||||||
|
|
||||||
|
let wrongPrivateKey;
|
||||||
|
if (name.startsWith('x')) {
|
||||||
|
wrongPrivateKey = name === 'x448' ? keys.x25519.privateKey : keys.x448.privateKey;
|
||||||
|
} else if (name.startsWith('p-')) {
|
||||||
|
wrongPrivateKey = name === 'p-256' ? keys['p-384'].privateKey : keys['p-256'].privateKey;
|
||||||
|
} else if (name.startsWith('ml-')) {
|
||||||
|
wrongPrivateKey = name === 'ml-kem-512' ? keys['ml-kem-768'].privateKey : keys['ml-kem-512'].privateKey;
|
||||||
|
} else {
|
||||||
|
wrongPrivateKey = keys.x25519.privateKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
// sync errors
|
||||||
|
{
|
||||||
|
const { ciphertext } = crypto.encapsulate(publicKey);
|
||||||
|
assert.throws(() => crypto.decapsulate(wrongPrivateKey, ciphertext), {
|
||||||
|
message: /Failed to (initialize|perform) decapsulation/,
|
||||||
|
code: 'ERR_CRYPTO_OPERATION_FAILED',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// async errors
|
||||||
|
{
|
||||||
|
crypto.encapsulate(publicKey, common.mustSucceed(({ ciphertext }) => {
|
||||||
|
crypto.decapsulate(wrongPrivateKey, ciphertext, common.mustCall((err) => {
|
||||||
|
assert(err);
|
||||||
|
assert.strictEqual(err.message, 'Deriving bits failed');
|
||||||
|
}));
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user