-
Notifications
You must be signed in to change notification settings - Fork 2
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
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
87f627a
wip
nbbeeken c7e2a00
chore(NODE-5455): benchmark FLE
nbbeeken f87296a
chore: print runtime and hw info
nbbeeken a68889e
perf: make use of mongocrypt_binary_t fields instead of getter functions
nbbeeken de52a5c
chore: lint
nbbeeken 6decb68
chore: fix assertions make crypto configurable
nbbeeken File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not opposed, we still need to leave the callbacks in the driver so bindings versions below the next release continue working.
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.
What's this referencing? Are you referring to what #18 is attempting, using builtin OpenSSL? Or something specific with FIPS mode?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See https://jira.mongodb.org/browse/NODE-5455?focusedCommentId=5590599&focusedId=5590599&page=com.atlassian.jira.plugin.system.issuetabpanels%3Acomment-tabpanel#comment-5590599
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is it breaking (for mongodb-client-encryption)?