Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
254 changes: 254 additions & 0 deletions benchmark/crypto/mac.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
'use strict';

const common = require('../common.js');
const { hasOpenSSL } = require('../../test/common/crypto.js');
const assert = require('node:assert');
const {
createHmac,
createMac,
getMacs,
} = require('node:crypto');

if (!hasOpenSSL(3) ||
process.features.openssl_is_boringssl ||
typeof createMac !== 'function' ||
typeof getMacs !== 'function') {
console.log('Skipping: generic MAC API requires OpenSSL >= 3');
process.exit(0);
}

const operations = [
'get-macs-cold',
'get-macs-warm',
'create-cold',
'create-warm',
'hmac-lifecycle',
'mac-lifecycle',
'mac-stream-lifecycle',
'update',
'stream',
'final-buffer',
'final-hex',
];
const configurations = {
'hmac-sha256': {
algorithm: 'HMAC',
key: Buffer.alloc(32, 0x42),
options: { digest: 'SHA256' },
},
'kmac-128': {
algorithm: 'KMAC-128',
key: Buffer.alloc(32, 0x42),
options: { outputLength: 32 },
},
};

const bench = common.createBenchmark(main, {
operation: operations,
algorithm: Object.keys(configurations),
length: [0, 64, 4096],
n: [1, 10_000, 20_000, 500_000],
}, {
combinationFilter({ operation, algorithm, length, n }) {
if (operation === 'get-macs-cold') {
return algorithm === 'hmac-sha256' && length === 0 && n === 1;
}
if (operation === 'get-macs-warm') {
return algorithm === 'hmac-sha256' && length === 0 && n === 500_000;
}
if (operation === 'create-cold')
return length === 0 && n === 1;
if (operation === 'create-warm')
return length === 0 && n === 20_000;
if (operation === 'hmac-lifecycle') {
return algorithm === 'hmac-sha256' && n === 10_000;
}
if (operation === 'mac-lifecycle' ||
operation === 'mac-stream-lifecycle') {
return n === 10_000;
}
if (operation === 'update' || operation === 'stream') {
return length === 64 && n === 500_000;
}
if (operation === 'final-buffer' || operation === 'final-hex') {
return algorithm === 'hmac-sha256' &&
length === 64 &&
n === 20_000;
}
return false;
},
test: {
operation: ['create-cold'],
algorithm: ['hmac-sha256'],
length: [0],
n: [1],
},
});

function main({ operation, algorithm, length, n }) {
const configuration = configurations[algorithm];
const data = Buffer.alloc(length, 0x61);

switch (operation) {
case 'get-macs-cold':
measureGetMacs(n, false);
break;
case 'get-macs-warm':
measureGetMacs(n, true);
break;
case 'create-cold':
measureCreate(configuration, n, false);
break;
case 'create-warm':
measureCreate(configuration, n, true);
break;
case 'hmac-lifecycle':
measureHmacLifecycle(configuration, data, n);
break;
case 'mac-lifecycle':
measureMacLifecycle(configuration, data, n);
break;
case 'mac-stream-lifecycle':
measureMacStreamLifecycle(configuration, data, n);
break;
case 'update':
measureUpdate(configuration, data, n);
break;
case 'stream':
measureStream(configuration, data, n);
break;
case 'final-buffer':
measureFinal(configuration, data, n);
break;
case 'final-hex':
measureFinal(configuration, data, n, 'hex');
break;
default:
throw new Error(`unknown operation: ${operation}`);
}
}

function measureGetMacs(n, warm) {
if (warm)
getMacs();

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

assert(Array.isArray(result));
}

function measureCreate({ algorithm, key, options }, n, warm) {
if (warm)
createMac(algorithm, key, options).final();

const contexts = new Array(n);
bench.start();
for (let i = 0; i < n; ++i)
contexts[i] = createMac(algorithm, key, options);
bench.end(n);

assert.strictEqual(typeof contexts[n - 1], 'object');
}

