ssb: Support publishing private messages from the command-line. #89
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 26m28s

This commit is contained in:
Cory McWilliams 2025-01-08 20:16:17 -05:00
parent 3352098284
commit 872201c886
4 changed files with 223 additions and 93 deletions

View File

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

View File

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

View File

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

View File

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