diff --git a/src/main.c b/src/main.c index c6c89a0a..0a358628 100644 --- a/src/main.c +++ b/src/main.c @@ -144,6 +144,7 @@ static void _create_directories_for_file(const char* path, int mode) static int _tf_command_export(const char* file, int argc, char* argv[]); static int _tf_command_import(const char* file, int argc, char* argv[]); static int _tf_command_publish(const char* file, int argc, char* argv[]); +static int _tf_command_private(const char* file, int argc, char* argv[]); static int _tf_command_run(const char* file, int argc, char* argv[]); static int _tf_command_sandbox(const char* file, int argc, char* argv[]); static int _tf_command_has_blob(const char* file, int argc, char* argv[]); @@ -168,6 +169,7 @@ const command_t k_commands[] = { { "import", _tf_command_import, "Import apps to SSB." }, { "export", _tf_command_export, "Export apps from SSB." }, { "publish", _tf_command_publish, "Append a message to a feed." }, + { "private", _tf_command_private, "Append a private post message to a feed." }, { "get_sequence", _tf_command_get_sequence, "Get the last sequence number for a feed." }, { "get_identity", _tf_command_get_identity, "Get the server account identity." }, { "get_profile", _tf_command_get_profile, "Get profile information for the given identity." }, @@ -505,6 +507,139 @@ static int _tf_command_publish(const char* file, int argc, char* argv[]) return result; } +static int _tf_command_private(const char* file, int argc, char* argv[]) +{ + const char* user = NULL; + const char* identity = NULL; + const char* default_db_path = _get_db_path(); + const char* db_path = default_db_path; + const char* text = NULL; + const char* recipients = NULL; + bool show_usage = false; + + while (!show_usage) + { + static const struct option k_options[] = { + { "user", required_argument, NULL, 'u' }, + { "id", required_argument, NULL, 'i' }, + { "recipients", required_argument, NULL, 'r' }, + { "db-path", required_argument, NULL, 'd' }, + { "text", required_argument, NULL, 'c' }, + { "help", no_argument, NULL, 'h' }, + { 0 }, + }; + int c = getopt_long(argc, argv, "u:i:d:t:r:h", k_options, NULL); + if (c == -1) + { + break; + } + + switch (c) + { + case '?': + case 'h': + default: + show_usage = true; + break; + case 'u': + user = optarg; + break; + case 'i': + identity = optarg; + break; + case 'd': + db_path = optarg; + break; + case 't': + text = optarg; + break; + case 'r': + recipients = optarg; + break; + } + } + + if (show_usage || !user || !identity || !recipients || !text) + { + tf_printf("\n%s private [options]\n\n", file); + tf_printf("options:\n"); + tf_printf(" -u, --user user User owning identity with which to publish.\n"); + tf_printf(" -i, --id identity Identity with which to publish message.\n"); + tf_printf(" -r, --recipients recipients Recipient identities.\n"); + tf_printf(" -d, --db-path db_path SQLite database path (default: %s).\n", default_db_path); + tf_printf(" -t, --text text Private post text.\n"); + tf_printf(" -h, --help Show this usage information.\n"); + tf_free((void*)default_db_path); + return EXIT_FAILURE; + } + + int result = EXIT_FAILURE; + tf_printf("Posting %s as account %s belonging to %s...\n", text, identity, user); + _create_directories_for_file(db_path, 0700); + tf_ssb_t* ssb = tf_ssb_create(NULL, NULL, db_path, NULL); + uint8_t private_key[512] = { 0 }; + const char* recipient_list[k_max_private_message_recipients] = { 0 }; + int recipient_count = 0; + + recipient_list[recipient_count++] = identity; + + if (tf_ssb_db_identity_get_private_key(ssb, user, identity, private_key, sizeof(private_key))) + { + char* copy = tf_strdup(recipients); + char* next = NULL; + const char* it = strtok_r(copy, ",", &next); + while (it) + { + if (recipient_count == k_max_private_message_recipients) + { + tf_printf("Too many recipients (max %d).\n", k_max_private_message_recipients); + goto done; + } + recipient_list[recipient_count++] = it; + it = strtok_r(NULL, ",", &next); + } + + JSContext* context = tf_ssb_get_context(ssb); + JSValue message = JS_NewObject(context); + JS_SetPropertyStr(context, message, "type", JS_NewString(context, "post")); + JS_SetPropertyStr(context, message, "text", JS_NewString(context, text)); + JSValue recps = JS_NewArray(context); + for (int i = 0; i < recipient_count; i++) + { + JS_SetPropertyUint32(context, recps, i, JS_NewString(context, recipient_list[i])); + } + JS_SetPropertyStr(context, message, "recps", recps); + JSValue json = JS_JSONStringify(context, message, JS_NULL, JS_NULL); + const char* message_str = JS_ToCString(context, json); + char* encrypted = tf_ssb_private_message_encrypt(private_key, recipient_list, recipient_count, message_str, strlen(message_str)); + if (encrypted) + { + int64_t sequence = 0; + char previous[k_id_base64_len] = { 0 }; + tf_ssb_db_get_latest_message_by_author(ssb, identity, &sequence, previous, sizeof(previous)); + + JSValue content = JS_NewString(context, encrypted); + JSValue to_publish = tf_ssb_sign_message(ssb, identity, private_key, content, previous, sequence); + tf_ssb_verify_strip_and_store_message(ssb, to_publish, _tf_published_callback, &result); + JS_FreeValue(context, to_publish); + JS_FreeValue(context, content); + } + tf_free(encrypted); + JS_FreeCString(context, message_str); + JS_FreeValue(context, json); + JS_FreeValue(context, message); + tf_free(copy); + } + else + { + tf_printf("Did not find private key for identity %s belonging to %s.\n", identity, user); + } +done: + tf_ssb_destroy(ssb); + tf_free((void*)default_db_path); + return result; +} + static int _tf_command_store_blob(const char* file, int argc, char* argv[]) { const char* default_db_path = _get_db_path(); diff --git a/src/ssb.c b/src/ssb.c index 673ce389..96a27fcc 100644 --- a/src/ssb.c +++ b/src/ssb.c @@ -4389,3 +4389,67 @@ tf_ssb_ebt_t* tf_ssb_connection_get_ebt(tf_ssb_connection_t* connection) { return connection ? connection->ebt : NULL; } + +char* tf_ssb_private_message_encrypt(uint8_t* private_key, const char** recipients, int recipients_count, const char* message, size_t message_size) +{ + 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)recipients_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)) * recipients_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 < recipients_count; i++) + { + uint8_t recipient[crypto_scalarmult_curve25519_SCALARBYTES] = { 0 }; + uint8_t key[crypto_box_PUBLICKEYBYTES] = { 0 }; + tf_ssb_id_str_to_bin(key, recipients[i]); + if (crypto_sign_ed25519_pk_to_curve25519(recipient, key) != 0) + { + return NULL; + } + + uint8_t shared_secret[crypto_secretbox_KEYBYTES] = { 0 }; + if (crypto_scalarmult(shared_secret, secret_key, recipient) != 0) + { + return NULL; + } + if (crypto_secretbox_easy(p, length_and_key, sizeof(length_and_key), nonce, shared_secret) != 0) + { + return NULL; + } + + p += crypto_secretbox_MACBYTES + sizeof(length_and_key); + } + + if (crypto_secretbox_easy(p, (const uint8_t*)message, message_size, nonce, body_key) != 0) + { + return NULL; + } + + 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); + + tf_free(payload); + return encoded; +} diff --git a/src/ssb.h b/src/ssb.h index c0a7a282..fc53b55e 100644 --- a/src/ssb.h +++ b/src/ssb.h @@ -30,6 +30,8 @@ enum k_ssb_blob_bytes_max = 5 * 1024 * 1024, k_ssb_peer_exchange_expires_seconds = 60 * 60, + + k_max_private_message_recipients = 8, }; /** @@ -1128,4 +1130,15 @@ int tf_ssb_connection_get_flags(tf_ssb_connection_t* connection); */ tf_ssb_ebt_t* tf_ssb_connection_get_ebt(tf_ssb_connection_t* connection); +/** +** Encrypt a private message to a set of recipients. +** @param private_key The private key of the author. +** @param recipients A list of recipient identities. +** @param recipients_count The number of recipients in recipients. +** @param message The plain text to post. +** @param message_size The length in bytes of message. +** @return A secret box string. Free with tf_free(). +*/ +char* tf_ssb_private_message_encrypt(uint8_t* private_key, const char** recipients, int recipients_count, const char* message, size_t message_size); + /** @} */ diff --git a/src/ssb.js.c b/src/ssb.js.c index 8a750a12..3ee531c6 100644 --- a/src/ssb.js.c +++ b/src/ssb.js.c @@ -1857,11 +1857,6 @@ static JSValue _tf_ssb_createTunnel(JSContext* context, JSValueConst this_val, i return result ? JS_TRUE : JS_FALSE; } -enum -{ - k_max_private_message_recipients = 8 -}; - static bool _tf_ssb_get_private_key_curve25519_internal(sqlite3* db, const char* user, const char* identity, uint8_t out_private_key[static crypto_sign_SECRETKEYBYTES]) { if (!user || !identity) @@ -1910,14 +1905,12 @@ 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]; + const char* recipients[k_max_private_message_recipients]; 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; @@ -1933,73 +1926,8 @@ static void _tf_ssb_private_message_encrypt_work(tf_ssb_t* ssb, void* user_data) 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); + work->encrypted = tf_ssb_private_message_encrypt(private_key, work->recipients, work->recipient_count, work->message, work->message_size); + work->encrypted_length = work->encrypted ? strlen(work->encrypted) : 0; } else { @@ -2012,13 +1940,9 @@ static void _tf_ssb_private_message_encrypt_after_work(tf_ssb_t* ssb, int status private_message_encrypt_t* work = user_data; JSContext* context = tf_ssb_get_context(ssb); JSValue result = JS_UNDEFINED; - if (work->error_secretbox_failed) + if (!work->encrypted) { - result = JS_ThrowInternalError(context, "crypto_secretbox_easy failed"); - } - else if (work->error_scalarmult_failed) - { - result = JS_ThrowInternalError(context, "crypto_scalarmult failed"); + result = JS_ThrowInternalError(context, "Encrypt failed."); } else if (work->error_id_not_found) { @@ -2030,6 +1954,10 @@ static void _tf_ssb_private_message_encrypt_after_work(tf_ssb_t* ssb, int status tf_free((void*)work->encrypted); } + for (int i = 0; i < work->recipient_count; i++) + { + tf_free((void*)work->recipients[i]); + } JSValue error = JS_Call(context, work->promise[0], JS_UNDEFINED, 1, &result); JS_FreeValue(context, result); tf_util_report_error(context, error); @@ -2051,24 +1979,14 @@ 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); } - uint8_t recipients[k_max_private_message_recipients][crypto_scalarmult_curve25519_SCALARBYTES] = { 0 }; + char* recipients[k_max_private_message_recipients] = { 0 }; for (int i = 0; i < recipient_count && JS_IsUndefined(result); i++) { JSValue recipient = JS_GetPropertyUint32(context, argv[2], i); const char* id = JS_ToCString(context, recipient); if (id) { - const char* type = strstr(id, ".ed25519"); - const char* id_start = *id == '@' ? id + 1 : id; - uint8_t key[crypto_box_PUBLICKEYBYTES] = { 0 }; - if (tf_base64_decode(id_start, type ? (size_t)(type - id_start) : strlen(id_start), key, sizeof(key)) != sizeof(key)) - { - result = JS_ThrowInternalError(context, "Invalid recipient: %s.\n", id); - } - else if (crypto_sign_ed25519_pk_to_curve25519(recipients[i], key) != 0) - { - result = JS_ThrowInternalError(context, "Failed to convert recipient ID.\n"); - } + recipients[i] = tf_strdup(id); JS_FreeCString(context, id); } JS_FreeValue(context, recipient);