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
24 changes: 24 additions & 0 deletions benchmark/crypto/create-hmac.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
'use strict';

const common = require('../common.js');
const { createHmac } = require('crypto');
const assert = require('assert');

const bench = common.createBenchmark(main, {
n: [1e5],
algo: ['sha1', 'sha256', 'sha512'],
keylen: [0, 16, 64, 1024],
});

function main({ n, algo, keylen }) {
const key = Buffer.alloc(keylen, 'k');
const hmacs = new Array(n);

bench.start();
for (let i = 0; i < n; ++i) {
hmacs[i] = createHmac(algo, key);
}
bench.end(n);

assert.strictEqual(typeof hmacs[n - 1], 'object');
}
70 changes: 70 additions & 0 deletions benchmark/crypto/hmac-throughput.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// Throughput benchmark
// creates a single HMAC, then pushes a bunch of data through it
'use strict';

const common = require('../common.js');
const { createHmac } = require('crypto');

const bench = common.createBenchmark(main, {
n: [500],
algo: ['sha1', 'sha256', 'sha512'],
keylen: [64],
type: ['asc', 'utf', 'buf'],
len: [2, 1024, 102400, 1024 * 1024],
api: ['update', 'stream'],
});

function main({ api, type, len, algo, keylen, n }) {
let message;
let encoding;
switch (type) {
case 'asc':
message = 'a'.repeat(len);
encoding = 'ascii';
break;
case 'utf':
message = '\u00fc'.repeat(len / 2);
encoding = 'utf8';
break;
case 'buf':
message = Buffer.alloc(len, 'b');
break;
default:
throw new Error(`unknown message type: ${type}`);
}

const fn = api === 'stream' ? streamWrite : updateDigest;
const key = Buffer.alloc(keylen, 'k');

bench.start();
fn(algo, key, message, encoding, n, len);
}

function updateDigest(algo, key, message, encoding, n, len) {
const written = n * len;
const bits = written * 8;
const gbits = bits / (1024 * 1024 * 1024);
const h = createHmac(algo, key);

while (n-- > 0)
h.update(message, encoding);

h.digest();

bench.end(gbits);
}

function streamWrite(algo, key, message, encoding, n, len) {
const written = n * len;
const bits = written * 8;
const gbits = bits / (1024 * 1024 * 1024);
const h = createHmac(algo, key);

while (n-- > 0)
h.write(message, encoding);

h.end();
h.read();

bench.end(gbits);
}
80 changes: 80 additions & 0 deletions benchmark/crypto/webcrypto-hmac.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
'use strict';

const common = require('../common.js');
const { subtle } = globalThis.crypto;

const signParams = { name: 'HMAC' };

let keys;
let currentHash;
let currentKeyLength;

const bench = common.createBenchmark(main, {
hash: ['SHA-1', 'SHA-256', 'SHA-512'],
mode: ['serial', 'parallel'],
keyReuse: ['shared', 'unique'],
keylen: [512],
len: [0, 256, 4096],
n: [1e3],
}, {
test: {
keylen: 512,
},
combinationFilter(p) {
// Unique only differs from shared when operations overlap (parallel);
// sequential calls have no contention so unique+serial adds no value.
if (p.keyReuse === 'unique') return p.mode === 'parallel';
return true;
},
});

async function createSigningKeys(n, hash, keylen) {
keys = new Array(n);
currentHash = hash;
currentKeyLength = keylen;

const algorithm = { name: 'HMAC', hash, length: keylen };
const key = await subtle.generateKey(algorithm, true, ['sign']);
const raw = await subtle.exportKey('raw', key);
for (let i = 0; i < n; ++i) {
keys[i] = await subtle.importKey('raw', raw, algorithm, false, ['sign']);
}
}

async function measureSerial(n, sharedKey, data) {
bench.start();
for (let i = 0; i < n; ++i) {
await subtle.sign(signParams, sharedKey || keys[i], data);
}
bench.end(n);
}

async function measureParallel(n, sharedKey, data) {
const promises = new Array(n);
bench.start();
for (let i = 0; i < n; ++i) {
promises[i] = subtle.sign(signParams, sharedKey || keys[i], data);
}
await Promise.all(promises);
bench.end(n);
}