function measureHmacLifecycle({ key, options }, data, n) {
createHmac(options.digest, key).update(data).digest();

let result;
bench.start();
for (let i = 0; i < n; ++i)
result = createHmac(options.digest, key).update(data).digest();
bench.end(n);

assert(Buffer.isBuffer(result));
}

function measureMacLifecycle({ algorithm, key, options }, data, n) {
createMac(algorithm, key, options).update(data).final();

let result;
bench.start();
for (let i = 0; i < n; ++i)
result = createMac(algorithm, key, options).update(data).final();
bench.end(n);

assert(Buffer.isBuffer(result));
}

function measureMacStreamLifecycle({ algorithm, key, options }, data, n) {
const warmup = createMac(algorithm, key, options);
warmup.end(data);
warmup.read();

let result;
bench.start();
for (let i = 0; i < n; ++i) {
const context = createMac(algorithm, key, options);
context.end(data);
result = context.read();
}
bench.end(n);

assert(Buffer.isBuffer(result));
}

function measureUpdate({ algorithm, key, options }, data, n) {
const warmup = createMac(algorithm, key, options);
warmup.update(data).final();

const context = createMac(algorithm, key, options);
bench.start();
for (let i = 0; i < n; ++i)
context.update(data);
bench.end(n);

assert(Buffer.isBuffer(context.final()));
}

function measureStream({ algorithm, key, options }, data, n) {
const warmup = createMac(algorithm, key, options);
warmup.end(data);
warmup.read();

const context = createMac(algorithm, key, options);
bench.start();
for (let i = 0; i < n; ++i)
context.write(data);
bench.end(n);

context.end();
assert(Buffer.isBuffer(context.read()));
}

function measureFinal({ algorithm, key, options }, data, n, encoding) {
const warmup = createMac(algorithm, key, options).update(data);
if (encoding === undefined)
warmup.final();
else
warmup.final(encoding);

const contexts = new Array(n);
for (let i = 0; i < n; ++i)
contexts[i] = createMac(algorithm, key, options).update(data);

let result;
if (encoding === undefined) {
bench.start();
for (let i = 0; i < n; ++i)
result = contexts[i].final();
bench.end(n);
} else {
bench.start();
for (let i = 0; i < n; ++i)
result = contexts[i].final(encoding);
bench.end(n);
}

if (encoding === undefined)
assert(Buffer.isBuffer(result));
else
assert.strictEqual(typeof result, 'string');
}
106 changes: 100 additions & 6 deletions deps/ncrypto/ncrypto.cc
Original file line number Diff line number Diff line change
Expand Up @@ -7200,6 +7200,79 @@ EVPMacPointer EVPMacPointer::Fetch(const char* algorithm) {
return EVPMacPointer(EVP_MAC_fetch(nullptr, algorithm, nullptr));
}

MacKind MacCache::GetKind(EVP_MAC* mac) {
if (EVP_MAC_is_a(mac, OSSL_MAC_NAME_HMAC)) return MacKind::kHmac;
if (EVP_MAC_is_a(mac, OSSL_MAC_NAME_CMAC)) return MacKind::kCmac;
if (EVP_MAC_is_a(mac, OSSL_MAC_NAME_GMAC)) return MacKind::kGmac;
return MacKind::kOther;
}

MacCache::Result MacCache::lookup(const char* name, uint64_t generation) const {
if (generation_ != generation || name == nullptr) return {};
const auto it = aliases_.find(name);
if (it == aliases_.end()) return {};
return lookup(it->second, generation);
}

