Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(NODE-5455): benchmark FLE #16

Merged
merged 6 commits into from
Jun 18, 2024
Merged
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
27 changes: 12 additions & 15 deletions addon/mongocrypt.cc
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,11 @@ std::unique_ptr<mongocrypt_binary_t, MongoCryptBinaryDeleter> Uint8ArrayToBinary
}

Uint8Array BufferFromBinary(Env env, mongocrypt_binary_t* binary) {
const uint8_t* data = mongocrypt_binary_data(binary);
size_t len = mongocrypt_binary_len(binary);
return Buffer<uint8_t>::Copy(env, data, len);
return Buffer<uint8_t>::Copy(env, (uint8_t*)binary->data, binary->len);
}

Uint8Array BufferWithLengthOf(Env env, mongocrypt_binary_t* binary) {
size_t len = mongocrypt_binary_len(binary);
return Buffer<uint8_t>::New(env, len);
return Buffer<uint8_t>::New(env, binary->len);
}

Uint8Array Uint8ArrayFromValue(Napi::Value v, std::string argument_name) {
Expand All @@ -64,13 +61,13 @@ Uint8Array Uint8ArrayFromValue(Napi::Value v, std::string argument_name) {
}

void CopyBufferData(mongocrypt_binary_t* out, Uint8Array buffer, size_t count) {
assert(count <= mongocrypt_binary_len(out));
assert(count <= out->len);
assert(count <= buffer.ByteLength());
memcpy(mongocrypt_binary_data(out), buffer.Data(), count);
memcpy(out->data, buffer.Data(), count);
}

void CopyBufferData(mongocrypt_binary_t* out, Uint8Array buffer) {
CopyBufferData(out, buffer, mongocrypt_binary_len(out));
CopyBufferData(out, buffer, out->len);
}

std::string errorStringFromStatus(mongocrypt_t* crypt) {
Expand Down Expand Up @@ -184,12 +181,12 @@ static bool aes_256_generic_hook(MongoCrypt* mongoCrypt,
Uint8Array keyBuffer = BufferFromBinary(env, key);
Uint8Array ivBuffer = BufferFromBinary(env, iv);
Uint8Array inBuffer = BufferFromBinary(env, in);
Uint8Array outBuffer = BufferWithLengthOf(env, out);
Uint8Array outputBuffer = BufferWithLengthOf(env, out);

Value result;
try {
result =
hook.Call(std::initializer_list<napi_value>{keyBuffer, ivBuffer, inBuffer, outBuffer});
result = hook.Call(
std::initializer_list<napi_value>{keyBuffer, ivBuffer, inBuffer, outputBuffer});
} catch (...) {
return false;
}
Expand All @@ -200,7 +197,7 @@ static bool aes_256_generic_hook(MongoCrypt* mongoCrypt,
}

*bytes_written = result.ToNumber().Uint32Value();
CopyBufferData(out, outBuffer, *bytes_written);
CopyBufferData(out, outputBuffer, *bytes_written);
return true;
}

Expand Down Expand Up @@ -262,11 +259,11 @@ bool MongoCrypt::setupCryptoHooks() {
HandleScope scope(env);
Function hook = mongoCrypt->GetCallback("randomHook");

Uint8Array outBuffer = BufferWithLengthOf(env, out);
Uint8Array outputBuffer = BufferWithLengthOf(env, out);
Napi::Value result;
try {
result =
hook.Call(std::initializer_list<napi_value>{outBuffer, Number::New(env, count)});
hook.Call(std::initializer_list<napi_value>{outputBuffer, Number::New(env, count)});
} catch (...) {
return false;
}
Expand All @@ -276,7 +273,7 @@ bool MongoCrypt::setupCryptoHooks() {
return false;
}

CopyBufferData(out, outBuffer);
CopyBufferData(out, outputBuffer);
return true;
};

Expand Down
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
"@types/mocha": "^10.0.6",
"@types/node": "^20.12.7",
"@typescript-eslint/eslint-plugin": "^7.7.0",
"bson": "^6.6.0",
"bson": "^6.7.0",
"chai": "^4.4.1",
"chai-subset": "^1.6.0",
"clang-format": "^1.8.0",
Expand Down
155 changes: 155 additions & 0 deletions test/benchmarks/bench.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
// @ts-check
/* eslint-disable no-console */
import os from 'node:os';
import path from 'node:path';
import url from 'node:url';
import process from 'node:process';
import fs from 'node:fs';
import { EJSON, BSON } from 'bson';
import { cryptoCallbacks } from './crypto_callbacks.mjs';
import { MongoCrypt } from '../../lib/index.js';

const NEED_MONGO_KEYS = 3;
const READY = 5;
const ERROR = 0;

const __dirname = path.dirname(url.fileURLToPath(import.meta.url));

const { CRYPT_SHARED_LIB_PATH: cryptSharedLibPath = '', BENCH_WITH_NATIVE_CRYPTO = '' } =
process.env;

const warmupSecs = 2;
const testInSecs = 57;
const fieldCount = 1500;

const LOCAL_KEY = new Uint8Array([
0x9d, 0x94, 0x4b, 0x0d, 0x93, 0xd0, 0xc5, 0x44, 0xa5, 0x72, 0xfd, 0x32, 0x1b, 0x94, 0x30, 0x90,
0x23, 0x35, 0x73, 0x7c, 0xf0, 0xf6, 0xc2, 0xf4, 0xda, 0x23, 0x56, 0xe7, 0x8f, 0x04, 0xcc, 0xfa,
0xde, 0x75, 0xb4, 0x51, 0x87, 0xf3, 0x8b, 0x97, 0xd7, 0x4b, 0x44, 0x3b, 0xac, 0x39, 0xa2, 0xc6,
0x4d, 0x91, 0x00, 0x3e, 0xd1, 0xfa, 0x4a, 0x30, 0xc1, 0xd2, 0xc6, 0x5e, 0xfb, 0xac, 0x41, 0xf2,
0x48, 0x13, 0x3c, 0x9b, 0x50, 0xfc, 0xa7, 0x24, 0x7a, 0x2e, 0x02, 0x63, 0xa3, 0xc6, 0x16, 0x25,
0x51, 0x50, 0x78, 0x3e, 0x0f, 0xd8, 0x6e, 0x84, 0xa6, 0xec, 0x8d, 0x2d, 0x24, 0x47, 0xe5, 0xaf
]);

const padNum = i => i.toString().padStart(4, '0');
const kmsProviders = { local: { key: LOCAL_KEY } };
const algorithm = 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic';
const keyDocument = EJSON.parse(
await fs.promises.readFile(path.join(__dirname, 'keyDocument.json'), 'utf8'),
{ relaxed: false }
);

function createEncryptedDocument(mongoCrypt) {
const { _id: keyId } = keyDocument;

const encrypted = {};

for (let i = 0; i < fieldCount; i++) {
const key = `key${padNum(i + 1)}`;
const v = `value ${padNum(i + 1)}`;

const ctx = mongoCrypt.makeExplicitEncryptionContext(BSON.serialize({ v }), {
keyId: keyId.buffer,
algorithm
});

if (ctx.state === NEED_MONGO_KEYS) {
ctx.addMongoOperationResponse(BSON.serialize(keyDocument));
ctx.finishMongoOperation();
}

if (ctx.state !== READY) throw new Error(`not ready: [${ctx.state}] ${ctx.status.message}`);
const result = ctx.finalize();
if (ctx.state === ERROR) throw new Error(`error: [${ctx.state}] ${ctx.status.message}`);
const { v: encryptedValue } = BSON.deserialize(result);
encrypted[key] = encryptedValue;
}

return encrypted;
}

function measureMedianOpsPerSecOfDecrypt(mongoCrypt, toDecrypt, seconds) {
let operationsPerSecond = [];

for (let second = 0; second < seconds; second++) {
const startTime = performance.now();
/** @type {number | null} */
let operations = 0;

while (performance.now() - startTime < 1000) {
const ctx = mongoCrypt.makeDecryptionContext(toDecrypt);
if (ctx.state === NEED_MONGO_KEYS) {
// We ran over a minute
operations = null;
break;
}

if (ctx.state !== READY) throw new Error(`NOT READY: ${ctx.state}`);

ctx.finalize();
operations += 1;
}

if (operations != null) operationsPerSecond.push(operations);
}

console.log('samples taken: ', operationsPerSecond.length);
operationsPerSecond.sort((a, b) => a - b);
return operationsPerSecond[Math.floor(operationsPerSecond.length / 2)];
}

function main() {
const hw = os.cpus();
const ram = os.totalmem() / 1024 ** 3;
const platform = { name: hw[0].model, cores: hw.length, ram: `${ram}GB` };

const systemInfo = () =>
[
`\n- cpu: ${platform.name}`,
`- node: ${process.version}`,
`- cores: ${platform.cores}`,
`- arch: ${os.arch()}`,
`- os: ${process.platform} (${os.release()})`,
`- ram: ${platform.ram}\n`
].join('\n');
console.log(systemInfo());

console.log(
`BenchmarkRunner is using ` +
`libmongocryptVersion=${MongoCrypt.libmongocryptVersion}, ` +
`warmupSecs=${warmupSecs}, ` +
`testInSecs=${testInSecs}`
);

const mongoCryptOptions = { kmsProviders: BSON.serialize(kmsProviders) };
if (!BENCH_WITH_NATIVE_CRYPTO) mongoCryptOptions.cryptoCallbacks = cryptoCallbacks;
if (cryptSharedLibPath) mongoCryptOptions.cryptSharedLibPath = cryptSharedLibPath;

const mongoCrypt = new MongoCrypt(mongoCryptOptions);

const encrypted = createEncryptedDocument(mongoCrypt);
const toDecrypt = BSON.serialize(encrypted);

const created_at = new Date();

// warmup
measureMedianOpsPerSecOfDecrypt(mongoCrypt, toDecrypt, warmupSecs);
// bench
const medianOpsPerSec = measureMedianOpsPerSecOfDecrypt(mongoCrypt, toDecrypt, testInSecs);

const completed_at = new Date();

console.log(`Decrypting 1500 fields median ops/sec : ${medianOpsPerSec}`);

const perfSend = {
info: { test_name: 'javascript_decrypt_1500' },
created_at,
completed_at,
artifacts: [],
metrics: [{ name: 'medianOpsPerSec', type: 'THROUGHPUT', value: medianOpsPerSec }],
sub_tests: []
};
console.log(perfSend);
}

main();
87 changes: 87 additions & 0 deletions test/benchmarks/crypto_callbacks.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import crypto from 'node:crypto';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of duplicating these here, can we add the driver as a dev dependency and use the hooks defined in the driver?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I appreciate the desire to de-dupe but I think that's overkill. I imagine we'll delete the ones in the driver after switching to native crypto, which would make this script no longer work with a ^ caret dependency on the driver. Additionally, those callbacks aren't public driver API so even if they don't get removed an innocent refactor could break an import here.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh but this looks like a perfect opportunity to move these backs into the bindings source code (i.e. next to index.ts)? I still believe that moving these into the driver was a mistake, if we need them here in the tests that's just yet another reason to move them back

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I imagine we'll delete the ones in the driver after switching to native crypto

There's probably going to be a need for JS crypto callbacks until something like the alternative approach mentioned in the ticket here is implemented – We can't use libmongocrypt's native crypto integration on macOS and Windows in mongosh because we need to be able to toggle the FIPS mode switch for those.

Copy link
Collaborator Author

@nbbeeken nbbeeken Jun 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

move these backs into the bindings source code (i.e. next to index.ts)?

Not opposed, we still need to leave the callbacks in the driver so bindings versions below the next release continue working.

We can't use libmongocrypt's native crypto integration on macOS and Windows

I think reintroducing them to the src will fit better in #18 since currently I've essentially prevented callbacks from working unless you build from libmongocrypt src using nocrypto.

I will ping you on that PR when it is ready.

something like the alternative approach mentioned in the ticket here is implemented

What's this referencing? Are you referring to what #18 is attempting, using builtin OpenSSL? Or something specific with FIPS mode?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea was to use the opensll that's bundled and exposed with Nodejs on all platforms for the crypto hooks (i.e., c++ crypto callbacks). That would take the place of #18.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After this morning's discussion I think we have what we need here for this PR. In the following PR where we implement C++ callbacks we can also bring in the JS callbacks if we desire. Is that accurate?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can't bring the JS callbacks over, that's breaking. But other than that, yes.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can't bring the JS callbacks over, that's breaking

Why is it breaking (for mongodb-client-encryption)?


function makeAES256Hook(method, mode) {
return function (key, iv, input, output) {
let result;
try {
const cipher = crypto[method](mode, key, iv);
cipher.setAutoPadding(false);
result = cipher.update(input);
const final = cipher.final();
if (final.length > 0) {
result = Buffer.concat([result, final]);
}
} catch (e) {
return e;
}
result.copy(output);
return result.length;
};
}

function randomHook(buffer, count) {
try {
crypto.randomFillSync(buffer, 0, count);
} catch (e) {
return e;
}
return count;
}

function sha256Hook(input, output) {
let result;
try {
result = crypto.createHash('sha256').update(input).digest();
} catch (e) {
return e;
}
result.copy(output);
return result.length;
}

function makeHmacHook(algorithm) {
return (key, input, output) => {
let result;
try {
result = crypto.createHmac(algorithm, key).update(input).digest();
} catch (e) {
return e;
}
result.copy(output);
return result.length;
};
}

function signRsaSha256Hook(key, input, output) {
let result;
try {
const signer = crypto.createSign('sha256WithRSAEncryption');
const privateKey = Buffer.from(
`-----BEGIN PRIVATE KEY-----\n${key.toString('base64')}\n-----END PRIVATE KEY-----\n`
);
result = signer.update(input).end().sign(privateKey);
} catch (e) {
return e;
}
result.copy(output);
return result.length;
}

const aes256CbcEncryptHook = makeAES256Hook('createCipheriv', 'aes-256-cbc');
const aes256CbcDecryptHook = makeAES256Hook('createDecipheriv', 'aes-256-cbc');
const aes256CtrEncryptHook = makeAES256Hook('createCipheriv', 'aes-256-ctr');
const aes256CtrDecryptHook = makeAES256Hook('createDecipheriv', 'aes-256-ctr');
const hmacSha512Hook = makeHmacHook('sha512');
const hmacSha256Hook = makeHmacHook('sha256');

export const cryptoCallbacks = {
randomHook,
sha256Hook,
signRsaSha256Hook,
aes256CbcEncryptHook,
aes256CbcDecryptHook,
aes256CtrEncryptHook,
aes256CtrDecryptHook,
hmacSha512Hook,
hmacSha256Hook
};
24 changes: 24 additions & 0 deletions test/benchmarks/keyDocument.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"_id": {
"$binary": {
"base64": "YWFhYWFhYWFhYWFhYWFhYQ==",
"subType": "04"
}
},
"keyMaterial": {
"$binary": {
"base64": "ACR7Hm33dDOAAD7l2ubZhSpSUWK8BkALUY+qW3UgBAEcTV8sBwZnaAWnzDsmrX55dgmYHWfynDlJogC/e33u6pbhyXvFTs5ow9OLCuCWBJ39T/Ivm3kMaZJybkejY0V+uc4UEdHvVVz/SbitVnzs2WXdMGmo1/HmDRrxGYZjewFslquv8wtUHF5pyB+QDlQBd/al9M444/8bJZFbMSmtIg==",
"subType": "00"
}
},
"creationDate": {
"$date": "2023-08-21T14:28:20.875Z"
},
"updateDate": {
"$date": "2023-08-21T14:28:20.875Z"
},
"status": 0,
"masterKey": {
"provider": "local"
}
}
Loading