Move the bulk of ssb.privateMessageEncrypt work (CPU + DB) off the main thread.

This commit is contained in:
Cory McWilliams 2024-06-16 17:07:12 -04:00
parent 9b52415b35
commit d5a7e19f1a
4 changed files with 200 additions and 85 deletions

View File

@ -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");
}
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);
}
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);
}
}
JS_FreeCString(context, signer_user);
JS_FreeCString(context, signer_identity);
JS_FreeCString(context, message);
return result;
}

View File

@ -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);
}

View File

@ -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);
/** @} */

View File

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