From d5a7e19f1a441c7724c501fe9d86ebddde688467 Mon Sep 17 00:00:00 2001 From: Cory McWilliams Date: Sun, 16 Jun 2024 17:07:12 -0400 Subject: [PATCH] Move the bulk of ssb.privateMessageEncrypt work (CPU + DB) off the main thread. --- src/ssb.js.c | 239 +++++++++++++++++++++++++++++++----------------- src/ssb.tests.c | 39 ++++++++ src/ssb.tests.h | 6 ++ src/tests.c | 1 + 4 files changed, 200 insertions(+), 85 deletions(-) diff --git a/src/ssb.js.c b/src/ssb.js.c index 9ba320ab..0a6d7463 100644 --- a/src/ssb.js.c +++ b/src/ssb.js.c @@ -1854,6 +1854,142 @@ static bool _tf_ssb_get_private_key_curve25519(sqlite3* db, const char* user, co return success; } +typedef struct _private_message_encrypt_t +{ + const char* signer_user; + const char* signer_identity; + uint8_t recipients[k_max_private_message_recipients][crypto_scalarmult_curve25519_SCALARBYTES]; + int recipient_count; + const char* message; + size_t message_size; + JSValue promise[2]; + bool error_id_not_found; + bool error_secretbox_failed; + bool error_scalarmult_failed; + char* encrypted; + size_t encrypted_length; +} private_message_encrypt_t; + +static void _tf_ssb_private_message_encrypt_work(tf_ssb_t* ssb, void* user_data) +{ + private_message_encrypt_t* work = user_data; + + uint8_t private_key[crypto_sign_SECRETKEYBYTES] = { 0 }; + sqlite3* db = tf_ssb_acquire_db_reader(ssb); + bool found = _tf_ssb_get_private_key_curve25519(db, work->signer_user, work->signer_identity, private_key); + tf_ssb_release_db_reader(ssb, db); + + if (found) + { + uint8_t public_key[crypto_box_PUBLICKEYBYTES] = { 0 }; + uint8_t secret_key[crypto_box_SECRETKEYBYTES] = { 0 }; + uint8_t nonce[crypto_box_NONCEBYTES] = { 0 }; + uint8_t body_key[crypto_box_SECRETKEYBYTES] = { 0 }; + crypto_box_keypair(public_key, secret_key); + randombytes_buf(nonce, sizeof(nonce)); + randombytes_buf(body_key, sizeof(body_key)); + + uint8_t length_and_key[1 + sizeof(body_key)]; + length_and_key[0] = (uint8_t)work->recipient_count; + memcpy(length_and_key + 1, body_key, sizeof(body_key)); + + size_t payload_size = + sizeof(nonce) + sizeof(public_key) + (crypto_secretbox_MACBYTES + sizeof(length_and_key)) * work->recipient_count + crypto_secretbox_MACBYTES + work->message_size; + + uint8_t* payload = tf_malloc(payload_size); + + uint8_t* p = payload; + memcpy(p, nonce, sizeof(nonce)); + p += sizeof(nonce); + + memcpy(p, public_key, sizeof(public_key)); + p += sizeof(public_key); + + for (int i = 0; i < work->recipient_count; i++) + { + uint8_t shared_secret[crypto_secretbox_KEYBYTES] = { 0 }; + if (crypto_scalarmult(shared_secret, secret_key, work->recipients[i]) == 0) + { + if (crypto_secretbox_easy(p, length_and_key, sizeof(length_and_key), nonce, shared_secret) != 0) + { + work->error_secretbox_failed = true; + break; + } + else + { + p += crypto_secretbox_MACBYTES + sizeof(length_and_key); + } + } + else + { + work->error_scalarmult_failed = true; + break; + } + } + + if (!work->error_secretbox_failed && !work->error_scalarmult_failed) + { + if (crypto_secretbox_easy(p, (const uint8_t*)work->message, work->message_size, nonce, body_key) != 0) + { + work->error_scalarmult_failed = true; + } + else + { + p += crypto_secretbox_MACBYTES + work->message_size; + assert((size_t)(p - payload) == payload_size); + + char* encoded = tf_malloc(payload_size * 2 + 5); + size_t encoded_length = tf_base64_encode(payload, payload_size, encoded, payload_size * 2 + 5); + memcpy(encoded + encoded_length, ".box", 5); + encoded_length += 4; + + work->encrypted = encoded; + work->encrypted_length = encoded_length; + } + } + tf_free(payload); + } + else + { + work->error_id_not_found = true; + } +} + +static void _tf_ssb_private_message_encrypt_after_work(tf_ssb_t* ssb, int status, void* user_data) +{ + private_message_encrypt_t* work = user_data; + JSContext* context = tf_ssb_get_context(ssb); + JSValue result = JS_UNDEFINED; + if (work->error_secretbox_failed) + { + result = JS_ThrowInternalError(context, "crypto_secretbox_easy failed"); + } + else if (work->error_scalarmult_failed) + { + result = JS_ThrowInternalError(context, "crypto_scalarmult failed"); + } + else if (work->error_id_not_found) + { + result = JS_ThrowInternalError(context, "Unable to get key for ID %s of user %s.", work->signer_identity, work->signer_user); + } + else + { + result = JS_NewStringLen(context, work->encrypted, work->encrypted_length); + tf_free((void*)work->encrypted); + } + + JSValue error = JS_Call(context, work->promise[0], JS_UNDEFINED, 1, &result); + JS_FreeValue(context, result); + tf_util_report_error(context, error); + JS_FreeValue(context, error); + JS_FreeValue(context, work->promise[0]); + JS_FreeValue(context, work->promise[1]); + JS_FreeCString(context, work->signer_user); + JS_FreeCString(context, work->signer_identity); + JS_FreeCString(context, work->message); + tf_free(work); +} + static JSValue _tf_ssb_private_message_encrypt(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv) { JSValue result = JS_UNDEFINED; @@ -1863,11 +1999,7 @@ static JSValue _tf_ssb_private_message_encrypt(JSContext* context, JSValueConst return JS_ThrowRangeError(context, "Number of recipients must be between 1 and %d.", k_max_private_message_recipients); } - const char* signer_user = JS_ToCString(context, argv[0]); - const char* signer_identity = JS_ToCString(context, argv[1]); - uint8_t recipients[k_max_private_message_recipients][crypto_scalarmult_curve25519_SCALARBYTES]; - size_t message_size = 0; - const char* message = JS_ToCStringLen(context, &message_size, argv[3]); + uint8_t recipients[k_max_private_message_recipients][crypto_scalarmult_curve25519_SCALARBYTES] = { 0 }; for (int i = 0; i < recipient_count && JS_IsUndefined(result); i++) { JSValue recipient = JS_GetPropertyUint32(context, argv[2], i); @@ -1892,89 +2024,26 @@ static JSValue _tf_ssb_private_message_encrypt(JSContext* context, JSValueConst if (JS_IsUndefined(result)) { + const char* signer_user = JS_ToCString(context, argv[0]); + const char* signer_identity = JS_ToCString(context, argv[1]); + size_t message_size = 0; + const char* message = JS_ToCStringLen(context, &message_size, argv[3]); + tf_ssb_t* ssb = JS_GetOpaque(this_val, _tf_ssb_classId); - uint8_t private_key[crypto_sign_SECRETKEYBYTES] = { 0 }; - - sqlite3* db = tf_ssb_acquire_db_reader(ssb); - bool found = _tf_ssb_get_private_key_curve25519(db, signer_user, signer_identity, private_key); - tf_ssb_release_db_reader(ssb, db); - - if (found) - { - uint8_t public_key[crypto_box_PUBLICKEYBYTES] = { 0 }; - uint8_t secret_key[crypto_box_SECRETKEYBYTES] = { 0 }; - uint8_t nonce[crypto_box_NONCEBYTES] = { 0 }; - uint8_t body_key[crypto_box_SECRETKEYBYTES] = { 0 }; - crypto_box_keypair(public_key, secret_key); - randombytes_buf(nonce, sizeof(nonce)); - randombytes_buf(body_key, sizeof(body_key)); - - uint8_t length_and_key[1 + sizeof(body_key)]; - length_and_key[0] = (uint8_t)recipient_count; - memcpy(length_and_key + 1, body_key, sizeof(body_key)); - - size_t payload_size = - sizeof(nonce) + sizeof(public_key) + (crypto_secretbox_MACBYTES + sizeof(length_and_key)) * recipient_count + crypto_secretbox_MACBYTES + message_size; - - uint8_t* payload = tf_malloc(payload_size); - - uint8_t* p = payload; - memcpy(p, nonce, sizeof(nonce)); - p += sizeof(nonce); - - memcpy(p, public_key, sizeof(public_key)); - p += sizeof(public_key); - - for (int i = 0; i < recipient_count && JS_IsUndefined(result); i++) - { - uint8_t shared_secret[crypto_secretbox_KEYBYTES] = { 0 }; - if (crypto_scalarmult(shared_secret, secret_key, recipients[i]) == 0) - { - if (crypto_secretbox_easy(p, length_and_key, sizeof(length_and_key), nonce, shared_secret) != 0) - { - result = JS_ThrowInternalError(context, "crypto_secretbox_easy failed"); - } - else - { - p += crypto_secretbox_MACBYTES + sizeof(length_and_key); - } - } - else - { - result = JS_ThrowInternalError(context, "crypto_scalarmult failed"); - } - } - - if (JS_IsUndefined(result)) - { - if (crypto_secretbox_easy(p, (const uint8_t*)message, message_size, nonce, body_key) != 0) - { - result = JS_ThrowInternalError(context, "crypto_secretbox_easy failed for the message.\n"); - } - else - { - p += crypto_secretbox_MACBYTES + message_size; - assert((size_t)(p - payload) == payload_size); - - char* encoded = tf_malloc(payload_size * 2 + 5); - size_t encoded_length = tf_base64_encode(payload, payload_size, encoded, payload_size * 2 + 5); - memcpy(encoded + encoded_length, ".box", 5); - encoded_length += 4; - result = JS_NewStringLen(context, encoded, encoded_length); - tf_free(encoded); - } - } - tf_free(payload); - } - else - { - result = JS_ThrowInternalError(context, "Unable to get key for ID %s of user %s.", signer_identity, signer_user); - } + private_message_encrypt_t* work = tf_malloc(sizeof(private_message_encrypt_t)); + *work = (private_message_encrypt_t) { + .signer_user = signer_user, + .signer_identity = signer_identity, + .recipient_count = recipient_count, + .message = message, + .message_size = message_size, + }; + static_assert(sizeof(work->recipients) == sizeof(recipients), "size mismatch"); + memcpy(work->recipients, recipients, sizeof(recipients)); + result = JS_NewPromiseCapability(context, work->promise); + tf_ssb_run_work(ssb, _tf_ssb_private_message_encrypt_work, _tf_ssb_private_message_encrypt_after_work, work); } - JS_FreeCString(context, signer_user); - JS_FreeCString(context, signer_identity); - JS_FreeCString(context, message); return result; } diff --git a/src/ssb.tests.c b/src/ssb.tests.c index 1c97c8b6..b4ed3311 100644 --- a/src/ssb.tests.c +++ b/src/ssb.tests.c @@ -819,3 +819,42 @@ void tf_ssb_test_go_ssb_room(const tf_test_options_t* options) uv_loop_close(&loop); } + +static void _write_file(const char* path, const char* contents) +{ + FILE* file = fopen(path, "w"); + if (!file) + { + printf("Unable to write %s: %s.\n", path, strerror(errno)); + fflush(stdout); + abort(); + } + fputs(contents, file); + fclose(file); +} + +#define TEST_ARGS " --ssb-port=0 --http-port=0 --https-port=0" + +void tf_ssb_test_encrypt(const tf_test_options_t* options) +{ + _write_file("out/test.js", + "async function main() {\n" + " let a = await ssb.createIdentity('test');\n" + " let b = await ssb.createIdentity('test');\n" + " let c = await ssb.privateMessageEncrypt('test', a, [a, b], {'foo': 1});\n" + " if (!c.endsWith('.box')) {\n" + " exit(1);\n" + " }\n" + "}\n" + "main().catch(() => exit(2));\n"); + + unlink("out/testdb.sqlite"); + char command[256]; + snprintf(command, sizeof(command), "%s run --db-path=out/testdb.sqlite -s out/test.js" TEST_ARGS, options->exe_path); + tf_printf("%s\n", command); + int result = system(command); + (void)result; + assert(WIFEXITED(result)); + printf("returned %d\n", WEXITSTATUS(result)); + assert(WEXITSTATUS(result) == 0); +} diff --git a/src/ssb.tests.h b/src/ssb.tests.h index d9d31c36..06dc4665 100644 --- a/src/ssb.tests.h +++ b/src/ssb.tests.h @@ -47,4 +47,10 @@ void tf_ssb_test_bench(const tf_test_options_t* options); */ void tf_ssb_test_go_ssb_room(const tf_test_options_t* options); +/** +** Test encrypting a private message. +** @param options The test options. +*/ +void tf_ssb_test_encrypt(const tf_test_options_t* options); + /** @} */ diff --git a/src/tests.c b/src/tests.c index b457b814..c2629896 100644 --- a/src/tests.c +++ b/src/tests.c @@ -914,6 +914,7 @@ void tf_tests(const tf_test_options_t* options) _tf_test_run(options, "bench", tf_ssb_test_bench, false); _tf_test_run(options, "auto", _test_auto, false); _tf_test_run(options, "go-ssb-room", tf_ssb_test_go_ssb_room, true); + _tf_test_run(options, "encrypt", tf_ssb_test_encrypt, false); tf_printf("Tests completed.\n"); #endif }