For key transport messages, Monal at least doesn't append the authentication tag to the plaintext key contents as that only makes sense if the key was used to encrypt something. This causes the key length check to fail and show the OMEMO message received but decryption failed. error to the user which is confusing because there is no user-originated message involved. Skip the length check for key transport messages as profanity only uses these to advance the ratchet and makes no use of the decrypted contents. Signed-off-by: Karel Balej <balejk@matfyz.cz>
2164 lines
74 KiB
C
2164 lines
74 KiB
C
/*
|
|
* omemo.c
|
|
* vim: expandtab:ts=4:sts=4:sw=4
|
|
*
|
|
* Copyright (C) 2019 Paul Fariello <paul@fariello.eu>
|
|
* Copyright (C) 2019 - 2026 Michael Vetter <jubalh@iodoru.org>
|
|
*
|
|
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
|
|
*/
|
|
#include "config.h"
|
|
|
|
#include <sys/time.h>
|
|
#include <sys/stat.h>
|
|
|
|
#include <assert.h>
|
|
#include <errno.h>
|
|
#include <glib.h>
|
|
#include <pthread.h>
|
|
|
|
#ifdef HAVE_LIBOMEMO_C
|
|
#include <omemo/key_helper.h>
|
|
#include <omemo/protocol.h>
|
|
#include <omemo/signal_protocol.h>
|
|
#include <omemo/session_builder.h>
|
|
#include <omemo/session_cipher.h>
|
|
#else
|
|
#include <signal/key_helper.h>
|
|
#include <signal/protocol.h>
|
|
#include <signal/signal_protocol.h>
|
|
#include <signal/session_builder.h>
|
|
#include <signal/session_cipher.h>
|
|
#endif
|
|
|
|
#include "config/account.h"
|
|
#include "config/files.h"
|
|
#include "config/preferences.h"
|
|
#include "log.h"
|
|
#include "omemo/crypto.h"
|
|
#include "omemo/omemo.h"
|
|
#include "omemo/store.h"
|
|
#include "ui/ui.h"
|
|
#include "ui/window.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"
|
|
|
|
#define AESGCM_URL_NONCE_LEN (2 * OMEMO_AESGCM_NONCE_LENGTH)
|
|
#define AESGCM_URL_KEY_LEN (2 * OMEMO_AESGCM_KEY_LENGTH)
|
|
|
|
static void _generate_pre_keys(int count);
|
|
static void _generate_signed_pre_key(void);
|
|
static gboolean _load_identity(void);
|
|
static void _load_trust(void);
|
|
static void _load_sessions(void);
|
|
static void _load_known_devices(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 char* _omemo_unformat_fingerprint(const char* const fingerprint_formatted);
|
|
static void _cache_device_identity(const char* const jid, uint32_t device_id, ec_public_key* identity);
|
|
static void _acquire_sender_devices_list(void);
|
|
|
|
typedef gboolean (*OmemoDeviceListHandler)(const char* const jid, GList* device_list);
|
|
|
|
typedef struct omemo_context
|
|
{
|
|
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;
|
|
uint32_t max_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;
|
|
prof_keyfile_t identity;
|
|
prof_keyfile_t trust;
|
|
prof_keyfile_t sessions;
|
|
prof_keyfile_t knowndevices;
|
|
GHashTable* known_devices;
|
|
gboolean loaded;
|
|
gboolean notifying;
|
|
} omemo_context;
|
|
|
|
static omemo_context omemo_ctx;
|
|
|
|
static struct omemo_static_data
|
|
{
|
|
pthread_mutexattr_t attr;
|
|
pthread_mutex_t lock;
|
|
GHashTable* fingerprint_ac;
|
|
} omemo_static_data;
|
|
|
|
static void
|
|
_omemo_close(void)
|
|
{
|
|
if (omemo_static_data.fingerprint_ac) {
|
|
g_hash_table_destroy(omemo_static_data.fingerprint_ac);
|
|
omemo_static_data.fingerprint_ac = NULL;
|
|
}
|
|
pthread_mutex_destroy(&omemo_static_data.lock);
|
|
}
|
|
|
|
void
|
|
omemo_init(void)
|
|
{
|
|
log_info("[OMEMO] initialising");
|
|
|
|
prof_add_shutdown_routine(_omemo_close);
|
|
|
|
gcry_error_t crypt_res = omemo_crypto_init();
|
|
if (crypt_res != 0) {
|
|
cons_show("Error initializing OMEMO crypto: %s", gcry_strerror(crypt_res));
|
|
}
|
|
|
|
pthread_mutexattr_init(&omemo_static_data.attr);
|
|
pthread_mutexattr_settype(&omemo_static_data.attr, PTHREAD_MUTEX_RECURSIVE);
|
|
pthread_mutex_init(&omemo_static_data.lock, &omemo_static_data.attr);
|
|
|
|
omemo_static_data.fingerprint_ac = g_hash_table_new_full(g_str_hash, g_str_equal, free, (GDestroyNotify)autocomplete_free);
|
|
|
|
wins_omemo_trust_changed(NULL);
|
|
}
|
|
|
|
static gboolean
|
|
_omemo_finalize_identity_load(ProfAccount* account)
|
|
{
|
|
auto_gchar gchar* omemo_dir = files_file_in_account_data_path(DIR_OMEMO, account->jid, NULL);
|
|
if (!omemo_dir) {
|
|
log_error("[OMEMO] failed creating directory");
|
|
return FALSE;
|
|
}
|
|
|
|
if (load_custom_keyfile(&omemo_ctx.trust, g_strdup_printf("%s/%s", omemo_dir, "trust.txt"))) {
|
|
_load_trust();
|
|
}
|
|
|
|
if (load_custom_keyfile(&omemo_ctx.sessions, g_strdup_printf("%s/%s", omemo_dir, "sessions.txt"))) {
|
|
_load_sessions();
|
|
}
|
|
|
|
if (load_custom_keyfile(&omemo_ctx.knowndevices, g_strdup_printf("%s/%s", omemo_dir, "known_devices.txt"))) {
|
|
_load_known_devices();
|
|
}
|
|
|
|
omemo_devicelist_subscribe();
|
|
|
|
omemo_ctx.notifying = TRUE;
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
void
|
|
omemo_on_connect(ProfAccount* account)
|
|
{
|
|
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: unable to set crypto provider");
|
|
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 = (void (*)(void*))g_hash_table_destroy,
|
|
.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 = (void (*)(void*))g_hash_table_destroy,
|
|
.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 = (void (*)(void*))g_hash_table_destroy,
|
|
.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 = (void (*)(void*))identity_key_store_destroy,
|
|
.user_data = &omemo_ctx.identity_key_store
|
|
};
|
|
signal_protocol_store_context_set_identity_key_store(omemo_ctx.store, &identity_key_store);
|
|
|
|
omemo_ctx.loaded = FALSE;
|
|
omemo_ctx.notifying = 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_destroy);
|
|
|
|
auto_gchar gchar* omemo_dir = files_file_in_account_data_path(DIR_OMEMO, account->jid, NULL);
|
|
if (!omemo_dir) {
|
|
log_error("[OMEMO] failed creating directory");
|
|
return;
|
|
}
|
|
|
|
if (load_custom_keyfile(&omemo_ctx.identity, g_strdup_printf("%s/%s", omemo_dir, "identity.txt"))) {
|
|
if (!_load_identity())
|
|
return;
|
|
}
|
|
|
|
_omemo_finalize_identity_load(account);
|
|
|
|
wins_omemo_trust_changed(NULL);
|
|
}
|
|
|
|
void
|
|
omemo_on_disconnect(void)
|
|
{
|
|
if (omemo_ctx.loaded) {
|
|
free_keyfile(&omemo_ctx.knowndevices);
|
|
free_keyfile(&omemo_ctx.sessions);
|
|
free_keyfile(&omemo_ctx.trust);
|
|
|
|
ec_public_key* pub = ratchet_identity_key_pair_get_public(omemo_ctx.identity_key_pair);
|
|
ec_private_key* priv = ratchet_identity_key_pair_get_private(omemo_ctx.identity_key_pair);
|
|
ratchet_identity_key_pair_destroy((signal_type_base*)omemo_ctx.identity_key_pair);
|
|
ec_private_key_destroy((signal_type_base*)priv);
|
|
ec_public_key_destroy((signal_type_base*)pub);
|
|
}
|
|
|
|
free_keyfile(&omemo_ctx.identity);
|
|
g_hash_table_destroy(omemo_ctx.known_devices);
|
|
g_hash_table_destroy(omemo_ctx.device_list_handler);
|
|
g_hash_table_destroy(omemo_ctx.device_list);
|
|
signal_protocol_store_context_destroy(omemo_ctx.store);
|
|
signal_context_destroy(omemo_ctx.signal);
|
|
memset(&omemo_ctx, 0, sizeof(omemo_ctx));
|
|
|
|
wins_omemo_trust_changed(NULL);
|
|
}
|
|
|
|
void
|
|
omemo_generate_crypto_materials(ProfAccount* account)
|
|
{
|
|
if (omemo_ctx.loaded) {
|
|
return;
|
|
}
|
|
|
|
log_info("Generate long term OMEMO cryptography materials");
|
|
|
|
/* 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);
|
|
SIGNAL_REF(ratchet_identity_key_pair_get_public(omemo_ctx.identity_key_pair));
|
|
SIGNAL_REF(ratchet_identity_key_pair_get_private(omemo_ctx.identity_key_pair));
|
|
|
|
ec_public_key_serialize(&omemo_ctx.identity_key_store.public, ratchet_identity_key_pair_get_public(omemo_ctx.identity_key_pair));
|
|
auto_gchar gchar* 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);
|
|
|
|
ec_private_key_serialize(&omemo_ctx.identity_key_store.private, ratchet_identity_key_pair_get_private(omemo_ctx.identity_key_pair));
|
|
auto_gchar gchar* 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);
|
|
|
|
/* 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 */
|
|
omemo_ctx.max_pre_key_id = 0;
|
|
_generate_pre_keys(100);
|
|
|
|
/* Signed pre key */
|
|
_generate_signed_pre_key();
|
|
|
|
omemo_identity_keyfile_save();
|
|
|
|
if ((omemo_ctx.loaded = _omemo_finalize_identity_load(account)) == FALSE)
|
|
return;
|
|
|
|
omemo_publish_crypto_materials();
|
|
omemo_start_sessions();
|
|
}
|
|
|
|
void
|
|
omemo_publish_crypto_materials(void)
|
|
{
|
|
if (omemo_ctx.loaded != TRUE) {
|
|
cons_show("OMEMO: cannot publish crypto materials before they are generated");
|
|
log_error("[OMEMO] cannot publish crypto materials before they are generated");
|
|
return;
|
|
}
|
|
|
|
omemo_bundle_publish(TRUE);
|
|
}
|
|
|
|
static void
|
|
_acquire_sender_devices_list(void)
|
|
{
|
|
const char* barejid = connection_get_barejid();
|
|
|
|
g_hash_table_insert(omemo_ctx.device_list_handler, strdup(barejid), _handle_own_device_list);
|
|
omemo_devicelist_request(barejid);
|
|
}
|
|
|
|
void
|
|
omemo_start_sessions(void)
|
|
{
|
|
// before any session may be started, a list on
|
|
// available sender devices must be acquired
|
|
log_debug("[OMEMO] Acquiring sender devices list");
|
|
_acquire_sender_devices_list();
|
|
|
|
GSList* contacts = roster_get_contacts(ROSTER_ORD_NAME);
|
|
if (contacts) {
|
|
GSList* curr;
|
|
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);
|
|
}
|
|
g_slist_free(contacts);
|
|
}
|
|
}
|
|
|
|
void
|
|
omemo_start_session(const char* const barejid)
|
|
{
|
|
if (omemo_loaded()) {
|
|
log_debug("[OMEMO] start session with %s", barejid);
|
|
GList* device_list = g_hash_table_lookup(omemo_ctx.device_list, barejid);
|
|
if (!device_list) {
|
|
log_debug("[OMEMO] missing device list for %s", barejid);
|
|
// Own devices are handled by _handle_own_device_list
|
|
// We won't add _handle_device_list_start_session for ourself
|
|
if (equals_our_barejid(barejid)) {
|
|
g_hash_table_insert(omemo_ctx.device_list_handler, strdup(barejid), _handle_device_list_start_session);
|
|
}
|
|
omemo_devicelist_request(barejid);
|
|
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* members = muc_members(roomjid);
|
|
GList* iter;
|
|
for (iter = members; iter != NULL; iter = iter->next) {
|
|
auto_jid Jid* jid = jid_create(iter->data);
|
|
omemo_start_session(jid->barejid);
|
|
}
|
|
g_list_free(members);
|
|
}
|
|
|
|
gboolean
|
|
omemo_loaded(void)
|
|
{
|
|
return omemo_ctx.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)
|
|
{
|
|
log_debug("[OMEMO] Setting device list for %s", STR_MAYBE_NULL(from));
|
|
auto_jid Jid* jid = jid_create(from ?: connection_get_fulljid());
|
|
|
|
if (omemo_ctx.notifying) {
|
|
GList* old_list = g_hash_table_lookup(omemo_ctx.device_list, jid->barejid);
|
|
GList* new_iter;
|
|
for (new_iter = device_list; new_iter != NULL; new_iter = new_iter->next) {
|
|
uint32_t dev_id = GPOINTER_TO_UINT(new_iter->data);
|
|
gboolean found = FALSE;
|
|
GList* old_iter;
|
|
for (old_iter = old_list; old_iter != NULL; old_iter = old_iter->next) {
|
|
if (GPOINTER_TO_UINT(old_iter->data) == dev_id) {
|
|
found = TRUE;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!found) {
|
|
auto_gchar gchar* dev_id_str = g_strdup_printf("%u", dev_id);
|
|
if (!g_key_file_has_key(omemo_ctx.knowndevices.keyfile, jid->barejid, dev_id_str, NULL)) {
|
|
if (equals_our_barejid(jid->barejid)) {
|
|
if (dev_id != omemo_ctx.device_id) {
|
|
cons_show("New OMEMO device (ID: %u) added to your account.", dev_id);
|
|
}
|
|
} else {
|
|
ProfChatWin* chatwin = wins_get_chat(jid->barejid);
|
|
if (chatwin) {
|
|
win_println((ProfWin*)chatwin, THEME_DEFAULT, "!", "New OMEMO device (ID: %u) found for %s.", dev_id, jid->barejid);
|
|
} else {
|
|
ProfMucWin* mucwin = wins_get_muc(jid->barejid);
|
|
if (mucwin) {
|
|
win_println((ProfWin*)mucwin, THEME_DEFAULT, "!", "New OMEMO device (ID: %u) found for %s.", dev_id, jid->barejid);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
} else {
|
|
log_debug("[OMEMO] No Device List Handler for %s", STR_MAYBE_NULL(from));
|
|
}
|
|
|
|
// OMEMO trustmode ToFu
|
|
auto_gchar gchar* trust_mode = prefs_get_string(PREF_OMEMO_TRUST_MODE);
|
|
if (g_strcmp0(trust_mode, "firstusage") == 0) {
|
|
log_debug("[OMEMO] Checking firstusage state for %s", jid->barejid);
|
|
GHashTable* trusted = g_hash_table_lookup(omemo_ctx.identity_key_store.trusted, jid->barejid);
|
|
if (trusted) {
|
|
if (g_hash_table_size(trusted) > 0) {
|
|
log_debug("[OMEMO] Found trusted device for %s - skip firstusage", jid->barejid);
|
|
return;
|
|
}
|
|
} else {
|
|
if (device_list) {
|
|
cons_show("OMEMO: No trusted devices found for %s", jid->barejid);
|
|
GList* device_id;
|
|
for (device_id = device_list; device_id != NULL; device_id = device_id->next) {
|
|
GHashTable* known_identities = g_hash_table_lookup(omemo_ctx.known_devices, jid->barejid);
|
|
if (known_identities) {
|
|
GList* fp = NULL;
|
|
for (fp = g_hash_table_get_keys(known_identities); fp != NULL; fp = fp->next) {
|
|
if (device_id->data == g_hash_table_lookup(known_identities, fp->data)) {
|
|
char* formatted = omemo_format_fingerprint(fp->data);
|
|
if (formatted) {
|
|
cons_show("OMEMO: Adding firstusage trust for %s device %d - Fingerprint %s", jid->barejid, device_id->data, formatted);
|
|
omemo_trust(jid->barejid, formatted);
|
|
free(formatted);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
wins_omemo_trust_changed(jid->barejid);
|
|
}
|
|
|
|
GKeyFile*
|
|
omemo_identity_keyfile(void)
|
|
{
|
|
return omemo_ctx.identity.keyfile;
|
|
}
|
|
|
|
void
|
|
omemo_identity_keyfile_save(void)
|
|
{
|
|
save_keyfile(&omemo_ctx.identity);
|
|
}
|
|
|
|
GKeyFile*
|
|
omemo_trust_keyfile(void)
|
|
{
|
|
return omemo_ctx.trust.keyfile;
|
|
}
|
|
|
|
void
|
|
omemo_trust_keyfile_save(void)
|
|
{
|
|
save_keyfile(&omemo_ctx.trust);
|
|
}
|
|
|
|
GKeyFile*
|
|
omemo_sessions_keyfile(void)
|
|
{
|
|
return omemo_ctx.sessions.keyfile;
|
|
}
|
|
|
|
static gboolean omemo_sessions_keyfile_save_disable = FALSE;
|
|
|
|
void
|
|
omemo_sessions_keyfile_save(void)
|
|
{
|
|
if (omemo_sessions_keyfile_save_disable)
|
|
return;
|
|
save_keyfile(&omemo_ctx.sessions);
|
|
}
|
|
|
|
void
|
|
omemo_known_devices_keyfile_save(void)
|
|
{
|
|
save_keyfile(&omemo_ctx.knowndevices);
|
|
}
|
|
|
|
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)
|
|
{
|
|
log_debug("[OMEMO] Starting device session for %s with device %d", jid, device_id);
|
|
signal_protocol_address jid_address = {
|
|
.name = jid,
|
|
.name_len = strlen(jid),
|
|
.device_id = device_id,
|
|
};
|
|
|
|
signal_protocol_address* address = NULL;
|
|
session_pre_key_bundle* bundle = NULL;
|
|
ec_public_key* prekey_public = NULL;
|
|
ec_public_key* signed_prekey = NULL;
|
|
session_builder* builder = NULL;
|
|
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(&jid_address, (uint8_t*)identity_key_raw, identity_key_len, &omemo_ctx.identity_key_store);
|
|
log_debug("[OMEMO] Trust %s (%d): %d", jid, device_id, trusted);
|
|
|
|
auto_gchar gchar* trust_mode = prefs_get_string(PREF_OMEMO_TRUST_MODE);
|
|
if ((g_strcmp0(trust_mode, "blind") == 0) && !trusted) {
|
|
auto_char char* fp = _omemo_fingerprint(identity_key, TRUE);
|
|
cons_show("Blind trust for %s device %d (%s)", jid, device_id, fp);
|
|
omemo_trust(jid, fp);
|
|
trusted = TRUE;
|
|
}
|
|
|
|
if (!trusted) {
|
|
log_debug("[OMEMO] We don't trust device %d for %s\n", device_id, jid);
|
|
goto out;
|
|
}
|
|
|
|
if (!contains_session(&jid_address, omemo_ctx.session_store)) {
|
|
log_debug("[OMEMO] There is no Session for %s ( %d) ,? building session.", jid_address.name, jid_address.device_id);
|
|
int res;
|
|
|
|
address = calloc(1, sizeof(signal_protocol_address));
|
|
if (!address) {
|
|
goto out;
|
|
}
|
|
|
|
address->name = strdup(jid);
|
|
if (!address->name) {
|
|
goto out;
|
|
}
|
|
address->name_len = strlen(jid);
|
|
address->device_id = device_id;
|
|
|
|
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;
|
|
size_t prekeys_len = g_list_length(prekeys);
|
|
if (prekeys_len == 0) {
|
|
log_error("[OMEMO] No prekeys found for %s device %d", jid, device_id);
|
|
goto out;
|
|
}
|
|
gcry_randomize(&prekey_index, sizeof(int), GCRY_STRONG_RANDOM);
|
|
prekey_index %= prekeys_len;
|
|
omemo_key_t* prekey = g_list_nth_data(prekeys, prekey_index);
|
|
|
|
curve_decode_point(&prekey_public, prekey->data, prekey->length, omemo_ctx.signal);
|
|
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_debug("[OMEMO] create session with %s device %d", jid, device_id);
|
|
if (omemo_ctx.notifying) {
|
|
ProfChatWin* chatwin = wins_get_chat(jid);
|
|
if (chatwin) {
|
|
win_println((ProfWin*)chatwin, THEME_DEFAULT, "!", "OMEMO session with %s (device %u) ready.", jid, device_id);
|
|
} else {
|
|
ProfMucWin* mucwin = wins_get_muc(jid);
|
|
if (mucwin) {
|
|
win_println((ProfWin*)mucwin, THEME_DEFAULT, "!", "OMEMO session with %s (device %u) ready.", jid, device_id);
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
log_debug("[OMEMO] session with %s device %d exists", jid, device_id);
|
|
}
|
|
|
|
out:
|
|
if (bundle)
|
|
session_pre_key_bundle_destroy((signal_type_base*)bundle);
|
|
if (signed_prekey)
|
|
ec_public_key_destroy((signal_type_base*)signed_prekey);
|
|
if (prekey_public)
|
|
ec_public_key_destroy((signal_type_base*)prekey_public);
|
|
session_builder_free(builder);
|
|
if (address)
|
|
free((void*)address->name);
|
|
free(address);
|
|
SIGNAL_UNREF(identity_key);
|
|
}
|
|
|
|
char*
|
|
omemo_on_message_send(ProfWin* win, const char* const message, gboolean request_receipt, gboolean muc, const char* const replace_id)
|
|
{
|
|
char* id = NULL;
|
|
int res;
|
|
const Jid* jid = connection_get_jid();
|
|
GList* keys = NULL;
|
|
|
|
unsigned char* key = NULL;
|
|
unsigned char* iv = NULL;
|
|
unsigned char* ciphertext = NULL;
|
|
unsigned char* tag = NULL;
|
|
unsigned char* key_tag = NULL;
|
|
size_t ciphertext_len, tag_len;
|
|
|
|
omemo_sessions_keyfile_save_disable = TRUE;
|
|
|
|
ciphertext_len = strlen(message);
|
|
ciphertext = malloc(ciphertext_len);
|
|
if (!ciphertext) {
|
|
goto out;
|
|
}
|
|
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 + AES128_GCM_IV_LENGTH, GCRY_VERY_STRONG_RANDOM);
|
|
iv = key + AES128_GCM_KEY_LENGTH;
|
|
|
|
res = aes128gcm_encrypt(ciphertext, &ciphertext_len, tag, &tag_len, (const unsigned char* const)message, strlen(message), iv, key);
|
|
if (res != 0) {
|
|
log_error("[OMEMO][SEND] 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);
|
|
|
|
// List of barejids of the recipients of this message
|
|
GList* recipients = NULL;
|
|
if (muc) {
|
|
ProfMucWin* mucwin = (ProfMucWin*)win;
|
|
assert(mucwin->memcheck == PROFMUCWIN_MEMCHECK);
|
|
GList* members = muc_members(mucwin->roomjid);
|
|
GList* iter;
|
|
for (iter = members; iter != NULL; iter = iter->next) {
|
|
auto_jid Jid* jidp = jid_create(iter->data);
|
|
recipients = g_list_append(recipients, strdup(jidp->barejid));
|
|
}
|
|
g_list_free(members);
|
|
} else {
|
|
ProfChatWin* chatwin = (ProfChatWin*)win;
|
|
assert(chatwin->memcheck == PROFCHATWIN_MEMCHECK);
|
|
recipients = g_list_append(recipients, strdup(chatwin->barejid));
|
|
}
|
|
|
|
GList* device_ids_iter;
|
|
|
|
omemo_ctx.identity_key_store.recv = FALSE;
|
|
|
|
// Encrypt keys for the recipients
|
|
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][SEND] cannot find device ids for %s", recipients_iter->data);
|
|
|
|
if (win_warn_needed(win, "omemo_no_device", recipients_iter->data)) {
|
|
win_println(win, THEME_ERROR, "!", "Can't find a OMEMO device id for %s.", recipients_iter->data);
|
|
win_warn_sent(win, "omemo_no_device", 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)
|
|
};
|
|
|
|
// Don't encrypt for this device (according to
|
|
// <https://xmpp.org/extensions/xep-0384.html#encrypt>).
|
|
// Yourself as recipients in case of MUC
|
|
if (equals_our_barejid(recipients_iter->data)) {
|
|
if ((uint32_t)GPOINTER_TO_INT(device_ids_iter->data) == omemo_ctx.device_id) {
|
|
log_debug("[OMEMO][SEND] Skipping %d (my device) ", GPOINTER_TO_INT(device_ids_iter->data));
|
|
continue;
|
|
}
|
|
}
|
|
|
|
log_debug("[OMEMO][SEND] recipients with device id %d for %s", GPOINTER_TO_INT(device_ids_iter->data), recipients_iter->data);
|
|
res = session_cipher_create(&cipher, omemo_ctx.store, &address, omemo_ctx.signal);
|
|
if (res != SG_SUCCESS) {
|
|
log_error("[OMEMO][SEND] cannot create cipher for %s device id %d - code: %d", address.name, address.device_id, res);
|
|
continue;
|
|
}
|
|
|
|
res = session_cipher_encrypt(cipher, key_tag, AES128_GCM_KEY_LENGTH + AES128_GCM_TAG_LENGTH, &ciphertext);
|
|
session_cipher_free(cipher);
|
|
if (res != SG_SUCCESS) {
|
|
log_info("[OMEMO][SEND] cannot encrypt key for %s device id %d - code: %d", address.name, address.device_id, res);
|
|
continue;
|
|
}
|
|
signal_buffer* buffer = ciphertext_message_get_serialized(ciphertext);
|
|
omemo_key_t* key = malloc(sizeof(omemo_key_t));
|
|
if (!key) {
|
|
SIGNAL_UNREF(ciphertext);
|
|
continue;
|
|
}
|
|
key->length = signal_buffer_len(buffer);
|
|
key->data = malloc(key->length);
|
|
if (!key->data) {
|
|
free(key);
|
|
SIGNAL_UNREF(ciphertext);
|
|
continue;
|
|
}
|
|
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);
|
|
|
|
// Don't send the message if no key could be encrypted.
|
|
// (Since none of the recipients would be able to read the message.)
|
|
if (keys == NULL) {
|
|
win_println(win, THEME_ERROR, "!", "This message cannot be encrypted for any recipient.");
|
|
|
|
// Check for untrusted fingerprints for each recipient
|
|
GList* recipients = NULL;
|
|
if (muc) {
|
|
ProfMucWin* mucwin = (ProfMucWin*)win;
|
|
GList* members = muc_members(mucwin->roomjid);
|
|
GList* iter;
|
|
for (iter = members; iter != NULL; iter = iter->next) {
|
|
auto_jid Jid* jidp = jid_create(iter->data);
|
|
recipients = g_list_append(recipients, strdup(jidp->barejid));
|
|
}
|
|
g_list_free(members);
|
|
} else {
|
|
ProfChatWin* chatwin = (ProfChatWin*)win;
|
|
recipients = g_list_append(recipients, strdup(chatwin->barejid));
|
|
}
|
|
|
|
GList* rec_iter;
|
|
for (rec_iter = recipients; rec_iter != NULL; rec_iter = rec_iter->next) {
|
|
GList* untrusted = omemo_get_jid_untrusted_fingerprints(rec_iter->data);
|
|
if (untrusted) {
|
|
win_println(win, THEME_ERROR, "!", "Untrusted OMEMO fingerprints for %s:", (char*)rec_iter->data);
|
|
GList* fp_iter;
|
|
for (fp_iter = untrusted; fp_iter != NULL; fp_iter = fp_iter->next) {
|
|
char* formatted = omemo_format_fingerprint(fp_iter->data);
|
|
win_println(win, THEME_ERROR, "!", " - %s", formatted);
|
|
free(formatted);
|
|
}
|
|
win_println(win, THEME_ERROR, "!", "Use \"/omemo trust %s <fingerprint>\" to trust them.", (char*)rec_iter->data);
|
|
g_list_free_full(untrusted, free);
|
|
}
|
|
}
|
|
g_list_free_full(recipients, free);
|
|
|
|
win_println(win, THEME_ERROR, "!", "It could also be that key bundles have not been received. Try \"/omemo end\", then \"/omemo start\".");
|
|
goto out;
|
|
}
|
|
|
|
// Encrypt keys for the sender
|
|
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)
|
|
};
|
|
log_debug("[OMEMO][SEND][Sender] Sending to device %d for %s ", address.device_id, address.name);
|
|
// Don't encrypt for this device (according to
|
|
// <https://xmpp.org/extensions/xep-0384.html#encrypt>).
|
|
if ((uint32_t)address.device_id == omemo_ctx.device_id) {
|
|
continue;
|
|
}
|
|
|
|
res = session_cipher_create(&cipher, omemo_ctx.store, &address, omemo_ctx.signal);
|
|
if (res != 0) {
|
|
log_info("[OMEMO][SEND][Sender] 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_info("[OMEMO][SEND][Sender] 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));
|
|
if (!key) {
|
|
SIGNAL_UNREF(ciphertext);
|
|
continue;
|
|
}
|
|
key->length = signal_buffer_len(buffer);
|
|
key->data = malloc(key->length);
|
|
if (!key->data) {
|
|
free(key);
|
|
SIGNAL_UNREF(ciphertext);
|
|
continue;
|
|
}
|
|
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);
|
|
}
|
|
}
|
|
|
|
// Send the message
|
|
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, replace_id);
|
|
} 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, replace_id);
|
|
}
|
|
|
|
out:
|
|
omemo_sessions_keyfile_save_disable = FALSE;
|
|
omemo_sessions_keyfile_save();
|
|
g_list_free_full(keys, (GDestroyNotify)omemo_key_free);
|
|
free(ciphertext);
|
|
gcry_free(key);
|
|
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, gboolean* trusted, omemo_error_t* error)
|
|
{
|
|
unsigned char* plaintext = NULL;
|
|
auto_jid Jid* sender = NULL;
|
|
auto_jid Jid* from = jid_create(from_jid);
|
|
|
|
*error = OMEMO_ERR_NONE;
|
|
|
|
if (!from) {
|
|
log_error("[OMEMO][RECV] Invalid jid %s", from_jid);
|
|
*error = OMEMO_ERR_INVALID_JID;
|
|
return NULL;
|
|
}
|
|
|
|
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][RECV] received a message with no corresponding key");
|
|
*error = OMEMO_ERR_NO_KEY;
|
|
return NULL;
|
|
}
|
|
|
|
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][RECV] cannot find MUC message sender fulljid");
|
|
*error = OMEMO_ERR_MUC_SENDER_NOT_FOUND;
|
|
return NULL;
|
|
}
|
|
} 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][RECV] cannot create session cipher");
|
|
*error = OMEMO_ERR_NO_SESSION;
|
|
return NULL;
|
|
}
|
|
|
|
if (key->prekey) {
|
|
log_debug("[OMEMO][RECV] decrypting message with prekey");
|
|
pre_key_signal_message* message;
|
|
ec_public_key* their_identity_key;
|
|
signal_buffer* identity_buffer = NULL;
|
|
|
|
omemo_ctx.identity_key_store.recv = TRUE;
|
|
|
|
pre_key_signal_message_deserialize(&message, key->data, key->length, omemo_ctx.signal);
|
|
their_identity_key = pre_key_signal_message_get_identity_key(message);
|
|
|
|
res = session_cipher_decrypt_pre_key_signal_message(cipher, message, NULL, &plaintext_key);
|
|
|
|
omemo_ctx.identity_key_store.recv = FALSE;
|
|
|
|
/* Perform a real check of the identity */
|
|
ec_public_key_serialize(&identity_buffer, their_identity_key);
|
|
*trusted = is_trusted_identity(&address, signal_buffer_data(identity_buffer),
|
|
signal_buffer_len(identity_buffer), &omemo_ctx.identity_key_store);
|
|
signal_buffer_free(identity_buffer);
|
|
|
|
/* Remove used pre_key from store */
|
|
uint32_t pre_key_id = pre_key_signal_message_get_pre_key_id(message);
|
|
signal_protocol_pre_key_remove_key(omemo_ctx.store, pre_key_id);
|
|
|
|
/* If we have few pre_keys left, generate more */
|
|
if (g_hash_table_size(omemo_ctx.pre_key_store) < 10) {
|
|
log_debug("[OMEMO] Few pre keys left, generating more");
|
|
_generate_pre_keys(100);
|
|
}
|
|
|
|
SIGNAL_UNREF(message);
|
|
omemo_bundle_publish(TRUE);
|
|
|
|
if (res == 0) {
|
|
/* Start a new session */
|
|
log_debug("[OMEMO][RECV] Res is 0 => omemo_bundle_request");
|
|
omemo_bundle_request(sender->barejid, sid, omemo_start_device_session_handle_bundle, free, strdup(sender->barejid));
|
|
}
|
|
} else {
|
|
log_debug("[OMEMO][RECV] decrypting message with existing session");
|
|
signal_message* message = NULL;
|
|
|
|
res = signal_message_deserialize(&message, key->data, key->length, omemo_ctx.signal);
|
|
|
|
if (res < 0) {
|
|
log_error("[OMEMO][RECV] cannot deserialize message");
|
|
} else {
|
|
res = session_cipher_decrypt_signal_message(cipher, message, NULL, &plaintext_key);
|
|
*trusted = TRUE;
|
|
SIGNAL_UNREF(message);
|
|
}
|
|
}
|
|
|
|
session_cipher_free(cipher);
|
|
if (res != 0) {
|
|
log_error("[OMEMO][RECV] cannot decrypt message key: %d", res);
|
|
if (!*trusted) {
|
|
*error = OMEMO_ERR_NOT_TRUSTED;
|
|
} else {
|
|
*error = OMEMO_ERR_DECRYPT_FAILED;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
if (payload == NULL) {
|
|
signal_buffer_free(plaintext_key);
|
|
*error = OMEMO_ERR_KEY_TRANSPORT;
|
|
return NULL;
|
|
}
|
|
|
|
if (signal_buffer_len(plaintext_key) != AES128_GCM_KEY_LENGTH + AES128_GCM_TAG_LENGTH) {
|
|
log_error("[OMEMO][RECV] invalid key length");
|
|
signal_buffer_free(plaintext_key);
|
|
*error = OMEMO_ERR_DECRYPT_FAILED;
|
|
return NULL;
|
|
}
|
|
|
|
size_t plaintext_len = payload_len;
|
|
plaintext = malloc(plaintext_len + 1);
|
|
res = aes128gcm_decrypt(plaintext, &plaintext_len, payload, payload_len, iv,
|
|
iv_len, 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][RECV] cannot decrypt message: %s", gcry_strerror(res));
|
|
free(plaintext);
|
|
*error = OMEMO_ERR_DECRYPT_FAILED;
|
|
return NULL;
|
|
}
|
|
|
|
plaintext[plaintext_len] = '\0';
|
|
|
|
return (char*)plaintext;
|
|
}
|
|
|
|
char*
|
|
omemo_format_fingerprint(const char* const fingerprint)
|
|
{
|
|
char* output = malloc(strlen(fingerprint) + strlen(fingerprint) / 8 + 1);
|
|
if (!output)
|
|
return NULL;
|
|
|
|
size_t 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;
|
|
}
|
|
|
|
static char*
|
|
_omemo_unformat_fingerprint(const char* const fingerprint_formatted)
|
|
{
|
|
char* fingerprint = malloc(strlen(fingerprint_formatted) + 1);
|
|
if (!fingerprint)
|
|
return NULL;
|
|
|
|
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';
|
|
|
|
return fingerprint;
|
|
}
|
|
|
|
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_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_UINT(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);
|
|
log_debug("[OMEMO] Device trusted %s (%u): %d", jid, GPOINTER_TO_UINT(device_id), trusted);
|
|
|
|
free(fingerprint_raw);
|
|
signal_buffer_free(buffer);
|
|
|
|
return trusted;
|
|
}
|
|
|
|
gboolean
|
|
omemo_is_jid_trusted(const char* const jid)
|
|
{
|
|
if (!omemo_loaded()) {
|
|
return FALSE;
|
|
}
|
|
|
|
GList* device_list = g_hash_table_lookup(omemo_ctx.device_list, jid);
|
|
if (!device_list) {
|
|
return FALSE;
|
|
}
|
|
|
|
GList* device_id_iter;
|
|
for (device_id_iter = device_list; device_id_iter != NULL; device_id_iter = device_id_iter->next) {
|
|
uint32_t device_id = GPOINTER_TO_UINT(device_id_iter->data);
|
|
if (device_id == omemo_ctx.device_id && equals_our_barejid(jid)) {
|
|
continue;
|
|
}
|
|
|
|
GHashTable* known_identities = g_hash_table_lookup(omemo_ctx.known_devices, jid);
|
|
if (!known_identities) {
|
|
return FALSE;
|
|
}
|
|
|
|
gboolean found = FALSE;
|
|
GList* fp_list = g_hash_table_get_keys(known_identities);
|
|
GList* fp_iter;
|
|
for (fp_iter = fp_list; fp_iter != NULL; fp_iter = fp_iter->next) {
|
|
if (device_id == GPOINTER_TO_UINT(g_hash_table_lookup(known_identities, fp_iter->data))) {
|
|
if (!omemo_is_trusted_identity(jid, fp_iter->data)) {
|
|
g_list_free(fp_list);
|
|
return FALSE;
|
|
}
|
|
found = TRUE;
|
|
break;
|
|
}
|
|
}
|
|
g_list_free(fp_list);
|
|
|
|
if (!found) {
|
|
return FALSE;
|
|
}
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
GList*
|
|
omemo_get_jid_untrusted_fingerprints(const char* const jid)
|
|
{
|
|
if (!omemo_loaded()) {
|
|
return NULL;
|
|
}
|
|
|
|
GList* device_list = g_hash_table_lookup(omemo_ctx.device_list, jid);
|
|
if (!device_list) {
|
|
return NULL;
|
|
}
|
|
|
|
GList* untrusted_fps = NULL;
|
|
GList* device_id_iter;
|
|
for (device_id_iter = device_list; device_id_iter != NULL; device_id_iter = device_id_iter->next) {
|
|
uint32_t device_id = GPOINTER_TO_UINT(device_id_iter->data);
|
|
if (device_id == omemo_ctx.device_id && equals_our_barejid(jid)) {
|
|
continue;
|
|
}
|
|
|
|
GHashTable* known_identities = g_hash_table_lookup(omemo_ctx.known_devices, jid);
|
|
if (!known_identities) {
|
|
continue;
|
|
}
|
|
|
|
GList* fp_list = g_hash_table_get_keys(known_identities);
|
|
GList* fp_iter;
|
|
for (fp_iter = fp_list; fp_iter != NULL; fp_iter = fp_iter->next) {
|
|
if (device_id == GPOINTER_TO_UINT(g_hash_table_lookup(known_identities, fp_iter->data))) {
|
|
if (!omemo_is_trusted_identity(jid, fp_iter->data)) {
|
|
untrusted_fps = g_list_append(untrusted_fps, g_strdup(fp_iter->data));
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
g_list_free(fp_list);
|
|
}
|
|
|
|
return untrusted_fps;
|
|
}
|
|
|
|
gboolean
|
|
omemo_is_device_active(const char* const jid, const char* const fingerprint)
|
|
{
|
|
if (!omemo_loaded()) {
|
|
return FALSE;
|
|
}
|
|
|
|
GList* device_list = g_hash_table_lookup(omemo_ctx.device_list, jid);
|
|
if (!device_list) {
|
|
return FALSE;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
uint32_t dev_id = GPOINTER_TO_UINT(device_id);
|
|
GList* iter;
|
|
for (iter = device_list; iter != NULL; iter = iter->next) {
|
|
if (GPOINTER_TO_UINT(iter->data) == dev_id) {
|
|
return TRUE;
|
|
}
|
|
}
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
static char*
|
|
_omemo_fingerprint(ec_public_key* identity, gboolean formatted)
|
|
{
|
|
size_t 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);
|
|
if (!fingerprint) {
|
|
signal_buffer_free(identity_public_key);
|
|
return NULL;
|
|
}
|
|
|
|
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);
|
|
if (!output) {
|
|
*len = 0;
|
|
return NULL;
|
|
}
|
|
|
|
size_t i;
|
|
size_t 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)
|
|
{
|
|
if (!jid || !fingerprint_formatted) {
|
|
return;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
char* fingerprint = _omemo_unformat_fingerprint(fingerprint_formatted);
|
|
if (!fingerprint) {
|
|
log_warning("[OMEMO] cannot unformat fingerprint");
|
|
cons_show("Cannot unformat fingerprint");
|
|
return;
|
|
}
|
|
|
|
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));
|
|
|
|
wins_omemo_trust_changed(jid);
|
|
}
|
|
|
|
void
|
|
omemo_untrust(const char* const jid, const char* const fingerprint_formatted)
|
|
{
|
|
size_t len;
|
|
unsigned char* identity = _omemo_fingerprint_decode(fingerprint_formatted, &len);
|
|
if (!identity)
|
|
return;
|
|
|
|
GHashTableIter iter;
|
|
gpointer key, value;
|
|
|
|
GHashTable* trusted = g_hash_table_lookup(omemo_ctx.identity_key_store.trusted, jid);
|
|
if (!trusted) {
|
|
free(identity);
|
|
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, identity, len) == 0) {
|
|
g_hash_table_remove(trusted, key);
|
|
}
|
|
}
|
|
free(identity);
|
|
|
|
auto_char char* fingerprint = _omemo_unformat_fingerprint(fingerprint_formatted);
|
|
if (!fingerprint) {
|
|
log_warning("[OMEMO] cannot unformat fingerprint");
|
|
cons_show("Cannot unformat fingerprint");
|
|
return;
|
|
}
|
|
|
|
/* Remove existing session */
|
|
GHashTable* known_identities = g_hash_table_lookup(omemo_ctx.known_devices, jid);
|
|
if (!known_identities) {
|
|
log_error("[OMEMO] cannot find known device while untrusting a fingerprint");
|
|
return;
|
|
}
|
|
|
|
uint32_t device_id = GPOINTER_TO_INT(g_hash_table_lookup(known_identities, fingerprint));
|
|
if (!device_id) {
|
|
log_error("[OMEMO] cannot find device id while untrusting a fingerprint");
|
|
return;
|
|
}
|
|
signal_protocol_address address = {
|
|
.name = jid,
|
|
.name_len = strlen(jid),
|
|
.device_id = device_id
|
|
};
|
|
|
|
delete_session(&address, omemo_ctx.session_store);
|
|
|
|
/* Remove from keyfile */
|
|
auto_gchar gchar* device_id_str = g_strdup_printf("%d", device_id);
|
|
g_key_file_remove_key(omemo_ctx.trust.keyfile, jid, device_id_str, NULL);
|
|
omemo_trust_keyfile_save();
|
|
|
|
wins_omemo_trust_changed(jid);
|
|
}
|
|
|
|
static void
|
|
_lock(void* user_data)
|
|
{
|
|
pthread_mutex_lock(&omemo_static_data.lock);
|
|
}
|
|
|
|
static void
|
|
_unlock(void* user_data)
|
|
{
|
|
pthread_mutex_unlock(&omemo_static_data.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][SIGNAL] %s", message);
|
|
break;
|
|
case SG_LOG_WARNING:
|
|
log_warning("[OMEMO][SIGNAL] %s", message);
|
|
break;
|
|
case SG_LOG_NOTICE:
|
|
case SG_LOG_INFO:
|
|
log_debug("[OMEMO][SIGNAL] %s", message);
|
|
break;
|
|
case SG_LOG_DEBUG:
|
|
log_debug("[OMEMO][SIGNAL] %s", message);
|
|
break;
|
|
}
|
|
}
|
|
|
|
static gboolean
|
|
_handle_own_device_list(const char* const jid, GList* device_list)
|
|
{
|
|
// We didn't find the own device id -> publish
|
|
if (!g_list_find(device_list, GINT_TO_POINTER(omemo_ctx.device_id))) {
|
|
cons_show("Could not find own OMEMO device ID. Going to publish own device ID: %d", GINT_TO_POINTER(omemo_ctx.device_id));
|
|
log_debug("[OMEMO] No device ID for our device. Publishing device list");
|
|
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);
|
|
}
|
|
|
|
log_debug("[OMEMO] Request OMEMO Bundles for our devices");
|
|
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)
|
|
{
|
|
log_debug("[OMEMO] Start session for %s - device_list", jid);
|
|
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, void* context)
|
|
{
|
|
Autocomplete ac = g_hash_table_lookup(omemo_static_data.fingerprint_ac, context);
|
|
if (ac != NULL) {
|
|
return autocomplete_complete(ac, search_str, FALSE, previous);
|
|
} else {
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
void
|
|
omemo_fingerprint_autocomplete_reset(void)
|
|
{
|
|
if (!omemo_static_data.fingerprint_ac)
|
|
return;
|
|
gpointer value;
|
|
GHashTableIter iter;
|
|
g_hash_table_iter_init(&iter, omemo_static_data.fingerprint_ac);
|
|
|
|
while (g_hash_table_iter_next(&iter, NULL, &value)) {
|
|
Autocomplete ac = value;
|
|
autocomplete_reset(ac);
|
|
}
|
|
}
|
|
|
|
gboolean
|
|
omemo_automatic_start(const char* const recipient)
|
|
{
|
|
gboolean result = FALSE;
|
|
ProfAccount* account = accounts_get_account(session_get_account_name());
|
|
prof_omemopolicy_t policy;
|
|
|
|
if (account->omemo_policy) {
|
|
// check default account setting
|
|
if (g_strcmp0(account->omemo_policy, "manual") == 0) {
|
|
policy = PROF_OMEMOPOLICY_MANUAL;
|
|
}
|
|
if (g_strcmp0(account->omemo_policy, "opportunistic") == 0) {
|
|
policy = PROF_OMEMOPOLICY_AUTOMATIC;
|
|
}
|
|
if (g_strcmp0(account->omemo_policy, "always") == 0) {
|
|
policy = PROF_OMEMOPOLICY_ALWAYS;
|
|
}
|
|
} else {
|
|
// check global setting
|
|
auto_gchar gchar* pref_omemo_policy = prefs_get_string(PREF_OMEMO_POLICY);
|
|
|
|
// pref defaults to manual
|
|
policy = PROF_OMEMOPOLICY_AUTOMATIC;
|
|
|
|
if (strcmp(pref_omemo_policy, "manual") == 0) {
|
|
policy = PROF_OMEMOPOLICY_MANUAL;
|
|
} else if (strcmp(pref_omemo_policy, "always") == 0) {
|
|
policy = PROF_OMEMOPOLICY_ALWAYS;
|
|
}
|
|
}
|
|
|
|
switch (policy) {
|
|
case PROF_OMEMOPOLICY_MANUAL:
|
|
result = FALSE;
|
|
break;
|
|
case PROF_OMEMOPOLICY_AUTOMATIC:
|
|
if (g_list_find_custom(account->omemo_enabled, recipient, (GCompareFunc)g_strcmp0)) {
|
|
result = TRUE;
|
|
} else if (g_list_find_custom(account->omemo_disabled, recipient, (GCompareFunc)g_strcmp0)) {
|
|
result = FALSE;
|
|
} else {
|
|
result = FALSE;
|
|
}
|
|
break;
|
|
case PROF_OMEMOPOLICY_ALWAYS:
|
|
if (g_list_find_custom(account->omemo_disabled, recipient, (GCompareFunc)g_strcmp0)) {
|
|
result = FALSE;
|
|
} else {
|
|
result = TRUE;
|
|
}
|
|
break;
|
|
}
|
|
|
|
account_free(account);
|
|
return result;
|
|
}
|
|
|
|
static gboolean
|
|
_load_identity(void)
|
|
{
|
|
auto_gerror GError* error = NULL;
|
|
log_info("[OMEMO] 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, &error);
|
|
if (error != NULL) {
|
|
log_error("[OMEMO] cannot load device id: %s", error->message);
|
|
return FALSE;
|
|
}
|
|
log_debug("[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, &error);
|
|
if (error != NULL) {
|
|
log_error("[OMEMO] cannot load registration id: %s", error->message);
|
|
return FALSE;
|
|
}
|
|
|
|
/* Identity key */
|
|
auto_gchar gchar* identity_key_public_b64 = g_key_file_get_string(omemo_ctx.identity.keyfile, OMEMO_STORE_GROUP_IDENTITY, OMEMO_STORE_KEY_IDENTITY_KEY_PUBLIC, &error);
|
|
if (!identity_key_public_b64) {
|
|
log_error("[OMEMO] cannot load identity public key: %s", PROF_GERROR_MESSAGE(error));
|
|
return FALSE;
|
|
}
|
|
|
|
size_t identity_key_public_len;
|
|
auto_guchar guchar* identity_key_public = g_base64_decode(identity_key_public_b64, &identity_key_public_len);
|
|
omemo_ctx.identity_key_store.public = signal_buffer_create(identity_key_public, identity_key_public_len);
|
|
|
|
auto_gchar gchar* identity_key_private_b64 = g_key_file_get_string(omemo_ctx.identity.keyfile, OMEMO_STORE_GROUP_IDENTITY, OMEMO_STORE_KEY_IDENTITY_KEY_PRIVATE, &error);
|
|
if (!identity_key_private_b64) {
|
|
log_error("[OMEMO] cannot load identity private key: %s", PROF_GERROR_MESSAGE(error));
|
|
return FALSE;
|
|
}
|
|
|
|
size_t identity_key_private_len;
|
|
auto_guchar guchar* identity_key_private = g_base64_decode(identity_key_private_b64, &identity_key_private_len);
|
|
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);
|
|
|
|
char** keys = NULL;
|
|
int i;
|
|
/* Pre keys */
|
|
i = 0;
|
|
omemo_ctx.max_pre_key_id = 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++) {
|
|
uint32_t id = strtoul(keys[i], NULL, 10);
|
|
if (id > omemo_ctx.max_pre_key_id) {
|
|
omemo_ctx.max_pre_key_id = id;
|
|
}
|
|
auto_gchar gchar* pre_key_b64 = g_key_file_get_string(omemo_ctx.identity.keyfile, OMEMO_STORE_GROUP_PREKEYS, keys[i], NULL);
|
|
size_t pre_key_len;
|
|
auto_guchar guchar* pre_key = g_base64_decode(pre_key_b64, &pre_key_len);
|
|
signal_buffer* buffer = signal_buffer_create(pre_key, pre_key_len);
|
|
g_hash_table_insert(omemo_ctx.pre_key_store, GINT_TO_POINTER(id), 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++) {
|
|
auto_gchar gchar* 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;
|
|
auto_guchar guchar* signed_pre_key = g_base64_decode(signed_pre_key_b64, &signed_pre_key_len);
|
|
signal_buffer* buffer = signal_buffer_create(signed_pre_key, signed_pre_key_len);
|
|
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();
|
|
}
|
|
|
|
omemo_ctx.loaded = TRUE;
|
|
|
|
omemo_identity_keyfile_save();
|
|
omemo_start_sessions();
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static void
|
|
_load_trust(void)
|
|
{
|
|
auto_gcharv gchar** groups = g_key_file_get_groups(omemo_ctx.trust.keyfile, NULL);
|
|
|
|
if (!groups) {
|
|
return;
|
|
}
|
|
|
|
for (int 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);
|
|
}
|
|
|
|
auto_gcharv gchar** keys = g_key_file_get_keys(omemo_ctx.trust.keyfile, groups[i], NULL, NULL);
|
|
for (int j = 0; keys[j] != NULL; j++) {
|
|
auto_gchar gchar* key_b64 = g_key_file_get_string(omemo_ctx.trust.keyfile, groups[i], keys[j], NULL);
|
|
size_t key_len;
|
|
auto_guchar guchar* key = g_base64_decode(key_b64, &key_len);
|
|
signal_buffer* buffer = signal_buffer_create(key, key_len);
|
|
uint32_t device_id = strtoul(keys[j], NULL, 10);
|
|
g_hash_table_insert(trusted, GINT_TO_POINTER(device_id), buffer);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void
|
|
_load_sessions(void)
|
|
{
|
|
auto_gcharv gchar** groups = g_key_file_get_groups(omemo_ctx.sessions.keyfile, NULL);
|
|
|
|
if (!groups) {
|
|
return;
|
|
}
|
|
|
|
for (int i = 0; groups[i] != NULL; i++) {
|
|
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);
|
|
}
|
|
|
|
auto_gcharv gchar** keys = g_key_file_get_keys(omemo_ctx.sessions.keyfile, groups[i], NULL, NULL);
|
|
for (int j = 0; keys[j] != NULL; j++) {
|
|
uint32_t id = strtoul(keys[j], NULL, 10);
|
|
auto_gchar gchar* record_b64 = g_key_file_get_string(omemo_ctx.sessions.keyfile, groups[i], keys[j], NULL);
|
|
size_t record_len;
|
|
auto_guchar guchar* record = g_base64_decode(record_b64, &record_len);
|
|
signal_buffer* buffer = signal_buffer_create(record, record_len);
|
|
g_hash_table_insert(device_store, GINT_TO_POINTER(id), buffer);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void
|
|
_load_known_devices(void)
|
|
{
|
|
auto_gcharv gchar** groups = g_key_file_get_groups(omemo_ctx.knowndevices.keyfile, NULL);
|
|
|
|
if (!groups) {
|
|
return;
|
|
}
|
|
|
|
for (int i = 0; groups[i] != NULL; i++) {
|
|
GHashTable* known_identities = NULL;
|
|
|
|
known_identities = g_hash_table_lookup(omemo_ctx.known_devices, groups[i]);
|
|
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(groups[i]), known_identities);
|
|
}
|
|
|
|
auto_gcharv gchar** keys = g_key_file_get_keys(omemo_ctx.knowndevices.keyfile, groups[i], NULL, NULL);
|
|
for (int j = 0; keys[j] != NULL; j++) {
|
|
uint32_t device_id = strtoul(keys[j], NULL, 10);
|
|
auto_gchar gchar* fingerprint = g_key_file_get_string(omemo_ctx.knowndevices.keyfile, groups[i], keys[j], NULL);
|
|
g_hash_table_insert(known_identities, strdup(fingerprint), GINT_TO_POINTER(device_id));
|
|
}
|
|
}
|
|
}
|
|
|
|
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);
|
|
if (!fingerprint) {
|
|
return;
|
|
}
|
|
|
|
if (omemo_ctx.notifying) {
|
|
if (!g_hash_table_contains(known_identities, fingerprint)) {
|
|
char* formatted = omemo_format_fingerprint(fingerprint);
|
|
if (equals_our_barejid(jid)) {
|
|
cons_show("New OMEMO fingerprint for your account: %s (ID: %u).", formatted, device_id);
|
|
} else {
|
|
ProfChatWin* chatwin = wins_get_chat(jid);
|
|
if (chatwin) {
|
|
win_println((ProfWin*)chatwin, THEME_DEFAULT, "!", "New OMEMO fingerprint for %s: %s (ID: %u).", jid, formatted, device_id);
|
|
} else {
|
|
ProfMucWin* mucwin = wins_get_muc(jid);
|
|
if (mucwin) {
|
|
win_println((ProfWin*)mucwin, THEME_DEFAULT, "!", "New OMEMO fingerprint for %s: %s (ID: %u).", jid, formatted, device_id);
|
|
}
|
|
}
|
|
}
|
|
free(formatted);
|
|
}
|
|
}
|
|
|
|
log_debug("[OMEMO] cache identity for %s:%d: %s", jid, device_id, fingerprint);
|
|
g_hash_table_insert(known_identities, strdup(fingerprint), GINT_TO_POINTER(device_id));
|
|
|
|
auto_gchar gchar* device_id_str = g_strdup_printf("%d", device_id);
|
|
g_key_file_set_string(omemo_ctx.knowndevices.keyfile, jid, device_id_str, fingerprint);
|
|
omemo_known_devices_keyfile_save();
|
|
|
|
Autocomplete ac = g_hash_table_lookup(omemo_static_data.fingerprint_ac, jid);
|
|
if (ac == NULL) {
|
|
ac = autocomplete_new();
|
|
g_hash_table_insert(omemo_static_data.fingerprint_ac, strdup(jid), ac);
|
|
}
|
|
|
|
if (!fingerprint) {
|
|
return;
|
|
}
|
|
|
|
char* formatted_fingerprint = omemo_format_fingerprint(fingerprint);
|
|
if (formatted_fingerprint) {
|
|
autocomplete_add(ac, formatted_fingerprint);
|
|
free(formatted_fingerprint);
|
|
}
|
|
free(fingerprint);
|
|
|
|
wins_omemo_trust_changed(jid);
|
|
}
|
|
|
|
static void
|
|
_generate_pre_keys(int count)
|
|
{
|
|
uint32_t start = omemo_ctx.max_pre_key_id + 1;
|
|
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);
|
|
|
|
omemo_ctx.max_pre_key_id += count;
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
void
|
|
omemo_free(void* a)
|
|
{
|
|
gcry_free(a);
|
|
}
|
|
|
|
char*
|
|
omemo_encrypt_file(FILE* in, FILE* out, off_t file_size, int* gcry_res)
|
|
{
|
|
unsigned char* key = gcry_random_bytes_secure(
|
|
OMEMO_AESGCM_KEY_LENGTH,
|
|
GCRY_VERY_STRONG_RANDOM);
|
|
|
|
// Create nonce/IV with random bytes.
|
|
unsigned char nonce[OMEMO_AESGCM_NONCE_LENGTH];
|
|
gcry_create_nonce(nonce, OMEMO_AESGCM_NONCE_LENGTH);
|
|
|
|
char* fragment = aes256gcm_create_secure_fragment(key, nonce);
|
|
*gcry_res = aes256gcm_crypt_file(in, out, file_size, key, nonce, TRUE);
|
|
|
|
if (*gcry_res != GPG_ERR_NO_ERROR) {
|
|
gcry_free(fragment);
|
|
fragment = NULL;
|
|
}
|
|
|
|
gcry_free(key);
|
|
|
|
return fragment;
|
|
}
|
|
|
|
void
|
|
_bytes_from_hex(const char* hex, size_t hex_size,
|
|
unsigned char* bytes, size_t bytes_size)
|
|
{
|
|
const unsigned char ht[] = {
|
|
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, // 01234567
|
|
0x08, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 89:;<=>?
|
|
0x00, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x00 // @ABCDEFG
|
|
};
|
|
const size_t ht_size = sizeof(ht);
|
|
|
|
unsigned char b0;
|
|
unsigned char b1;
|
|
|
|
memset(bytes, 0, bytes_size);
|
|
|
|
for (size_t i = 0; (i < hex_size) && (i / 2 < bytes_size); i += 2) {
|
|
b0 = ((unsigned char)hex[i + 0] & 0x1f) ^ 0x10;
|
|
b1 = ((unsigned char)hex[i + 1] & 0x1f) ^ 0x10;
|
|
|
|
if (b0 <= ht_size && b1 <= ht_size) {
|
|
bytes[i / 2] = (unsigned char)(ht[b0] << 4) | ht[b1];
|
|
}
|
|
}
|
|
}
|
|
|
|
gcry_error_t
|
|
omemo_decrypt_file(FILE* in, FILE* out, off_t file_size, const char* fragment)
|
|
{
|
|
char nonce_hex[AESGCM_URL_NONCE_LEN];
|
|
char key_hex[AESGCM_URL_KEY_LEN];
|
|
|
|
const int nonce_pos = 0;
|
|
const int key_pos = AESGCM_URL_NONCE_LEN;
|
|
|
|
memcpy(nonce_hex, &(fragment[nonce_pos]), AESGCM_URL_NONCE_LEN);
|
|
memcpy(key_hex, &(fragment[key_pos]), AESGCM_URL_KEY_LEN);
|
|
|
|
unsigned char nonce[OMEMO_AESGCM_NONCE_LENGTH];
|
|
unsigned char* key = gcry_malloc_secure(OMEMO_AESGCM_KEY_LENGTH);
|
|
|
|
_bytes_from_hex(nonce_hex, AESGCM_URL_NONCE_LEN,
|
|
nonce, OMEMO_AESGCM_NONCE_LENGTH);
|
|
_bytes_from_hex(key_hex, AESGCM_URL_KEY_LEN,
|
|
key, OMEMO_AESGCM_KEY_LENGTH);
|
|
|
|
gcry_error_t crypt_res;
|
|
crypt_res = aes256gcm_crypt_file(in, out, file_size, key, nonce, FALSE);
|
|
|
|
gcry_free(key);
|
|
|
|
return crypt_res;
|
|
}
|
|
|
|
int
|
|
omemo_parse_aesgcm_url(const char* aesgcm_url,
|
|
char** https_url,
|
|
char** fragment)
|
|
{
|
|
CURLUcode ret;
|
|
CURLU* url = curl_url();
|
|
|
|
// Required to allow for the "aesgcm://" scheme that OMEMO Media Sharing
|
|
// uses.
|
|
unsigned int curl_flags = CURLU_NON_SUPPORT_SCHEME;
|
|
|
|
ret = curl_url_set(url, CURLUPART_URL, aesgcm_url, curl_flags);
|
|
if (ret) {
|
|
goto out;
|
|
}
|
|
|
|
ret = curl_url_get(url, CURLUPART_FRAGMENT, fragment, curl_flags);
|
|
if (ret) {
|
|
goto out;
|
|
}
|
|
|
|
if (strlen(*fragment) != AESGCM_URL_NONCE_LEN + AESGCM_URL_KEY_LEN) {
|
|
ret = 1;
|
|
goto out;
|
|
}
|
|
|
|
// Clear fragment from HTTPS URL as it's not required for download.
|
|
ret = curl_url_set(url, CURLUPART_FRAGMENT, NULL, curl_flags);
|
|
if (ret) {
|
|
goto out;
|
|
}
|
|
|
|
ret = curl_url_set(url, CURLUPART_SCHEME, "https", curl_flags);
|
|
if (ret) {
|
|
goto out;
|
|
}
|
|
|
|
ret = curl_url_get(url, CURLUPART_URL, https_url, curl_flags);
|
|
if (ret) {
|
|
goto out;
|
|
}
|
|
|
|
out:
|
|
curl_url_cleanup(url);
|
|
return ret;
|
|
}
|
|
|
|
/* returns a string in the format `xmpp:<user@server>?omemo-sid-<numerical-sid>=<omemo-fingerprint-hex-string>`
|
|
* used for verification over QR codes
|
|
*/
|
|
char*
|
|
omemo_qrcode_str()
|
|
{
|
|
auto_char char* fingerprint = omemo_own_fingerprint(FALSE);
|
|
uint32_t sid = omemo_device_id();
|
|
|
|
char* qrstr = g_strdup_printf("xmpp:%s?omemo-sid-%d=%s", connection_get_barejid(), sid, fingerprint);
|
|
|
|
return qrstr;
|
|
}
|