MacCache::Result MacCache::insert(const char* name,
EVPMacPointer&& mac,
uint64_t generation) {
if (generation_ != generation || generation != getFipsStateGeneration() ||
name == nullptr || mac == nullptr) {
return {};
}

const char* canonical_name = EVP_MAC_get0_name(mac.get());
const OSSL_PROVIDER* provider = EVP_MAC_get0_provider(mac.get());
if (canonical_name == nullptr || provider == nullptr) return {};

for (size_t index = 0; index < macs_.size(); index++) {
EVP_MAC* cached = macs_[index].mac.get();
if (cached == nullptr) continue;
const char* cached_name = EVP_MAC_get0_name(cached);
if (EVP_MAC_get0_provider(cached) == provider && cached_name != nullptr &&
CaseInsensitiveNameEqual()(cached_name, canonical_name)) {
if (generation != getFipsStateGeneration()) return {};
const int32_t id = static_cast<int32_t>(first_id_ + index);
aliases_.insert_or_assign(name, id);
return {cached, id, macs_[index].kind};
}
}

if (next_id_ == UINT32_MAX) return {};

std::vector<std::string> aliases;
{
MarkPopErrorOnReturn mark_pop_error_on_return;
if (EVP_MAC_names_do_all(mac.get(), PushAlgorithmAlias, &aliases) != 1) {
return {};
}
}
if (generation != getFipsStateGeneration()) return {};

const MacKind kind = GetKind(mac.get());
macs_.push_back({std::move(mac), kind});
const int32_t id = static_cast<int32_t>(next_id_++);
const size_t index = macs_.size() - 1;

for (const std::string& alias : aliases) aliases_.emplace(alias, id);
aliases_.insert_or_assign(name, id);

return {macs_[index].mac.get(), id, kind};
}

void MacCache::reset(uint64_t generation) {
if (generation_ == generation) return;
aliases_.clear();
macs_.clear();
first_id_ = next_id_;
generation_ = generation;
}

const MacCache::AliasMap& MacCache::aliases() const {
return aliases_;
}

EVPMacCtxPointer::EVPMacCtxPointer(EVP_MAC_CTX* ctx) : ctx_(ctx) {}

EVPMacCtxPointer::EVPMacCtxPointer(EVPMacCtxPointer&& other) noexcept
Expand Down Expand Up @@ -7227,22 +7300,42 @@ EVP_MAC_CTX* EVPMacCtxPointer::release() {
bool EVPMacCtxPointer::init(const Buffer<const void>& key,
const OSSL_PARAM* params) {
if (!ctx_) return false;
return EVP_MAC_init(ctx_.get(),
static_cast<const unsigned char*>(key.data),
key.len,
params) == 1;

static constexpr unsigned char kEmptyKey = 0;
const unsigned char* key_data = static_cast<const unsigned char*>(key.data);
if (key_data == nullptr) {
if (key.len != 0) return false;
key_data = &kEmptyKey;
}

return EVP_MAC_init(ctx_.get(), key_data, key.len, params) == 1;
}

bool EVPMacCtxPointer::update(const Buffer<const void>& data) {
if (!ctx_) return false;
if (data.len == 0) return true;
if (data.data == nullptr) return false;
return EVP_MAC_update(ctx_.get(),
static_cast<const unsigned char*>(data.data),
data.len) == 1;
}

size_t EVPMacCtxPointer::getSize() const {
return ctx_ ? EVP_MAC_CTX_get_mac_size(ctx_.get()) : 0;
}

const OSSL_PARAM* EVPMacCtxPointer::getSettableParams() const {
return ctx_ ? EVP_MAC_CTX_settable_params(ctx_.get()) : nullptr;
}

DataPointer EVPMacCtxPointer::final(size_t length) {
if (!ctx_) return {};
auto buf = DataPointer::Alloc(length);

// DataPointer uses a null allocation to represent failure. Retain a
// one-byte allocation for a successful zero-length result while passing the
// requested zero capacity to OpenSSL. A non-null output pointer is required
// to actually finalize; nullptr only queries the output length.
auto buf = DataPointer::Alloc(length == 0 ? 1 : length);
if (!buf) return {};

size_t result_len = length;
Expand All @@ -7252,8 +7345,9 @@ DataPointer EVPMacCtxPointer::final(size_t length) {
length) != 1) {
return {};
}
if (result_len > length) return {};

return buf;
return buf.resize(result_len);
}

EVPMacCtxPointer EVPMacCtxPointer::New(EVP_MAC* mac) {
Expand Down
Loading
Loading