Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
f82a804
crypto: use internal random buffer bounds
panva Sep 22, 2026
1cbd118
crypto: match algorithm names as ASCII
panva Sep 22, 2026
131cc41
crypto: reject short AES-KW inputs
panva Sep 22, 2026
0e286f0
crypto: isolate cipher job errors
panva Sep 22, 2026
5d64d76
crypto: convert getRandomValues views
panva Sep 22, 2026
bd27c23
crypto: validate normalized hash names
panva Sep 22, 2026
7129683
crypto: allow short AES-GCM IVs
panva Sep 22, 2026
3d8975b
crypto: allow unbound SubtleCrypto.supports
panva Sep 22, 2026
7f417a0
crypto: resolve supports overloads by type
panva Sep 22, 2026
9b6c75e
crypto: copy detached parameters as empty
panva Sep 22, 2026
a45414a
crypto: convert importKey data as a union
panva Sep 22, 2026
677404d
crypto: copy parameters without array species
panva Sep 22, 2026
4f7f16f
crypto: check RSA JWK alg with SHA-3 hashes
panva Sep 22, 2026
191d52a
crypto: reject duplicate unknown key usages
panva Sep 22, 2026
2ae6b20
crypto: handle PBKDF2 iteration limits
panva Sep 22, 2026
e937421
crypto: separate conversion from validation
panva Sep 22, 2026
36bb4f6
crypto: copy derived bits without species
panva Sep 22, 2026
716fc4e
crypto: minimize RSA public exponent metadata
panva Sep 22, 2026
02d6adf
crypto: create key usage arrays without species
panva Sep 22, 2026
6e9984c
crypto: derive Argon2 without worker threads
panva Sep 22, 2026
baaf5fc
crypto: report actual RSA modulus lengths
panva Sep 22, 2026
ac0931c
crypto: split hybrid keys without species
panva Sep 22, 2026
a95665b
crypto: use the public CryptoKey prototype
panva Sep 22, 2026
3e45b42
crypto: include EC public keys in PKCS8 exports
panva Sep 22, 2026
1f701b0
crypto: map JWK export failures to OperationError
panva Sep 22, 2026
36d932d
crypto: check EC coordinate conversion results
panva Sep 22, 2026
4a5c46f
benchmark: cover Web Crypto conversion costs
panva Sep 23, 2026
a216416
crypto: use backend cSHAKE and KMAC
panva Sep 23, 2026
4f26a35
crypto: verify empty KMAC outputs
panva Sep 23, 2026
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
47 changes: 47 additions & 0 deletions benchmark/crypto/webcrypto-export.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
'use strict';

const common = require('../common.js');
const fixtures = require('../../test/common/fixtures.js');
const { createPrivateKey, createPublicKey, subtle } = require('node:crypto');

const bench = common.createBenchmark(main, {
keyType: ['hmac', 'aes-gcm', 'ecdsa-private', 'rsa-private', 'rsa-public'],
n: [1e5],
});

async function createKey(keyType) {
switch (keyType) {
case 'hmac':
return subtle.importKey(
'raw', new Uint8Array(32), { name: 'HMAC', hash: 'SHA-256' },
true, ['sign', 'verify']);
case 'aes-gcm':
return subtle.importKey(
'raw', new Uint8Array(32), 'AES-GCM',
true, ['encrypt', 'decrypt', 'wrapKey', 'unwrapKey']);
case 'ecdsa-private':
return createPrivateKey(fixtures.readKey('ec_p256_private.pem'))
.toCryptoKey({ name: 'ECDSA', namedCurve: 'P-256' }, true, ['sign']);
case 'rsa-private':
return createPrivateKey(fixtures.readKey('rsa_private_2048.pem'))
.toCryptoKey({ name: 'RSA-PSS', hash: 'SHA-256' }, true, ['sign']);
case 'rsa-public':
return createPublicKey(fixtures.readKey('rsa_private_2048.pem'))
.toCryptoKey({ name: 'RSA-PSS', hash: 'SHA-256' }, true, ['verify']);
default:
throw new Error(`Unsupported key type: ${keyType}`);
}
}

