diff options
author | Michael Vetter <jubalh@iodoru.org> | 2019-04-11 10:58:22 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-04-11 10:58:22 +0200 |
commit | 61df0c8e8513a1aa9912e37019a63778ec3ed06c (patch) | |
tree | a52850f2f5fc225759c2287d1672ed22b5ef7f0a /src/omemo | |
parent | 6b064cfde4456c25bd9dbcbfe0a79262ebcb3599 (diff) | |
parent | f75e1d7a7b05c68f03b6b13163ac9f2b8824e7df (diff) | |
download | profani-tty-61df0c8e8513a1aa9912e37019a63778ec3ed06c.tar.gz |
Merge pull request #1039 from paulfariello/feature/omemo
Add basic OMEMO support.
Diffstat (limited to 'src/omemo')
-rw-r--r-- | src/omemo/crypto.c | 331 | ||||
-rw-r--r-- | src/omemo/crypto.h | 148 | ||||
-rw-r--r-- | src/omemo/omemo.c | 1410 | ||||
-rw-r--r-- | src/omemo/omemo.h | 55 | ||||
-rw-r--r-- | src/omemo/store.c | 382 | ||||
-rw-r--r-- | src/omemo/store.h | 250 |
6 files changed, 2576 insertions, 0 deletions
diff --git a/src/omemo/crypto.c b/src/omemo/crypto.c new file mode 100644 index 00000000..9d64a701 --- /dev/null +++ b/src/omemo/crypto.c @@ -0,0 +1,331 @@ +#include <assert.h> +#include <signal/signal_protocol.h> +#include <signal/signal_protocol_types.h> +#include <gcrypt.h> + +#include "log.h" +#include "omemo/omemo.h" +#include "omemo/crypto.h" + +int +omemo_crypto_init(void) +{ + if (!gcry_check_version(GCRYPT_VERSION)) { + return -1; + } + + gcry_control(GCRYCTL_INITIALIZATION_FINISHED, 0); + + return 0; +} + +int +omemo_random_func(uint8_t *data, size_t len, void *user_data) +{ + gcry_randomize(data, len, GCRY_VERY_STRONG_RANDOM); + return 0; +} + +int +omemo_hmac_sha256_init_func(void **hmac_context, const uint8_t *key, size_t key_len, void *user_data) +{ + gcry_error_t res; + gcry_mac_hd_t hd; + + res = gcry_mac_open(&hd, GCRY_MAC_HMAC_SHA256, 0, NULL); + if (res != GPG_ERR_NO_ERROR) { + log_error("OMEMO: %s", gcry_strerror(res)); + return OMEMO_ERR_GCRYPT; + } + + *hmac_context = hd; + res = gcry_mac_setkey(hd, key, key_len); + if (res != GPG_ERR_NO_ERROR) { + log_error("OMEMO: %s", gcry_strerror(res)); + return OMEMO_ERR_GCRYPT; + } + + return 0; +} + +int +omemo_hmac_sha256_update_func(void *hmac_context, const uint8_t *data, size_t data_len, void *user_data) +{ + gcry_error_t res; + + res = gcry_mac_write(hmac_context, data, data_len); + if (res != GPG_ERR_NO_ERROR) { + log_error("OMEMO: %s", gcry_strerror(res)); + return OMEMO_ERR_GCRYPT; + } + + return 0; +} + +int +omemo_hmac_sha256_final_func(void *hmac_context, signal_buffer **output, void *user_data) +{ + gcry_error_t res; + size_t mac_len = 32; + unsigned char out[mac_len]; + + res = gcry_mac_read(hmac_context, out, &mac_len); + if (res != GPG_ERR_NO_ERROR) { + log_error("OMEMO: %s", gcry_strerror(res)); + return OMEMO_ERR_GCRYPT; + } + + *output = signal_buffer_create(out, mac_len); + return 0; +} + +void +omemo_hmac_sha256_cleanup_func(void *hmac_context, void *user_data) +{ + gcry_mac_close(hmac_context); +} + +int +omemo_sha512_digest_init_func(void **digest_context, void *user_data) +{ + gcry_error_t res; + gcry_md_hd_t hd; + + res = gcry_md_open(&hd, GCRY_MD_SHA512, 0); + if (res != GPG_ERR_NO_ERROR) { + log_error("OMEMO: %s", gcry_strerror(res)); + return OMEMO_ERR_GCRYPT; + } + + *digest_context = hd; + + return 0; +} + +int +omemo_sha512_digest_update_func(void *digest_context, const uint8_t *data, size_t data_len, void *user_data) +{ + gcry_md_write(digest_context, data, data_len); + + return 0; +} + +int +omemo_sha512_digest_final_func(void *digest_context, signal_buffer **output, void *user_data) +{ + gcry_error_t res; + unsigned char out[64]; + + res = gcry_md_extract(digest_context, GCRY_MD_SHA512, out, 64); + if (res != GPG_ERR_NO_ERROR) { + log_error("OMEMO: %s", gcry_strerror(res)); + return OMEMO_ERR_GCRYPT; + } + + *output = signal_buffer_create(out, 64); + return 0; +} + +void +omemo_sha512_digest_cleanup_func(void *digest_context, void *user_data) +{ + gcry_md_close(digest_context); +} + +int +omemo_encrypt_func(signal_buffer **output, int cipher, const uint8_t *key, size_t key_len, const uint8_t *iv, size_t iv_len, + const uint8_t *plaintext, size_t plaintext_len, void *user_data) +{ + gcry_cipher_hd_t hd; + unsigned char *padded_plaintext; + unsigned char *ciphertext; + size_t ciphertext_len; + int mode; + int algo; + uint8_t padding = 0; + + switch (key_len) { + case 32: + algo = GCRY_CIPHER_AES256; + break; + default: + return OMEMO_ERR_UNSUPPORTED_CRYPTO; + } + + switch (cipher) { + case SG_CIPHER_AES_CBC_PKCS5: + mode = GCRY_CIPHER_MODE_CBC; + break; + default: + return OMEMO_ERR_UNSUPPORTED_CRYPTO; + } + + gcry_cipher_open(&hd, algo, mode, GCRY_CIPHER_SECURE); + + gcry_cipher_setkey(hd, key, key_len); + + switch (cipher) { + case SG_CIPHER_AES_CBC_PKCS5: + gcry_cipher_setiv(hd, iv, iv_len); + padding = 16 - (plaintext_len % 16); + break; + default: + assert(FALSE); + } + + padded_plaintext = malloc(plaintext_len + padding); + memcpy(padded_plaintext, plaintext, plaintext_len); + memset(padded_plaintext + plaintext_len, padding, padding); + + ciphertext_len = plaintext_len + padding; + ciphertext = malloc(ciphertext_len); + gcry_cipher_encrypt(hd, ciphertext, ciphertext_len, padded_plaintext, plaintext_len + padding); + + *output = signal_buffer_create(ciphertext, ciphertext_len); + free(padded_plaintext); + free(ciphertext); + + gcry_cipher_close(hd); + + return SG_SUCCESS; +} + +int +omemo_decrypt_func(signal_buffer **output, int cipher, const uint8_t *key, size_t key_len, const uint8_t *iv, size_t iv_len, + const uint8_t *ciphertext, size_t ciphertext_len, void *user_data) +{ + int ret = SG_SUCCESS; + gcry_cipher_hd_t hd; + unsigned char *plaintext; + size_t plaintext_len; + int mode; + int algo; + uint8_t padding = 0; + + switch (key_len) { + case 32: + algo = GCRY_CIPHER_AES256; + break; + default: + return OMEMO_ERR_UNSUPPORTED_CRYPTO; + } + + switch (cipher) { + case SG_CIPHER_AES_CBC_PKCS5: + mode = GCRY_CIPHER_MODE_CBC; + break; + default: + return OMEMO_ERR_UNSUPPORTED_CRYPTO; + } + + gcry_cipher_open(&hd, algo, mode, GCRY_CIPHER_SECURE); + + gcry_cipher_setkey(hd, key, key_len); + + switch (cipher) { + case SG_CIPHER_AES_CBC_PKCS5: + gcry_cipher_setiv(hd, iv, iv_len); + break; + default: + assert(FALSE); + } + + plaintext_len = ciphertext_len; + plaintext = malloc(plaintext_len); + gcry_cipher_decrypt(hd, plaintext, plaintext_len, ciphertext, ciphertext_len); + + switch (cipher) { + case SG_CIPHER_AES_CBC_PKCS5: + padding = plaintext[plaintext_len - 1]; + break; + default: + assert(FALSE); + } + + int i; + for (i = 0; i < padding; i++) { + if (plaintext[plaintext_len - 1 - i] != padding) { + ret = SG_ERR_UNKNOWN; + goto out; + } + } + + *output = signal_buffer_create(plaintext, plaintext_len - padding); + +out: + free(plaintext); + + gcry_cipher_close(hd); + + return ret; +} + +int +aes128gcm_encrypt(unsigned char *ciphertext, size_t *ciphertext_len, unsigned char *tag, size_t *tag_len, const unsigned char *const plaintext, size_t plaintext_len, const unsigned char *const iv, const unsigned char *const key) +{ + gcry_error_t res; + gcry_cipher_hd_t hd; + + res = gcry_cipher_open(&hd, GCRY_CIPHER_AES128, GCRY_CIPHER_MODE_GCM, GCRY_CIPHER_SECURE); + if (res != GPG_ERR_NO_ERROR) { + goto out; + } + res = gcry_cipher_setkey(hd, key, AES128_GCM_KEY_LENGTH); + if (res != GPG_ERR_NO_ERROR) { + goto out; + } + res = gcry_cipher_setiv(hd, iv, AES128_GCM_IV_LENGTH); + if (res != GPG_ERR_NO_ERROR) { + goto out; + } + + res = gcry_cipher_encrypt(hd, ciphertext, *ciphertext_len, plaintext, plaintext_len); + if (res != GPG_ERR_NO_ERROR) { + goto out; + } + + res = gcry_cipher_gettag(hd, tag, *tag_len); + if (res != GPG_ERR_NO_ERROR) { + goto out; + } + +out: + gcry_cipher_close(hd); + return res; +} + +int +aes128gcm_decrypt(unsigned char *plaintext, size_t *plaintext_len, const unsigned char *const ciphertext, size_t ciphertext_len, const unsigned char *const iv, const unsigned char *const key, const unsigned char *const tag) +{ + gcry_error_t res; + gcry_cipher_hd_t hd; + + res = gcry_cipher_open(&hd, GCRY_CIPHER_AES128, GCRY_CIPHER_MODE_GCM, GCRY_CIPHER_SECURE); + if (res != GPG_ERR_NO_ERROR) { + goto out; + } + + res = gcry_cipher_setkey(hd, key, AES128_GCM_KEY_LENGTH); + if (res != GPG_ERR_NO_ERROR) { + goto out; + } + + res = gcry_cipher_setiv(hd, iv, AES128_GCM_IV_LENGTH); + if (res != GPG_ERR_NO_ERROR) { + goto out; + } + + res = gcry_cipher_decrypt(hd, plaintext, *plaintext_len, ciphertext, ciphertext_len); + if (res != GPG_ERR_NO_ERROR) { + goto out; + } + + res = gcry_cipher_checktag(hd, tag, AES128_GCM_TAG_LENGTH); + if (res != GPG_ERR_NO_ERROR) { + goto out; + } + +out: + gcry_cipher_close(hd); + return res; +} diff --git a/src/omemo/crypto.h b/src/omemo/crypto.h new file mode 100644 index 00000000..4b882455 --- /dev/null +++ b/src/omemo/crypto.h @@ -0,0 +1,148 @@ +#include <signal/signal_protocol_types.h> + +#define AES128_GCM_KEY_LENGTH 16 +#define AES128_GCM_IV_LENGTH 16 +#define AES128_GCM_TAG_LENGTH 16 + +int omemo_crypto_init(void); +/** +* Callback for a secure random number generator. +* This function shall fill the provided buffer with random bytes. +* +* @param data pointer to the output buffer +* @param len size of the output buffer +* @return 0 on success, negative on failure +*/ +int omemo_random_func(uint8_t *data, size_t len, void *user_data); + +/** +* Callback for an HMAC-SHA256 implementation. +* This function shall initialize an HMAC context with the provided key. +* +* @param hmac_context private HMAC context pointer +* @param key pointer to the key +* @param key_len length of the key +* @return 0 on success, negative on failure +*/ +int omemo_hmac_sha256_init_func(void **hmac_context, const uint8_t *key, size_t key_len, void *user_data); + +/** +* Callback for an HMAC-SHA256 implementation. +* This function shall update the HMAC context with the provided data +* +* @param hmac_context private HMAC context pointer +* @param data pointer to the data +* @param data_len length of the data +* @return 0 on success, negative on failure +*/ +int omemo_hmac_sha256_update_func(void *hmac_context, const uint8_t *data, size_t data_len, void *user_data); + +/** +* Callback for an HMAC-SHA256 implementation. +* This function shall finalize an HMAC calculation and populate the output +* buffer with the result. +* +* @param hmac_context private HMAC context pointer +* @param output buffer to be allocated and populated with the result +* @return 0 on success, negative on failure +*/ +int omemo_hmac_sha256_final_func(void *hmac_context, signal_buffer **output, void *user_data); + +/** +* Callback for an HMAC-SHA256 implementation. +* This function shall free the private context allocated in +* hmac_sha256_init_func. +* +* @param hmac_context private HMAC context pointer +*/ +void omemo_hmac_sha256_cleanup_func(void *hmac_context, void *user_data); + +/** +* Callback for a SHA512 message digest implementation. +* This function shall initialize a digest context. +* +* @param digest_context private digest context pointer +* @return 0 on success, negative on failure +*/ +int omemo_sha512_digest_init_func(void **digest_context, void *user_data); + +/** +* Callback for a SHA512 message digest implementation. +* This function shall update the digest context with the provided data. +* +* @param digest_context private digest context pointer +* @param data pointer to the data +* @param data_len length of the data +* @return 0 on success, negative on failure +*/ +int omemo_sha512_digest_update_func(void *digest_context, const uint8_t *data, size_t data_len, void *user_data); + +/** +* Callback for a SHA512 message digest implementation. +* This function shall finalize the digest calculation, populate the +* output buffer with the result, and prepare the context for reuse. +* +* @param digest_context private digest context pointer +* @param output buffer to be allocated and populated with the result +* @return 0 on success, negative on failure +*/ +int omemo_sha512_digest_final_func(void *digest_context, signal_buffer **output, void *user_data); + +/** +* Callback for a SHA512 message digest implementation. +* This function shall free the private context allocated in +* sha512_digest_init_func. +* +* @param digest_context private digest context pointer +*/ +void omemo_sha512_digest_cleanup_func(void *digest_context, void *user_data); + +/** +* Callback for an AES encryption implementation. +* +* @param output buffer to be allocated and populated with the ciphertext +* @param cipher specific cipher variant to use, either SG_CIPHER_AES_CTR_NOPADDING or SG_CIPHER_AES_CBC_PKCS5 +* @param key the encryption key +* @param key_len length of the encryption key +* @param iv the initialization vector +* @param iv_len length of the initialization vector +* @param plaintext the plaintext to encrypt +* @param plaintext_len length of the plaintext +* @return 0 on success, negative on failure +*/ +int omemo_encrypt_func(signal_buffer **output, + int cipher, + const uint8_t *key, size_t key_len, + const uint8_t *iv, size_t iv_len, + const uint8_t *plaintext, size_t plaintext_len, + void *user_data); + +/** +* Callback for an AES decryption implementation. +* +* @param output buffer to be allocated and populated with the plaintext +* @param cipher specific cipher variant to use, either SG_CIPHER_AES_CTR_NOPADDING or SG_CIPHER_AES_CBC_PKCS5 +* @param key the encryption key +* @param key_len length of the encryption key +* @param iv the initialization vector +* @param iv_len length of the initialization vector +* @param ciphertext the ciphertext to decrypt +* @param ciphertext_len length of the ciphertext +* @return 0 on success, negative on failure +*/ +int omemo_decrypt_func(signal_buffer **output, + int cipher, + const uint8_t *key, size_t key_len, + const uint8_t *iv, size_t iv_len, + const uint8_t *ciphertext, size_t ciphertext_len, + void *user_data); + +int aes128gcm_encrypt(unsigned char *ciphertext, size_t *ciphertext_len, + unsigned char *tag, size_t *tag_len, + const unsigned char *const plaintext, size_t plaintext_len, + const unsigned char *const iv, const unsigned char *const key); + +int aes128gcm_decrypt(unsigned char *plaintext, + size_t *plaintext_len, const unsigned char *const ciphertext, + size_t ciphertext_len, const unsigned char *const iv, + const unsigned char *const key, const unsigned char *const tag); diff --git a/src/omemo/omemo.c b/src/omemo/omemo.c new file mode 100644 index 00000000..7b3855dd --- /dev/null +++ b/src/omemo/omemo.c @@ -0,0 +1,1410 @@ +#include <sys/time.h> +#include <sys/stat.h> + +#include <assert.h> +#include <errno.h> +#include <glib.h> +#include <pthread.h> +#include <signal/key_helper.h> +#include <signal/protocol.h> +#include <signal/signal_protocol.h> +#include <signal/session_builder.h> +#include <signal/session_cipher.h> +#include <gcrypt.h> + +#include "config/account.h" +#include "config/files.h" +#include "log.h" +#include "omemo/crypto.h" +#include "omemo/omemo.h" +#include "omemo/store.h" +#include "ui/ui.h" +#include "ui/window_list.h" +#include "xmpp/connection.h" +#include "xmpp/muc.h" +#include "xmpp/omemo.h" +#include "xmpp/roster_list.h" +#include "xmpp/xmpp.h" + +static gboolean loaded; + +static void _generate_pre_keys(int count); +static void _generate_signed_pre_key(void); +static void _load_identity(void); +static void _load_trust(void); +static void _load_sessions(void); +static void _lock(void *user_data); +static void _unlock(void *user_data); +static void _omemo_log(int level, const char *message, size_t len, void *user_data); +static gboolean _handle_own_device_list(const char *const jid, GList *device_list); +static gboolean _handle_device_list_start_session(const char *const jid, GList *device_list); +static char * _omemo_fingerprint(ec_public_key *identity, gboolean formatted); +static unsigned char *_omemo_fingerprint_decode(const char *const fingerprint, size_t *len); +static void _cache_device_identity(const char *const jid, uint32_t device_id, ec_public_key *identity); +static void _g_hash_table_free(GHashTable *hash_table); + +typedef gboolean (*OmemoDeviceListHandler)(const char *const jid, GList *device_list); + +struct omemo_context_t { + pthread_mutexattr_t attr; + pthread_mutex_t lock; + signal_context *signal; + uint32_t device_id; + GHashTable *device_list; + GHashTable *device_list_handler; + ratchet_identity_key_pair *identity_key_pair; + uint32_t registration_id; + uint32_t signed_pre_key_id; + signal_protocol_store_context *store; + GHashTable *session_store; + GHashTable *pre_key_store; + GHashTable *signed_pre_key_store; + identity_key_store_t identity_key_store; + GHashTable *device_ids; + GString *identity_filename; + GKeyFile *identity_keyfile; + GString *trust_filename; + GKeyFile *trust_keyfile; + GString *sessions_filename; + GKeyFile *sessions_keyfile; + GHashTable *known_devices; + Autocomplete fingerprint_ac; +}; + +static omemo_context omemo_ctx; + +void +omemo_init(void) +{ + log_info("OMEMO: initialising"); + if (omemo_crypto_init() != 0) { + cons_show("Error initializing OMEMO crypto"); + } + + pthread_mutexattr_init(&omemo_ctx.attr); + pthread_mutexattr_settype(&omemo_ctx.attr, PTHREAD_MUTEX_RECURSIVE); + pthread_mutex_init(&omemo_ctx.lock, &omemo_ctx.attr); + + omemo_ctx.fingerprint_ac = autocomplete_new(); +} + +void +omemo_on_connect(ProfAccount *account) +{ + GError *error = NULL; + + if (signal_context_create(&omemo_ctx.signal, &omemo_ctx) != 0) { + cons_show("Error initializing OMEMO context"); + return; + } + + if (signal_context_set_log_function(omemo_ctx.signal, _omemo_log) != 0) { + cons_show("Error initializing OMEMO log"); + } + + signal_crypto_provider crypto_provider = { + .random_func = omemo_random_func, + .hmac_sha256_init_func = omemo_hmac_sha256_init_func, + .hmac_sha256_update_func = omemo_hmac_sha256_update_func, + .hmac_sha256_final_func = omemo_hmac_sha256_final_func, + .hmac_sha256_cleanup_func = omemo_hmac_sha256_cleanup_func, + .sha512_digest_init_func = omemo_sha512_digest_init_func, + .sha512_digest_update_func = omemo_sha512_digest_update_func, + .sha512_digest_final_func = omemo_sha512_digest_final_func, + .sha512_digest_cleanup_func = omemo_sha512_digest_cleanup_func, + .encrypt_func = omemo_encrypt_func, + .decrypt_func = omemo_decrypt_func, + .user_data = NULL + }; + + if (signal_context_set_crypto_provider(omemo_ctx.signal, &crypto_provider) != 0) { + cons_show("Error initializing OMEMO crypto"); + return; + } + + signal_context_set_locking_functions(omemo_ctx.signal, _lock, _unlock); + + signal_protocol_store_context_create(&omemo_ctx.store, omemo_ctx.signal); + + omemo_ctx.session_store = session_store_new(); + signal_protocol_session_store session_store = { + .load_session_func = load_session, + .get_sub_device_sessions_func = get_sub_device_sessions, + .store_session_func = store_session, + .contains_session_func = contains_session, + .delete_session_func = delete_session, + .delete_all_sessions_func = delete_all_sessions, + .destroy_func = NULL, + .user_data = omemo_ctx.session_store + }; + signal_protocol_store_context_set_session_store(omemo_ctx.store, &session_store); + + omemo_ctx.pre_key_store = pre_key_store_new(); + signal_protocol_pre_key_store pre_key_store = { + .load_pre_key = load_pre_key, + .store_pre_key = store_pre_key, + .contains_pre_key = contains_pre_key, + .remove_pre_key = remove_pre_key, + .destroy_func = NULL, + .user_data = omemo_ctx.pre_key_store + }; + signal_protocol_store_context_set_pre_key_store(omemo_ctx.store, &pre_key_store); + + omemo_ctx.signed_pre_key_store = signed_pre_key_store_new(); + signal_protocol_signed_pre_key_store signed_pre_key_store = { + .load_signed_pre_key = load_signed_pre_key, + .store_signed_pre_key = store_signed_pre_key, + .contains_signed_pre_key = contains_signed_pre_key, + .remove_signed_pre_key = remove_signed_pre_key, + .destroy_func = NULL, + .user_data = omemo_ctx.signed_pre_key_store + }; + signal_protocol_store_context_set_signed_pre_key_store(omemo_ctx.store, &signed_pre_key_store); + + identity_key_store_new(&omemo_ctx.identity_key_store); + signal_protocol_identity_key_store identity_key_store = { + .get_identity_key_pair = get_identity_key_pair, + .get_local_registration_id = get_local_registration_id, + .save_identity = save_identity, + .is_trusted_identity = is_trusted_identity, + .destroy_func = NULL, + .user_data = &omemo_ctx.identity_key_store + }; + signal_protocol_store_context_set_identity_key_store(omemo_ctx.store, &identity_key_store); + + + loaded = FALSE; + omemo_ctx.device_list = g_hash_table_new_full(g_str_hash, g_str_equal, free, (GDestroyNotify)g_list_free); + omemo_ctx.device_list_handler = g_hash_table_new_full(g_str_hash, g_str_equal, free, NULL); + omemo_ctx.known_devices = g_hash_table_new_full(g_str_hash, g_str_equal, free, (GDestroyNotify)_g_hash_table_free); + + omemo_ctx.fingerprint_ac = autocomplete_new(); + + char *omemodir = files_get_data_path(DIR_OMEMO); + GString *basedir = g_string_new(omemodir); + free(omemodir); + gchar *account_dir = str_replace(account->jid, "@", "_at_"); + g_string_append(basedir, "/"); + g_string_append(basedir, account_dir); + g_string_append(basedir, "/"); + free(account_dir); + + omemo_ctx.identity_filename = g_string_new(basedir->str); + g_string_append(omemo_ctx.identity_filename, "identity.txt"); + omemo_ctx.trust_filename = g_string_new(basedir->str); + g_string_append(omemo_ctx.trust_filename, "trust.txt"); + omemo_ctx.sessions_filename = g_string_new(basedir->str); + g_string_append(omemo_ctx.sessions_filename, "sessions.txt"); + + + errno = 0; + int res = g_mkdir_with_parents(basedir->str, S_IRWXU); + if (res == -1) { + char *errmsg = strerror(errno); + if (errmsg) { + log_error("OMEMO: error creating directory: %s, %s", basedir->str, errmsg); + } else { + log_error("OMEMO: creating directory: %s", basedir->str); + } + } + + g_string_free(basedir, TRUE); + + omemo_devicelist_subscribe(); + + omemo_ctx.identity_keyfile = g_key_file_new(); + omemo_ctx.trust_keyfile = g_key_file_new(); + omemo_ctx.sessions_keyfile = g_key_file_new(); + + if (g_key_file_load_from_file(omemo_ctx.identity_keyfile, omemo_ctx.identity_filename->str, G_KEY_FILE_KEEP_COMMENTS, &error)) { + _load_identity(); + } else if (error->code != G_FILE_ERROR_NOENT) { + log_warning("OMEMO: error loading identity from: %s, %s", omemo_ctx.identity_filename->str, error->message); + return; + } + + error = NULL; + if (g_key_file_load_from_file(omemo_ctx.trust_keyfile, omemo_ctx.trust_filename->str, G_KEY_FILE_KEEP_COMMENTS, &error)) { + _load_trust(); + } else if (error->code != G_FILE_ERROR_NOENT) { + log_warning("OMEMO: error loading trust from: %s, %s", omemo_ctx.sessions_filename->str, error->message); + } + + error = NULL; + if (g_key_file_load_from_file(omemo_ctx.sessions_keyfile, omemo_ctx.sessions_filename->str, G_KEY_FILE_KEEP_COMMENTS, &error)) { + _load_sessions(); + } else if (error->code != G_FILE_ERROR_NOENT) { + log_warning("OMEMO: error loading sessions from: %s, %s", omemo_ctx.sessions_filename->str, error->message); + } +} + +void +omemo_on_disconnect(void) +{ + signal_protocol_signed_pre_key_remove_key(omemo_ctx.store, omemo_ctx.signed_pre_key_id); + _g_hash_table_free(omemo_ctx.signed_pre_key_store); + + GHashTableIter iter; + gpointer id; + + g_hash_table_iter_init(&iter, omemo_ctx.pre_key_store); + while (g_hash_table_iter_next(&iter, &id, NULL)) { + signal_protocol_pre_key_remove_key(omemo_ctx.store, GPOINTER_TO_INT(id)); + } + + _g_hash_table_free(omemo_ctx.pre_key_store); + + g_string_free(omemo_ctx.identity_filename, TRUE); + g_key_file_free(omemo_ctx.identity_keyfile); + g_string_free(omemo_ctx.trust_filename, TRUE); + g_key_file_free(omemo_ctx.trust_keyfile); + g_string_free(omemo_ctx.sessions_filename, TRUE); + g_key_file_free(omemo_ctx.sessions_keyfile); +} + +void +omemo_generate_crypto_materials(ProfAccount *account) +{ + if (loaded) { + return; + } + + log_info("Generate long term OMEMO cryptography metarials"); + + /* Device ID */ + gcry_randomize(&omemo_ctx.device_id, 4, GCRY_VERY_STRONG_RANDOM); + omemo_ctx.device_id &= 0x7fffffff; + g_key_file_set_uint64(omemo_ctx.identity_keyfile, OMEMO_STORE_GROUP_IDENTITY, OMEMO_STORE_KEY_DEVICE_ID, omemo_ctx.device_id); + log_info("OMEMO: device id: %d", omemo_ctx.device_id); + + /* Identity key */ + signal_protocol_key_helper_generate_identity_key_pair(&omemo_ctx.identity_key_pair, omemo_ctx.signal); + + ec_public_key_serialize(&omemo_ctx.identity_key_store.public, ratchet_identity_key_pair_get_public(omemo_ctx.identity_key_pair)); + char *identity_key_public = g_base64_encode(signal_buffer_data(omemo_ctx.identity_key_store.public), signal_buffer_len(omemo_ctx.identity_key_store.public)); + g_key_file_set_string(omemo_ctx.identity_keyfile, OMEMO_STORE_GROUP_IDENTITY, OMEMO_STORE_KEY_IDENTITY_KEY_PUBLIC, identity_key_public); + g_free(identity_key_public); + + ec_private_key_serialize(&omemo_ctx.identity_key_store.private, ratchet_identity_key_pair_get_private(omemo_ctx.identity_key_pair)); + char *identity_key_private = g_base64_encode(signal_buffer_data(omemo_ctx.identity_key_store.private), signal_buffer_len(omemo_ctx.identity_key_store.private)); + g_key_file_set_string(omemo_ctx.identity_keyfile, OMEMO_STORE_GROUP_IDENTITY, OMEMO_STORE_KEY_IDENTITY_KEY_PRIVATE, identity_key_private); + g_free(identity_key_private); + + /* Registration ID */ + signal_protocol_key_helper_generate_registration_id(&omemo_ctx.registration_id, 0, omemo_ctx.signal); + g_key_file_set_uint64(omemo_ctx.identity_keyfile, OMEMO_STORE_GROUP_IDENTITY, OMEMO_STORE_KEY_REGISTRATION_ID, omemo_ctx.registration_id); + + /* Pre keys */ + _generate_pre_keys(100); + + /* Signed pre key */ + _generate_signed_pre_key(); + + omemo_identity_keyfile_save(); + + loaded = TRUE; + + omemo_publish_crypto_materials(); + omemo_start_sessions(); +} + +void +omemo_publish_crypto_materials(void) +{ + if (loaded != TRUE) { + log_error("OMEMO: cannot publish crypto materials before they are generated"); + return; + } + + Jid *jid = jid_create(connection_get_fulljid()); + + /* Ensure we get our current device list, and it gets updated with our + * device_id */ + g_hash_table_insert(omemo_ctx.device_list_handler, strdup(jid->barejid), _handle_own_device_list); + omemo_devicelist_request(jid->barejid); + + omemo_bundle_publish(true); + + jid_destroy(jid); +} + +void +omemo_start_sessions(void) +{ + GSList *contacts = roster_get_contacts(ROSTER_ORD_NAME); + if (contacts) { + GSList *curr = contacts; + for (curr = contacts; curr != NULL; curr = g_slist_next(curr)){ + PContact contact = curr->data; + const char *jid = p_contact_barejid(contact); + omemo_start_session(jid); + } + } +} + +void +omemo_start_session(const char *const barejid) +{ + log_info("OMEMO: start session with %s", barejid); + GList *device_list = g_hash_table_lookup(omemo_ctx.device_list, barejid); + if (!device_list) { + log_info("OMEMO: missing device list for %s", barejid); + omemo_devicelist_request(barejid); + g_hash_table_insert(omemo_ctx.device_list_handler, strdup(barejid), _handle_device_list_start_session); + return; + } + + GList *device_id; + for (device_id = device_list; device_id != NULL; device_id = device_id->next) { + omemo_bundle_request(barejid, GPOINTER_TO_INT(device_id->data), omemo_start_device_session_handle_bundle, free, strdup(barejid)); + } +} + +void +omemo_start_muc_sessions(const char *const roomjid) +{ + GList *roster = muc_roster(roomjid); + GList *iter; + for (iter = roster; iter != NULL; iter = iter->next) { + Occupant *occupant = (Occupant *)iter->data; + Jid *jid = jid_create(occupant->jid); + omemo_start_session(jid->barejid); + jid_destroy(jid); + } + g_list_free(roster); +} + +gboolean +omemo_loaded(void) +{ + return loaded; +} + +uint32_t +omemo_device_id(void) +{ + return omemo_ctx.device_id; +} + +void +omemo_identity_key(unsigned char **output, size_t *length) +{ + signal_buffer *buffer = NULL; + ec_public_key_serialize(&buffer, ratchet_identity_key_pair_get_public(omemo_ctx.identity_key_pair)); + *length = signal_buffer_len(buffer); + *output = malloc(*length); + memcpy(*output, signal_buffer_data(buffer), *length); + signal_buffer_free(buffer); +} + +void +omemo_signed_prekey(unsigned char **output, size_t *length) +{ + session_signed_pre_key *signed_pre_key; + signal_buffer *buffer = NULL; + + if (signal_protocol_signed_pre_key_load_key(omemo_ctx.store, &signed_pre_key, omemo_ctx.signed_pre_key_id) != SG_SUCCESS) { + *output = NULL; + *length = 0; + return; + } + + ec_public_key_serialize(&buffer, ec_key_pair_get_public(session_signed_pre_key_get_key_pair(signed_pre_key))); + SIGNAL_UNREF(signed_pre_key); + *length = signal_buffer_len(buffer); + *output = malloc(*length); + memcpy(*output, signal_buffer_data(buffer), *length); + signal_buffer_free(buffer); +} + +void +omemo_signed_prekey_signature(unsigned char **output, size_t *length) +{ + session_signed_pre_key *signed_pre_key; + + if (signal_protocol_signed_pre_key_load_key(omemo_ctx.store, &signed_pre_key, omemo_ctx.signed_pre_key_id) != SG_SUCCESS) { + *output = NULL; + *length = 0; + return; + } + + *length = session_signed_pre_key_get_signature_len(signed_pre_key); + *output = malloc(*length); + memcpy(*output, session_signed_pre_key_get_signature(signed_pre_key), *length); + SIGNAL_UNREF(signed_pre_key); +} + +void +omemo_prekeys(GList **prekeys, GList **ids, GList **lengths) +{ + GHashTableIter iter; + gpointer id; + + g_hash_table_iter_init(&iter, omemo_ctx.pre_key_store); + while (g_hash_table_iter_next(&iter, &id, NULL)) { + session_pre_key *pre_key; + int ret; + ret = signal_protocol_pre_key_load_key(omemo_ctx.store, &pre_key, GPOINTER_TO_INT(id)); + if (ret != SG_SUCCESS) { + continue; + } + + signal_buffer *public_key; + ec_public_key_serialize(&public_key, ec_key_pair_get_public(session_pre_key_get_key_pair(pre_key))); + SIGNAL_UNREF(pre_key); + size_t length = signal_buffer_len(public_key); + unsigned char *prekey_value = malloc(length); + memcpy(prekey_value, signal_buffer_data(public_key), length); + signal_buffer_free(public_key); + + *prekeys = g_list_append(*prekeys, prekey_value); + *ids = g_list_append(*ids, GINT_TO_POINTER(id)); + *lengths = g_list_append(*lengths, GINT_TO_POINTER(length)); + } +} + +void +omemo_set_device_list(const char *const from, GList * device_list) +{ + Jid *jid; + if (from) { + jid = jid_create(from); + } else { + jid = jid_create(connection_get_fulljid()); + } + + g_hash_table_insert(omemo_ctx.device_list, strdup(jid->barejid), device_list); + + OmemoDeviceListHandler handler = g_hash_table_lookup(omemo_ctx.device_list_handler, jid->barejid); + if (handler) { + gboolean keep = handler(jid->barejid, device_list); + if (!keep) { + g_hash_table_remove(omemo_ctx.device_list_handler, jid->barejid); + } + } + + jid_destroy(jid); +} + +GKeyFile * +omemo_identity_keyfile(void) +{ + return omemo_ctx.identity_keyfile; +} + +void +omemo_identity_keyfile_save(void) +{ + GError *error = NULL; + + if (!g_key_file_save_to_file(omemo_ctx.identity_keyfile, omemo_ctx.identity_filename->str, &error)) { + log_error("OMEMO: error saving identity to: %s, %s", omemo_ctx.identity_filename->str, error->message); + } +} + +GKeyFile * +omemo_trust_keyfile(void) +{ + return omemo_ctx.trust_keyfile; +} + +void +omemo_trust_keyfile_save(void) +{ + GError *error = NULL; + + if (!g_key_file_save_to_file(omemo_ctx.trust_keyfile, omemo_ctx.trust_filename->str, &error)) { + log_error("OMEMO: error saving trust to: %s, %s", omemo_ctx.trust_filename->str, error->message); + } +} + +GKeyFile * +omemo_sessions_keyfile(void) +{ + return omemo_ctx.sessions_keyfile; +} + +void +omemo_sessions_keyfile_save(void) +{ + GError *error = NULL; + + if (!g_key_file_save_to_file(omemo_ctx.sessions_keyfile, omemo_ctx.sessions_filename->str, &error)) { + log_error("OMEMO: error saving sessions to: %s, %s", omemo_ctx.sessions_filename->str, error->message); + } +} + +void +omemo_start_device_session(const char *const jid, uint32_t device_id, + GList *prekeys, uint32_t signed_prekey_id, + const unsigned char *const signed_prekey_raw, size_t signed_prekey_len, + const unsigned char *const signature, size_t signature_len, + const unsigned char *const identity_key_raw, size_t identity_key_len) +{ + signal_protocol_address address = { + .name = jid, + .name_len = strlen(jid), + .device_id = device_id, + }; + + ec_public_key *identity_key; + curve_decode_point(&identity_key, identity_key_raw, identity_key_len, omemo_ctx.signal); + _cache_device_identity(jid, device_id, identity_key); + + gboolean trusted = is_trusted_identity(&address, (uint8_t *)identity_key_raw, identity_key_len, &omemo_ctx.identity_key_store); + + if (!trusted) { + goto out; + } + + if (!contains_session(&address, omemo_ctx.session_store)) { + int res; + session_pre_key_bundle *bundle; + signal_protocol_address *address; + + address = malloc(sizeof(signal_protocol_address)); + address->name = strdup(jid); + address->name_len = strlen(jid); + address->device_id = device_id; + + session_builder *builder; + res = session_builder_create(&builder, omemo_ctx.store, address, omemo_ctx.signal); + if (res != 0) { + log_error("OMEMO: cannot create session builder for %s device %d", jid, device_id); + goto out; + } + + int prekey_index; + gcry_randomize(&prekey_index, sizeof(int), GCRY_STRONG_RANDOM); + prekey_index %= g_list_length(prekeys); + omemo_key_t *prekey = g_list_nth_data(prekeys, prekey_index); + + ec_public_key *prekey_public; + curve_decode_point(&prekey_public, prekey->data, prekey->length, omemo_ctx.signal); + ec_public_key *signed_prekey; + curve_decode_point(&signed_prekey, signed_prekey_raw, signed_prekey_len, omemo_ctx.signal); + + res = session_pre_key_bundle_create(&bundle, 0, device_id, prekey->id, prekey_public, signed_prekey_id, signed_prekey, signature, signature_len, identity_key); + if (res != 0) { + log_error("OMEMO: cannot create pre key bundle for %s device %d", jid, device_id); + goto out; + } + + res = session_builder_process_pre_key_bundle(builder, bundle); + if (res != 0) { + log_error("OMEMO: cannot process pre key bundle for %s device %d", jid, device_id); + goto out; + } + + log_info("OMEMO: create session with %s device %d", jid, device_id); + } + +out: + SIGNAL_UNREF(identity_key); +} + +char * +omemo_on_message_send(ProfWin *win, const char *const message, gboolean request_receipt, gboolean muc) +{ + char *id = NULL; + int res; + Jid *jid = jid_create(connection_get_fulljid()); + GList *keys = NULL; + + unsigned char *key; + unsigned char *iv; + unsigned char *ciphertext; + unsigned char *tag; + unsigned char *key_tag; + size_t ciphertext_len, tag_len; + + ciphertext_len = strlen(message); + ciphertext = malloc(ciphertext_len); + tag_len = AES128_GCM_TAG_LENGTH; + tag = gcry_malloc_secure(tag_len); + key_tag = gcry_malloc_secure(AES128_GCM_KEY_LENGTH + AES128_GCM_TAG_LENGTH); + + key = gcry_random_bytes_secure(AES128_GCM_KEY_LENGTH, GCRY_VERY_STRONG_RANDOM); + iv = gcry_random_bytes_secure(AES128_GCM_IV_LENGTH, GCRY_VERY_STRONG_RANDOM); + + res = aes128gcm_encrypt(ciphertext, &ciphertext_len, tag, &tag_len, (const unsigned char * const)message, strlen(message), iv, key); + if (res != 0) { + log_error("OMEMO: cannot encrypt message"); + goto out; + } + + memcpy(key_tag, key, AES128_GCM_KEY_LENGTH); + memcpy(key_tag + AES128_GCM_KEY_LENGTH, tag, AES128_GCM_TAG_LENGTH); + + GList *recipients = NULL; + if (muc) { + ProfMucWin *mucwin = (ProfMucWin *)win; + assert(mucwin->memcheck == PROFMUCWIN_MEMCHECK); + GList *roster = muc_roster(mucwin->roomjid); + GList *iter; + for (iter = roster; iter != NULL; iter = iter->next) { + Occupant *occupant = (Occupant *)iter->data; + Jid *jid = jid_create(occupant->jid); + if (!jid->barejid) { + log_warning("OMEMO: missing barejid for MUC %s occupant %s", mucwin->roomjid, occupant->nick); + } else { + recipients = g_list_append(recipients, strdup(jid->barejid)); + } + jid_destroy(jid); + } + g_list_free(roster); + } else { + ProfChatWin *chatwin = (ProfChatWin *)win; + assert(chatwin->memcheck == PROFCHATWIN_MEMCHECK); + recipients = g_list_append(recipients, strdup(chatwin->barejid)); + } + + GList *device_ids_iter; + + GList *recipients_iter; + for (recipients_iter = recipients; recipients_iter != NULL; recipients_iter = recipients_iter->next) { + GList *recipient_device_id = NULL; + recipient_device_id = g_hash_table_lookup(omemo_ctx.device_list, recipients_iter->data); + if (!recipient_device_id) { + log_warning("OMEMO: cannot find device ids for %s", recipients_iter->data); + continue; + } + + for (device_ids_iter = recipient_device_id; device_ids_iter != NULL; device_ids_iter = device_ids_iter->next) { + int res; + ciphertext_message *ciphertext; + session_cipher *cipher; + signal_protocol_address address = { + .name = recipients_iter->data, + .name_len = strlen(recipients_iter->data), + .device_id = GPOINTER_TO_INT(device_ids_iter->data) + }; + + res = session_cipher_create(&cipher, omemo_ctx.store, &address, omemo_ctx.signal); + if (res != 0) { + log_error("OMEMO: cannot create cipher for %s device id %d", address.name, address.device_id); + continue; + } + + res = session_cipher_encrypt(cipher, key_tag, AES128_GCM_KEY_LENGTH + AES128_GCM_TAG_LENGTH, &ciphertext); + session_cipher_free(cipher); + if (res != 0) { + log_error("OMEMO: cannot encrypt key for %s device id %d", address.name, address.device_id); + continue; + } + signal_buffer *buffer = ciphertext_message_get_serialized(ciphertext); + omemo_key_t *key = malloc(sizeof(omemo_key_t)); + key->length = signal_buffer_len(buffer); + key->data = malloc(key->length); + memcpy(key->data, signal_buffer_data(buffer), key->length); + key->device_id = GPOINTER_TO_INT(device_ids_iter->data); + key->prekey = ciphertext_message_get_type(ciphertext) == CIPHERTEXT_PREKEY_TYPE; + keys = g_list_append(keys, key); + SIGNAL_UNREF(ciphertext); + } + } + + g_list_free_full(recipients, free); + + if (!muc) { + GList *sender_device_id = g_hash_table_lookup(omemo_ctx.device_list, jid->barejid); + for (device_ids_iter = sender_device_id; device_ids_iter != NULL; device_ids_iter = device_ids_iter->next) { + int res; + ciphertext_message *ciphertext; + session_cipher *cipher; + signal_protocol_address address = { + .name = jid->barejid, + .name_len = strlen(jid->barejid), + .device_id = GPOINTER_TO_INT(device_ids_iter->data) + }; + + res = session_cipher_create(&cipher, omemo_ctx.store, &address, omemo_ctx.signal); + if (res != 0) { + log_error("OMEMO: cannot create cipher for %s device id %d", address.name, address.device_id); + continue; + } + + res = session_cipher_encrypt(cipher, key_tag, AES128_GCM_KEY_LENGTH + AES128_GCM_TAG_LENGTH, &ciphertext); + session_cipher_free(cipher); + if (res != 0) { + log_error("OMEMO: cannot encrypt key for %s device id %d", address.name, address.device_id); + continue; + } + signal_buffer *buffer = ciphertext_message_get_serialized(ciphertext); + omemo_key_t *key = malloc(sizeof(omemo_key_t)); + key->length = signal_buffer_len(buffer); + key->data = malloc(key->length); + memcpy(key->data, signal_buffer_data(buffer), key->length); + key->device_id = GPOINTER_TO_INT(device_ids_iter->data); + key->prekey = ciphertext_message_get_type(ciphertext) == CIPHERTEXT_PREKEY_TYPE; + keys = g_list_append(keys, key); + SIGNAL_UNREF(ciphertext); + } + } + + if (muc) { + ProfMucWin *mucwin = (ProfMucWin *)win; + assert(mucwin->memcheck == PROFMUCWIN_MEMCHECK); + id = message_send_chat_omemo(mucwin->roomjid, omemo_ctx.device_id, keys, iv, AES128_GCM_IV_LENGTH, ciphertext, ciphertext_len, request_receipt, TRUE); + } else { + ProfChatWin *chatwin = (ProfChatWin *)win; + assert(chatwin->memcheck == PROFCHATWIN_MEMCHECK); + id = message_send_chat_omemo(chatwin->barejid, omemo_ctx.device_id, keys, iv, AES128_GCM_IV_LENGTH, ciphertext, ciphertext_len, request_receipt, FALSE); + } + +out: + jid_destroy(jid); + g_list_free_full(keys, (GDestroyNotify)omemo_key_free); + free(ciphertext); + gcry_free(key); + gcry_free(iv); + gcry_free(tag); + gcry_free(key_tag); + + return id; +} + +char * +omemo_on_message_recv(const char *const from_jid, uint32_t sid, + const unsigned char *const iv, size_t iv_len, GList *keys, + const unsigned char *const payload, size_t payload_len, gboolean muc) +{ + unsigned char *plaintext = NULL; + Jid *sender = NULL; + Jid *from = jid_create(from_jid); + if (!from) { + log_error("Invalid jid %s", from_jid); + goto out; + } + + int res; + GList *key_iter; + omemo_key_t *key = NULL; + for (key_iter = keys; key_iter != NULL; key_iter = key_iter->next) { + if (((omemo_key_t *)key_iter->data)->device_id == omemo_ctx.device_id) { + key = key_iter->data; + break; + } + } + + if (!key) { + log_warning("OMEMO: Received a message with no corresponding key"); + goto out; + } + + if (muc) { + GList *roster = muc_roster(from->barejid); + GList *iter; + for (iter = roster; iter != NULL; iter = iter->next) { + Occupant *occupant = (Occupant *)iter->data; + if (g_strcmp0(occupant->nick, from->resourcepart) == 0) { + sender = jid_create(occupant->jid); + break; + } + } + g_list_free(roster); + if (!sender) { + log_warning("OMEMO: cannot find MUC message sender fulljid"); + goto out; + } + } else { + sender = jid_create(from->barejid); + } + + session_cipher *cipher; + signal_buffer *plaintext_key; + signal_protocol_address address = { + .name = sender->barejid, + .name_len = strlen(sender->barejid), + .device_id = sid + }; + + res = session_cipher_create(&cipher, omemo_ctx.store, &address, omemo_ctx.signal); + if (res != 0) { + log_error("OMEMO: cannot create session cipher"); + goto out; + } + + if (key->prekey) { + log_debug("OMEMO: decrypting message with prekey"); + pre_key_signal_message *message; + + pre_key_signal_message_deserialize(&message, key->data, key->length, omemo_ctx.signal); + + res = session_cipher_decrypt_pre_key_signal_message(cipher, message, NULL, &plaintext_key); + /* Replace used pre_key in bundle */ + uint32_t pre_key_id = pre_key_signal_message_get_pre_key_id(message); + ec_key_pair *ec_pair; + session_pre_key *new_pre_key; + curve_generate_key_pair(omemo_ctx.signal, &ec_pair); + session_pre_key_create(&new_pre_key, pre_key_id, ec_pair); + signal_protocol_pre_key_store_key(omemo_ctx.store, new_pre_key); + SIGNAL_UNREF(new_pre_key); + SIGNAL_UNREF(message); + SIGNAL_UNREF(ec_pair); + omemo_bundle_publish(true); + + if (res == 0) { + /* Start a new session */ + omemo_bundle_request(sender->barejid, sid, omemo_start_device_session_handle_bundle, free, strdup(sender->barejid)); + } + } else { + log_debug("OMEMO: decrypting message with existing session"); + signal_message *message; + signal_message_deserialize(&message, key->data, key->length, omemo_ctx.signal); + res = session_cipher_decrypt_signal_message(cipher, message, NULL, &plaintext_key); + SIGNAL_UNREF(message); + } + + session_cipher_free(cipher); + if (res != 0) { + log_error("OMEMO: cannot decrypt message key"); + goto out; + } + + if (signal_buffer_len(plaintext_key) != AES128_GCM_KEY_LENGTH + AES128_GCM_TAG_LENGTH) { + log_error("OMEMO: invalid key length"); + signal_buffer_free(plaintext_key); + goto out; + } + + size_t plaintext_len = payload_len; + plaintext = malloc(plaintext_len + 1); + res = aes128gcm_decrypt(plaintext, &plaintext_len, payload, payload_len, iv, + signal_buffer_data(plaintext_key), + signal_buffer_data(plaintext_key) + AES128_GCM_KEY_LENGTH); + signal_buffer_free(plaintext_key); + if (res != 0) { + log_error("OMEMO: cannot decrypt message: %s", gcry_strerror(res)); + free(plaintext); + plaintext = NULL; + goto out; + } + + plaintext[plaintext_len] = '\0'; + +out: + jid_destroy(from); + jid_destroy(sender); + return (char *)plaintext; +} + +char * +omemo_format_fingerprint(const char *const fingerprint) +{ + char *output = malloc(strlen(fingerprint) + strlen(fingerprint) / 8); + + int i, j; + for (i = 0, j = 0; i < strlen(fingerprint); i++) { + if (i > 0 && i % 8 == 0) { + output[j++] = '-'; + } + output[j++] = fingerprint[i]; + } + + output[j] = '\0'; + + return output; +} + +char * +omemo_own_fingerprint(gboolean formatted) +{ + ec_public_key *identity = ratchet_identity_key_pair_get_public(omemo_ctx.identity_key_pair); + return _omemo_fingerprint(identity, formatted); +} + +GList * +omemo_known_device_identities(const char *const jid) +{ + GHashTable *known_identities = g_hash_table_lookup(omemo_ctx.known_devices, jid); + if (!known_identities) { + return NULL; + } + + return g_hash_table_get_keys(known_identities); +} + +gboolean +omemo_is_trusted_jid(const char *const jid) +{ + GHashTable *trusted = g_hash_table_lookup(omemo_ctx.identity_key_store.trusted, jid); + if (!trusted) { + return FALSE; + } + + if (g_hash_table_size(trusted) > 0) { + return TRUE; + } + + return FALSE; +} + +gboolean +omemo_is_trusted_identity(const char *const jid, const char *const fingerprint) +{ + GHashTable *known_identities = g_hash_table_lookup(omemo_ctx.known_devices, jid); + if (!known_identities) { + return FALSE; + } + + void *device_id = g_hash_table_lookup(known_identities, fingerprint); + if (!device_id) { + return FALSE; + } + + signal_protocol_address address = { + .name = jid, + .name_len = strlen(jid), + .device_id = GPOINTER_TO_INT(device_id), + }; + + size_t fingerprint_len; + unsigned char *fingerprint_raw = _omemo_fingerprint_decode(fingerprint, &fingerprint_len); + unsigned char djb_type[] = {'\x05'}; + signal_buffer *buffer = signal_buffer_create(djb_type, 1); + buffer = signal_buffer_append(buffer, fingerprint_raw, fingerprint_len); + + gboolean trusted = is_trusted_identity(&address, signal_buffer_data(buffer), signal_buffer_len(buffer), &omemo_ctx.identity_key_store); + + free(fingerprint_raw); + signal_buffer_free(buffer); + + return trusted; +} + +static char * +_omemo_fingerprint(ec_public_key *identity, gboolean formatted) +{ + int i; + signal_buffer *identity_public_key; + + ec_public_key_serialize(&identity_public_key, identity); + size_t identity_public_key_len = signal_buffer_len(identity_public_key); + unsigned char *identity_public_key_data = signal_buffer_data(identity_public_key); + + /* Skip first byte corresponding to signal DJB_TYPE */ + identity_public_key_len--; + identity_public_key_data = &identity_public_key_data[1]; + + char *fingerprint = malloc(identity_public_key_len * 2 + 1); + + for (i = 0; i < identity_public_key_len; i++) { + fingerprint[i * 2] = (identity_public_key_data[i] & 0xf0) >> 4; + fingerprint[i * 2] += '0'; + if (fingerprint[i * 2] > '9') { + fingerprint[i * 2] += 0x27; + } + + fingerprint[(i * 2) + 1] = identity_public_key_data[i] & 0x0f; + fingerprint[(i * 2) + 1] += '0'; + if (fingerprint[(i * 2) + 1] > '9') { + fingerprint[(i * 2) + 1] += 0x27; + } + } + + fingerprint[i * 2] = '\0'; + signal_buffer_free(identity_public_key); + + if (!formatted) { + return fingerprint; + } else { + char *formatted_fingerprint = omemo_format_fingerprint(fingerprint); + free(fingerprint); + return formatted_fingerprint; + } +} + +static unsigned char * +_omemo_fingerprint_decode(const char *const fingerprint, size_t *len) +{ + unsigned char *output = malloc(strlen(fingerprint) / 2 + 1); + + int i; + int j; + for (i = 0, j = 0; i < strlen(fingerprint);) { + if (!g_ascii_isxdigit(fingerprint[i])) { + i++; + continue; + } + + output[j] = g_ascii_xdigit_value(fingerprint[i++]) << 4; + output[j] |= g_ascii_xdigit_value(fingerprint[i++]); + j++; + } + + *len = j; + + return output; +} + +void +omemo_trust(const char *const jid, const char *const fingerprint_formatted) +{ + size_t len; + + GHashTable *known_identities = g_hash_table_lookup(omemo_ctx.known_devices, jid); + if (!known_identities) { + log_warning("OMEMO: cannot trust unknown device: %s", fingerprint_formatted); + cons_show("Cannot trust unknown device: %s", fingerprint_formatted); + return; + } + + /* Unformat fingerprint */ + char *fingerprint = malloc(strlen(fingerprint_formatted)); + int i; + int j; + for (i = 0, j = 0; fingerprint_formatted[i] != '\0'; i++) { + if (!g_ascii_isxdigit(fingerprint_formatted[i])) { + continue; + } + fingerprint[j++] = fingerprint_formatted[i]; + } + + fingerprint[j] = '\0'; + + uint32_t device_id = GPOINTER_TO_INT(g_hash_table_lookup(known_identities, fingerprint)); + free(fingerprint); + + if (!device_id) { + log_warning("OMEMO: cannot trust unknown device: %s", fingerprint_formatted); + cons_show("Cannot trust unknown device: %s", fingerprint_formatted); + return; + } + + /* TODO should not hardcode DJB_TYPE here + * should instead store identity key in known_identities along with + * device_id */ + signal_protocol_address address = { + .name = jid, + .name_len = strlen(jid), + .device_id = device_id, + }; + unsigned char *fingerprint_raw = _omemo_fingerprint_decode(fingerprint_formatted, &len); + unsigned char djb_type[] = {'\x05'}; + signal_buffer *buffer = signal_buffer_create(djb_type, 1); + buffer = signal_buffer_append(buffer, fingerprint_raw, len); + save_identity(&address, signal_buffer_data(buffer), signal_buffer_len(buffer), &omemo_ctx.identity_key_store); + free(fingerprint_raw); + signal_buffer_free(buffer); + + omemo_bundle_request(jid, device_id, omemo_start_device_session_handle_bundle, free, strdup(jid)); +} + +void +omemo_untrust(const char *const jid, const char *const fingerprint_formatted) +{ + size_t len; + unsigned char *fingerprint = _omemo_fingerprint_decode(fingerprint_formatted, &len); + + GHashTableIter iter; + gpointer key, value; + + GHashTable *trusted = g_hash_table_lookup(omemo_ctx.identity_key_store.trusted, jid); + if (!trusted) { + return; + } + + g_hash_table_iter_init(&iter, trusted); + while (g_hash_table_iter_next(&iter, &key, &value)) { + signal_buffer *buffer = value; + unsigned char *original = signal_buffer_data(buffer); + /* Skip DJB_TYPE byte */ + original++; + if ((signal_buffer_len(buffer) - 1) == len && memcmp(original, fingerprint, len) == 0) { + g_hash_table_remove(trusted, key); + } + } + free(fingerprint); +} + +static void +_lock(void *user_data) +{ + omemo_context *ctx = (omemo_context *)user_data; + pthread_mutex_lock(&ctx->lock); +} + +static void +_unlock(void *user_data) +{ + omemo_context *ctx = (omemo_context *)user_data; + pthread_mutex_unlock(&ctx->lock); +} + +static void +_omemo_log(int level, const char *message, size_t len, void *user_data) +{ + switch (level) { + case SG_LOG_ERROR: + log_error("OMEMO: %s", message); + break; + case SG_LOG_WARNING: + log_warning("OMEMO: %s", message); + break; + case SG_LOG_NOTICE: + case SG_LOG_INFO: + log_info("OMEMO: %s", message); + break; + case SG_LOG_DEBUG: + log_debug("OMEMO: %s", message); + break; + } +} + +static gboolean +_handle_own_device_list(const char *const jid, GList *device_list) +{ + if (!g_list_find(device_list, GINT_TO_POINTER(omemo_ctx.device_id))) { + device_list = g_list_copy(device_list); + device_list = g_list_append(device_list, GINT_TO_POINTER(omemo_ctx.device_id)); + g_hash_table_insert(omemo_ctx.device_list, strdup(jid), device_list); + omemo_devicelist_publish(device_list); + } + + GList *device_id; + for (device_id = device_list; device_id != NULL; device_id = device_id->next) { + omemo_bundle_request(jid, GPOINTER_TO_INT(device_id->data), omemo_start_device_session_handle_bundle, free, strdup(jid)); + } + + return TRUE; +} + +static gboolean +_handle_device_list_start_session(const char *const jid, GList *device_list) +{ + omemo_start_session(jid); + + return FALSE; +} + +void +omemo_key_free(omemo_key_t *key) +{ + if (key == NULL) { + return; + } + + free(key->data); + free(key); +} + +char* +omemo_fingerprint_autocomplete(const char *const search_str, gboolean previous) +{ + return autocomplete_complete(omemo_ctx.fingerprint_ac, search_str, FALSE, previous); +} + +void +omemo_fingerprint_autocomplete_reset(void) +{ + autocomplete_reset(omemo_ctx.fingerprint_ac); +} + +static void +_load_identity(void) +{ + log_info("Loading OMEMO identity"); + + /* Device ID */ + omemo_ctx.device_id = g_key_file_get_uint64(omemo_ctx.identity_keyfile, OMEMO_STORE_GROUP_IDENTITY, OMEMO_STORE_KEY_DEVICE_ID, NULL); + log_info("OMEMO: device id: %d", omemo_ctx.device_id); + + /* Registration ID */ + omemo_ctx.registration_id = g_key_file_get_uint64(omemo_ctx.identity_keyfile, OMEMO_STORE_GROUP_IDENTITY, OMEMO_STORE_KEY_REGISTRATION_ID, NULL); + + /* Identity key */ + char *identity_key_public_b64 = g_key_file_get_string(omemo_ctx.identity_keyfile, OMEMO_STORE_GROUP_IDENTITY, OMEMO_STORE_KEY_IDENTITY_KEY_PUBLIC, NULL); + size_t identity_key_public_len; + unsigned char *identity_key_public = g_base64_decode(identity_key_public_b64, &identity_key_public_len); + g_free(identity_key_public_b64); + omemo_ctx.identity_key_store.public = signal_buffer_create(identity_key_public, identity_key_public_len); + + char *identity_key_private_b64 = g_key_file_get_string(omemo_ctx.identity_keyfile, OMEMO_STORE_GROUP_IDENTITY, OMEMO_STORE_KEY_IDENTITY_KEY_PRIVATE, NULL); + size_t identity_key_private_len; + unsigned char *identity_key_private = g_base64_decode(identity_key_private_b64, &identity_key_private_len); + g_free(identity_key_private_b64); + omemo_ctx.identity_key_store.private = signal_buffer_create(identity_key_private, identity_key_private_len); + + ec_public_key *public_key; + curve_decode_point(&public_key, identity_key_public, identity_key_public_len, omemo_ctx.signal); + ec_private_key *private_key; + curve_decode_private_point(&private_key, identity_key_private, identity_key_private_len, omemo_ctx.signal); + ratchet_identity_key_pair_create(&omemo_ctx.identity_key_pair, public_key, private_key); + + g_free(identity_key_public); + g_free(identity_key_private); + + char **keys = NULL; + int i; + /* Pre keys */ + i = 0; + keys = g_key_file_get_keys(omemo_ctx.identity_keyfile, OMEMO_STORE_GROUP_PREKEYS, NULL, NULL); + if (keys) { + for (i = 0; keys[i] != NULL; i++) { + char *pre_key_b64 = g_key_file_get_string(omemo_ctx.identity_keyfile, OMEMO_STORE_GROUP_PREKEYS, keys[i], NULL); + size_t pre_key_len; + unsigned char *pre_key = g_base64_decode(pre_key_b64, &pre_key_len); + g_free(pre_key_b64); + signal_buffer *buffer = signal_buffer_create(pre_key, pre_key_len); + g_free(pre_key); + g_hash_table_insert(omemo_ctx.pre_key_store, GINT_TO_POINTER(strtoul(keys[i], NULL, 10)), buffer); + } + + g_strfreev(keys); + } + + /* Ensure we have at least 100 pre keys */ + if (i < 100) { + _generate_pre_keys(100 - i); + } + + /* Signed pre keys */ + i = 0; + keys = g_key_file_get_keys(omemo_ctx.identity_keyfile, OMEMO_STORE_GROUP_SIGNED_PREKEYS, NULL, NULL); + if (keys) { + for (i = 0; keys[i] != NULL; i++) { + char *signed_pre_key_b64 = g_key_file_get_string(omemo_ctx.identity_keyfile, OMEMO_STORE_GROUP_SIGNED_PREKEYS, keys[i], NULL); + size_t signed_pre_key_len; + unsigned char *signed_pre_key = g_base64_decode(signed_pre_key_b64, &signed_pre_key_len); + g_free(signed_pre_key_b64); + signal_buffer *buffer = signal_buffer_create(signed_pre_key, signed_pre_key_len); + g_free(signed_pre_key); + g_hash_table_insert(omemo_ctx.signed_pre_key_store, GINT_TO_POINTER(strtoul(keys[i], NULL, 10)), buffer); + omemo_ctx.signed_pre_key_id = strtoul(keys[i], NULL, 10); + } + g_strfreev(keys); + } + + if (i == 0) { + _generate_signed_pre_key(); + } + + loaded = TRUE; + + omemo_identity_keyfile_save(); + omemo_start_sessions(); +} + +static void +_load_trust(void) +{ + char **keys = NULL; + char **groups = g_key_file_get_groups(omemo_ctx.trust_keyfile, NULL); + if (groups) { + int i; + for (i = 0; groups[i] != NULL; i++) { + GHashTable *trusted; + + trusted = g_hash_table_lookup(omemo_ctx.identity_key_store.trusted, groups[i]); + if (!trusted) { + trusted = g_hash_table_new_full(g_direct_hash, g_direct_equal, NULL, (GDestroyNotify)signal_buffer_free); + g_hash_table_insert(omemo_ctx.identity_key_store.trusted, strdup(groups[i]), trusted); + } + + keys = g_key_file_get_keys(omemo_ctx.trust_keyfile, groups[i], NULL, NULL); + int j; + for (j = 0; keys[j] != NULL; j++) { + char *key_b64 = g_key_file_get_string(omemo_ctx.trust_keyfile, groups[i], keys[j], NULL); + size_t key_len; + unsigned char *key = g_base64_decode(key_b64, &key_len); + g_free(key_b64); + signal_buffer *buffer = signal_buffer_create(key, key_len); + g_free(key); + uint32_t device_id = strtoul(keys[j], NULL, 10); + g_hash_table_insert(trusted, GINT_TO_POINTER(device_id), buffer); + } + g_strfreev(keys); + } + g_strfreev(groups); + } +} + +static void +_load_sessions(void) +{ + int i; + char **groups = g_key_file_get_groups(omemo_ctx.sessions_keyfile, NULL); + if (groups) { + for (i = 0; groups[i] != NULL; i++) { + int j; + GHashTable *device_store = NULL; + + device_store = g_hash_table_lookup(omemo_ctx.session_store, groups[i]); + if (!device_store) { + device_store = g_hash_table_new_full(g_direct_hash, g_direct_equal, NULL, (GDestroyNotify)signal_buffer_free); + g_hash_table_insert(omemo_ctx.session_store, strdup(groups[i]), device_store); + } + + char **keys = g_key_file_get_keys(omemo_ctx.sessions_keyfile, groups[i], NULL, NULL); + for (j = 0; keys[j] != NULL; j++) { + uint32_t id = strtoul(keys[j], NULL, 10); + char *record_b64 = g_key_file_get_string(omemo_ctx.sessions_keyfile, groups[i], keys[j], NULL); + size_t record_len; + unsigned char *record = g_base64_decode(record_b64, &record_len); + g_free(record_b64); + signal_buffer *buffer = signal_buffer_create(record, record_len); + g_free(record); + g_hash_table_insert(device_store, GINT_TO_POINTER(id), buffer); + } + g_strfreev(keys); + } + g_strfreev(groups); + } +} + +static void +_cache_device_identity(const char *const jid, uint32_t device_id, ec_public_key *identity) +{ + GHashTable *known_identities = g_hash_table_lookup(omemo_ctx.known_devices, jid); + if (!known_identities) { + known_identities = g_hash_table_new_full(g_str_hash, g_str_equal, free, NULL); + g_hash_table_insert(omemo_ctx.known_devices, strdup(jid), known_identities); + } + + char *fingerprint = _omemo_fingerprint(identity, FALSE); + log_info("OMEMO: cache identity for %s:%d: %s", jid, device_id, fingerprint); + g_hash_table_insert(known_identities, strdup(fingerprint), GINT_TO_POINTER(device_id)); + + char *formatted_fingerprint = omemo_format_fingerprint(fingerprint); + autocomplete_add(omemo_ctx.fingerprint_ac, formatted_fingerprint); + free(formatted_fingerprint); + free(fingerprint); +} + +static void +_g_hash_table_free(GHashTable *hash_table) +{ + g_hash_table_remove_all(hash_table); + g_hash_table_unref(hash_table); +} + +static void +_generate_pre_keys(int count) +{ + unsigned int start; + gcry_randomize(&start, sizeof(unsigned int), GCRY_VERY_STRONG_RANDOM); + signal_protocol_key_helper_pre_key_list_node *pre_keys_head; + signal_protocol_key_helper_generate_pre_keys(&pre_keys_head, start, count, omemo_ctx.signal); + + signal_protocol_key_helper_pre_key_list_node *p; + for (p = pre_keys_head; p != NULL; p = signal_protocol_key_helper_key_list_next(p)) { + session_pre_key *prekey = signal_protocol_key_helper_key_list_element(p); + signal_protocol_pre_key_store_key(omemo_ctx.store, prekey); + } + signal_protocol_key_helper_key_list_free(pre_keys_head); +} + +static void +_generate_signed_pre_key(void) +{ + session_signed_pre_key *signed_pre_key; + struct timeval tv; + gettimeofday(&tv, NULL); + unsigned long long timestamp = (unsigned long long)(tv.tv_sec) * 1000 + (unsigned long long)(tv.tv_usec) / 1000; + + omemo_ctx.signed_pre_key_id = 1; + signal_protocol_key_helper_generate_signed_pre_key(&signed_pre_key, omemo_ctx.identity_key_pair, omemo_ctx.signed_pre_key_id, timestamp, omemo_ctx.signal); + signal_protocol_signed_pre_key_store_key(omemo_ctx.store, signed_pre_key); + SIGNAL_UNREF(signed_pre_key); +} diff --git a/src/omemo/omemo.h b/src/omemo/omemo.h new file mode 100644 index 00000000..166a5292 --- /dev/null +++ b/src/omemo/omemo.h @@ -0,0 +1,55 @@ +#include <glib.h> + +#include "ui/ui.h" +#include "config/account.h" + +#define OMEMO_ERR_UNSUPPORTED_CRYPTO -10000 +#define OMEMO_ERR_GCRYPT -20000 + +typedef struct omemo_context_t omemo_context; + +typedef struct omemo_key { + unsigned char *data; + size_t length; + gboolean prekey; + uint32_t device_id; + uint32_t id; +} omemo_key_t; + +void omemo_init(void); +void omemo_on_connect(ProfAccount *account); +void omemo_on_disconnect(void); +void omemo_generate_crypto_materials(ProfAccount *account); +void omemo_key_free(omemo_key_t *key); +void omemo_publish_crypto_materials(void); + +uint32_t omemo_device_id(void); +void omemo_identity_key(unsigned char **output, size_t *length); +void omemo_signed_prekey(unsigned char **output, size_t *length); +void omemo_signed_prekey_signature(unsigned char **output, size_t *length); +void omemo_prekeys(GList **prekeys, GList **ids, GList **lengths); +void omemo_set_device_list(const char *const jid, GList * device_list); +GKeyFile *omemo_identity_keyfile(void); +void omemo_identity_keyfile_save(void); +GKeyFile *omemo_trust_keyfile(void); +void omemo_trust_keyfile_save(void); +GKeyFile *omemo_sessions_keyfile(void); +void omemo_sessions_keyfile_save(void); +char *omemo_format_fingerprint(const char *const fingerprint); +char *omemo_own_fingerprint(gboolean formatted); +void omemo_trust(const char *const jid, const char *const fingerprint); +void omemo_untrust(const char *const jid, const char *const fingerprint); +GList *omemo_known_device_identities(const char *const jid); +gboolean omemo_is_trusted_jid(const char *const jid); +gboolean omemo_is_trusted_identity(const char *const jid, const char *const fingerprint); +char *omemo_fingerprint_autocomplete(const char *const search_str, gboolean previous); +void omemo_fingerprint_autocomplete_reset(void); + +void omemo_start_sessions(void); +void omemo_start_session(const char *const barejid); +void omemo_start_muc_sessions(const char *const roomjid); +void omemo_start_device_session(const char *const jid, uint32_t device_id, GList *prekeys, uint32_t signed_prekey_id, const unsigned char *const signed_prekey, size_t signed_prekey_len, const unsigned char *const signature, size_t signature_len, const unsigned char *const identity_key, size_t identity_key_len); + +gboolean omemo_loaded(void); +char * omemo_on_message_send(ProfWin *win, const char *const message, gboolean request_receipt, gboolean muc); +char * omemo_on_message_recv(const char *const from, uint32_t sid, const unsigned char *const iv, size_t iv_len, GList *keys, const unsigned char *const payload, size_t payload_len, gboolean muc); diff --git a/src/omemo/store.c b/src/omemo/store.c new file mode 100644 index 00000000..76b7449c --- /dev/null +++ b/src/omemo/store.c @@ -0,0 +1,382 @@ +#include <glib.h> +#include <signal/signal_protocol.h> + +#include "config.h" +#include "omemo/omemo.h" +#include "omemo/store.h" + +GHashTable * +session_store_new(void) +{ + return g_hash_table_new_full(g_str_hash, g_str_equal, free, NULL); +} + +GHashTable * +pre_key_store_new(void) +{ + return g_hash_table_new_full(g_direct_hash, g_direct_equal, NULL, (GDestroyNotify)signal_buffer_free); +} + +GHashTable * +signed_pre_key_store_new(void) +{ + return g_hash_table_new_full(g_direct_hash, g_direct_equal, NULL, (GDestroyNotify)signal_buffer_free); +} + +void +identity_key_store_new(identity_key_store_t *identity_key_store) +{ + identity_key_store->trusted = g_hash_table_new_full(g_str_hash, g_str_equal, free, (GDestroyNotify)signal_buffer_free); + identity_key_store->private = NULL; + identity_key_store->public = NULL; +} + +#ifdef HAVE_LIBSIGNAL_LT_2_3_2 +int +load_session(signal_buffer **record, const signal_protocol_address *address, + void *user_data) +#else +int +load_session(signal_buffer **record, signal_buffer **user_record, + const signal_protocol_address *address, void *user_data) +#endif +{ + GHashTable *session_store = (GHashTable *)user_data; + GHashTable *device_store = NULL; + + device_store = g_hash_table_lookup(session_store, address->name); + if (!device_store) { + *record = NULL; + return 0; + } + + signal_buffer *original = g_hash_table_lookup(device_store, GINT_TO_POINTER(address->device_id)); + if (!original) { + *record = NULL; + return 0; + } + *record = signal_buffer_copy(original); + return 1; +} + +int +get_sub_device_sessions(signal_int_list **sessions, const char *name, + size_t name_len, void *user_data) +{ + GHashTable *session_store = (GHashTable *)user_data; + GHashTable *device_store = NULL; + GHashTableIter iter; + gpointer key, value; + + device_store = g_hash_table_lookup(session_store, name); + if (!device_store) { + return SG_SUCCESS; + } + + *sessions = signal_int_list_alloc(); + g_hash_table_iter_init(&iter, device_store); + while (g_hash_table_iter_next(&iter, &key, &value)) { + signal_int_list_push_back(*sessions, GPOINTER_TO_INT(key)); + } + + + return SG_SUCCESS; +} + +#ifdef HAVE_LIBSIGNAL_LT_2_3_2 +int +store_session(const signal_protocol_address *address, uint8_t *record, + size_t record_len, void *user_data) +#else +int +store_session(const signal_protocol_address *address, + uint8_t *record, size_t record_len, + uint8_t *user_record, size_t user_record_len, + void *user_data) +#endif +{ + GHashTable *session_store = (GHashTable *)user_data; + GHashTable *device_store = NULL; + + device_store = g_hash_table_lookup(session_store, (void *)address->name); + if (!device_store) { + device_store = g_hash_table_new_full(g_direct_hash, g_direct_equal, NULL, (GDestroyNotify)signal_buffer_free); + g_hash_table_insert(session_store, strdup(address->name), device_store); + } + + signal_buffer *buffer = signal_buffer_create(record, record_len); + g_hash_table_insert(device_store, GINT_TO_POINTER(address->device_id), buffer); + + + char *record_b64 = g_base64_encode(record, record_len); + char *device_id = g_strdup_printf("%d", address->device_id); + g_key_file_set_string(omemo_sessions_keyfile(), address->name, device_id, record_b64); + free(device_id); + g_free(record_b64); + + omemo_sessions_keyfile_save(); + + return SG_SUCCESS; +} + +int +contains_session(const signal_protocol_address *address, void *user_data) +{ + GHashTable *session_store = (GHashTable *)user_data; + GHashTable *device_store = NULL; + + device_store = g_hash_table_lookup(session_store, address->name); + if (!device_store) { + return 0; + } + + if (!g_hash_table_lookup(device_store, GINT_TO_POINTER(address->device_id))) { + return 0; + } + + return 1; +} + +int +delete_session(const signal_protocol_address *address, void *user_data) +{ + GHashTable *session_store = (GHashTable *)user_data; + GHashTable *device_store = NULL; + + device_store = g_hash_table_lookup(session_store, address->name); + if (!device_store) { + return SG_SUCCESS; + } + + return g_hash_table_remove(device_store, GINT_TO_POINTER(address->device_id)); +} + +int +delete_all_sessions(const char *name, size_t name_len, void *user_data) +{ + GHashTable *session_store = (GHashTable *)user_data; + GHashTable *device_store = NULL; + + device_store = g_hash_table_lookup(session_store, name); + if (!device_store) { + return SG_SUCCESS; + } + + guint len = g_hash_table_size(device_store); + g_hash_table_remove_all(device_store); + return len; +} + +int +load_pre_key(signal_buffer **record, uint32_t pre_key_id, void *user_data) +{ + signal_buffer *original; + GHashTable *pre_key_store = (GHashTable *)user_data; + + original = g_hash_table_lookup(pre_key_store, GINT_TO_POINTER(pre_key_id)); + if (original == NULL) { + return SG_ERR_INVALID_KEY_ID; + } + + *record = signal_buffer_copy(original); + return SG_SUCCESS; +} + +int +store_pre_key(uint32_t pre_key_id, uint8_t *record, size_t record_len, + void *user_data) +{ + GHashTable *pre_key_store = (GHashTable *)user_data; + + signal_buffer *buffer = signal_buffer_create(record, record_len); + g_hash_table_insert(pre_key_store, GINT_TO_POINTER(pre_key_id), buffer); + + /* Long term storage */ + char *pre_key_id_str = g_strdup_printf("%d", pre_key_id); + char *record_b64 = g_base64_encode(record, record_len); + g_key_file_set_string(omemo_identity_keyfile(), OMEMO_STORE_GROUP_PREKEYS, pre_key_id_str, record_b64); + g_free(pre_key_id_str); + g_free(record_b64); + + omemo_identity_keyfile_save(); + + return SG_SUCCESS; +} + +int +contains_pre_key(uint32_t pre_key_id, void *user_data) +{ + GHashTable *pre_key_store = (GHashTable *)user_data; + + return g_hash_table_lookup(pre_key_store, GINT_TO_POINTER(pre_key_id)) != NULL; +} + +int +remove_pre_key(uint32_t pre_key_id, void *user_data) +{ + GHashTable *pre_key_store = (GHashTable *)user_data; + + int ret = g_hash_table_remove(pre_key_store, GINT_TO_POINTER(pre_key_id)); + + /* Long term storage */ + char *pre_key_id_str = g_strdup_printf("%d", pre_key_id); + g_key_file_remove_key(omemo_identity_keyfile(), OMEMO_STORE_GROUP_PREKEYS, pre_key_id_str, NULL); + g_free(pre_key_id_str); + + omemo_identity_keyfile_save(); + + if (ret > 0) { + return SG_SUCCESS; + } else { + return SG_ERR_INVALID_KEY_ID; + } +} + +int +load_signed_pre_key(signal_buffer **record, uint32_t signed_pre_key_id, + void *user_data) +{ + signal_buffer *original; + GHashTable *signed_pre_key_store = (GHashTable *)user_data; + + original = g_hash_table_lookup(signed_pre_key_store, GINT_TO_POINTER(signed_pre_key_id)); + if (!original) { + return SG_ERR_INVALID_KEY_ID; + } + + *record = signal_buffer_copy(original); + return SG_SUCCESS; +} + +int +store_signed_pre_key(uint32_t signed_pre_key_id, uint8_t *record, + size_t record_len, void *user_data) +{ + GHashTable *signed_pre_key_store = (GHashTable *)user_data; + + signal_buffer *buffer = signal_buffer_create(record, record_len); + g_hash_table_insert(signed_pre_key_store, GINT_TO_POINTER(signed_pre_key_id), buffer); + + /* Long term storage */ + char *signed_pre_key_id_str = g_strdup_printf("%d", signed_pre_key_id); + char *record_b64 = g_base64_encode(record, record_len); + g_key_file_set_string(omemo_identity_keyfile(), OMEMO_STORE_GROUP_SIGNED_PREKEYS, signed_pre_key_id_str, record_b64); + g_free(signed_pre_key_id_str); + g_free(record_b64); + + omemo_identity_keyfile_save(); + + return SG_SUCCESS; +} + +int +contains_signed_pre_key(uint32_t signed_pre_key_id, void *user_data) +{ + GHashTable *signed_pre_key_store = (GHashTable *)user_data; + + return g_hash_table_lookup(signed_pre_key_store, GINT_TO_POINTER(signed_pre_key_id)) != NULL; +} + +int +remove_signed_pre_key(uint32_t signed_pre_key_id, void *user_data) +{ + GHashTable *signed_pre_key_store = (GHashTable *)user_data; + + int ret = g_hash_table_remove(signed_pre_key_store, GINT_TO_POINTER(signed_pre_key_id)); + + /* Long term storage */ + char *signed_pre_key_id_str = g_strdup_printf("%d", signed_pre_key_id); + g_key_file_remove_key(omemo_identity_keyfile(), OMEMO_STORE_GROUP_PREKEYS, signed_pre_key_id_str, NULL); + g_free(signed_pre_key_id_str); + + omemo_identity_keyfile_save(); + + return ret; +} + +int +get_identity_key_pair(signal_buffer **public_data, signal_buffer **private_data, + void *user_data) +{ + identity_key_store_t *identity_key_store = (identity_key_store_t *)user_data; + + *public_data = signal_buffer_copy(identity_key_store->public); + *private_data = signal_buffer_copy(identity_key_store->private); + + return SG_SUCCESS; +} + +int +get_local_registration_id(void *user_data, uint32_t *registration_id) +{ + identity_key_store_t *identity_key_store = (identity_key_store_t *)user_data; + + *registration_id = identity_key_store->registration_id; + + return SG_SUCCESS; +} + +int +save_identity(const signal_protocol_address *address, uint8_t *key_data, + size_t key_len, void *user_data) +{ + identity_key_store_t *identity_key_store = (identity_key_store_t *)user_data; + + signal_buffer *buffer = signal_buffer_create(key_data, key_len); + + GHashTable *trusted = g_hash_table_lookup(identity_key_store->trusted, strdup(address->name)); + if (!trusted) { + trusted = g_hash_table_new_full(g_direct_hash, g_direct_equal, NULL, (GDestroyNotify)signal_buffer_free); + g_hash_table_insert(identity_key_store->trusted, strdup(address->name), trusted); + } + g_hash_table_insert(trusted, GINT_TO_POINTER(address->device_id), buffer); + + /* Long term storage */ + char *key_b64 = g_base64_encode(key_data, key_len); + char *device_id = g_strdup_printf("%d", address->device_id); + g_key_file_set_string(omemo_trust_keyfile(), address->name, strdup(device_id), key_b64); + g_free(device_id); + g_free(key_b64); + + omemo_trust_keyfile_save(); + + return SG_SUCCESS; +} + +int +is_trusted_identity(const signal_protocol_address *address, uint8_t *key_data, + size_t key_len, void *user_data) +{ + int ret; + identity_key_store_t *identity_key_store = (identity_key_store_t *)user_data; + + GHashTable *trusted = g_hash_table_lookup(identity_key_store->trusted, address->name); + if (!trusted) { + return 0; + } + + signal_buffer *buffer = signal_buffer_create(key_data, key_len); + signal_buffer *original = g_hash_table_lookup(trusted, GINT_TO_POINTER(address->device_id)); + + ret = original != NULL && signal_buffer_compare(buffer, original) == 0; + + signal_buffer_free(buffer); + + return ret; +} + +int +store_sender_key(const signal_protocol_sender_key_name *sender_key_name, + uint8_t *record, size_t record_len, uint8_t *user_record, + size_t user_record_len, void *user_data) +{ + return SG_SUCCESS; +} + +int +load_sender_key(signal_buffer **record, signal_buffer **user_record, + const signal_protocol_sender_key_name *sender_key_name, + void *user_data) +{ + return SG_SUCCESS; +} diff --git a/src/omemo/store.h b/src/omemo/store.h new file mode 100644 index 00000000..d4096c90 --- /dev/null +++ b/src/omemo/store.h @@ -0,0 +1,250 @@ +#include <signal/signal_protocol.h> + +#include "config.h" + +#define OMEMO_STORE_GROUP_IDENTITY "identity" +#define OMEMO_STORE_GROUP_PREKEYS "prekeys" +#define OMEMO_STORE_GROUP_SIGNED_PREKEYS "signed_prekeys" +#define OMEMO_STORE_KEY_DEVICE_ID "device_id" +#define OMEMO_STORE_KEY_REGISTRATION_ID "registration_id" +#define OMEMO_STORE_KEY_IDENTITY_KEY_PUBLIC "identity_key_public" +#define OMEMO_STORE_KEY_IDENTITY_KEY_PRIVATE "identity_key_private" + +typedef struct { + signal_buffer *public; + signal_buffer *private; + uint32_t registration_id; + GHashTable *trusted; +} identity_key_store_t; + +GHashTable * session_store_new(void); +GHashTable * pre_key_store_new(void); +GHashTable * signed_pre_key_store_new(void); +void identity_key_store_new(identity_key_store_t *identity_key_store); + +/** + * Returns a copy of the serialized session record corresponding to the + * provided recipient ID + device ID tuple. + * + * @param record pointer to a freshly allocated buffer containing the + * serialized session record. Unset if no record was found. + * The Signal Protocol library is responsible for freeing this buffer. + * @param address the address of the remote client + * @return 1 if the session was loaded, 0 if the session was not found, negative on failure + */ +#ifdef HAVE_LIBSIGNAL_LT_2_3_2 +int load_session(signal_buffer **record, const signal_protocol_address *address, void *user_data); +#else +int load_session(signal_buffer **record, signal_buffer **user_record, const signal_protocol_address *address, void *user_data); +#endif + +/** + * Returns all known devices with active sessions for a recipient + * + * @param pointer to an array that will be allocated and populated with the result + * @param name the name of the remote client + * @param name_len the length of the name + * @return size of the sessions array, or negative on failure + */ +int get_sub_device_sessions(signal_int_list **sessions, const char *name, size_t name_len, void *user_data); + +/** + * Commit to storage the session record for a given + * recipient ID + device ID tuple. + * + * @param address the address of the remote client + * @param record pointer to a buffer containing the serialized session + * record for the remote client + * @param record_len length of the serialized session record + * @return 0 on success, negative on failure + */ +#ifdef HAVE_LIBSIGNAL_LT_2_3_2 +int store_session(const signal_protocol_address *address, uint8_t *record, size_t record_len, void *user_data); +#else +int store_session(const signal_protocol_address *address, uint8_t *record, size_t record_len, uint8_t *user_record, size_t user_record_len, void *user_data); +#endif + +/** + * Determine whether there is a committed session record for a + * recipient ID + device ID tuple. + * + * @param address the address of the remote client + * @return 1 if a session record exists, 0 otherwise. + */ +int contains_session(const signal_protocol_address *address, void *user_data); + +/** + * Remove a session record for a recipient ID + device ID tuple. + * + * @param address the address of the remote client + * @return 1 if a session was deleted, 0 if a session was not deleted, negative on error + */ +int delete_session(const signal_protocol_address *address, void *user_data); + +/** + * Remove the session records corresponding to all devices of a recipient ID. + * + * @param name the name of the remote client + * @param name_len the length of the name + * @return the number of deleted sessions on success, negative on failure + */ +int delete_all_sessions(const char *name, size_t name_len, void *user_data); + +/** + * Load a local serialized PreKey record. + * + * @param record pointer to a newly allocated buffer containing the record, + * if found. Unset if no record was found. + * The Signal Protocol library is responsible for freeing this buffer. + * @param pre_key_id the ID of the local serialized PreKey record + * @retval SG_SUCCESS if the key was found + * @retval SG_ERR_INVALID_KEY_ID if the key could not be found + */ +int load_pre_key(signal_buffer **record, uint32_t pre_key_id, void *user_data); + +/** + * Store a local serialized PreKey record. + * + * @param pre_key_id the ID of the PreKey record to store. + * @param record pointer to a buffer containing the serialized record + * @param record_len length of the serialized record + * @return 0 on success, negative on failure + */ +int store_pre_key(uint32_t pre_key_id, uint8_t *record, size_t record_len, void *user_data); + +/** + * Determine whether there is a committed PreKey record matching the + * provided ID. + * + * @param pre_key_id A PreKey record ID. + * @return 1 if the store has a record for the PreKey ID, 0 otherwise + */ +int contains_pre_key(uint32_t pre_key_id, void *user_data); + +/** + * Delete a PreKey record from local storage. + * + * @param pre_key_id The ID of the PreKey record to remove. + * @return 0 on success, negative on failure + */ +int remove_pre_key(uint32_t pre_key_id, void *user_data); + +/** + * Load a local serialized signed PreKey record. + * + * @param record pointer to a newly allocated buffer containing the record, + * if found. Unset if no record was found. + * The Signal Protocol library is responsible for freeing this buffer. + * @param signed_pre_key_id the ID of the local signed PreKey record + * @retval SG_SUCCESS if the key was found + * @retval SG_ERR_INVALID_KEY_ID if the key could not be found + */ +int load_signed_pre_key(signal_buffer **record, uint32_t signed_pre_key_id, void *user_data); + +/** + * Store a local serialized signed PreKey record. + * + * @param signed_pre_key_id the ID of the signed PreKey record to store + * @param record pointer to a buffer containing the serialized record + * @param record_len length of the serialized record + * @return 0 on success, negative on failure + */ +int store_signed_pre_key(uint32_t signed_pre_key_id, uint8_t *record, size_t record_len, void *user_data); + +/** + * Determine whether there is a committed signed PreKey record matching + * the provided ID. + * + * @param signed_pre_key_id A signed PreKey record ID. + * @return 1 if the store has a record for the signed PreKey ID, 0 otherwise + */ +int contains_signed_pre_key(uint32_t signed_pre_key_id, void *user_data); + +/** + * Delete a SignedPreKeyRecord from local storage. + * + * @param signed_pre_key_id The ID of the signed PreKey record to remove. + * @return 0 on success, negative on failure + */ +int remove_signed_pre_key(uint32_t signed_pre_key_id, void *user_data); + +/** + * Get the local client's identity key pair. + * + * @param public_data pointer to a newly allocated buffer containing the + * public key, if found. Unset if no record was found. + * The Signal Protocol library is responsible for freeing this buffer. + * @param private_data pointer to a newly allocated buffer containing the + * private key, if found. Unset if no record was found. + * The Signal Protocol library is responsible for freeing this buffer. + * @return 0 on success, negative on failure + */ +int get_identity_key_pair(signal_buffer **public_data, signal_buffer **private_data, void *user_data); + +/** + * Return the local client's registration ID. + * + * Clients should maintain a registration ID, a random number + * between 1 and 16380 that's generated once at install time. + * + * @param registration_id pointer to be set to the local client's + * registration ID, if it was successfully retrieved. + * @return 0 on success, negative on failure + */ +int get_local_registration_id(void *user_data, uint32_t *registration_id); + +/** + * Save a remote client's identity key + * <p> + * Store a remote client's identity key as trusted. + * The value of key_data may be null. In this case remove the key data + * from the identity store, but retain any metadata that may be kept + * alongside it. + * + * @param address the address of the remote client + * @param key_data Pointer to the remote client's identity key, may be null + * @param key_len Length of the remote client's identity key + * @return 0 on success, negative on failure + */ +int save_identity(const signal_protocol_address *address, uint8_t *key_data, size_t key_len, void *user_data); + +/** + * Verify a remote client's identity key. + * + * Determine whether a remote client's identity is trusted. Convention is + * that the TextSecure protocol is 'trust on first use.' This means that + * an identity key is considered 'trusted' if there is no entry for the recipient + * in the local store, or if it matches the saved key for a recipient in the local + * store. Only if it mismatches an entry in the local store is it considered + * 'untrusted.' + * + * @param address the address of the remote client + * @param identityKey The identity key to verify. + * @param key_data Pointer to the identity key to verify + * @param key_len Length of the identity key to verify + * @return 1 if trusted, 0 if untrusted, negative on failure + */ +int is_trusted_identity(const signal_protocol_address *address, uint8_t *key_data, size_t key_len, void *user_data); + +/** + * Store a serialized sender key record for a given + * (groupId + senderId + deviceId) tuple. + * + * @param sender_key_name the (groupId + senderId + deviceId) tuple + * @param record pointer to a buffer containing the serialized record + * @param record_len length of the serialized record + * @return 0 on success, negative on failure + */ +int store_sender_key(const signal_protocol_sender_key_name *sender_key_name, uint8_t *record, size_t record_len, uint8_t *user_record, size_t user_record_len, void *user_data); + +/** + * Returns a copy of the sender key record corresponding to the + * (groupId + senderId + deviceId) tuple. + * + * @param record pointer to a newly allocated buffer containing the record, + * if found. Unset if no record was found. + * The Signal Protocol library is responsible for freeing this buffer. + * @param sender_key_name the (groupId + senderId + deviceId) tuple + * @return 1 if the record was loaded, 0 if the record was not found, negative on failure + */ +int load_sender_key(signal_buffer **record, signal_buffer **user_record, const signal_protocol_sender_key_name *sender_key_name, void *user_data); |