async function main({ n, mode, keyReuse, hash, keylen, len }) {
if (!keys || keys.length !== n ||
currentHash !== hash || currentKeyLength !== keylen) {
await createSigningKeys(n, hash, keylen);
}

const data = new Uint8Array(len);
data.fill(0x62);
const sharedKey = keyReuse === 'shared' ? keys[0] : undefined;

switch (mode) {
case 'serial':
await measureSerial(n, sharedKey, data);
break;
case 'parallel':
await measureParallel(n, sharedKey, data);
break;
}
}
91 changes: 89 additions & 2 deletions deps/ncrypto/ncrypto.cc
Original file line number Diff line number Diff line change
Expand Up @@ -4629,6 +4629,7 @@ bool extractP1363(const Buffer<const unsigned char>& buf,

// ============================================================================

#if !OPENSSL_WITH_EVP_MAC
HMACCtxPointer::HMACCtxPointer() : ctx_(nullptr) {}

HMACCtxPointer::HMACCtxPointer(HMAC_CTX* ctx) : ctx_(ctx) {}
Expand Down Expand Up @@ -4688,8 +4689,9 @@ bool HMACCtxPointer::digestInto(Buffer<void>* buf) {
HMACCtxPointer HMACCtxPointer::New() {
return HMACCtxPointer(HMAC_CTX_new());
}
#endif // !OPENSSL_WITH_EVP_MAC

#if OPENSSL_WITH_KMAC
#if OPENSSL_WITH_EVP_MAC
EVPMacPointer::EVPMacPointer(EVP_MAC* mac) : mac_(mac) {}

EVPMacPointer::EVPMacPointer(EVPMacPointer&& other) noexcept
Expand Down Expand Up @@ -4777,7 +4779,92 @@ EVPMacCtxPointer EVPMacCtxPointer::New(EVP_MAC* mac) {
if (!mac) return EVPMacCtxPointer();
return EVPMacCtxPointer(EVP_MAC_CTX_new(mac));
}
#endif // OPENSSL_WITH_KMAC

HMACCtxPointer::HMACCtxPointer() = default;

HMACCtxPointer::HMACCtxPointer(EVPMacPointer&& mac, EVPMacCtxPointer&& ctx)
: mac_(std::move(mac)), ctx_(std::move(ctx)) {}

HMACCtxPointer::HMACCtxPointer(HMACCtxPointer&& other) noexcept
: mac_(std::move(other.mac_)),
ctx_(std::move(other.ctx_)),
md_size_(other.md_size_) {
other.md_size_ = 0;
}

HMACCtxPointer& HMACCtxPointer::operator=(HMACCtxPointer&& other) noexcept {
if (this == &other) return *this;
mac_ = std::move(other.mac_);
ctx_ = std::move(other.ctx_);
md_size_ = other.md_size_;
other.md_size_ = 0;
return *this;
}

HMACCtxPointer::~HMACCtxPointer() {
reset();
}

void HMACCtxPointer::reset() {
ctx_.reset();
mac_.reset();
md_size_ = 0;
}

bool HMACCtxPointer::init(const Buffer<const void>& buf, const Digest& md) {
if (!ctx_ || !md) return false;

const char* md_name = EVP_MD_get0_name(md);
if (md_name == nullptr) return false;

OSSL_PARAM params[] = {
OSSL_PARAM_construct_utf8_string(
OSSL_MAC_PARAM_DIGEST, const_cast<char*>(md_name), 0),
OSSL_PARAM_construct_end(),
};

if (!ctx_.init(buf, params)) return false;
md_size_ = md.size();
return true;
}

bool HMACCtxPointer::update(const Buffer<const void>& buf) {
if (!ctx_) return false;
return ctx_.update(buf);
}

DataPointer HMACCtxPointer::digest() {
if (md_size_ == 0) return {};
auto data = DataPointer::Alloc(md_size_);
if (!data) return {};
Buffer<void> buf = data;
if (!digestInto(&buf)) return {};
return data.resize(buf.len);
}

bool HMACCtxPointer::digestInto(Buffer<void>* buf) {
if (!ctx_) return false;

size_t len = buf->len;
if (EVP_MAC_final(
ctx_.get(), static_cast<unsigned char*>(buf->data), &len, buf->len) !=
1)
return false;

buf->len = len;
return true;
}

HMACCtxPointer HMACCtxPointer::New() {
auto mac = EVPMacPointer::Fetch(OSSL_MAC_NAME_HMAC);
if (!mac) return {};

auto ctx = EVPMacCtxPointer::New(mac.get());
if (!ctx) return {};

return HMACCtxPointer(std::move(mac), std::move(ctx));
}
#endif // OPENSSL_WITH_EVP_MAC

DataPointer hashDigest(const Buffer<const unsigned char>& buf,
const EVP_MD* md) {
Expand Down
37 changes: 33 additions & 4 deletions deps/ncrypto/ncrypto.h
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,9 @@
#endif

#if OPENSSL_VERSION_PREREQ(3, 0)
#define OPENSSL_WITH_KMAC 1
#define OPENSSL_WITH_EVP_MAC 1
#else
#define OPENSSL_WITH_KMAC 0
#define OPENSSL_WITH_EVP_MAC 0
#endif

#if defined(OPENSSL_IS_BORINGSSL) || OPENSSL_VERSION_PREREQ(3, 2)
Expand Down Expand Up @@ -1528,6 +1528,7 @@ class EVPMDCtxPointer final {
DeleteFnPtr<EVP_MD_CTX, EVP_MD_CTX_free> ctx_;
};

#if !OPENSSL_WITH_EVP_MAC
class HMACCtxPointer final {
public:
HMACCtxPointer();
Expand All @@ -1554,8 +1555,9 @@ class HMACCtxPointer final {
private:
DeleteFnPtr<HMAC_CTX, HMAC_CTX_free> ctx_;
};
#endif // !OPENSSL_WITH_EVP_MAC

#if OPENSSL_WITH_KMAC
#if OPENSSL_WITH_EVP_MAC
class EVPMacPointer final {
public:
EVPMacPointer() = default;
Expand Down Expand Up @@ -1603,7 +1605,34 @@ class EVPMacCtxPointer final {
private:
DeleteFnPtr<EVP_MAC_CTX, EVP_MAC_CTX_free> ctx_;
};
#endif // OPENSSL_WITH_KMAC

class HMACCtxPointer final {
public:
HMACCtxPointer();
HMACCtxPointer(HMACCtxPointer&& other) noexcept;
HMACCtxPointer& operator=(HMACCtxPointer&& other) noexcept;
NCRYPTO_DISALLOW_COPY(HMACCtxPointer)
~HMACCtxPointer();

inline bool operator==(std::nullptr_t) noexcept { return ctx_ == nullptr; }
inline operator bool() const { return ctx_ != nullptr; }
void reset();

bool init(const Buffer<const void>& buf, const Digest& md);
bool update(const Buffer<const void>& buf);
DataPointer digest();
bool digestInto(Buffer<void>* buf);

static HMACCtxPointer New();

private:
HMACCtxPointer(EVPMacPointer&& mac, EVPMacCtxPointer&& ctx);

EVPMacPointer mac_;
EVPMacCtxPointer ctx_;
size_t md_size_ = 0;
};
#endif // OPENSSL_WITH_EVP_MAC

#ifndef OPENSSL_NO_ENGINE
class EnginePointer final {
Expand Down
8 changes: 7 additions & 1 deletion src/crypto/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,12 +90,18 @@ using ECPointPointer = DeleteFnPtr<EC_POINT, EC_POINT_free>;
using ECKeyPointer = DeleteFnPtr<EC_KEY, EC_KEY_free>;
using DHPointer = DeleteFnPtr<DH, DH_free>;
using ECDSASigPointer = DeleteFnPtr<ECDSA_SIG, ECDSA_SIG_free>;
using HMACCtxPointer = DeleteFnPtr<HMAC_CTX, HMAC_CTX_free>;
using CipherCtxPointer = DeleteFnPtr<EVP_CIPHER_CTX, EVP_CIPHER_CTX_free>;
```

Examples of these being used are pervasive through the `src/crypto` code.

`HMACCtxPointer` is a dedicated HMAC state wrapper rather than a plain
`DeleteFnPtr` alias. On OpenSSL 3 and later it owns the provider-backed
`EVP_MAC`/`EVP_MAC_CTX` state. On OpenSSL 1.1.1 and BoringSSL it owns the
legacy `HMAC_CTX` state. HMAC call sites should use `HMACCtxPointer::New()`,
`init()`, `update()`, and `digest()`/`digestInto()` so the backend selection
stays contained in ncrypto.

### `ByteSource`

The `ByteSource` class is a helper utility representing a _read-only_ byte
Expand Down
4 changes: 1 addition & 3 deletions src/crypto/crypto_hmac.cc
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,7 @@ using v8::Uint32;
using v8::Value;

namespace crypto {
Hmac::Hmac(Environment* env, Local<Object> wrap)
: BaseObject(env, wrap),
ctx_(nullptr) {
Hmac::Hmac(Environment* env, Local<Object> wrap) : BaseObject(env, wrap) {
MakeWeak();
}

Expand Down
Loading
Loading