async function main({ n, keyType }) {
const key = await createKey(keyType);
let result;

bench.start();
for (let i = 0; i < n; i++)
result = await subtle.exportKey('jwk', key);
bench.end(n);

if (result.kty === undefined)
throw new Error('Missing key type');
}
117 changes: 117 additions & 0 deletions benchmark/misc/webcrypto-util.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
'use strict';

const common = require('../common.js');

const inputs = [
'arraybuffer',
'uint8array',
'dataview',
'buffer',
'sharedview',
'empty-arraybuffer',
'empty-uint8array',
'empty-dataview',
'detached-arraybuffer',
'detached-uint8array',
'detached-dataview',
];

const bench = common.createBenchmark(main, {
op: [
...inputs.flatMap((input) => [`byteLength:${input}`, `bytes:${input}`]),
'truncate:arraybuffer:100',
'truncate:arraybuffer:128',
'truncate:uint8array:100',
'truncate:uint8array:128',
'usages:0',
'usages:1',
'usages:2',
'usages:4',
],
n: [1e6],
}, { flags: ['--expose-internals'] });

function createInput(input) {
const empty = input.startsWith('empty-');
const detached = input.startsWith('detached-');
const type = input.replace(/^(empty|detached)-/, '');
const size = empty ? 0 : 32;
const buffer = new ArrayBuffer(size + 16);
let value;
switch (type) {
case 'arraybuffer':
value = new ArrayBuffer(size);
break;
case 'uint8array':
value = new Uint8Array(buffer, 8, size);
break;
case 'dataview':
value = new DataView(buffer, 8, size);
break;
case 'buffer':
value = Buffer.from(buffer, 8, size);
break;
case 'sharedview':
value = new Uint8Array(new SharedArrayBuffer(size));
break;
default:
throw new Error(`Unsupported input: ${input}`);
}
if (detached) {
const backing = type === 'arraybuffer' ? value : buffer;
structuredClone(backing, { transfer: [backing] });
}
return value;
}

