wip: scuttler is a gobject, authentication

This commit is contained in:
Gergely Polonkai 2019-01-03 14:55:38 +01:00
parent 661867e0ba
commit c59f8ab201
5 changed files with 689 additions and 13 deletions

View File

@ -5,9 +5,13 @@ i18n = import('i18n')
glib_required = '>= 2.40'
gtk_required = '>= 3.20'
json_glib_required = '>= 1.4.0'
libsodium_required = '>= 1.0.0'
glib = dependency('glib-2.0', version: glib_required)
gtk = dependency('gtk+-3.0', version: gtk_required)
json_glib = dependency('json-glib-1.0', version: json_glib_required)
libsodium = dependency('libsodium', version: libsodium_required)
subdir('data')
subdir('ssb-gtk')

View File

@ -6,5 +6,5 @@ sources = [
]
executable('ssb-gtk', sources, ssb_resources,
dependencies: [glib, gtk],
dependencies: [glib, gtk, json_glib, libsodium],
install: true)

View File

@ -1,22 +1,678 @@
#include <glib.h>
#include <json-glib/json-glib.h>
#include <sodium.h>
gboolean do_scuttling = TRUE;
#include "sbot.h"
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];
gsize rx_buf_pos;
gsize rx_buf_len;
gboolean noauth;
gboolean wrote_goodbye;
GSocketClient *socket_client;
GSocketConnection *connection;
};
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;
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
};
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
initialise(SsbScuttler *scuttler, 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);
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);
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");
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);
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;
}
/**
* ensure_scuttler():
* @ssb_dir: (nullable): the SSB directory
*
* Ensure the scuttler singleton is initialised with @ssb_dir as its base directory.
*/
static inline gboolean
ensure_scuttler(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;
}
static gboolean
second_hook_cb()
{
g_print("Scuttling…\n");
return TRUE;
}
static gboolean
ssb_scuttler_send(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;
}
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;
}
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;
}
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(scuttler, buf, sizeof(buf), &err)) {
g_propagate_error(error, err);
return FALSE;
}
// Receive challenge response
if (!ssb_scuttler_read(scuttler, buf, sizeof(buf), &err)) {
g_propagate_error(error, err);
return FALSE;
}
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(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;
return TRUE;
}
static gboolean
ssb_scuttler_connect(SsbScuttler *scuttler, GError **error)
{
GInetAddress *address;
GSocketAddress *destination;
GError *err = NULL;
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);
scuttler->connection = g_socket_client_connect(scuttler->socket_client,
G_SOCKET_CONNECTABLE(destination),
NULL,
&err);
g_object_unref(destination);
// TODO: This is only required if noauth is not set
if (!ssb_scuttler_shs_connect(scuttler, &err)) {
g_propagate_error(error, err);
return FALSE;
}
return TRUE;
}
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_class_init(SsbScuttlerClass *klass)
{
GObjectClass *gobject_class = G_OBJECT_CLASS(klass);
gobject_class->dispose = ssb_scuttler_dispose;
gobject_class->finalize = ssb_scuttler_finalize;
}
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;
}
gpointer
scuttle(gchar *ssb_dir)
{
gchar *config_file = g_strdup_printf("%s/config", ssb_dir);
g_print("Read config file %s\n", config_file);
g_free(config_file);
GMainContext *scuttle_context;
GSource *second_hook;
GError *err = NULL;
g_print("Starting scuttle\n");
if (sodium_init() < 0) {
g_critical("Can not initialise sodium");
while (do_scuttling) {
g_usleep(G_USEC_PER_SEC);
g_print("Scuttle…\n");
return NULL;
}
g_print("Scuttling stopped\n");
// Since scuttler is a singleton global to this file, we dont need the return value of this
// function now.
if (G_UNLIKELY(!ensure_scuttler(ssb_dir))) {
g_critical("Can not reinitialise scuttler with a new SSB directory.");
return NULL;
}
g_object_ref_sink(singleton);
scuttle_context = g_main_context_new();
scuttle_loop = g_main_loop_new(scuttle_context, FALSE);
// TODO: This is for debugging purposes only, lets remove it as soon as the scuttler works
second_hook = g_timeout_source_new_seconds(1);
g_source_set_callback(second_hook,
G_SOURCE_FUNC(second_hook_cb),
g_main_loop_ref(scuttle_loop),
(GDestroyNotify)g_main_loop_unref);
g_source_attach(second_hook, scuttle_context);
// Set scuttle_context as the default context for this thread
g_main_context_push_thread_default(scuttle_context);
if (!ssb_scuttler_connect(singleton, &err))
{
g_critical("Could not connect: %s", err->message);
return NULL;
}
g_debug("Starting scuttle");
g_main_loop_run(scuttle_loop);
g_main_context_pop_thread_default(scuttle_context);
g_main_loop_unref(scuttle_loop);
g_object_unref(singleton);
g_debug("Scuttling stopped");
scuttle_loop = NULL;
return NULL;
}
void
stop_scuttling(void)
{
if (scuttle_loop) {
g_main_loop_quit(scuttle_loop);
}
}

View File

@ -1,10 +1,26 @@
#ifndef __SBOT_H__
# define __SBOT_H__
# include <glib.h>
# include <glib-object.h>
extern gboolean do_scuttling;
# define SSB_TYPE_SCUTTLER ssb_scuttler_get_type()
G_DECLARE_FINAL_TYPE(SsbScuttler, ssb_scuttler, SSB, SCUTTLER, GInitiallyUnowned)
typedef enum {
SSB_SCUTTLER_ERROR_KEYGEN,
SSB_SCUTTLER_ERROR_KEYAUTH,
SSB_SCUTTLER_ERROR_KEYVERIFY,
SSB_SCUTTLER_ERROR_AUTH,
} SsbScuttlerError;
G_BEGIN_DECLS
gpointer scuttle(gchar *ssb_dir);
void stop_scuttling(void);
# define SSB_SCUTTLER_ERROR (ssb_scuttler_error_quark())
GQuark ssb_scuttler_error_quark(void);
G_END_DECLS
#endif /* __SBOT_H__ */

View File

@ -115,7 +115,7 @@ ssb_app_shutdown(GApplication *gapp)
{
SsbApp *app = SSB_APP(gapp);
do_scuttling = FALSE;
stop_scuttling();
(void)g_thread_join(app->scuttle_thread);