diff --git a/addon/mongocrypt.cc b/addon/mongocrypt.cc index 4ed2a8a..70046ca 100644 --- a/addon/mongocrypt.cc +++ b/addon/mongocrypt.cc @@ -45,14 +45,11 @@ std::unique_ptr 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::Copy(env, data, len); + return Buffer::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::New(env, len); + return Buffer::New(env, binary->len); } Uint8Array Uint8ArrayFromValue(Napi::Value v, std::string argument_name) { @@ -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) { @@ -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{keyBuffer, ivBuffer, inBuffer, outBuffer}); + result = hook.Call( + std::initializer_list{keyBuffer, ivBuffer, inBuffer, outputBuffer}); } catch (...) { return false; } @@ -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; } @@ -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{outBuffer, Number::New(env, count)}); + hook.Call(std::initializer_list{outputBuffer, Number::New(env, count)}); } catch (...) { return false; } @@ -276,7 +273,7 @@ bool MongoCrypt::setupCryptoHooks() { return false; } - CopyBufferData(out, outBuffer); + CopyBufferData(out, outputBuffer); return true; }; diff --git a/package-lock.json b/package-lock.json index fadf63e..219556c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,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", @@ -1537,9 +1537,9 @@ "dev": true }, "node_modules/bson": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/bson/-/bson-6.6.0.tgz", - "integrity": "sha512-BVINv2SgcMjL4oYbBuCQTpE3/VKOSxrOA8Cj/wQP7izSzlBGVomdm+TcUd0Pzy0ytLSSDweCKQ6X3f5veM5LQA==", + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/bson/-/bson-6.7.0.tgz", + "integrity": "sha512-w2IquM5mYzYZv6rs3uN2DZTOBe2a0zXLj53TGDqwF4l6Sz/XsISrisXOJihArF9+BZ6Cq/GjVht7Sjfmri7ytQ==", "dev": true, "engines": { "node": ">=16.20.1" diff --git a/package.json b/package.json index 590c879..868c48c 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/test/benchmarks/bench.mjs b/test/benchmarks/bench.mjs new file mode 100644 index 0000000..7062a5d --- /dev/null +++ b/test/benchmarks/bench.mjs @@ -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(); diff --git a/test/benchmarks/crypto_callbacks.mjs b/test/benchmarks/crypto_callbacks.mjs new file mode 100644 index 0000000..1190351 --- /dev/null +++ b/test/benchmarks/crypto_callbacks.mjs @@ -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 +}; diff --git a/test/benchmarks/keyDocument.json b/test/benchmarks/keyDocument.json new file mode 100644 index 0000000..8ab2724 --- /dev/null +++ b/test/benchmarks/keyDocument.json @@ -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" + } +}