function main({ n, op }) {
const {
getBufferSourceByteLength,
getBufferSourceBytes,
getUsagesFromMask,
getUsagesMask,
truncateToBitLength,
} = require('internal/crypto/util');
const [operation, input, length] = op.split(':');
let run;
switch (operation) {
case 'byteLength': {
const value = createInput(input);
run = () => getBufferSourceByteLength(value);
break;
}
case 'bytes': {
const value = createInput(input);
run = () => getBufferSourceBytes(value);
break;
}
case 'truncate': {
const value = createInput(input);
const bits = Number(length);
run = () => truncateToBitLength(bits, value);
break;
}
case 'usages': {
const usages = {
0: [],
1: ['sign'],
2: ['sign', 'verify'],
4: ['encrypt', 'decrypt', 'wrapKey', 'unwrapKey'],
};
const mask = getUsagesMask(usages[input]);
run = () => getUsagesFromMask(mask);
break;
}
default:
throw new Error(`Unsupported operation: ${operation}`);
}

let result;
bench.start();
for (let i = 0; i < n; i++)
result = run();
bench.end(n);

if (result === undefined)
throw new Error('Missing benchmark result');
}
59 changes: 57 additions & 2 deletions benchmark/misc/webcrypto-webidl.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ const bench = common.createBenchmark(main, {
op: [
'normalizeAlgorithm-string',
'normalizeAlgorithm-dict',
'normalizeAlgorithm-validate-aes-gcm',
'normalizeAlgorithm-validate-aes-cbc',
'normalizeAlgorithm-validate-aes-ctr',
'normalizeAlgorithm-validate-aes-generate',
'normalizeAlgorithm-validate-hkdf',
'normalizeAlgorithm-validate-hmac',
'normalizeAlgorithm-validate-rsa',
'webidl-dict',
'webidl-algorithm-identifier-string',
'webidl-algorithm-identifier-object',
Expand All @@ -17,7 +24,7 @@ const bench = common.createBenchmark(main, {
}, { flags: ['--expose-internals'] });

function main({ n, op }) {
const { normalizeAlgorithm } = require('internal/crypto/util');
const { normalizeAlgorithm, validateAlgorithm } = require('internal/crypto/util');

switch (op) {
case 'normalizeAlgorithm-string': {
Expand All @@ -37,6 +44,54 @@ function main({ n, op }) {
bench.end(n);
break;
}
case 'normalizeAlgorithm-validate-aes-gcm':
case 'normalizeAlgorithm-validate-aes-cbc':
case 'normalizeAlgorithm-validate-aes-ctr':
case 'normalizeAlgorithm-validate-aes-generate':
case 'normalizeAlgorithm-validate-hkdf':
case 'normalizeAlgorithm-validate-hmac':
case 'normalizeAlgorithm-validate-rsa': {
const cases = {
'aes-gcm': [
{ name: 'AES-GCM', iv: new Uint8Array(12), tagLength: 128 },
'encrypt',
],
'aes-cbc': [
{ name: 'AES-CBC', iv: new Uint8Array(16) },
'encrypt',
],
'aes-ctr': [
{ name: 'AES-CTR', counter: new Uint8Array(16), length: 64 },
'encrypt',
],
'aes-generate': [{ name: 'AES-GCM', length: 256 }, 'generateKey'],
'hkdf': [
{
name: 'HKDF', hash: 'SHA-256',
salt: new Uint8Array(32), info: new Uint8Array(32),
},
'deriveBits',
],
'hmac': [{ name: 'HMAC', hash: 'SHA-256', length: 256 }, 'importKey'],
'rsa': [
{
name: 'RSA-PSS', hash: 'SHA-256', modulusLength: 2048,
publicExponent: new Uint8Array([1, 0, 1]),
},
'generateKey',
],
};
const name = op.slice('normalizeAlgorithm-validate-'.length);
const [input, operation] = cases[name];
bench.start();
for (let i = 0; i < n; i++) {
const normalized = normalizeAlgorithm(input, operation);
// Older revisions validate inside normalizeAlgorithm.
validateAlgorithm?.(normalized, operation);
}
bench.end(n);
break;
}
case 'webidl-dict': {
// WebIDL dictionary converter in isolation.
const webidl = require('internal/crypto/webidl');
Expand Down Expand Up @@ -85,7 +140,7 @@ function main({ n, op }) {
break;
}
case 'webidl-dict-ensure-sha': {
// Exercises ensureSHA on a hash member.
// Converts a dictionary containing a hash identifier.
const webidl = require('internal/crypto/webidl');
const input = { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' };
const opts = { prefix: 'test', context: 'test' };
Expand Down
46 changes: 38 additions & 8 deletions deps/ncrypto/ncrypto.cc
Original file line number Diff line number Diff line change
Expand Up @@ -2937,7 +2937,9 @@ DataPointer argon2(const Buffer<const char>& pass,
// per-context. It inherits no configuration, so availability is checked
// against the default context, otherwise Argon2 works in FIPS mode.
DeleteFnPtr<OSSL_LIB_CTX, OSSL_LIB_CTX_free> ctx;
if (lanes > 1) {
uint32_t threads = lanes == 0 ? 0 : 1;
if (lanes > 1 && (OSSL_get_thread_support_flags() &
OSSL_THREAD_SUPPORT_FLAG_DEFAULT_SPAWN) != 0) {
if (!KDF::Fetch(algorithm.data())) {
return {};
}
Expand All @@ -2947,8 +2949,13 @@ DataPointer argon2(const Buffer<const char>& pass,
return {};
}

if (OSSL_set_max_threads(ctx.get(), lanes) != 1) {
return {};
MarkPopErrorOnReturn mark_pop_error_on_return;
if (OSSL_set_max_threads(ctx.get(), lanes) == 1) {
threads = lanes;
} else {
// Lane count is an Argon2 input; worker threads are only an
// optimization. Compute the same lanes serially if unavailable.
ctx.reset();
}
}

Expand All @@ -2966,7 +2973,8 @@ DataPointer argon2(const Buffer<const char>& pass,
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_THREADS, &threads));
params.push_back(
OSSL_PARAM_construct_uint32(OSSL_KDF_PARAM_ARGON2_LANES, &lanes));
params.push_back(
Expand Down Expand Up @@ -5934,8 +5942,10 @@ bool ECKeyPointer::setPublicKeyRaw(const BignumPointer& x,
if (!buf) return false;
unsigned char* ptr = static_cast<unsigned char*>(buf.get());
ptr[0] = POINT_CONVERSION_UNCOMPRESSED;
x.encodePaddedInto(ptr + 1, field_len);
y.encodePaddedInto(ptr + 1 + field_len, field_len);
if (x.encodePaddedInto(ptr + 1, field_len) != field_len ||
y.encodePaddedInto(ptr + 1 + field_len, field_len) != field_len) {
return false;
}

auto point = ECPointPointer::New(group);
if (!point) return false;
Expand Down Expand Up @@ -6163,8 +6173,10 @@ bool ECKeyPointer::setPublicKeyRaw(const BignumPointer& x,
if (!buf) return false;
unsigned char* ptr = static_cast<unsigned char*>(buf.get());
ptr[0] = POINT_CONVERSION_UNCOMPRESSED;
x.encodePaddedInto(ptr + 1, field_len);
y.encodePaddedInto(ptr + 1 + field_len, field_len);
if (x.encodePaddedInto(ptr + 1, field_len) != field_len ||
y.encodePaddedInto(ptr + 1 + field_len, field_len) != field_len) {
return false;
}

auto point = ECPointPointer::New(group_.get());
if (!point || !point.setFromBuffer({ptr, uncompressed_len}, group_.get())) {
Expand Down Expand Up @@ -7376,6 +7388,24 @@ int Ec::getCurve() const {
return EC_GROUP_get_curve_name(getGroup());
}

BIOPointer Ec::ExportPrivatePkcs8(const EVPKeyPointer& key) {
MarkPopErrorOnReturn mark_pop_error_on_return;
if (!key || !key.isA(KeyAlgorithm::EC)) return {};
auto ec = ECKeyPointer(key).clone();
if (!ec) return {};
#if NCRYPTO_USE_LEGACY_KEY_TYPES
// Decoding an ECPrivateKey without publicKey reconstructs the public point
// but retains a flag that omits it from subsequent encodings.
EC_KEY_set_enc_flags(ec.get(),
EC_KEY_get_enc_flags(ec.get()) & ~EC_PKEY_NO_PUBKEY);
#endif
auto export_key = EVPKeyPointer::New();
if (!export_key || !export_key.set(ec)) return {};
auto encoded = export_key.writePrivateKey({});
if (!encoded) return {};
return std::move(encoded.value);
}

DataPointer Ec::TryExportPublic(const EVPKeyPointer& key,
point_conversion_form_t form) {
if (!key || form != POINT_CONVERSION_UNCOMPRESSED) return {};
Expand Down
1 change: 1 addition & 0 deletions deps/ncrypto/ncrypto.h
Original file line number Diff line number Diff line change
Expand Up @@ -778,6 +778,7 @@ class Ec final {
static DataPointer TryExportPublic(const EVPKeyPointer& key,
point_conversion_form_t form);
static DataPointer ExportPrivate(const EVPKeyPointer& key);
static BIOPointer ExportPrivatePkcs8(const EVPKeyPointer& key);
static bool GetKeyComponents(const EVPKeyPointer& key,
BignumPointer* x,
BignumPointer* y,
Expand Down
Loading
Loading