Skip to content

Commit

Permalink
chore(NODE-5455): benchmark FLE (#16)
Browse files Browse the repository at this point in the history
  • Loading branch information
nbbeeken authored Jun 18, 2024
1 parent 270ecc6 commit 95849dd
Show file tree
Hide file tree
Showing 6 changed files with 283 additions and 20 deletions.
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';

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"
}
}

0 comments on commit 95849dd

Please sign in to comment.