From c293bd845e87491f109976114cf5ba3a11289951 Mon Sep 17 00:00:00 2001 From: Gergely Polonkai Date: Tue, 22 Jan 2019 15:14:03 +0100 Subject: [PATCH] =?UTF-8?q?wip:=20i=20really=20don=E2=80=99t=20know=20what?= =?UTF-8?q?=20i=20did=20here?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ssb-gtk/meson.build | 2 +- ssb-gtk/sbot.c | 1305 ---------------------------- ssb-gtk/ssb-scuttler.c | 1245 ++++++++++++++++++++++++++ ssb-gtk/{sbot.h => ssb-scuttler.h} | 0 4 files changed, 1246 insertions(+), 1306 deletions(-) delete mode 100644 ssb-gtk/sbot.c create mode 100644 ssb-gtk/ssb-scuttler.c rename ssb-gtk/{sbot.h => ssb-scuttler.h} (100%) diff --git a/ssb-gtk/meson.build b/ssb-gtk/meson.build index 5cd57be..9ccce07 100644 --- a/ssb-gtk/meson.build +++ b/ssb-gtk/meson.build @@ -3,7 +3,7 @@ sources = [ 'ssb-app.c', 'ssb-window.c', 'ssb-profile.c', - 'sbot.c', + 'ssb-scuttler.c', ] executable('ssb-gtk', sources, ssb_resources, diff --git a/ssb-gtk/sbot.c b/ssb-gtk/sbot.c deleted file mode 100644 index 662129d..0000000 --- a/ssb-gtk/sbot.c +++ /dev/null @@ -1,1305 +0,0 @@ -#include -#include -#include - -#include "sbot.h" - -#define MAX_MESSAGE_SIZE 32768 -#define BOXS_MAXLEN 4096 - -struct _SsbScuttler { - GObject parent_instance; - - // If TRUE, the scuttler is already initialised - gboolean initialised; - - // The SSB directory - gchar *ssb_dir; - - guchar *app_key; - guchar *private_key; - guchar *encrypt_key; - guchar *decrypt_key; - guchar nonce1[24]; - guchar nonce2[24]; - guchar rx_nonce[24]; - guchar rx_buf[BOXS_MAXLEN]; - gsize rx_buf_pos; - gsize rx_buf_len; - gboolean noauth; - gboolean wrote_goodbye; - GSocketClient *socket_client; - GSocketConnection *connection; - guint request_id; -}; - -typedef enum { - SSB_PACKET_TYPE_BUFFER = 0, - SSB_PACKET_TYPE_STRING = 1, - SSB_PACKET_TYPE_JSON = 2, -} SsbPacketType; - -typedef enum { - SSB_PACKET_FLAGS_BUFFER = 0, - SSB_PACKET_FLAGS_STRING = 1, - SSB_PACKET_FLAGS_JSON = 2, - SSB_PACKET_FLAGS_END = 4, - SSB_PACKET_FLAGS_STREAM = 8, -} SsbPacketFlags; - -typedef enum { - SSB_RPC_ASYNC, - SSB_RPC_SOURCE, - SSB_RPC_SINK, - SSB_RPC_DUPLEX -} SsbRPCType; - -typedef enum { - SSB_STREAM_STATE_OPEN, - SSB_STREAM_STATE_ENDED_OK, - SSB_STREAM_STATE_ENDED_ERROR, -} SsbStreamState; - -G_DEFINE_QUARK(ssb_scttler_error_quark, ssb_scuttler_error); -G_DEFINE_TYPE(SsbScuttler, ssb_scuttler, G_TYPE_INITIALLY_UNOWNED); - -typedef struct { - gboolean valid; - guint line; - guint column; -} ParseData; - -typedef struct { - guint32 len; - gint32 request_id; -} PacketHeader; - -typedef struct { - guint16 len; - guint8 mac[16]; -} BoxsHeader; - -static const guchar zeros[24] = {0}; - -static const guchar ssb_cap[] = { - 0xd4, 0xa1, 0xcb, 0x88, 0xa6, 0x6f, 0x02, 0xf8, - 0xdb, 0x63, 0x5c, 0xe2, 0x64, 0x41, 0xcc, 0x5d, - 0xac, 0x1b, 0x08, 0x42, 0x0c, 0xea, 0xac, 0x23, - 0x08, 0x39, 0xb7, 0x55, 0x84, 0x5a, 0x9f, 0xfb -}; - -enum { - SIGNAL_INITIALISED, - SIGNAL_CONNECTED, - SIGNAL_LAST -}; - -enum { - PROP_0, - PROP_INITIALISED, - PROP_CONNECTED, - PROP_COUNT -}; - -static guint ssb_scuttler_signals[SIGNAL_LAST] = {0}; -static GParamSpec *ssb_scuttler_properties[PROP_COUNT]; - -GMainLoop *scuttle_loop = NULL; -SsbScuttler *singleton = NULL; - -static gchar * -read_config(const gchar *base_dir, const gchar *file_name, GError **error) { - gchar *config_file_name; - GFile *config_file; - GFileInfo *config_info; - goffset config_size; - GFileInputStream *config_stream; - GError *err = NULL; - gchar *config_data = NULL; - - config_file_name = g_strdup_printf("%s/%s", base_dir, file_name); - config_file = g_file_new_for_path(config_file_name); - g_free(config_file_name); - - if ((config_info = g_file_query_info(config_file, G_FILE_ATTRIBUTE_STANDARD_SIZE, G_FILE_QUERY_INFO_NONE, NULL, &err)) == NULL) { - g_propagate_error(error, err); - - goto ret_err1; - } - - config_size = g_file_info_get_size(config_info); - - config_data = g_new0(gchar, config_size); - - if ((config_stream = g_file_read(config_file, NULL, &err)) == NULL) { - g_propagate_error(error, err); - g_clear_pointer(&config_data, g_free); - - goto ret_err2; - } - - if (!g_input_stream_read_all(G_INPUT_STREAM(config_stream), config_data, config_size, NULL, NULL, &err)) { - g_propagate_error(error, err); - g_clear_pointer(&config_data, g_free); - - goto ret_err3; - } - - if (!g_input_stream_close(G_INPUT_STREAM(config_stream), NULL, &err)) { - g_propagate_error(error, err); - g_clear_pointer(&config_data, g_free); - - goto ret_err3; - } - - ret_err3: - g_object_unref(config_info); - - ret_err2: - g_object_unref(config_stream); - - ret_err1: - g_object_unref(config_file); - - return config_data; -} - -static void -json_error(JsonParser *parser, GError *error, ParseData *parse_data) -{ - if ((error->domain != JSON_PARSER_ERROR) || (error->code != JSON_PARSER_ERROR_INVALID_BAREWORD)) { - parse_data->valid = FALSE; - } else { - parse_data->valid = TRUE; - parse_data->line = json_parser_get_current_line(parser) - 1; - parse_data->column = json_parser_get_current_pos(parser) - 1; - } -} - -static JsonNode * -parse_commented_json(gchar *json_data, GError **error) -{ - ParseData parse_data; - JsonParser *parser; - GError *err = NULL; - JsonNode *result = NULL; - - parse_data.valid = FALSE; - parser = json_parser_new(); - g_signal_connect(parser, "error", G_CALLBACK(json_error), &parse_data); - - while (!json_parser_load_from_data(parser, json_data, -1, &err)) { - gchar **config_lines; - - if (parse_data.valid != TRUE) { - g_propagate_error(error, err); - - break; - } - - // XXX: Will this work under OS/X and Windows? - config_lines = g_strsplit(json_data, "\n", 0); - - if (config_lines[parse_data.line][parse_data.column] == '#') { - guint i; - guint start_pos = 0; - guint end_pos; - - for (i = 0; i <= parse_data.line; i++) { - if (i < parse_data.line) { - start_pos += strlen(config_lines[i]) + 1; - } - - end_pos = start_pos; - end_pos += strlen(config_lines[i]); - } - - start_pos += parse_data.column; - - for (i = start_pos; i < end_pos; i++) { - json_data[i] = ' '; - } - - g_clear_error(&err); - parse_data.valid = FALSE; - } - - g_strfreev(config_lines); - } - - result = json_parser_steal_root(parser); - - g_object_unref(parser); - - return result; -} - -static void -emit_initialised(SsbScuttler *scuttler, gboolean success) -{ - g_signal_emit(scuttler, ssb_scuttler_signals[SIGNAL_INITIALISED], 0, success); -} - -static void -initialise(SsbScuttler *scuttler, const gchar *ssb_dir) -{ - gchar *config_data = NULL; - GError *err = NULL; - JsonNode *json_content = NULL; - - g_return_if_fail(scuttler != NULL); - - if (scuttler->initialised == TRUE) { - return; - } - - if ((config_data = read_config(ssb_dir, "config", &err)) != NULL) { - if ((json_content = parse_commented_json(config_data, &err)) == NULL) { - g_critical("Could not parse configuration data"); - - g_free(config_data); - - emit_initialised(scuttler, FALSE); - - return; - } - - g_free(config_data); - g_clear_pointer(&json_content, json_node_free); - } - - if ((config_data = read_config(ssb_dir, "secret", &err)) != NULL) { - JsonObject *secret_object; - gchar *private_key = NULL; - guchar *decoded_private_key = NULL; - size_t private_key_len; - gsize key_len; - - if ((json_content = parse_commented_json(config_data, &err)) == NULL) { - g_critical("Could not parse secret data"); - - g_free(config_data); - - emit_initialised(scuttler, FALSE); - - return; - } - - secret_object = json_node_get_object(json_content); - - if ((private_key = g_strdup(json_object_get_string_member(secret_object, "private"))) == NULL) { - g_critical("Could not read private key, can not continue"); - - emit_initialised(scuttler, FALSE); - - return; - } - - private_key_len = strlen(private_key); - - if ((strlen(private_key) > 8) && - (strcmp(private_key + private_key_len - 8, ".ed25519") == 0)) { - private_key[private_key_len - 8] = 0; - } - - if ((decoded_private_key = g_base64_decode(private_key, &key_len)) == NULL) { - g_critical("Could not decode private key, can not continue"); - g_free(private_key); - - emit_initialised(scuttler, FALSE); - - return; - } - - g_free(private_key); - g_free(config_data); - g_clear_pointer(&json_content, json_node_free); - - g_clear_pointer(&(scuttler->private_key), g_free); - scuttler->private_key = decoded_private_key; - } - - scuttler->app_key = g_new0(guchar, sizeof(ssb_cap)); - memcpy(scuttler->app_key, ssb_cap, sizeof(ssb_cap)); - - scuttler->initialised = TRUE; - - emit_initialised(scuttler, TRUE); -} - -typedef struct { - SsbScuttler *scuttler; - gpointer read_buffer; -} DataReadType; - -static gboolean check_incoming_data(SsbScuttler *scuttler); - -static void -data_read(GObject *source, GAsyncResult *result, gpointer user_data) -{ - gssize bytes_read; - /* SsbScuttler *scuttler = ((DataReadType *)user_data)->scuttler; */ - /* gpointer read_buffer = ((DataReadType *)user_data)->read_buffer; */ - GError *err = NULL; - - g_debug("g_input_stream_read_async() finished"); - - // We no longer need this struct, just the pointers within which are saved few lines above - g_slice_free(DataReadType, user_data); - - if ((bytes_read = g_input_stream_read_finish(G_INPUT_STREAM(source), result, &err)) == -1) { - g_error("Error while reading from socket: %s", err->message); - - return; - } - - g_debug("Read %ld bytes", bytes_read); -} - -static gboolean -read_inbound(GSocket *socket, GIOCondition condition, SsbScuttler *scuttler) -{ - DataReadType *data = g_slice_new(DataReadType); - GInputStream *stream = g_io_stream_get_input_stream(G_IO_STREAM(scuttler->connection)); - - g_debug("Data is flowing in!"); - - data->scuttler = scuttler; - data->read_buffer = g_malloc(MAX_MESSAGE_SIZE); - - g_input_stream_read_async(stream, - data->read_buffer, 2, - G_PRIORITY_DEFAULT, - NULL, - data_read, data); - - return FALSE; -} - -/** - * ssb_scuttler_ensure(): - * @ssb_dir: (nullable): the SSB directory - * - * Ensure the scuttler singleton is initialised with @ssb_dir as its base directory. - */ -gboolean -ssb_scuttler_ensure(const gchar *ssb_dir) -{ - if (singleton != NULL) { - if ((ssb_dir != NULL) && g_strcmp0(ssb_dir, singleton->ssb_dir)) { - return FALSE; - } - - return TRUE; - } - - singleton = g_object_new(SSB_TYPE_SCUTTLER, NULL); - - if (ssb_dir != NULL) { - initialise(singleton, ssb_dir); - } - - return TRUE; -} - -// was: write_all -static gboolean -ssb_scuttler_send_plain(SsbScuttler *scuttler, gpointer data, gsize data_len, GError **error) -{ - GOutputStream *stream = g_io_stream_get_output_stream(G_IO_STREAM(scuttler->connection)); - GError *err = NULL; - - if (!g_output_stream_write_all(stream, data, data_len, NULL, NULL, &err)) { - g_propagate_error(error, err); - - return FALSE; - } - - return TRUE; -} - -// was: read_all -static gboolean -ssb_scuttler_read(SsbScuttler *scuttler, gpointer buffer, gsize buffer_len, GError **error) -{ - GInputStream *stream = g_io_stream_get_input_stream(G_IO_STREAM(scuttler->connection)); - GError *err = NULL; - - if (g_input_stream_read(stream, buffer, buffer_len, NULL, &err) < 0) { - g_propagate_error(error, err); - - return FALSE; - } - - return TRUE; -} - -// was: shs_connect -static gboolean -ssb_scuttler_shs_connect(SsbScuttler *scuttler, GError **error) -{ - guchar kx_pk[crypto_box_PUBLICKEYBYTES]; - guchar kx_sk[crypto_box_SECRETKEYBYTES]; - guchar local_app_mac[crypto_box_PUBLICKEYBYTES]; - guchar remote_app_mac[crypto_box_PUBLICKEYBYTES]; - guchar remote_kx_pk[crypto_box_PUBLICKEYBYTES]; - guchar buf[2 * crypto_box_PUBLICKEYBYTES]; - guchar secret[crypto_box_SECRETKEYBYTES]; - guchar remote_pk_curve[crypto_box_PUBLICKEYBYTES]; - guchar a_bob[crypto_box_PUBLICKEYBYTES]; - guchar secret2a[crypto_box_PUBLICKEYBYTES * 3]; - guchar secret2[crypto_box_PUBLICKEYBYTES]; - guchar shash[crypto_box_PUBLICKEYBYTES]; - guchar signed1[3 * crypto_box_PUBLICKEYBYTES]; - guchar sig[2 * crypto_box_PUBLICKEYBYTES]; - guchar hello[3 * crypto_box_PUBLICKEYBYTES]; - guchar boxed_auth[112]; - guchar boxed_response[80]; - guchar local_sk_curve[crypto_box_SECRETKEYBYTES]; - guchar b_alice[crypto_box_SECRETKEYBYTES]; - guchar secret3a[4 * crypto_box_SECRETKEYBYTES]; - guchar secret3[crypto_box_SECRETKEYBYTES]; - guchar signed2[160]; - guchar enc_key_hashed[crypto_box_PUBLICKEYBYTES + crypto_box_SECRETKEYBYTES]; - guchar dec_key_hashed[crypto_box_PUBLICKEYBYTES + crypto_box_SECRETKEYBYTES]; - GError *err = NULL; - - if (crypto_box_keypair(kx_pk, kx_sk) < 0) { - g_set_error(error, - SSB_SCUTTLER_ERROR, SSB_SCUTTLER_ERROR_KEYGEN, - "Could not generate auth keypair"); - - return FALSE; - } - - g_debug("SHS auth keypair generated"); - - if (crypto_auth(local_app_mac, kx_pk, crypto_box_PUBLICKEYBYTES, scuttler->app_key) < 0) { - g_set_error(error, - SSB_SCUTTLER_ERROR, SSB_SCUTTLER_ERROR_KEYAUTH, - "Failed to generate app mac"); - - return FALSE; - } - - // Send challenge - memcpy(buf, local_app_mac, crypto_box_PUBLICKEYBYTES); - memcpy(buf + crypto_box_PUBLICKEYBYTES, kx_pk, crypto_box_PUBLICKEYBYTES); - - if (!ssb_scuttler_send_plain(scuttler, buf, sizeof(buf), &err)) { - g_propagate_error(error, err); - - return FALSE; - } - - g_debug("Challenge sent"); - - // Receive challenge response - if (!ssb_scuttler_read(scuttler, buf, sizeof(buf), &err)) { - g_propagate_error(error, err); - - return FALSE; - } - - g_debug("Challenge response read"); - - memcpy(remote_app_mac, buf, crypto_box_PUBLICKEYBYTES); - memcpy(remote_kx_pk, buf + crypto_box_PUBLICKEYBYTES, crypto_box_PUBLICKEYBYTES); - - if (crypto_auth_verify(buf, remote_kx_pk, crypto_box_PUBLICKEYBYTES, scuttler->app_key) < 0) { - g_set_error(error, - SSB_SCUTTLER_ERROR, SSB_SCUTTLER_ERROR_KEYVERIFY, - "Wrong protocol (version?)"); - - return FALSE; - } - - // Send auth - if (crypto_scalarmult(secret, kx_sk, remote_kx_pk) < 0) { - g_set_error(error, SSB_SCUTTLER_ERROR, SSB_SCUTTLER_ERROR_AUTH, "Failed to derive shared secret"); - - return FALSE; - } - - if (crypto_sign_ed25519_pk_to_curve25519(remote_pk_curve, scuttler->private_key + crypto_box_SECRETKEYBYTES) < 0) { - g_set_error(error, SSB_SCUTTLER_ERROR, SSB_SCUTTLER_ERROR_AUTH, "Failed to curvify remote public key"); - - return FALSE; - } - - if (crypto_scalarmult(a_bob, kx_sk, remote_pk_curve) < 0) { - g_set_error(error, SSB_SCUTTLER_ERROR, SSB_SCUTTLER_ERROR_AUTH, "Failed to derive a_bob"); - - return FALSE; - } - - memcpy(secret2a, scuttler->app_key, crypto_box_PUBLICKEYBYTES); - memcpy(secret2a + crypto_box_PUBLICKEYBYTES, secret, crypto_box_PUBLICKEYBYTES); - memcpy(secret2a + 2 * crypto_box_PUBLICKEYBYTES, a_bob, crypto_box_PUBLICKEYBYTES); - - if (crypto_hash_sha256(secret2, secret2a, sizeof(secret2a)) < 0) { - g_set_error(error, SSB_SCUTTLER_ERROR, SSB_SCUTTLER_ERROR_AUTH, "Failed to hash secret2"); - - return FALSE; - } - - if (crypto_hash_sha256(shash, secret, sizeof(secret)) < 0) { - g_set_error(error, SSB_SCUTTLER_ERROR, SSB_SCUTTLER_ERROR_AUTH, "Failed to hash secret"); - - return FALSE; - } - - memcpy(signed1, scuttler->app_key, crypto_box_PUBLICKEYBYTES); - memcpy(signed1 + crypto_box_PUBLICKEYBYTES, scuttler->private_key + crypto_box_SECRETKEYBYTES, crypto_box_PUBLICKEYBYTES); - memcpy(signed1 + 2 * crypto_box_PUBLICKEYBYTES, shash, crypto_box_PUBLICKEYBYTES); - - if (crypto_sign_detached(sig, NULL, signed1, sizeof(signed1), scuttler->private_key) < 0) { - g_set_error(error, SSB_SCUTTLER_ERROR, SSB_SCUTTLER_ERROR_AUTH, "Failed to sign inner hello"); - - return FALSE; - } - - memcpy(hello, sig, 2 * crypto_box_PUBLICKEYBYTES); - memcpy(hello + 2 * crypto_box_PUBLICKEYBYTES, scuttler->private_key + crypto_box_SECRETKEYBYTES, crypto_box_PUBLICKEYBYTES); - - if (crypto_secretbox_easy(boxed_auth, hello, sizeof(hello), zeros, secret2) < 0) { - g_set_error(error, SSB_SCUTTLER_ERROR, SSB_SCUTTLER_ERROR_AUTH, "Failed to box hello"); - - return FALSE; - } - - if (!ssb_scuttler_send_plain(scuttler, boxed_auth, sizeof(boxed_auth), &err)) { - g_propagate_error(error, err); - - return FALSE; - } - - // Verify the auth response - - if (!ssb_scuttler_read(scuttler, boxed_response, sizeof(boxed_response), &err)) { - g_propagate_error(error, err); - - return FALSE; - } - - if (crypto_sign_ed25519_sk_to_curve25519(local_sk_curve, scuttler->private_key) < 0) { - g_set_error(error, SSB_SCUTTLER_ERROR, SSB_SCUTTLER_ERROR_AUTH, "Failed to curvify local secret key"); - - return FALSE; - } - - if (crypto_scalarmult(b_alice, local_sk_curve, remote_kx_pk) < 0) { - g_set_error(error, SSB_SCUTTLER_ERROR, SSB_SCUTTLER_ERROR_AUTH, "Failed to derive b_alice"); - - return FALSE; - } - - memcpy(secret3a, scuttler->app_key, 32); - memcpy(secret3a + crypto_box_SECRETKEYBYTES, secret, crypto_box_SECRETKEYBYTES); - memcpy(secret3a + 2 * crypto_box_SECRETKEYBYTES, a_bob, crypto_box_PUBLICKEYBYTES); - memcpy(secret3a + 2 * crypto_box_SECRETKEYBYTES + crypto_box_PUBLICKEYBYTES, b_alice, crypto_box_SECRETKEYBYTES); - - if (crypto_hash_sha256(secret3, secret3a, sizeof(secret3a)) < 0) { - g_set_error(error, SSB_SCUTTLER_ERROR, SSB_SCUTTLER_ERROR_AUTH, "Failed to hash secret3"); - - return FALSE; - } - - if (crypto_secretbox_open_easy(sig, boxed_response, sizeof(boxed_response), zeros, secret3) < 0) { - g_set_error(error, SSB_SCUTTLER_ERROR, SSB_SCUTTLER_ERROR_AUTH, "Failed to unbox the okay"); - - return FALSE; - } - - memcpy(signed2, scuttler->app_key, crypto_box_PUBLICKEYBYTES); - memcpy(signed2 + crypto_box_PUBLICKEYBYTES, hello, 96); - memcpy(signed2 + 128, shash, 32); - - if (crypto_sign_verify_detached(sig, signed2, sizeof(signed2), scuttler->private_key + crypto_box_SECRETKEYBYTES) < 0) { - g_set_error(error, SSB_SCUTTLER_ERROR, SSB_SCUTTLER_ERROR_AUTH, "Server not authenticated"); - - return FALSE; - } - - if (crypto_hash_sha256(secret, secret3, crypto_box_PUBLICKEYBYTES) < 0) { - g_set_error(error, SSB_SCUTTLER_ERROR, SSB_SCUTTLER_ERROR_AUTH, "Failed to hash secret3"); - - return FALSE; - } - - memcpy(enc_key_hashed, secret, crypto_box_SECRETKEYBYTES); - memcpy(enc_key_hashed + crypto_box_SECRETKEYBYTES, scuttler->private_key + crypto_box_SECRETKEYBYTES, crypto_box_PUBLICKEYBYTES); - - g_clear_pointer(&(scuttler->encrypt_key), g_free); - scuttler->encrypt_key = g_new0(guchar, crypto_box_SECRETKEYBYTES); - - if (crypto_hash_sha256(scuttler->encrypt_key, enc_key_hashed, 64) < 0) { - g_set_error(error, SSB_SCUTTLER_ERROR, SSB_SCUTTLER_ERROR_AUTH, "Failed to hash the encrypt key"); - - return FALSE; - } - - g_clear_pointer(&(scuttler->decrypt_key), g_free); - scuttler->decrypt_key = g_new0(guchar, crypto_box_SECRETKEYBYTES); - - memcpy(dec_key_hashed, secret, crypto_box_SECRETKEYBYTES); - memcpy(dec_key_hashed + crypto_box_SECRETKEYBYTES, scuttler->private_key + crypto_box_SECRETKEYBYTES, crypto_box_PUBLICKEYBYTES); - - if (crypto_hash_sha256(scuttler->decrypt_key, dec_key_hashed, 64) < 0) { - g_set_error(error, SSB_SCUTTLER_ERROR, SSB_SCUTTLER_ERROR_AUTH, "Failed to hash the decrypt_key"); - - return FALSE; - } - - memcpy(scuttler->nonce1, remote_app_mac, 24); - memcpy(scuttler->nonce2, remote_app_mac, 24); - memcpy(scuttler->rx_nonce, local_app_mac, 24); - scuttler->rx_buf_pos = 0; - scuttler->rx_buf_len = 0; - scuttler->noauth = FALSE; - scuttler->wrote_goodbye = FALSE; - - g_debug("SHS succeeded"); - - return TRUE; -} - -static void -emit_connected(SsbScuttler *scuttler, gboolean success) -{ - g_signal_emit(scuttler, ssb_scuttler_signals[SIGNAL_CONNECTED], 0, success); -} - -static void -connection_finished(GObject *source, GAsyncResult *result, gpointer user_data) -{ - GSocketClient *socket_client = G_SOCKET_CLIENT(source); - GTask *task = user_data; - SsbScuttler *scuttler = g_task_get_task_data(task); - GError *err = NULL; - GSocketConnection *connection = g_socket_client_connect_finish(socket_client, result, &err); - GSocket *socket; - GSource *socket_source; - - if (connection == NULL) { - g_warning("Unable to connect to SBOT/* "); - - g_task_return_error(task, err); - g_object_unref(task); - - return; - } - - g_clear_object(&(scuttler->connection)); - scuttler->connection = connection; - - g_debug("Connected to SBOT"); - - // TODO: This is only required if noauth is not set - if (!ssb_scuttler_shs_connect(scuttler, &err)) { - g_task_return_error(task, err); - g_object_unref(task); - - return; - } - - socket = g_socket_connection_get_socket(connection); - g_socket_set_blocking(socket, FALSE); - socket_source = g_socket_create_source(socket, G_IO_IN | G_IO_PRI, NULL); - g_source_set_callback(socket_source, - (GSourceFunc)read_inbound, - g_object_ref(scuttler), g_object_unref); - g_source_attach(socket_source, g_main_context_get_thread_default()); - - g_task_return_boolean(task, TRUE); - emit_connected(scuttler, TRUE); - g_object_unref(task); -} - -void -ssb_scuttler_connect_async(SsbScuttler *scuttler, - GCancellable *cancellable, - GAsyncReadyCallback callback, - gpointer user_data) -{ - GTask *task; - GInetAddress *address; - GSocketAddress *destination; - - g_return_if_fail(SSB_IS_SCUTTLER(scuttler)); - g_return_if_fail((cancellable == NULL) || G_IS_CANCELLABLE(cancellable)); - - task = g_task_new(scuttler, cancellable, callback, user_data); - - // TODO: Maybe there should be a better condition hereā€¦ - if (scuttler->connection) { - g_task_return_boolean(task, TRUE); - g_object_unref(task); - - return; - } - - g_clear_object(&(scuttler->socket_client)); - scuttler->socket_client = g_socket_client_new(); - - // TODO: Implement the UNIX socket version, too! Also, read this stuff from the - // config/command line! - address = g_inet_address_new_from_string("127.0.0.1"); - destination = g_inet_socket_address_new(address, 8008); - g_object_unref(address); - - g_task_set_task_data(task, g_object_ref(scuttler), (GDestroyNotify)g_object_unref); - - g_socket_client_connect_async(scuttler->socket_client, G_SOCKET_CONNECTABLE(destination), cancellable, connection_finished, task); - g_object_unref(destination); -} - -gboolean -ssb_scuttler_connect_finish(SsbScuttler *scuttler, GAsyncResult *result, GError **error) -{ - g_return_val_if_fail(SSB_IS_SCUTTLER(scuttler), FALSE); - g_return_val_if_fail(g_task_is_valid(result, scuttler), FALSE); - - return g_task_propagate_boolean(G_TASK(result), error); -} - -static void -increment_nonce(guchar nonce[24]) -{ - int i; - - for (i = 23; (i >= 0) && (nonce[i] == 0xff); i--) { - nonce[i] = 0; - } - - if (i >= 0) { - nonce[i]++; - } -} - -// was: bs_write_packet -static gboolean -ssb_scuttler_write_packet(SsbScuttler *scuttler, const guchar *data, guint16 data_len, GError **error) -{ - gsize boxed_len = data_len + 34; - guchar boxed[boxed_len]; - GError *err = NULL; - - increment_nonce(scuttler->nonce2); - - if (crypto_secretbox_easy(boxed + 18, data, data_len, scuttler->nonce2, scuttler->encrypt_key) < 0) { - g_set_error(error, SSB_SCUTTLER_ERROR, SSB_SCUTTLER_ERROR_SEND, "Failed to box packet data"); - - return FALSE; - } - - BoxsHeader header; - - header.len = g_htons(data_len); - memcpy(header.mac, boxed + 18, 16); - - if (crypto_secretbox_easy(boxed, (guchar *)&header, 18, scuttler->nonce1, scuttler->encrypt_key) < 0) { - g_set_error(error, SSB_SCUTTLER_ERROR, SSB_SCUTTLER_ERROR_SEND, "Failed to box packet header"); - - return FALSE; - } - - increment_nonce(scuttler->nonce1); - increment_nonce(scuttler->nonce1); - increment_nonce(scuttler->nonce2); - - if (!ssb_scuttler_send_plain(scuttler, boxed, 34, &err)) { - g_propagate_error(error, err); - - return FALSE; - } - - return TRUE; -} - -// was: bs_write -static gboolean -ssb_scuttler_send_bytes(SsbScuttler *scuttler, const guchar *data, gsize data_len, GError **error) -{ - GError *err = NULL; - - while(data_len > 0) { - gsize len = data_len > BOXS_MAXLEN ? BOXS_MAXLEN : data_len; - - if (!ssb_scuttler_write_packet(scuttler, data, len, &err)) { - g_propagate_error(error, err); - - return FALSE; - } - - data_len -= len; - data += len; - } - - return TRUE; -} - -// was: ps_write -static gboolean -ssb_scuttler_send_packet(SsbScuttler *scuttler, - SsbPacketType type, - const gchar *data, - gsize data_len, - int request_id, - gboolean stream, - gboolean end, - GError **error) -{ - gsize out_len = data_len + 9; - guchar out_buf[out_len]; - PacketHeader header = { - g_htonl(data_len), - g_htonl(request_id) - }; - GError *err = NULL; - - out_buf[0] = (stream << 3) | (end << 2) | (type & 3); - memcpy(out_buf + 1, &header, 8); - memcpy(out_buf + 9, data, data_len); - - if (!ssb_scuttler_send_bytes(scuttler, out_buf, out_len, &err)) { - g_propagate_error(error, err); - - return FALSE; - } - - return TRUE; -} - -static gboolean -bs_read_packet(SsbScuttler *scuttler, gpointer buf, gsize *lenp, GError **error) -{ - guchar boxed_header[34]; - gsize len; - GError *err = NULL; - - if (!ssb_scuttler_read(scuttler, boxed_header, 34, &err)) { - if (err->code == G_IO_ERROR_BROKEN_PIPE) { - g_propagate_error(error, err); - } else { - g_set_error(error, - SSB_SCUTTLER_ERROR, SSB_SCUTTLER_ERROR_READ, - "Failed to read boxed packet header"); - } - - return FALSE; - } - - BoxsHeader header; - - if (crypto_secretbox_open_easy((guchar * - )&header, boxed_header, 34, scuttler->rx_nonce, scuttler->decrypt_key) < 0) { - g_set_error(error, - SSB_SCUTTLER_ERROR, SSB_SCUTTLER_ERROR_DECRYPT, - "Failed to unbox packet header"); - - return FALSE; - } - - increment_nonce(scuttler->rx_nonce); - - if ((header.len == 0) && !memcmp(header.mac, zeros, 16)) { - g_set_error(error, - G_IO_ERROR, G_IO_ERROR_BROKEN_PIPE, - "Broken pipe"); - - return FALSE; - } - - len = g_ntohs(header.len); - - if (len > BOXS_MAXLEN) { - g_set_error(error, - SSB_SCUTTLER_ERROR, SSB_SCUTTLER_ERROR_RESPONSE_TOO_LARGE, - "Received boxed packet too large"); - - return FALSE; - } - - guchar boxed_data[len + 16]; - - if (!ssb_scuttler_read(scuttler, boxed_data + 16, len, &err)) { - g_propagate_error(error, err); - - return FALSE; - } - - memcpy(boxed_data, header.mac, 16); - - if (crypto_secretbox_open_easy(buf, boxed_data, len + 16, scuttler->rx_nonce, scuttler->decrypt_key) < 0) { - g_set_error(error, - SSB_SCUTTLER_ERROR, SSB_SCUTTLER_ERROR_CRYPT, - "Failed to unbx packet data"); - - return FALSE; - } - - increment_nonce(scuttler->rx_nonce); - - if (lenp) { - *lenp = len; - } - - return TRUE; -} - -static gboolean -bs_read(SsbScuttler *scuttler, gpointer buf, gsize len) -{ - size_t remaining; - GError *err = NULL; - - while (len > 0) { - remaining = (scuttler->rx_buf_len > len) ? len : scuttler->rx_buf_len; - - if (buf) { - memcpy(buf, scuttler->rx_buf + scuttler->rx_buf_pos, remaining); - } - - scuttler->rx_buf_len -= remaining; - scuttler->rx_buf_pos += remaining; - len -= remaining; - buf += remaining; - - if (len == 0) { - return TRUE; - } - - if (bs_read_packet(scuttler, scuttler->rx_buf, &(scuttler->rx_buf_len), &err) < 0) { - return FALSE; - } - - scuttler->rx_buf_pos = 0; - } - - return TRUE; -} - -static gboolean -ps_read_header(SsbScuttler *scuttler, gsize *len, guint32 *req_id, SsbPacketFlags *flags) -{ - char buf[9]; - PacketHeader header; - - if (!bs_read(scuttler, buf, sizeof(buf))) { - return FALSE; - } - - memcpy(&header, buf + 1, 8); - - if (len) { - *len = g_ntohl(header.len); - } - - if (req_id) { - *req_id = g_ntohl(header.request_id); - } - - if (flags) { - *flags = buf[0]; - } - - return TRUE; -} - -static gboolean -ps_reject(SsbScuttler *scuttler, gsize len, guint32 req, SsbPacketFlags flags, GError **error) -{ - g_debug("Ignoring packet"); - - if (bs_read_out(scuttler, len) < 0) { - g_set_error(error, - SSB_SCUTTLER_ERROR, SSB_SCUTTLER_ERROR_READ, - "bs_read_out"); - - return FALSE; - } -} - -static gboolean -ssb_scuttler_read_async(SsbScuttler *scuttler, guint32 req_id, GError **error) -{ - guint32 req; - gsize len; - SsbPacketFlags flags; - - while (TRUE) { - if (!ps_read_header(scuttler, &len, &req, &flags)) { - g_set_error(error, SSB_SCUTTLER_ERROR, SSB_SCUTTLER_ERROR_READ, "ps_read_header"); - - return FALSE; - } - - if (req == -req_id) { - break; - } - - if ((req == 0) && (len == 0)) { - g_set_error(error, SSB_SCUTTLER_ERROR, SSB_SCUTTLER_ERROR_STREAM_END, "Unexpected end of parent stream"); - - return FALSE; - } - - ps_reject(scuttler, len, req, flags); - } - - if (flags & SSB_PACKET_FLAGS_END) { - gint rc; - - if ((rc = ps_read_error(scuttler, flags, len)) < 0) { - g_set_error(error, SSB_SCUTTLER_ERROR, SSB_SCUTTLER_ERROR_READ, "bs_read_error"); - - return FALSE; - } - - if (rc == 1) { - return 2; // TODO: What is this for? - } - - return 1; - } - - if (bs_read_out(bs, buffer, len) < 0) { - g_set_error(error, SSB_SCUTTLER_ERROR, SSB_SCUTTLER_ERROR_READ, "bs_read_out"); - - return FALSE; - } -} - -// was: muxrpc_call -/** - * ssb_scuttler_muxrpc_call: - * @scuttler: an #SsbScuttler instance - * @method: the name of the RPC method to call - * @argument: the argument for the method - * @type: the type of the RPC call - * @typestr: TODO - * @req_id: the request ID to use in the RPC call - * @error: (nullable): placeholder for a #GError, or %NULL - * - * Start an RPC call to SBOT. - * - * This is a low-level call and unless a specific RPC method is not exposed via other #SsbScuttler - * method, it should not be called explicitly. - */ -void -ssb_scuttler_muxrpc_call(SsbScuttler *scuttler, - const gchar *method, - const gchar *argument, - SsbRPCType type, - const gchar *typestr, - GError **error) -{ - gchar *request; - gsize request_len; - gint req_id; - gboolean is_request = (type == SSB_RPC_ASYNC); - JsonObject *req_object = json_object_new(); - JsonNode *req_node = json_node_new(JSON_NODE_OBJECT); - GError *err = NULL; - - g_debug("Calling %s SBOT method", method); - - json_object_set_string_member(req_object, "name", method); - json_object_set_string_member(req_object, "args", argument); - - if (!is_request) { - json_object_set_string_member(req_object, "type", typestr); - } - - json_node_take_object(req_node, req_object); - request = json_to_string(req_node, FALSE); - json_node_free(req_node); - - if ((request_len = strlen(request)) > MAX_MESSAGE_SIZE) { - g_set_error(error, SSB_SCUTTLER_ERROR, - SSB_SCUTTLER_ERROR_REQUEST_TOO_LARGE, - "Request too large"); - - return; - } - - req_id = g_atomic_int_add(&(scuttler->request_id), 1); - - if (!ssb_scuttler_send_packet(scuttler, - SSB_PACKET_TYPE_JSON, request, request_len, - req_id, !is_request, FALSE, - &err)) { - g_propagate_error(error, err); - - return; - } - - g_debug("%s request sent", method); - - g_free(request); -} - -static void -ssb_scuttler_dispose(GObject *gobject) -{ - SsbScuttler *scuttler = SSB_SCUTTLER(gobject); - GError *err = NULL; - - if (scuttler->connection && !g_io_stream_is_closed(G_IO_STREAM(scuttler->connection))) { - g_io_stream_close(G_IO_STREAM(scuttler->connection), NULL, &err); - scuttler->connection = NULL; - } - - g_clear_pointer(&(scuttler->ssb_dir), g_free); - g_clear_pointer(&(scuttler->app_key), g_free); - g_clear_pointer(&(scuttler->private_key), g_free); -} - -static void -ssb_scuttler_finalize(GObject *gobject) -{ - SsbScuttler *scuttler = SSB_SCUTTLER(gobject); - - g_clear_pointer(&(scuttler->socket_client), g_object_unref); - g_clear_pointer(&(scuttler->ssb_dir), g_free); - g_clear_pointer(&(scuttler->encrypt_key), g_free); - -} - -static void -ssb_scuttler_get_property(GObject *gobject, - guint property_id, - GValue *value, - GParamSpec *pspec) -{ - SsbScuttler *scuttler = SSB_SCUTTLER(gobject); - - switch (property_id) { - case PROP_INITIALISED: - g_value_set_boolean(value, scuttler->initialised); - - break; - - case PROP_CONNECTED: - g_value_set_boolean(value, scuttler->connection != NULL); - - break; - - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID(gobject, property_id, pspec); - - break; - } -} - -static void -ssb_scuttler_class_init(SsbScuttlerClass *klass) -{ - GObjectClass *gobject_class = G_OBJECT_CLASS(klass); - - if (sodium_init() < 0) { - g_critical("Can not initialise sodium"); - - return; - } - - gobject_class->dispose = ssb_scuttler_dispose; - gobject_class->finalize = ssb_scuttler_finalize; - gobject_class->get_property = ssb_scuttler_get_property; - - /** - * SsbScuttler::initialised: - * @success: if %TRUE, initialisation succeeded - * - * Emitted when the SsbScuttler object is initialised. - */ - ssb_scuttler_signals[SIGNAL_INITIALISED] = g_signal_new( - "initialised", - G_TYPE_FROM_CLASS(klass), - G_SIGNAL_RUN_FIRST, - 0, NULL, NULL, - g_cclosure_marshal_VOID__BOOLEAN, G_TYPE_NONE, 1, - G_TYPE_BOOLEAN); - - /** - * SsbScuttler::connected: - * @success: if %TRUE, the connection and handshake has succeeded - * - * Emitted when the SsbScuttler object is connected to the SBOT server. - */ - ssb_scuttler_signals[SIGNAL_CONNECTED] = g_signal_new( - "connected", - G_TYPE_FROM_CLASS(klass), - G_SIGNAL_RUN_FIRST, - 0, NULL, NULL, - g_cclosure_marshal_VOID__BOOLEAN, G_TYPE_NONE, 1, - G_TYPE_BOOLEAN); - - /** - * SsbScuttler:initialised: - * - * If %TRUE, the scuttler is connected - */ - ssb_scuttler_properties[PROP_INITIALISED] = g_param_spec_boolean( - "initialised", "initialised", "initialised", - FALSE, - G_PARAM_STATIC_STRINGS | G_PARAM_READABLE); - - ssb_scuttler_properties[PROP_CONNECTED] = g_param_spec_boolean( - "connected", "connected", "connected", - FALSE, - G_PARAM_STATIC_STRINGS | G_PARAM_READABLE); - - g_object_class_install_properties(gobject_class, PROP_COUNT, ssb_scuttler_properties); -} - - -void -ssb_scuttler_whoami_async(SsbScuttler *scuttler, - GCancellable *cancellable, - GAsyncReadyCallback callback, - gpointer user_data) -{ - GTask *task; - GError *err = NULL; - - g_return_if_fail(SSB_IS_SCUTTLER(scuttler)); - g_return_if_fail((cancellable == NULL) || G_IS_CANCELLABLE(cancellable)); - - task = g_task_new(scuttler, cancellable, callback, user_data); - - if (!scuttler->connection) { - g_task_return_new_error(task, - SSB_SCUTTLER_ERROR, SSB_SCUTTLER_ERROR_NOTCONNECTED, - "Scuttler is not connected"); - g_object_unref(task); - - return; - } - - ssb_scuttler_muxrpc_call(scuttler, "whoami", NULL, SSB_RPC_ASYNC, NULL, &err); -} - -static void -ssb_scuttler_init(SsbScuttler *scuttler) -{ - scuttler->initialised = FALSE; - scuttler->ssb_dir = NULL; - scuttler->socket_client = NULL; - scuttler->connection = NULL; - scuttler->app_key = NULL; - scuttler->private_key = NULL; - scuttler->encrypt_key = NULL; - scuttler->request_id = 0; -} - -SsbScuttler * -ssb_scuttler_get(void) -{ - if (singleton == NULL) { - return NULL; - } - - return g_object_ref(singleton); -} diff --git a/ssb-gtk/ssb-scuttler.c b/ssb-gtk/ssb-scuttler.c new file mode 100644 index 0000000..d77171c --- /dev/null +++ b/ssb-gtk/ssb-scuttler.c @@ -0,0 +1,1245 @@ +/* + * sbotc.c + * Copyright (c) 2017 Secure Scuttlebutt Consortium + * + * Usage of the works is permitted provided that this instrument is + * retained with the works, so that any entity that uses the works is + * notified of this instrument. + * + * DISCLAIMER: THE WORKS ARE WITHOUT WARRANTY. + */ + +#include "ssb-scuttler.h" + +#include +#include + +#define BOXS_MAXLEN 4096 + +#define write_buf(fd, buf) \ + write_all(fd, buf, sizeof(buf)-1) + +typedef struct { + uint16_t len; + uint8_t mac[16]; +} BoxsHeader; + +struct _SsbScuttler { + GObject parent_instance; + + gboolean ip4_only; + gboolean ip6_only; + GSocketClient *socket_client; + GSocketConnection *connection; + + int s; + unsigned char encrypt_key[32]; + unsigned char decrypt_key[32]; + unsigned char nonce1[24]; + unsigned char nonce2[24]; + unsigned char rx_nonce[24]; + unsigned char rx_buf[BOXS_MAXLEN]; + size_t rx_buf_pos; + size_t rx_buf_len; + gboolean noauth; + gboolean wrote_goodbye; +}; + +typedef enum { + pkt_type_buffer = 0, + pkt_type_string = 1, + pkt_type_json = 2, +} SsbPacketType; + +typedef enum { + pkt_flags_buffer = 0, + pkt_flags_string = 1, + pkt_flags_json = 2, + pkt_flags_end = 4, + pkt_flags_stream = 8, +} SsbPacketFlags; + +typedef struct __attribute__((packed)){ + uint32_t len; + int32_t req; +} SsbPacketHeader; + +enum muxrpc_type { + muxrpc_type_async, + muxrpc_type_source, + muxrpc_type_sink, + muxrpc_type_duplex, +}; + +enum stream_state { + stream_state_open, + stream_state_ended_ok, + stream_state_ended_error, +}; + +enum ip_family { + ip_family_ipv4, + ip_family_ipv6, + ip_family_any, +}; + +G_DEFINE_TYPE(SsbScuttler, ssb_scuttler, G_TYPE_INITIALLY_UNOWNED); + +static unsigned char zeros[24] = {0}; + +static const unsigned char ssb_cap[] = { + 0xd4, 0xa1, 0xcb, 0x88, 0xa6, 0x6f, 0x02, 0xf8, + 0xdb, 0x63, 0x5c, 0xe2, 0x64, 0x41, 0xcc, 0x5d, + 0xac, 0x1b, 0x08, 0x42, 0x0c, 0xea, 0xac, 0x23, + 0x08, 0x39, 0xb7, 0x55, 0x84, 0x5a, 0x9f, 0xfb +}; + +static gboolean +tcp_connect(SsbScuttler *scuttler, const char *host, guint16 port, GError **error) { + GList *addresses; + GResolver *resolver; + GError *err = NULL; + GSocketConnection *connection = NULL; + + resolver = g_resolver_get_default(); + addresses = g_resolver_lookup_by_name(resolver, host, NULL, &err); + g_object_unref(resolver); + + if (addresses == NULL) { + g_propagate_error(error, err); + + return FALSE; + } + + for (GList *elem = addresses; elem; elem = elem->next) { + GInetAddress *address = elem->data; + GSocketAddress *connectable; + + if (((g_inet_address_get_family(address) == G_SOCKET_FAMILY_IPV4) && scuttler->ip6_only) || + ((g_inet_address_get_family(address) == G_SOCKET_FAMILY_IPV6) && scuttler->ip4_only)) { + continue; + } + + connectable = g_inet_socket_address_new(address, port); + + if ((connection = g_socket_client_connect(scuttler->socket_client, G_SOCKET_CONNECTABLE(connectable), NULL, &err)) != NULL) { + break; + } + + g_clear_object(&connectable); + } + + if (connection == NULL) { + g_propagate_error(error, err); + + return FALSE; + } + + g_clear_object(&(scuttler->connection)); + scuttler->connection = connection; + + return TRUE; +} + +static gboolean +unix_connect(SsbScuttler *scuttler, const gchar *path, GError **error) +{ + GSocketAddress *connectable = g_unix_socket_address_new(path); + GError *err = NULL; + GSocketConnection *connection; + + if ((connection = g_socket_client_connect(scuttler->socket_client, G_SOCKET_CONNECTABLE(connectable), NULL, &err)) == NULL) { + g_propagate_error(error, err); + + return FALSE; + } + + g_clear_object(&(scuttler->connection)); + scuttler->connection = connection; + + return TRUE; +} + +static gboolean +get_socket_path(gchar **socket_path, const gchar *app_dir) +{ + *socket_path = g_strdup_printf("%s/socket", app_dir); + GFile *socket_file = g_file_new_for_path(*socket_path); + GFileType socket_type = g_file_query_file_type(socket_file, G_FILE_QUERY_INFO_NONE, NULL); + + g_object_unref(socket_file); + + return (socket_type == G_FILE_TYPE_SPECIAL); +} + +static int +read_all(SsbScuttler *scuttler, void *buf, size_t count, GError **error) +{ + gssize nbytes; + GInputStream *stream = g_io_stream_get_input_stream(G_IO_STREAM(scuttler->connection)); + GError *err = NULL; + + while (count > 0) { + nbytes = g_input_stream_read(stream, buf, count, NULL, &err); + + if (nbytes == 0) { + if (err) { + g_propagate_error(error, err); + } else { + g_set_error(error, G_IO_ERROR, G_IO_ERROR_BROKEN_PIPE, "Unexpected end of stream"); + } + + return FALSE; + } + + if (nbytes < 0) { + g_propagate_error(error, err); + + return FALSE; + } + + buf += nbytes; + count -= nbytes; + } + + return TRUE; +} + +static gboolean +read_some(SsbScuttler *scuttler, unsigned char *buf, size_t *lenp, GError **error) +{ + ssize_t nbytes; + GInputStream *stream = g_io_stream_get_input_stream(G_IO_STREAM(scuttler->connection)); + GError *err = NULL; + + do { + nbytes = g_input_stream_read(stream, buf, *lenp, NULL, &err); + } while (nbytes < 0); + + if (nbytes == 0) { + g_set_error(error, G_IO_ERROR, G_IO_ERROR_BROKEN_PIPE, "Unexpected end of stream"); + + return FALSE; + } + + if (nbytes < 0) { + g_propagate_error(error, err); + + return FALSE; + } + + if (lenp) { + *lenp = nbytes; + } + + return TRUE; +} + +static gboolean +write_all(SsbScuttler *scuttler, const void *buf, size_t count, GError **error) +{ + gssize nbytes; + GOutputStream *stream = g_io_stream_get_output_stream(G_IO_STREAM(scuttler->connection)); + GError *err = NULL; + + while (count > 0) { + nbytes = g_output_stream_write(stream, buf, count, NULL, &err); + + if (nbytes < 0 && errno == EINTR) continue; + if (nbytes < 0) return -1; + buf += nbytes; + count -= nbytes; + } + return 0; +} + +static void shs_connect(int sfd, int infd, int outfd, + const unsigned char pubkey[32], + const unsigned char seckey[64], + const unsigned char appkey[32], + const unsigned char server_pubkey[32], + SsbScuttler *bs) { + int rc; + unsigned char local_app_mac[32], remote_app_mac[32]; + + unsigned char kx_pk[32], kx_sk[32]; + rc = crypto_box_keypair(kx_pk, kx_sk); + if (rc < 0) errx(1, "failed to generate auth keypair"); + + rc = crypto_auth(local_app_mac, kx_pk, 32, appkey); + if (rc < 0) err(1, "failed to generate app mac"); + + // send challenge + unsigned char buf[64]; + memcpy(buf, local_app_mac, 32); + memcpy(buf+32, kx_pk, 32); + rc = write_all(outfd, buf, sizeof(buf)); + if (rc < 0) err(1, "failed to send challenge"); + + // recv challenge + unsigned char remote_kx_pk[32]; + rc = read_all(scuttler, buf, sizeof(buf)); + if (rc < 0) err(1, "challenge not accepted"); + memcpy(remote_app_mac, buf, 32); + memcpy(remote_kx_pk, buf+32, 32); + rc = crypto_auth_verify(buf, remote_kx_pk, 32, appkey); + if (rc < 0) errx(1, "wrong protocol (version?)"); + + // send auth + + unsigned char secret[32]; + rc = crypto_scalarmult(secret, kx_sk, remote_kx_pk); + if (rc < 0) errx(1, "failed to derive shared secret"); + + unsigned char remote_pk_curve[32]; + rc = crypto_sign_ed25519_pk_to_curve25519(remote_pk_curve, server_pubkey); + if (rc < 0) errx(1, "failed to curvify remote public key"); + + unsigned char a_bob[32]; + rc = crypto_scalarmult(a_bob, kx_sk, remote_pk_curve); + if (rc < 0) errx(1, "failed to derive a_bob"); + + unsigned char secret2a[96]; + memcpy(secret2a, appkey, 32); + memcpy(secret2a+32, secret, 32); + memcpy(secret2a+64, a_bob, 32); + + unsigned char secret2[32]; + rc = crypto_hash_sha256(secret2, secret2a, sizeof(secret2a)); + if (rc < 0) errx(1, "failed to hash secret2"); + + unsigned char shash[32]; + rc = crypto_hash_sha256(shash, secret, sizeof(secret)); + if (rc < 0) errx(1, "failed to hash secret"); + + unsigned char signed1[96]; + memcpy(signed1, appkey, 32); + memcpy(signed1+32, server_pubkey, 32); + memcpy(signed1+64, shash, 32); + + unsigned char sig[64]; + rc = crypto_sign_detached(sig, NULL, signed1, sizeof(signed1), seckey); + if (rc < 0) errx(1, "failed to sign inner hello"); + + unsigned char hello[96]; + memcpy(hello, sig, 64); + memcpy(hello+64, pubkey, 32); + + unsigned char boxed_auth[112]; + rc = crypto_secretbox_easy(boxed_auth, hello, sizeof(hello), zeros, secret2); + if (rc < 0) errx(1, "failed to box hello"); + + rc = write_all(outfd, boxed_auth, sizeof(boxed_auth)); + if (rc < 0) errx(1, "failed to send auth"); + + // verify accept + + unsigned char boxed_okay[80]; + rc = read_all(scuttler, boxed_okay, sizeof(boxed_okay)); + if (rc < 0) err(1, "hello not accepted"); + + unsigned char local_sk_curve[32]; + rc = crypto_sign_ed25519_sk_to_curve25519(local_sk_curve, seckey); + if (rc < 0) errx(1, "failed to curvify local secret key"); + + unsigned char b_alice[32]; + rc = crypto_scalarmult(b_alice, local_sk_curve, remote_kx_pk); + if (rc < 0) errx(1, "failed to derive b_alice"); + + unsigned char secret3a[128]; + memcpy(secret3a, appkey, 32); + memcpy(secret3a+32, secret, 32); + memcpy(secret3a+64, a_bob, 32); + memcpy(secret3a+96, b_alice, 32); + + unsigned char secret3[32]; + rc = crypto_hash_sha256(secret3, secret3a, sizeof(secret3a)); + if (rc < 0) errx(1, "failed to hash secret3"); + + rc = crypto_secretbox_open_easy(sig, boxed_okay, sizeof(boxed_okay), zeros, secret3); + if (rc < 0) errx(1, "failed to unbox the okay"); + + unsigned char signed2[160]; + memcpy(signed2, appkey, 32); + memcpy(signed2+32, hello, 96); + memcpy(signed2+128, shash, 32); + + rc = crypto_sign_verify_detached(sig, signed2, sizeof(signed2), server_pubkey); + if (rc < 0) errx(1, "server not authenticated"); + + rc = crypto_hash_sha256(secret, secret3, 32); + if (rc < 0) errx(1, "failed to hash secret3"); + + unsigned char enc_key_hashed[64]; + memcpy(enc_key_hashed, secret, 32); + memcpy(enc_key_hashed+32, server_pubkey, 32); + rc = crypto_hash_sha256(bs->encrypt_key, enc_key_hashed, 64); + if (rc < 0) errx(1, "failed to hash the encrypt key"); + + unsigned char dec_key_hashed[64]; + memcpy(dec_key_hashed, secret, 32); + memcpy(dec_key_hashed+32, pubkey, 32); + rc = crypto_hash_sha256(bs->decrypt_key, dec_key_hashed, 64); + if (rc < 0) errx(1, "failed to hash the decrypt key"); + + memcpy(bs->nonce1, remote_app_mac, 24); + memcpy(bs->nonce2, remote_app_mac, 24); + memcpy(bs->rx_nonce, local_app_mac, 24); + + bs->rx_buf_pos = 0; + bs->rx_buf_len = 0; + bs->s = sfd; + bs->noauth = false; + bs->wrote_goodbye = false; +} + +static int pubkey_decode(const char *key_str, unsigned char key[32]) { + if (!key_str) { errno = EPROTO; return -1; } + if (!*key_str) { errno = EPROTO; return -1; } + if (*key_str == '@') key_str++; + size_t len = strlen(key_str); + if (len == 52 && strcmp(key_str+44, ".ed25519") == 0) {} + else if (len != 44) { errno = EMSGSIZE; return -1; } + return base64_decode(key_str, 44, key, 32); +} + +static jsmntok_t *json_lookup(const char *buf, jsmntok_t *tok, const char *prop, size_t prop_len) { + jsmntok_t *end = tok + tok->size + 1; + if (tok->type != JSMN_OBJECT) { errno = EPROTO; return NULL; } + tok++; + while (tok < end) { + if (tok + 1 >= end) { errno = EPROTO; return NULL; } + if (tok->type == JSMN_STRING + && tok->end - tok->start == (int)prop_len + && memcmp(buf + tok->start, prop, prop_len) == 0) + return tok + 1; + tok += tok->size + 1; + end += tok->size; + } + return NULL; +} + +static ssize_t json_get_value(const char *buf, const char *path, const char **value) { + static const int num_tokens = 1024; + jsmntok_t tokens[num_tokens], *tok = tokens; + jsmn_parser parser; + + jsmn_init(&parser); + switch (jsmn_parse(&parser, buf, tokens, num_tokens)) { + case JSMN_ERROR_NOMEM: errno = ENOMEM; return -1; + case JSMN_ERROR_INVAL: errno = EINVAL; return -1; + case JSMN_ERROR_PART: errno = EMSGSIZE; return -1; + case JSMN_SUCCESS: break; + default: errno = EPROTO; return -1; + } + + while (*path) { + const char *end = strchr(path, '.'); + size_t part_len = end ? (size_t)end - (size_t)path : strlen(path); + tok = json_lookup(buf, tok, path, part_len); + if (!tok) { errno = ENOMSG; return -1; } + path += part_len; + if (*path == '.') path++; + } + + *value = buf + tok->start; + return tok->end - tok->start; +} + +static void get_app_dir(char *dir, size_t len) { + const char *path, *home, *appname; + int rc; + path = getenv("ssb_path"); + if (path) { + if (strlen(path) > len) errx(1, "ssb_path too long"); + strncpy(dir, path, len); + return; + } + home = getenv("HOME"); + if (!home) home = "."; + appname = getenv("ssb_appname"); + if (!appname) appname = "ssb"; + rc = snprintf(dir, len, "%s/.%s", home, appname); + if (rc < 0) err(1, "failed to get app dir"); + if ((size_t)rc >= len) errx(1, "path to app dir too long"); +} + +static ssize_t read_file(char *buf, size_t len, const char *fmt, ...) { + va_list ap; + int rc; + struct stat st; + int fd; + + va_start(ap, fmt); + rc = vsnprintf(buf, len, fmt, ap); + va_end(ap); + if (rc < 0) return -1; + if ((size_t)rc >= len) { errno = ENAMETOOLONG; return -1; } + + rc = stat(buf, &st); + if (rc < 0) return -1; + if (st.st_size > (off_t)(len-1)) { errno = EMSGSIZE; return -1; } + + fd = open(buf, O_RDONLY); + if (fd < 0) return -1; + + rc = read_all(scuttler, buf, st.st_size); + if (rc < 0) return -1; + buf[st.st_size] = '\0'; + + close(fd); + return st.st_size; +} + + +static void read_private_key(const char *dir, unsigned char pk[64]) { + ssize_t len; + char buf[8192]; + const char *pk_b64; + int rc; + ssize_t key_len; + char *line; + + len = read_file(buf, sizeof(buf), "%s/secret", dir); + if (len < 0) err(1, "failed to read secret file"); + + // strip comments + for (line = buf; *line; ) { + if (*line == '#') while (*line && *line != '\n') *line++ = ' '; + else while (*line && *line++ != '\n'); + } + + key_len = json_get_value(buf, "private", &pk_b64); + if (key_len < 0) err(1, "unable to read private key"); + + if (key_len > 8 && memcmp(pk_b64 + key_len - 8, ".ed25519", 8) == 0) + key_len -= 8; + rc = base64_decode(pk_b64, key_len, pk, 64); + if (rc < 0) err(1, "unable to decode private key"); +} + +static void increment_nonce(uint8_t nonce[24]) { + int i; + for (i = 23; i >= 0 && nonce[i] == 0xff; i--) nonce[i] = 0; + if (i >= 0) nonce[i]++; +} + +static void bs_write_end_box(SsbScuttler *bs) { + unsigned char boxed[34]; + int rc = crypto_secretbox_easy(boxed, zeros, 18, bs->nonce1, bs->encrypt_key); + if (rc < 0) errx(1, "failed to box packet end header"); + increment_nonce(bs->nonce1); + increment_nonce(bs->nonce2); + rc = write_all(bs->s, boxed, 34); + if (rc < 0) err(1, "failed to write boxed end header"); +} + +static void bs_write_packet(SsbScuttler *bs, const unsigned char *buf, uint16_t len) { + size_t boxed_len = len + 34; + unsigned char boxed[boxed_len]; + increment_nonce(bs->nonce2); + int rc = crypto_secretbox_easy(boxed + 18, buf, len, bs->nonce2, bs->encrypt_key); + if (rc < 0) errx(1, "failed to box packet data"); + BoxsHeader header; + header.len = htons(len); + memcpy(header.mac, boxed + 18, 16); + rc = crypto_secretbox_easy(boxed, (unsigned char *)&header, 18, bs->nonce1, bs->encrypt_key); + if (rc < 0) errx(1, "failed to box packet header"); + increment_nonce(bs->nonce1); + increment_nonce(bs->nonce1); + increment_nonce(bs->nonce2); + rc = write_all(bs->s, boxed, boxed_len); + if (rc < 0) err(1, "failed to write boxed packet"); +} + +static void bs_end(SsbScuttler *bs) { + if (!bs->noauth) { + bs_write_end_box(bs); + } + shutdown(bs->s, SHUT_WR); +} + +static int bs_read_packet(struct boxs *bs, void *buf, size_t *lenp) { + int rc; + if (bs->noauth) { + rc = read_some(bs->s, buf, lenp); + if (rc < 0 && errno == EPIPE) return -1; + if (rc < 0) err(1, "failed to read packet data"); + return 0; + } + unsigned char boxed_header[34]; + BoxsHeader header; + rc = read_all(scuttler, boxed_header, 34); + if (rc < 0 && errno == EPIPE) errx(1, "unexpected end of parent stream"); + if (rc < 0) err(1, "failed to read boxed packet header"); + rc = crypto_secretbox_open_easy((unsigned char *)&header, boxed_header, 34, bs->rx_nonce, bs->decrypt_key); + if (rc < 0) errx(1, "failed to unbox packet header"); + increment_nonce(bs->rx_nonce); + if (header.len == 0 && !memcmp(header.mac, zeros, 16)) { errno = EPIPE; return -1; } + size_t len = ntohs(header.len); + if (len > BOXS_MAXLEN) errx(1, "received boxed packet too large"); + unsigned char boxed_data[len + 16]; + rc = read_all(scuttler, boxed_data + 16, len); + if (rc < 0) err(1, "failed to read boxed packet data"); + memcpy(boxed_data, header.mac, 16); + rc = crypto_secretbox_open_easy(buf, boxed_data, len+16, bs->rx_nonce, bs->decrypt_key); + if (rc < 0) errx(1, "failed to unbox packet data"); + increment_nonce(bs->rx_nonce); + *lenp = len; + return 0; +} + +static int bs_read(struct boxs *bs, char *buf, size_t len) { + if (bs->noauth) { + int rc = read_all(scuttler, buf, len); + if (rc < 0) err(1, "failed to read packet data"); + return 0; + } + size_t remaining; + while (len > 0) { + remaining = bs->rx_buf_len > len ? len : bs->rx_buf_len; + if (buf) memcpy(buf, bs->rx_buf + bs->rx_buf_pos, remaining); + bs->rx_buf_len -= remaining; + bs->rx_buf_pos += remaining; + len -= remaining; + buf += remaining; + if (len == 0) return 0; + if (bs_read_packet(bs, bs->rx_buf, &bs->rx_buf_len) < 0) return -1; + bs->rx_buf_pos = 0; + } + return 0; +} + +static enum stream_state bs_read_out_1(struct boxs *bs, int fd) { + size_t buf[4096]; + size_t len = sizeof(buf); + int rc; + rc = bs_read_packet(bs, buf, &len); + if (rc < 0 && errno == EPIPE) return stream_state_ended_ok; + if (rc < 0) return stream_state_ended_error; + rc = write_all(fd, buf, len); + if (rc < 0) return stream_state_ended_error; + return stream_state_open; +} + +static int bs_read_out(struct boxs *bs, int fd, size_t len) { + size_t chunk; + char buf[4096]; + int rc; + while (len > 0) { + chunk = len > sizeof(buf) ? sizeof(buf) : len; + rc = bs_read(bs, buf, chunk); + if (rc < 0) return -1; + rc = write_all(fd, buf, chunk); + if (rc < 0) return -1; + len -= chunk; + } + return 0; +} + +static int bs_read_error(struct boxs *bs, int errfd, SsbPacketFlags flags, size_t len, gboolean no_newline) { + // suppress printing "true" indicating end without error + if (flags & pkt_flags_json && len == 4) { + char buf[4]; + if (bs_read(bs, buf, 4) < 0) return -1; + if (strncmp(buf, "true", 0) == 0) { + return 0; + } + if (write_all(errfd, buf, 4) < 0) return -1; + } else { + if (bs_read_out(bs, errfd, len) < 0) return -1; + } + if (flags & (pkt_flags_json | pkt_flags_string) && !no_newline) { + if (write_buf(errfd, "\n") < 0) return -1; + } + return 1; +} + +static void bs_write(struct boxs *bs, const unsigned char *buf, size_t len) { + if (bs->noauth) { + int rc = write_all(bs->s, buf, len); + if (rc < 0) err(1, "failed to write packet"); + return; + } + while (len > 0) { + size_t l = len > BOXS_MAXLEN ? BOXS_MAXLEN : len; + bs_write_packet(bs, buf, l); + len -= l; + buf += l; + } +} + +static enum stream_state bs_write_in_1(struct boxs *bs, int fd) { + unsigned char buf[4096]; + ssize_t sz = read(fd, buf, sizeof(buf)); + if (sz < 0) err(1, "read"); + if (sz == 0) { + bs_end(bs); + return stream_state_ended_ok; + } + bs_write(bs, buf, sz); + return stream_state_open; +} + +static void ps_write(struct boxs *bs, + const char *data, + size_t len, + SsbPacketType type, + int req_id, + gboolean stream, + gboolean end) { + size_t out_len = 9 + len; + unsigned char out_buf[out_len]; + SsbPacketHeader header = {htonl(len), htonl(req_id)}; + out_buf[0] = (stream << 3) | (end << 2) | (type & 3); + memcpy(out_buf+1, &header, 8); + memcpy(out_buf+9, data, len); + bs_write(bs, out_buf, out_len); +} + +static void ps_goodbye(struct boxs *bs) { + if (bs->wrote_goodbye) return; + bs->wrote_goodbye = true; + bs_write(bs, zeros, 9); +} + +static int ps_read_header(struct boxs *bs, size_t *len, int *req_id, SsbPacketFlags *flags) { + char buf[9]; + SsbPacketHeader header; + if (bs_read(bs, buf, sizeof(buf)) < 0) return -1; + memcpy(&header, buf+1, 8); + if (len) *len = ntohl(header.len); + if (req_id) *req_id = ntohl(header.req); + if (flags) *flags = buf[0]; + return 0; +} + +static void muxrpc_call(struct boxs *bs, + const char *method, + const char *argument, + enum muxrpc_type type, + const char *typestr, + int req_id) { + char req[33792]; // 32768 max message value size + 1024 extra + ssize_t reqlen; + gboolean is_request = type == muxrpc_type_async; + + if (is_request) { + reqlen = snprintf(req, sizeof(req), + "{\"name\":%s,\"args\":%s}", + method, argument); + } else { + reqlen = snprintf(req, sizeof(req), + "{\"name\":%s,\"args\":%s,\"type\":\"%s\"}", + method, argument, typestr); + } + if (reqlen < 0) err(1, "failed to construct request"); + if ((size_t)reqlen >= sizeof(req)) errx(1, "request too large"); + + ps_write(bs, req, reqlen, pkt_type_json, req_id, !is_request, false); +} + +static int bs_passthrough(struct boxs *bs, int infd, int outfd) { + int rc; + fd_set rd; + int sfd = bs->s; + int maxfd = infd > sfd ? infd : sfd; + enum stream_state in = stream_state_open; + enum stream_state out = stream_state_open; + + while (out == stream_state_open + || (in == stream_state_open && out != stream_state_ended_error)) { + FD_ZERO(&rd); + if (in == stream_state_open) FD_SET(infd, &rd); + if (out == stream_state_open) FD_SET(sfd, &rd); + rc = select(maxfd + 1, &rd, 0, 0, NULL); + if (rc < 0) err(1, "select"); + if (FD_ISSET(infd, &rd)) in = bs_write_in_1(bs, infd); + if (FD_ISSET(sfd, &rd)) out = bs_read_out_1(bs, outfd); + } + + return in == stream_state_ended_ok && out == stream_state_ended_ok ? 0 : + in == stream_state_ended_error || out == stream_state_ended_error ? 2 : 1; +} + +static void ps_reject(struct boxs *bs, size_t len, int32_t req, SsbPacketFlags flags) { + // ignore the packet. if this is a request, the substream on the other end + // will just have to wait until the rpc connection closes. + (void)req; + (void)flags; + write_buf(STDERR_FILENO, "ignoring packet: "); + int rc = bs_read_out(bs, STDERR_FILENO, len); + if (rc < 0) err(1, "bs_read_out"); + write_buf(STDERR_FILENO, "\n"); +} + +static enum stream_state muxrpc_read_source_1(struct boxs *bs, int outfd, int req_id, gboolean no_newline) { + SsbPacketFlags flags; + size_t len; + int32_t req; + int rc = ps_read_header(bs, &len, &req, &flags); + if (rc < 0) err(1, "ps_read_header"); + if (req == 0 && len == 0) { + if (bs->wrote_goodbye) return stream_state_ended_ok; + warnx("unexpected end of parent stream"); + return stream_state_ended_error; + } + if (req != -req_id) { + ps_reject(bs, len, req, flags); + return stream_state_open; + } + if (flags & pkt_flags_end) { + rc = bs_read_error(bs, STDERR_FILENO, flags, len, no_newline); + if (rc < 0) err(1, "bs_read_error"); + if (rc == 1) return stream_state_ended_error; + return stream_state_ended_ok; + } + rc = bs_read_out(bs, outfd, len); + if (rc < 0) err(1, "bs_read_out"); + if (flags & (pkt_flags_json | pkt_flags_string) && !no_newline) { + rc = write_buf(outfd, "\n"); + if (rc < 0) err(1, "write_buf"); + } + return stream_state_open; +} + +static int muxrpc_read_source(struct boxs *bs, int outfd, int req_id, gboolean no_newline) { + enum stream_state state; + while ((state = muxrpc_read_source_1(bs, outfd, req_id, no_newline)) == stream_state_open); + return state == stream_state_ended_ok ? 0 : + state == stream_state_ended_error ? 2 : 1; +} + +static int muxrpc_read_async(struct boxs *bs, int outfd, int req_id, gboolean no_newline) { + SsbPacketFlags flags; + size_t len; + int32_t req; + int rc; + + while (1) { + rc = ps_read_header(bs, &len, &req, &flags); + if (rc < 0) err(1, "ps_read_header"); + if (req == -req_id) break; + if (req == 0 && len == 0) errx(1, "unexpected end of parent stream"); + ps_reject(bs, len, req, flags); + } + if (flags & pkt_flags_end) { + rc = bs_read_error(bs, STDERR_FILENO, flags, len, no_newline); + if (rc < 0) err(1, "bs_read_error"); + if (rc == 1) return 2; + return 1; + } + rc = bs_read_out(bs, outfd, len); + if (rc < 0) err(1, "bs_read_out"); + if (flags & (pkt_flags_json | pkt_flags_string) && !no_newline) { + rc = write_buf(outfd, "\n"); + if (rc < 0) err(1, "write_buf"); + } + return 0; +} + +static enum stream_state muxrpc_write_sink_1(struct boxs *bs, int infd, + SsbPacketType ptype, int req_id) { + char buf[4096]; + ssize_t sz = read(infd, buf, sizeof(buf)); + if (sz < 0) err(1, "read"); + if (sz == 0) { + ps_write(bs, "true", 4, pkt_type_json, req_id, true, true); + return stream_state_ended_ok; + } + ps_write(bs, buf, sz, ptype, req_id, true, false); + return stream_state_open; +} + +static enum stream_state muxrpc_write_sink_1_hashed(struct boxs *bs, int infd, + crypto_hash_sha256_state *hash_state, int req_id) { + int rc; + unsigned char buf[4096]; + ssize_t sz = read(infd, buf, sizeof(buf)); + if (sz < 0) err(1, "read"); + if (sz == 0) { + ps_write(bs, "true", 4, pkt_type_json, req_id, true, true); + return stream_state_ended_ok; + } + rc = crypto_hash_sha256_update(hash_state, buf, sz); + if (rc < 0) errx(1, "hash update failed"); + ps_write(bs, (char *)buf, sz, pkt_type_buffer, req_id, true, false); + return stream_state_open; +} + +static int muxrpc_write_sink(struct boxs *bs, int infd, SsbPacketType ptype, int req_id, gboolean no_newline) { + int rc; + fd_set rd; + int sfd = bs->s; + int maxfd = infd > sfd ? infd : sfd; + enum stream_state in = stream_state_open; + enum stream_state out = stream_state_open; + + while (out == stream_state_open) { + FD_ZERO(&rd); + if (in == stream_state_open) FD_SET(infd, &rd); + if (out == stream_state_open) FD_SET(sfd, &rd); + rc = select(maxfd + 1, &rd, 0, 0, NULL); + if (rc < 0) err(1, "select"); + if (FD_ISSET(infd, &rd)) in = muxrpc_write_sink_1(bs, infd, ptype, req_id); + if (FD_ISSET(sfd, &rd)) out = muxrpc_read_source_1(bs, -1, req_id, no_newline); + } + + return in == stream_state_ended_ok && out == stream_state_ended_ok ? 0 : + in == stream_state_ended_error || out == stream_state_ended_error ? 2 : 1; +} + +static int muxrpc_write_blob_add(struct boxs *bs, int infd, int outfd, int req_id, gboolean no_newline) { + int rc; + fd_set rd; + int sfd = bs->s; + int maxfd = infd > sfd ? infd : sfd; + enum stream_state in = stream_state_open; + enum stream_state out = stream_state_open; + crypto_hash_sha256_state hash_state; + unsigned char hash[32]; + char id[54] = "&"; + + rc = crypto_hash_sha256_init(&hash_state); + if (rc < 0) { errno = EINVAL; return -1; } + + while (out == stream_state_open) { + FD_ZERO(&rd); + if (in == stream_state_open) FD_SET(infd, &rd); + if (out == stream_state_open) FD_SET(sfd, &rd); + rc = select(maxfd + 1, &rd, 0, 0, NULL); + if (rc < 0) err(1, "select"); + if (FD_ISSET(infd, &rd)) in = muxrpc_write_sink_1_hashed(bs, infd, &hash_state, req_id); + if (FD_ISSET(sfd, &rd)) out = muxrpc_read_source_1(bs, -1, req_id, no_newline); + } + + rc = crypto_hash_sha256_final(&hash_state, hash); + if (rc < 0) errx(1, "hash finalize failed"); + + rc = base64_encode(hash, 32, id+1, sizeof(id)-1); + if (rc < 0) err(1, "encoding hash failed"); + strcpy(id + 45, ".sha256\n"); + rc = write_all(outfd, id, sizeof(id)-1); + if (rc < 0) err(1, "writing hash failed"); + + return in == stream_state_ended_ok && out == stream_state_ended_ok ? 0 : + in == stream_state_ended_error || out == stream_state_ended_error ? 2 : 1; +} + +static int muxrpc_duplex(struct boxs *bs, int infd, int outfd, SsbPacketType in_ptype, int req_id, gboolean no_newline) { + int rc; + fd_set rd; + int sfd = bs->s; + int maxfd = infd > sfd ? infd : sfd; + enum stream_state in = stream_state_open; + enum stream_state out = stream_state_open; + + while (out == stream_state_open + || (in == stream_state_open && out != stream_state_ended_error)) { + FD_ZERO(&rd); + if (in == stream_state_open) FD_SET(infd, &rd); + if (out == stream_state_open) FD_SET(sfd, &rd); + rc = select(maxfd + 1, &rd, 0, 0, NULL); + if (rc < 0) err(1, "select"); + if (FD_ISSET(infd, &rd)) in = muxrpc_write_sink_1(bs, infd, in_ptype, req_id); + if (FD_ISSET(sfd, &rd)) out = muxrpc_read_source_1(bs, outfd, req_id, no_newline); + } + + return in == stream_state_ended_ok && out == stream_state_ended_ok ? 0 : + in == stream_state_ended_error || out == stream_state_ended_error ? 2 : 1; +} + +static int method_to_json(char *out, size_t outlen, const char *str) { + // blobs.get => ["blobs", "get"] + size_t i = 0; + char c; + if (i+2 > outlen) return -1; + out[i++] = '['; + out[i++] = '"'; + while ((c = *str++)) { + if (c == '.') { + if (i+3 > outlen) return -1; + out[i++] = '"'; + out[i++] = ','; + out[i++] = '"'; + } else if (c == '"') { + if (i+2 > outlen) return -1; + out[i++] = '\\'; + out[i++] = '"'; + } else { + if (i+1 > outlen) return -1; + out[i++] = c; + } + } + if (i+3 > outlen) return -1; + out[i++] = '"'; + out[i++] = ']'; + out[i++] = '\0'; + return i; +} + +static int args_to_json_length(int argc, char *argv[]) { + int i = 0; + int len = 3; // "[]\0" + for (i = 0; i < argc; i++) + len += strlen(argv[i])+1; + return len; +} + +static int args_to_json(char *out, size_t outlen, unsigned int argc, char *argv[]) { + size_t i = 0; + size_t j; + if (i+1 > outlen) return -1; + out[i++] = '['; + for (j = 0; j < argc; j++) { + size_t len = strlen(argv[j]); + if (j > 0) out[i++] = ','; + if (i+len > outlen) return -1; + strncpy(out+i, argv[j], len); + i += len; + } + if (i+2 > outlen) return -1; + out[i++] = ']'; + out[i++] = '\0'; + return i; +} + +int main(int argc, char *argv[]) { + int i, s, infd, outfd, rc; + const char *key = NULL; + const char *keypair_seed_str = NULL; + const char *host = NULL; + const char *port = "8008"; + const char *typestr = NULL, *methodstr = NULL; + const char *shs_cap_key_str = NULL; + const char *socket_path = NULL; + size_t argument_len; + unsigned char private_key[64]; + unsigned char public_key[32]; + unsigned char remote_key[32]; + unsigned char shs_cap_key[32]; + enum muxrpc_type type; + SsbPacketType ptype = pkt_type_buffer; + char method[256]; + char app_dir[_POSIX_PATH_MAX]; + ssize_t len; + gboolean test = false; + gboolean noauth = false; + gboolean no_newline = false; + gboolean host_arg = false; + gboolean port_arg = false; + gboolean key_arg = false; + gboolean shs_cap_key_str_arg = false; + gboolean ipv4_arg = false; + gboolean ipv6_arg = false; + gboolean passthrough = false; + enum ip_family ip_family; + + get_app_dir(app_dir, sizeof(app_dir)); + + char config_buf[8192]; + len = read_file(config_buf, sizeof(config_buf), "%s/config", app_dir); + if (len > 0) { + ssize_t key_len = json_get_value(config_buf, "key", &key); + ssize_t host_len = json_get_value(config_buf, "host", &host); + ssize_t port_len = json_get_value(config_buf, "port", &port); + ssize_t shs_cap_len = json_get_value(config_buf, "caps.shs", &shs_cap_key_str); + if (key_len >= 0) ((char *)key)[key_len] = '\0'; + if (host_len >= 0) ((char *)host)[host_len] = '\0'; + if (port_len >= 0) ((char *)port)[port_len] = '\0'; + if (shs_cap_len >= 0) ((char *)shs_cap_key_str)[shs_cap_len] = '\0'; + } else if (len < 0 && errno != ENOENT) { + err(1, "failed to read config"); + } + + for (i = 1; i < argc && (argv[i][0] == '-'); i++) { + switch (argv[i][1]) { + case 'c': shs_cap_key_str = argv[++i]; shs_cap_key_str_arg = true; break; + case 'j': ptype = pkt_type_json; break; + case 'T': test = true; break; + case 's': host = argv[++i]; host_arg = true; break; + case 'k': key = argv[++i]; key_arg = true; break; + case 'K': keypair_seed_str = argv[++i]; break; + case 'p': port = argv[++i]; port_arg = true; break; + case 'u': socket_path = argv[++i]; break; + case 't': typestr = argv[++i]; break; + case 'n': noauth = true; break; + case '4': ipv4_arg = true; break; + case '6': ipv6_arg = true; break; + case 'a': passthrough = true; break; + case 'l': no_newline = true; break; + default: usage(); + } + } + if (i < argc) methodstr = argv[i++]; + else if (!test && !passthrough) usage(); + + if (ipv4_arg && ipv6_arg) errx(1, "options -4 and -6 conflict"); + ip_family = + ipv4_arg ? ip_family_ipv4 : + ipv6_arg ? ip_family_ipv6 : + ip_family_any; + + if (shs_cap_key_str) { + rc = pubkey_decode(shs_cap_key_str, shs_cap_key); + if (rc < 0) err(1, "unable to decode cap key '%s'", shs_cap_key_str); + } else { + memcpy(shs_cap_key, ssb_cap, 32); + } + + argument_len = test ? 0 : args_to_json_length(argc-i, argv+i); + char argument[argument_len]; + + if (passthrough) { + if (methodstr) errx(1, "-a option conflicts with method"); + if (typestr) errx(1, "-a option conflicts with -t option"); + if (argc-i > 0) errx(1, "-a option conflicts with method arguments"); + if (test) errx(1, "-a option conflicts with -T test"); + + } else if (!test) { + rc = args_to_json(argument, sizeof(argument), argc-i, argv+i); + if (rc < 0) errx(1, "unable to collect arguments"); + + char manifest_buf[8192]; + if (!typestr) { + len = read_file(manifest_buf, sizeof(manifest_buf), + "%s/manifest.json", app_dir); + if (len < 0) err(1, "failed to read manifest file"); + + ssize_t type_len = json_get_value(manifest_buf, methodstr, &typestr); + if (!typestr && errno == ENOMSG) errx(1, + "unable to find method '%s' in manifest", methodstr); + if (!typestr) err(1, "unable to read manifest %s/%s", manifest_buf, methodstr); + ((char *)typestr)[type_len] = '\0'; + } + if (strcmp(typestr, "sync") == 0) type = muxrpc_type_async; + else if (strcmp(typestr, "async") == 0) type = muxrpc_type_async; + else if (strcmp(typestr, "sink") == 0) type = muxrpc_type_sink; + else if (strcmp(typestr, "source") == 0) type = muxrpc_type_source; + else if (strcmp(typestr, "duplex") == 0) type = muxrpc_type_duplex; + else errx(1, "type must be one of "); + + rc = method_to_json(method, sizeof(method), methodstr); + if (rc < 0) errx(0, "unable to convert method name"); + } + + if (keypair_seed_str) { + unsigned char seed[crypto_sign_SEEDBYTES]; + unsigned char ed25519_skpk[crypto_sign_ed25519_SECRETKEYBYTES]; + + rc = pubkey_decode(keypair_seed_str, ed25519_skpk); + if (rc < 0) err(1, "unable to decode private key"); + rc = crypto_sign_ed25519_sk_to_seed(seed, ed25519_skpk); + if (rc < 0) err(1, "unable to convert private key to seed"); + rc = crypto_sign_seed_keypair(public_key, private_key, seed); + if (rc < 0) err(1, "unable to generate keypair from seed"); + } else { + read_private_key(app_dir, private_key); + memcpy(public_key, private_key+32, 32); + } + + if (key) { + rc = pubkey_decode(key, remote_key); + if (rc < 0) err(1, "unable to decode remote key '%s'", key); + } else { + memcpy(remote_key, public_key, 32); + } + + gboolean implied_tcp = host_arg || port_arg || ipv4_arg || ipv6_arg; + gboolean implied_auth = key_arg || keypair_seed_str || shs_cap_key_str_arg || test; + + if (test) { + infd = STDIN_FILENO; + outfd = STDOUT_FILENO; + s = -1; + + } else if (socket_path) { + if (implied_tcp) errx(1, "-u option conflicts with -s and -p options"); + if (!unix_connect(scuttler, socket_path, &err)) { + g_error("Error during connect: %s", err->message); + + return 1; + } + } else if (!implied_tcp && !implied_auth) { + gchar *socket_path_buf; + + if (!get_socket_path(socket_path_buf, app_dir)) { + if (noauth) { + g_error("Error getting socket path"); + + return 1; + } + + goto do_tcp_connect; + } + + if (!unix_connect(scuttler, socket_path_buf, &err)) { + if (noauth) { + g_error("Error during connect: %s", err->message); + + return 1; + } + + goto do_tcp_connect; + } + + noauth = true; + } else { +do_tcp_connect: + if (!tcp_connect(host, port, &err)) { + g_error("Could not connect: %s", err->message); + + return 1; + } + } + + struct boxs bs; + if (noauth) { + bs.s = s; + bs.noauth = true; + if (implied_auth) errx(1, "-n option conflicts with -k, -K, -c and -T options."); + } else { + shs_connect(s, infd, outfd, public_key, private_key, shs_cap_key, remote_key, &bs); + } + + if (test) { + rc = write_all(outfd, bs.encrypt_key, sizeof(bs.encrypt_key)); + rc |= write_all(outfd, bs.nonce1, sizeof(bs.nonce1)); + rc |= write_all(outfd, bs.decrypt_key, sizeof(bs.decrypt_key)); + rc |= write_all(outfd, bs.rx_nonce, sizeof(bs.rx_nonce)); + if (rc < 0) err(1, "failed to write handshake result"); + return 0; + } + + if (passthrough) { + rc = bs_passthrough(&bs, STDIN_FILENO, STDOUT_FILENO); + close(s); + return rc; + } + + muxrpc_call(&bs, method, argument, type, typestr, 1); + + switch (type) { + case muxrpc_type_async: + rc = muxrpc_read_async(&bs, STDOUT_FILENO, 1, no_newline); + break; + case muxrpc_type_source: + rc = muxrpc_read_source(&bs, STDOUT_FILENO, 1, no_newline); + break; + case muxrpc_type_sink: + if (!strcmp(methodstr, "blobs.add")) { + rc = muxrpc_write_blob_add(&bs, STDIN_FILENO, STDOUT_FILENO, 1, no_newline); + } else { + rc = muxrpc_write_sink(&bs, STDIN_FILENO, ptype, 1, no_newline); + } + break; + case muxrpc_type_duplex: + rc = muxrpc_duplex(&bs, STDIN_FILENO, STDOUT_FILENO, ptype, 1, no_newline); + break; + } + + ps_goodbye(&bs); + bs_end(&bs); + close(s); + return rc; +} diff --git a/ssb-gtk/sbot.h b/ssb-gtk/ssb-scuttler.h similarity index 100% rename from ssb-gtk/sbot.h rename to ssb-gtk/ssb-scuttler.h