ssb: Support publishing private messages from the command-line. #89
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 26m28s
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 26m28s
This commit is contained in:
parent
3352098284
commit
872201c886
135
src/main.c
135
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();
|
||||
|
64
src/ssb.c
64
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;
|
||||
}
|
||||
|
13
src/ssb.h
13
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);
|
||||
|
||||
/** @} */
|
||||
|
104
src/ssb.js.c
104
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);
|
||||
|
Loading…
x
Reference in New Issue
Block a user