about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorMichael Vetter <jubalh@iodoru.org>2019-04-11 10:58:22 +0200
committerGitHub <noreply@github.com>2019-04-11 10:58:22 +0200
commit61df0c8e8513a1aa9912e37019a63778ec3ed06c (patch)
treea52850f2f5fc225759c2287d1672ed22b5ef7f0a
parent6b064cfde4456c25bd9dbcbfe0a79262ebcb3599 (diff)
parentf75e1d7a7b05c68f03b6b13163ac9f2b8824e7df (diff)
downloadprofani-tty-61df0c8e8513a1aa9912e37019a63778ec3ed06c.tar.gz
Merge pull request #1039 from paulfariello/feature/omemo
Add basic OMEMO support.
-rw-r--r--Makefile.am15
-rw-r--r--configure.ac26
-rw-r--r--src/command/cmd_ac.c82
-rw-r--r--src/command/cmd_defs.c51
-rw-r--r--src/command/cmd_funcs.c493
-rw-r--r--src/command/cmd_funcs.h10
-rw-r--r--src/config/files.h1
-rw-r--r--src/config/preferences.c34
-rw-r--r--src/config/preferences.h3
-rw-r--r--src/event/client_events.c157
-rw-r--r--src/event/server_events.c220
-rw-r--r--src/event/server_events.h11
-rw-r--r--src/log.c98
-rw-r--r--src/log.h9
-rw-r--r--src/main.c6
-rw-r--r--src/omemo/crypto.c331
-rw-r--r--src/omemo/crypto.h148
-rw-r--r--src/omemo/omemo.c1410
-rw-r--r--src/omemo/omemo.h55
-rw-r--r--src/omemo/store.c382
-rw-r--r--src/omemo/store.h250
-rw-r--r--src/plugins/api.c2
-rw-r--r--src/profanity.c7
-rw-r--r--src/ui/chatwin.c4
-rw-r--r--src/ui/console.c24
-rw-r--r--src/ui/mucwin.c53
-rw-r--r--src/ui/titlebar.c30
-rw-r--r--src/ui/ui.h7
-rw-r--r--src/ui/win_types.h3
-rw-r--r--src/ui/window.c5
-rw-r--r--src/ui/window_list.c1
-rw-r--r--src/xmpp/connection.c23
-rw-r--r--src/xmpp/connection.h2
-rw-r--r--src/xmpp/iq.c26
-rw-r--r--src/xmpp/iq.h6
-rw-r--r--src/xmpp/message.c259
-rw-r--r--src/xmpp/message.h4
-rw-r--r--src/xmpp/omemo.c448
-rw-r--r--src/xmpp/omemo.h11
-rw-r--r--src/xmpp/roster.c4
-rw-r--r--src/xmpp/session.c21
-rw-r--r--src/xmpp/stanza.c340
-rw-r--r--src/xmpp/stanza.h22
-rw-r--r--src/xmpp/xmpp.h8
-rw-r--r--tests/unittests/log/stub_log.c8
-rw-r--r--tests/unittests/omemo/stub_omemo.c68
-rw-r--r--tests/unittests/ui/stub_ui.c4
-rw-r--r--tests/unittests/xmpp/stub_xmpp.c5
48 files changed, 5084 insertions, 103 deletions
diff --git a/Makefile.am b/Makefile.am
index c1784f32..e161d854 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -69,6 +69,9 @@ unittest_sources = \
 	src/ui/ui.h \
 	src/otr/otr.h \
 	src/pgp/gpg.h \
+	src/omemo/omemo.h \
+	src/omemo/crypto.h \
+	src/omemo/store.h \
 	src/command/cmd_defs.h src/command/cmd_defs.c \
 	src/command/cmd_funcs.h src/command/cmd_funcs.c \
 	src/command/cmd_ac.h src/command/cmd_ac.c \
@@ -168,6 +171,13 @@ otr3_sources = \
 otr4_sources = \
 	src/otr/otrlib.h src/otr/otrlibv4.c src/otr/otr.h src/otr/otr.c
 
+omemo_sources = \
+	src/omemo/omemo.h src/omemo/omemo.c src/omemo/crypto.h src/omemo/crypto.c \
+	src/omemo/store.h src/omemo/store.c src/xmpp/omemo.h src/xmpp/omemo.c
+
+omemo_unittest_sources = \
+	tests/unittests/omemo/stub_omemo.c
+
 if BUILD_PYTHON_API
 core_sources += $(python_sources)
 unittest_sources += $(python_sources)
@@ -204,6 +214,11 @@ core_sources += $(otr4_sources)
 endif
 endif
 
+if BUILD_OMEMO
+core_sources += $(omemo_sources)
+unittest_sources += $(omemo_unittest_sources)
+endif
+
 AM_CFLAGS = @AM_CFLAGS@ -I$(srcdir)/src
 
 bin_PROGRAMS = profanity
diff --git a/configure.ac b/configure.ac
index 0b8e4512..06daa8d9 100644
--- a/configure.ac
+++ b/configure.ac
@@ -60,6 +60,8 @@ AC_ARG_ENABLE([otr],
     [AS_HELP_STRING([--enable-otr], [enable otr encryption])])
 AC_ARG_ENABLE([pgp],
     [AS_HELP_STRING([--enable-pgp], [enable pgp])])
+AC_ARG_ENABLE([omemo],
+    [AS_HELP_STRING([--enable-omemo], [enable OMEMO encryption])])
 AC_ARG_WITH([xscreensaver],
     [AS_HELP_STRING([--with-xscreensaver], [use libXScrnSaver to determine idle time])])
 AC_ARG_WITH([themes],
@@ -264,6 +266,30 @@ if test "x$enable_otr" != xno; then
             [AC_MSG_NOTICE([libotr not found, otr encryption support not enabled])])])
 fi
 
+AM_CONDITIONAL([BUILD_OMEMO], [false])
+if test "x$enable_omemo" != xno; then
+   AM_CONDITIONAL([BUILD_OMEMO], [true])
+    PKG_CHECK_MODULES([libsignal], [libsignal-protocol-c >= 2.3.2],
+        [LIBS="-lsignal-protocol-c $LIBS"],
+        [AC_MSG_NOTICE([libsignal >= 2.3.2 not found, checking for libsignal 2.3.x...])
+         PKG_CHECK_MODULES([libsignal], [libsignal-protocol-c >= 2.3.0],
+            [LIBS="-lsignal-protocol-c $LIBS"
+             AC_DEFINE([HAVE_LIBSIGNAL_LT_2_3_2], [1], [Have libsignal-protocol-c < 2.3.2])],
+            [AM_CONDITIONAL([BUILD_OMEMO], [false])
+             AS_IF([test "x$enable_omemo" = xyes],
+                   [AC_MSG_ERROR([libsignal-protocol-c is required for OMEMO support])],
+                   [AC_MSG_NOTICE([libsignal-protocol-c not found, OMEMO support not enabled])])])])
+
+    AC_CHECK_LIB([gcrypt], [gcry_check_version],
+        [LIBS="-lgcrypt $LIBS"],
+        [AM_CONDITIONAL([BUILD_OMEMO], [false])
+         AS_IF([test "x$enable_omemo" = xyes],
+               [AC_MSG_ERROR([gcrypt is required for OMEMO support])],
+               [AC_MSG_NOTICE([gcrypt not found, OMEMO support not enabled])])])
+
+   AM_COND_IF([BUILD_OMEMO], [AC_DEFINE([HAVE_OMEMO], [1], [Have OMEMO])])
+fi
+
 AS_IF([test "x$with_themes" = xno],
     [THEMES_INSTALL="false"],
     [THEMES_INSTALL="true"])
diff --git a/src/command/cmd_ac.c b/src/command/cmd_ac.c
index 58ad758a..0cc28bb3 100644
--- a/src/command/cmd_ac.c
+++ b/src/command/cmd_ac.c
@@ -57,6 +57,10 @@
 #include "pgp/gpg.h"
 #endif
 
+#ifdef HAVE_OMEMO
+#include "omemo/omemo.h"
+#endif
+
 static char* _sub_autocomplete(ProfWin *window, const char *const input, gboolean previous);
 static char* _notify_autocomplete(ProfWin *window, const char *const input, gboolean previous);
 static char* _theme_autocomplete(ProfWin *window, const char *const input, gboolean previous);
@@ -69,6 +73,7 @@ static char* _group_autocomplete(ProfWin *window, const char *const input, gbool
 static char* _bookmark_autocomplete(ProfWin *window, const char *const input, gboolean previous);
 static char* _otr_autocomplete(ProfWin *window, const char *const input, gboolean previous);
 static char* _pgp_autocomplete(ProfWin *window, const char *const input, gboolean previous);
+static char* _omemo_autocomplete(ProfWin *window, const char *const input, gboolean previous);
 static char* _connect_autocomplete(ProfWin *window, const char *const input, gboolean previous);
 static char* _alias_autocomplete(ProfWin *window, const char *const input, gboolean previous);
 static char* _join_autocomplete(ProfWin *window, const char *const input, gboolean previous);
@@ -157,6 +162,8 @@ static Autocomplete bookmark_property_ac;
 static Autocomplete otr_ac;
 static Autocomplete otr_log_ac;
 static Autocomplete otr_policy_ac;
+static Autocomplete omemo_ac;
+static Autocomplete omemo_log_ac;
 static Autocomplete connect_property_ac;
 static Autocomplete tls_property_ac;
 static Autocomplete alias_ac;
@@ -237,6 +244,7 @@ cmd_ac_init(void)
     autocomplete_add(prefs_ac, "presence");
     autocomplete_add(prefs_ac, "otr");
     autocomplete_add(prefs_ac, "pgp");
+    autocomplete_add(prefs_ac, "omemo");
 
     notify_ac = autocomplete_new();
     autocomplete_add(notify_ac, "chat");
@@ -574,6 +582,21 @@ cmd_ac_init(void)
     autocomplete_add(otr_policy_ac, "opportunistic");
     autocomplete_add(otr_policy_ac, "always");
 
+    omemo_ac = autocomplete_new();
+    autocomplete_add(omemo_ac, "gen");
+    autocomplete_add(omemo_ac, "log");
+    autocomplete_add(omemo_ac, "start");
+    autocomplete_add(omemo_ac, "end");
+    autocomplete_add(omemo_ac, "trust");
+    autocomplete_add(omemo_ac, "untrust");
+    autocomplete_add(omemo_ac, "fingerprint");
+    autocomplete_add(omemo_ac, "clear_device_list");
+
+    omemo_log_ac = autocomplete_new();
+    autocomplete_add(omemo_log_ac, "on");
+    autocomplete_add(omemo_log_ac, "off");
+    autocomplete_add(omemo_log_ac, "redact");
+
     connect_property_ac = autocomplete_new();
     autocomplete_add(connect_property_ac, "server");
     autocomplete_add(connect_property_ac, "port");
@@ -983,6 +1006,9 @@ cmd_ac_reset(ProfWin *window)
 #ifdef HAVE_LIBGPGME
     p_gpg_autocomplete_key_reset();
 #endif
+#ifdef HAVE_OMEMO
+    omemo_fingerprint_autocomplete_reset();
+#endif
     autocomplete_reset(help_ac);
     autocomplete_reset(help_commands_ac);
     autocomplete_reset(notify_ac);
@@ -1052,6 +1078,8 @@ cmd_ac_reset(ProfWin *window)
     autocomplete_reset(otr_ac);
     autocomplete_reset(otr_log_ac);
     autocomplete_reset(otr_policy_ac);
+    autocomplete_reset(omemo_ac);
+    autocomplete_reset(omemo_log_ac);
     autocomplete_reset(connect_property_ac);
     autocomplete_reset(tls_property_ac);
     autocomplete_reset(alias_ac);
@@ -1179,6 +1207,8 @@ cmd_ac_uninit(void)
     autocomplete_free(otr_ac);
     autocomplete_free(otr_log_ac);
     autocomplete_free(otr_policy_ac);
+    autocomplete_free(omemo_ac);
+    autocomplete_free(omemo_log_ac);
     autocomplete_free(connect_property_ac);
     autocomplete_free(tls_property_ac);
     autocomplete_free(alias_ac);
@@ -1438,6 +1468,7 @@ _cmd_ac_complete_params(ProfWin *window, const char *const input, gboolean previ
     g_hash_table_insert(ac_funcs, "/autoconnect",   _autoconnect_autocomplete);
     g_hash_table_insert(ac_funcs, "/otr",           _otr_autocomplete);
     g_hash_table_insert(ac_funcs, "/pgp",           _pgp_autocomplete);
+    g_hash_table_insert(ac_funcs, "/omemo",         _omemo_autocomplete);
     g_hash_table_insert(ac_funcs, "/connect",       _connect_autocomplete);
     g_hash_table_insert(ac_funcs, "/alias",         _alias_autocomplete);
     g_hash_table_insert(ac_funcs, "/join",          _join_autocomplete);
@@ -2118,6 +2149,57 @@ _pgp_autocomplete(ProfWin *window, const char *const input, gboolean previous)
 }
 
 static char*
+_omemo_autocomplete(ProfWin *window, const char *const input, gboolean previous)
+{
+    char *found = NULL;
+
+    jabber_conn_status_t conn_status = connection_get_status();
+
+    if (conn_status == JABBER_CONNECTED) {
+        found = autocomplete_param_with_func(input, "/omemo start", roster_contact_autocomplete, previous);
+        if (found) {
+            return found;
+        }
+    }
+
+    found = autocomplete_param_with_func(input, "/omemo fingerprint", roster_contact_autocomplete, previous);
+    if (found) {
+        return found;
+    }
+
+#ifdef HAVE_OMEMO
+    if (window->type == WIN_CHAT) {
+        found = autocomplete_param_with_func(input, "/omemo trust", omemo_fingerprint_autocomplete, previous);
+        if (found) {
+            return found;
+        }
+    } else {
+        found = autocomplete_param_with_func(input, "/omemo trust", roster_contact_autocomplete, previous);
+        if (found) {
+            return found;
+        }
+
+        found = autocomplete_param_no_with_func(input, "/omemo trust", 4, omemo_fingerprint_autocomplete, previous);
+        if (found) {
+            return found;
+        }
+    }
+#endif
+
+    found = autocomplete_param_with_ac(input, "/omemo log", omemo_log_ac, TRUE, previous);
+    if (found) {
+        return found;
+    }
+
+    found = autocomplete_param_with_ac(input, "/omemo", omemo_ac, TRUE, previous);
+    if (found) {
+        return found;
+    }
+
+    return NULL;
+}
+
+static char*
 _plugins_autocomplete(ProfWin *window, const char *const input, gboolean previous)
 {
     char *result = NULL;
diff --git a/src/command/cmd_defs.c b/src/command/cmd_defs.c
index 4447020b..ee86aaba 100644
--- a/src/command/cmd_defs.c
+++ b/src/command/cmd_defs.c
@@ -2134,7 +2134,7 @@ static struct cmd_t command_defs[] =
         CMD_MAINFUNC(cmd_prefs)
         CMD_NOTAGS
         CMD_SYN(
-            "/prefs [ui|desktop|chat|log|conn|presence|otr|pgp]")
+            "/prefs [ui|desktop|chat|log|conn|presence|otr|pgp|omemo]")
         CMD_DESC(
             "Show preferences for different areas of functionality. "
             "Passing no arguments shows all preferences.")
@@ -2146,7 +2146,8 @@ static struct cmd_t command_defs[] =
             { "conn",     "Connection handling preferences." },
             { "presence", "Chat presence preferences." },
             { "otr",      "Off The Record preferences." },
-            { "pgp",      "OpenPGP preferences." })
+            { "pgp",      "OpenPGP preferences." },
+            { "omemo",    "OMEMO preferences." })
         CMD_NOEXAMPLES
     },
 
@@ -2328,7 +2329,51 @@ static struct cmd_t command_defs[] =
         CMD_EXAMPLES(
             "/cmd list",
             "/cmd exec ping")
-    }
+    },
+
+    { "/omemo",
+        parse_args, 1, 3, NULL,
+        CMD_SUBFUNCS(
+            { "gen", cmd_omemo_gen },
+            { "log", cmd_omemo_log },
+            { "start", cmd_omemo_start },
+            { "end", cmd_omemo_end },
+            { "trust", cmd_omemo_trust },
+            { "untrust", cmd_omemo_untrust },
+            { "fingerprint", cmd_omemo_fingerprint },
+            { "char", cmd_omemo_char },
+            { "clear_device_list", cmd_omemo_clear_device_list })
+        CMD_NOMAINFUNC
+        CMD_TAGS(
+            CMD_TAG_CHAT,
+            CMD_TAG_UI)
+        CMD_SYN(
+            "/omemo gen",
+            "/omemo log on|off|redact",
+            "/omemo start [<contact>]",
+            "/omemo trust [<contact>] <fingerprint>",
+            "/omemo end",
+            "/omemo fingerprint [<contact>]",
+            "/omemo char <char>",
+            "/omemo clear_device_list")
+        CMD_DESC(
+            "OMEMO commands to manage keys, and perform encryption during chat sessions.")
+        CMD_ARGS(
+            { "gen",                     "Generate OMEMO crytographic materials for current account." },
+            { "start [<contact>]",       "Start an OMEMO session with contact, or current recipient if omitted." },
+            { "end",                     "End the current OMEMO session." },
+            { "log on|off",              "Enable or disable plaintext logging of OMEMO encrypted messages." },
+            { "log redact",              "Log OMEMO encrypted messages, but replace the contents with [redacted]. This is the default." },
+            { "fingerprint [<contact>]", "Show contact fingerprints, or current recipient if omitted." },
+            { "char <char>",             "Set the character to be displayed next to OMEMO encrypted messages." },
+            { "clear_device_list",       "Clear your own device list on server side. Each client will reannounce itself when connected back."})
+        CMD_EXAMPLES(
+            "/omemo gen",
+            "/omemo start buddy@buddychat.org",
+            "/omemo trust c4f9c875-144d7a3b-0c4a05b6-ca3be51a-a037f329-0bd3ae62-07f99719-55559d2a",
+            "/omemo untrust buddy@buddychat.org c4f9c875-144d7a3b-0c4a05b6-ca3be51a-a037f329-0bd3ae62-07f99719-55559d2a",
+            "/omemo char *")
+    },
 };
 
 static GHashTable *search_index;
diff --git a/src/command/cmd_funcs.c b/src/command/cmd_funcs.c
index b2f0ee7f..fe289f0b 100644
--- a/src/command/cmd_funcs.c
+++ b/src/command/cmd_funcs.c
@@ -85,6 +85,11 @@
 #include "pgp/gpg.h"
 #endif
 
+#ifdef HAVE_OMEMO
+#include "omemo/omemo.h"
+#include "xmpp/omemo.h"
+#endif
+
 #ifdef HAVE_GTK
 #include "ui/tray.h"
 #endif
@@ -1674,6 +1679,10 @@ cmd_prefs(ProfWin *window, const char *const command, gchar **args)
         cons_show("");
         cons_show_pgp_prefs();
         cons_show("");
+    } else if (strcmp(args[0], "omemo") == 0) {
+        cons_show("");
+        cons_show_omemo_prefs();
+        cons_show("");
     } else {
         cons_bad_cmd_usage(command);
     }
@@ -2141,17 +2150,67 @@ cmd_msg(ProfWin *window, const char *const command, gchar **args)
         }
         ui_focus_win((ProfWin*)chatwin);
 
+#ifdef HAVE_OMEMO
+#ifndef HAVE_LIBOTR
+        if (omemo_is_trusted_jid(barejid)) {
+            omemo_start_session(barejid);
+            chatwin->is_omemo = TRUE;
+        }
+
+        if (msg) {
+            cl_ev_send_msg(chatwin, msg, NULL);
+        }
+
+        return TRUE;
+#endif
+#endif
+
+#ifdef HAVE_OMEMO
+#ifdef HAVE_LIBOTR
+        if (omemo_is_trusted_jid(barejid) && otr_is_secure(barejid)) {
+            win_println(window, THEME_DEFAULT, '!', "Chat could be either OMEMO or OTR encrypted. Use '/omemo start %s' or '/otr start %s' to start a session.", usr, usr);
+            return TRUE;
+        } else if (omemo_is_trusted_jid(barejid)) {
+            omemo_start_session(barejid);
+            chatwin->is_omemo = TRUE;
+        }
+
         if (msg) {
             cl_ev_send_msg(chatwin, msg, NULL);
         } else {
+            if (otr_is_secure(barejid)) {
+                chatwin_otr_secured(chatwin, otr_is_trusted(barejid));
+            }
+        }
+
+        return TRUE;
+#endif
+#endif
+
+#ifndef HAVE_OMEMO
 #ifdef HAVE_LIBOTR
+        if (msg) {
+            cl_ev_send_msg(chatwin, msg, NULL);
+        } else {
             if (otr_is_secure(barejid)) {
                 chatwin_otr_secured(chatwin, otr_is_trusted(barejid));
             }
+        }
+
+        return TRUE;
 #endif
+#endif
+
+#ifndef HAVE_OMEMO
+#ifndef HAVE_LIBOTR
+        if (msg) {
+            cl_ev_send_msg(chatwin, msg, NULL);
         }
 
         return TRUE;
+#endif
+#endif
+
     }
 }
 
@@ -7304,6 +7363,11 @@ cmd_otr_start(ProfWin *window, const char *const command, gchar **args)
             return TRUE;
         }
 
+        if (chatwin->is_omemo) {
+            win_println(window, THEME_DEFAULT, '!', "You must disable OMEMO before starting an OTR session.");
+            return TRUE;
+        }
+
         if (chatwin->is_otr) {
             win_println(window, THEME_DEFAULT, '!', "You are already in an OTR session.");
             return TRUE;
@@ -7872,3 +7936,432 @@ _cmd_set_boolean_preference(gchar *arg, const char *const command,
     g_string_free(enabled, TRUE);
     g_string_free(disabled, TRUE);
 }
+
+gboolean
+cmd_omemo_gen(ProfWin *window, const char *const command, gchar **args)
+{
+#ifdef HAVE_OMEMO
+    if (connection_get_status() != JABBER_CONNECTED) {
+        cons_show("You must be connected with an account to initialize OMEMO.");
+        return TRUE;
+    }
+
+    if (omemo_loaded()) {
+        cons_show("OMEMO crytographic materials have already been generated.");
+        return TRUE;
+    }
+
+    ProfAccount *account = accounts_get_account(session_get_account_name());
+    omemo_generate_crypto_materials(account);
+    cons_show("OMEMO crytographic materials generated.");
+    return TRUE;
+#else
+    cons_show("This version of Profanity has not been built with OMEMO support enabled");
+    return TRUE;
+#endif
+}
+
+gboolean
+cmd_omemo_start(ProfWin *window, const char *const command, gchar **args)
+{
+#ifdef HAVE_OMEMO
+    if (connection_get_status() != JABBER_CONNECTED) {
+        cons_show("You must be connected with an account to load OMEMO information.");
+        return TRUE;
+    }
+
+    if (!omemo_loaded()) {
+        win_println(window, THEME_DEFAULT, '!', "You have not generated or loaded a cryptographic materials, use '/omemo gen'");
+        return TRUE;
+    }
+
+    // recipient supplied
+    if (args[1]) {
+        char *contact = args[1];
+        char *barejid = roster_barejid_from_name(contact);
+        if (barejid == NULL) {
+            barejid = contact;
+        }
+
+        ProfChatWin *chatwin = wins_get_chat(barejid);
+        if (!chatwin) {
+            chatwin = chatwin_new(barejid);
+        }
+        ui_focus_win((ProfWin*)chatwin);
+
+        if (chatwin->pgp_send) {
+            win_println(window, THEME_DEFAULT, '!', "You must disable PGP encryption before starting an OMEMO session.");
+            return TRUE;
+        }
+
+        if (chatwin->is_otr) {
+            win_println(window, THEME_DEFAULT, '!', "You must disable OTR encryption before starting an OMEMO session.");
+            return TRUE;
+        }
+
+        if (chatwin->is_omemo) {
+            win_println(window, THEME_DEFAULT, '!', "You are already in an OMEMO session.");
+            return TRUE;
+        }
+
+        omemo_start_session(barejid);
+        chatwin->is_omemo = TRUE;
+    } else {
+        if (window->type == WIN_CHAT) {
+            ProfChatWin *chatwin = (ProfChatWin*)window;
+            assert(chatwin->memcheck == PROFCHATWIN_MEMCHECK);
+            if (chatwin->pgp_send) {
+                win_println(window, THEME_DEFAULT, '!', "You must disable PGP encryption before starting an OMEMO session.");
+                return TRUE;
+            }
+
+            if (chatwin->is_otr) {
+                win_println(window, THEME_DEFAULT, '!', "You must disable OTR encryption before starting an OMEMO session.");
+                return TRUE;
+            }
+
+            if (chatwin->is_omemo) {
+                win_println(window, THEME_DEFAULT, '!', "You are already in an OMEMO session.");
+                return TRUE;
+            }
+
+            omemo_start_session(chatwin->barejid);
+            chatwin->is_omemo = TRUE;
+        } else if (window->type == WIN_MUC) {
+            ProfMucWin *mucwin = (ProfMucWin*)window;
+            assert(mucwin->memcheck == PROFMUCWIN_MEMCHECK);
+
+            /* TODO: Check room is configured correctly, no anonymous and access to
+             * full jid */
+            omemo_start_muc_sessions(mucwin->roomjid);
+
+            mucwin->is_omemo = TRUE;
+        } else {
+            win_println(window, THEME_DEFAULT, '-', "You must be in a regular chat window to start an OMEMO session.");
+            return TRUE;
+        }
+
+    }
+
+    return TRUE;
+#else
+    cons_show("This version of Profanity has not been built with OMEMO support enabled");
+    return TRUE;
+#endif
+}
+
+gboolean
+cmd_omemo_char(ProfWin *window, const char *const command, gchar **args)
+{
+#ifdef HAVE_OMEMO
+    if (args[1] == NULL) {
+        cons_bad_cmd_usage(command);
+    } else if (strlen(args[1]) != 1) {
+        cons_bad_cmd_usage(command);
+    } else {
+        prefs_set_omemo_char(args[1][0]);
+        cons_show("OMEMO char set to %c.", args[1][0]);
+    }
+    return TRUE;
+#else
+    cons_show("This version of Profanity has not been built with OMEMO support enabled");
+    return TRUE;
+#endif
+}
+
+gboolean
+cmd_omemo_log(ProfWin *window, const char *const command, gchar **args)
+{
+#ifdef HAVE_OMEMO
+    char *choice = args[1];
+    if (g_strcmp0(choice, "on") == 0) {
+        prefs_set_string(PREF_OMEMO_LOG, "on");
+        cons_show("OMEMO messages will be logged as plaintext.");
+        if (!prefs_get_boolean(PREF_CHLOG)) {
+            cons_show("Chat logging is currently disabled, use '/chlog on' to enable.");
+        }
+    } else if (g_strcmp0(choice, "off") == 0) {
+        prefs_set_string(PREF_OMEMO_LOG, "off");
+        cons_show("OMEMO message logging disabled.");
+    } else if (g_strcmp0(choice, "redact") == 0) {
+        prefs_set_string(PREF_OMEMO_LOG, "redact");
+        cons_show("OMEMO messages will be logged as '[redacted]'.");
+        if (!prefs_get_boolean(PREF_CHLOG)) {
+            cons_show("Chat logging is currently disabled, use '/chlog on' to enable.");
+        }
+    } else {
+        cons_bad_cmd_usage(command);
+    }
+    return TRUE;
+#else
+    cons_show("This version of Profanity has not been built with OMEMO support enabled");
+    return TRUE;
+#endif
+}
+
+gboolean
+cmd_omemo_end(ProfWin *window, const char *const command, gchar **args)
+{
+#ifdef HAVE_OMEMO
+    if (connection_get_status() != JABBER_CONNECTED) {
+        cons_show("You must be connected with an account to load OMEMO information.");
+        return TRUE;
+    }
+
+    if (window->type == WIN_CHAT) {
+        ProfChatWin *chatwin = (ProfChatWin*)window;
+        assert(chatwin->memcheck == PROFCHATWIN_MEMCHECK);
+
+        if (!chatwin->is_omemo) {
+            win_println(window, THEME_DEFAULT, '!', "You are not currently in an OMEMO session.");
+            return TRUE;
+        }
+
+        chatwin->is_omemo = FALSE;
+    } else if (window->type == WIN_MUC) {
+        ProfMucWin *mucwin = (ProfMucWin*)window;
+        assert(mucwin->memcheck == PROFMUCWIN_MEMCHECK);
+
+        if (!mucwin->is_omemo) {
+            win_println(window, THEME_DEFAULT, '!', "You are not currently in an OMEMO session.");
+            return TRUE;
+        }
+
+        mucwin->is_omemo = FALSE;
+    } else {
+        win_println(window, THEME_DEFAULT, '-', "You must be in a regular chat window to start an OMEMO session.");
+        return TRUE;
+    }
+
+    return TRUE;
+#else
+    cons_show("This version of Profanity has not been built with OMEMO support enabled");
+    return TRUE;
+#endif
+}
+
+gboolean
+cmd_omemo_fingerprint(ProfWin *window, const char *const command, gchar **args)
+{
+#ifdef HAVE_OMEMO
+    if (connection_get_status() != JABBER_CONNECTED) {
+        cons_show("You must be connected with an account to load OMEMO information.");
+        return TRUE;
+    }
+
+    if (!omemo_loaded()) {
+        win_println(window, THEME_DEFAULT, '!', "You have not generated or loaded a cryptographic materials, use '/omemo gen'");
+        return TRUE;
+    }
+
+    Jid *jid;
+    if (!args[1]) {
+        if (window->type == WIN_CONSOLE) {
+            char *fingerprint = omemo_own_fingerprint(TRUE);
+            cons_show("Your OMEMO fingerprint: %s", fingerprint);
+            free(fingerprint);
+            jid = jid_create(connection_get_fulljid());
+        } else if (window->type == WIN_CHAT) {
+            ProfChatWin *chatwin = (ProfChatWin*)window;
+            jid = jid_create(chatwin->barejid);
+        } else {
+            win_println(window, THEME_DEFAULT, '-', "You must be in a regular chat window to print fingerprint without providing the contact.");
+            return TRUE;
+        }
+    } else {
+        char *barejid = roster_barejid_from_name(args[1]);
+        if (barejid) {
+            jid = jid_create(barejid);
+        } else {
+            jid = jid_create(args[1]);
+            if (!jid) {
+                cons_show("%s is not a valid jid", args[1]);
+                return TRUE;
+            }
+        }
+    }
+
+    GList *fingerprints = omemo_known_device_identities(jid->barejid);
+    GList *fingerprint;
+
+    if (!fingerprints) {
+        win_println(window, THEME_DEFAULT, '-', "There is no known fingerprints for %s", jid->barejid);
+        return TRUE;
+    }
+
+    for (fingerprint = fingerprints; fingerprint != NULL; fingerprint = fingerprint->next) {
+        char *formatted_fingerprint = omemo_format_fingerprint(fingerprint->data);
+        gboolean trusted = omemo_is_trusted_identity(jid->barejid, fingerprint->data);
+
+        win_println(window, THEME_DEFAULT, '-', "%s's OMEMO fingerprint: %s%s", jid->barejid, formatted_fingerprint, trusted ? " (trusted)" : "");
+
+        free(formatted_fingerprint);
+    }
+
+    g_list_free(fingerprints);
+
+    win_println(window, THEME_DEFAULT, '-', "You can trust it with '/omemo trust <fingerprint>'");
+    win_println(window, THEME_DEFAULT, '-', "You can untrust it with '/omemo untrust <fingerprint>'");
+
+    return TRUE;
+#else
+    cons_show("This version of Profanity has not been built with OMEMO support enabled");
+    return TRUE;
+#endif
+}
+
+gboolean
+cmd_omemo_trust(ProfWin *window, const char *const command, gchar **args)
+{
+#ifdef HAVE_OMEMO
+    if (connection_get_status() != JABBER_CONNECTED) {
+        cons_show("You must be connected with an account to load OMEMO information.");
+        return TRUE;
+    }
+
+    if (!args[1]) {
+        cons_bad_cmd_usage(command);
+        return TRUE;
+    }
+
+    if (!omemo_loaded()) {
+        win_println(window, THEME_DEFAULT, '!', "You have not generated or loaded a cryptographic materials, use '/omemo gen'");
+        return TRUE;
+    }
+
+    char *fingerprint;
+    char *barejid;
+
+    /* Contact not provided */
+    if (!args[2]) {
+        fingerprint = args[1];
+
+        if (window->type != WIN_CHAT) {
+            win_println(window, THEME_DEFAULT, '-', "You must be in a regular chat window to trust a device without providing the contact.");
+            return TRUE;
+        }
+
+        ProfChatWin *chatwin = (ProfChatWin*)window;
+        assert(chatwin->memcheck == PROFCHATWIN_MEMCHECK);
+        barejid = chatwin->barejid;
+    } else {
+        fingerprint = args[2];
+        char *contact = args[1];
+        barejid = roster_barejid_from_name(contact);
+        if (barejid == NULL) {
+            barejid = contact;
+        }
+    }
+
+    omemo_trust(barejid, fingerprint);
+
+    char *unformatted_fingerprint = malloc(strlen(fingerprint));
+    int i;
+    int j;
+    for (i = 0, j = 0; fingerprint[i] != '\0'; i++) {
+        if (!g_ascii_isxdigit(fingerprint[i])) {
+            continue;
+        }
+        unformatted_fingerprint[j++] = fingerprint[i];
+    }
+
+    unformatted_fingerprint[j] = '\0';
+    gboolean trusted = omemo_is_trusted_identity(barejid, unformatted_fingerprint);
+
+    win_println(window, THEME_DEFAULT, '-', "%s's OMEMO fingerprint: %s%s", barejid, fingerprint, trusted ? " (trusted)" : "");
+
+    free(unformatted_fingerprint);
+
+    return TRUE;
+#else
+    cons_show("This version of Profanity has not been built with OMEMO support enabled");
+    return TRUE;
+#endif
+}
+
+gboolean
+cmd_omemo_untrust(ProfWin *window, const char *const command, gchar **args)
+{
+#ifdef HAVE_OMEMO
+    if (connection_get_status() != JABBER_CONNECTED) {
+        cons_show("You must be connected with an account to load OMEMO information.");
+        return TRUE;
+    }
+
+    if (!args[1]) {
+        cons_bad_cmd_usage(command);
+        return TRUE;
+    }
+
+    if (!omemo_loaded()) {
+        win_println(window, THEME_DEFAULT, '!', "You have not generated or loaded a cryptographic materials, use '/omemo gen'");
+        return TRUE;
+    }
+
+    char *fingerprint;
+    char *barejid;
+
+    /* Contact not provided */
+    if (!args[2]) {
+        fingerprint = args[1];
+
+        if (window->type != WIN_CHAT) {
+            win_println(window, THEME_DEFAULT, '-', "You must be in a regular chat window to trust a device without providing the contact.");
+            return TRUE;
+        }
+
+        ProfChatWin *chatwin = (ProfChatWin*)window;
+        assert(chatwin->memcheck == PROFCHATWIN_MEMCHECK);
+        barejid = chatwin->barejid;
+    } else {
+        fingerprint = args[2];
+        char *contact = args[1];
+        barejid = roster_barejid_from_name(contact);
+        if (barejid == NULL) {
+            barejid = contact;
+        }
+    }
+
+    omemo_untrust(barejid, fingerprint);
+
+    char *unformatted_fingerprint = malloc(strlen(fingerprint));
+    int i;
+    int j;
+    for (i = 0, j = 0; fingerprint[i] != '\0'; i++) {
+        if (!g_ascii_isxdigit(fingerprint[i])) {
+            continue;
+        }
+        unformatted_fingerprint[j++] = fingerprint[i];
+    }
+
+    unformatted_fingerprint[j] = '\0';
+    gboolean trusted = omemo_is_trusted_identity(barejid, unformatted_fingerprint);
+
+    win_println(window, THEME_DEFAULT, '-', "%s's OMEMO fingerprint: %s%s", barejid, fingerprint, trusted ? " (trusted)" : "");
+
+    free(unformatted_fingerprint);
+
+    return TRUE;
+#else
+    cons_show("This version of Profanity has not been built with OMEMO support enabled");
+    return TRUE;
+#endif
+}
+
+gboolean
+cmd_omemo_clear_device_list(ProfWin *window, const char *const command, gchar **args)
+{
+#ifdef HAVE_OMEMO
+    if (connection_get_status() != JABBER_CONNECTED) {
+        cons_show("You must be connected with an account to initialize OMEMO.");
+        return TRUE;
+    }
+
+    omemo_devicelist_publish(NULL);
+    cons_show("Cleared OMEMO device list");
+    return TRUE;
+#else
+    cons_show("This version of Profanity has not been built with OMEMO support enabled");
+    return TRUE;
+#endif
+}
diff --git a/src/command/cmd_funcs.h b/src/command/cmd_funcs.h
index 89166ba1..249b50fe 100644
--- a/src/command/cmd_funcs.h
+++ b/src/command/cmd_funcs.h
@@ -214,4 +214,14 @@ gboolean cmd_wins_swap(ProfWin *window, const char *const command, gchar **args)
 
 gboolean cmd_form_field(ProfWin *window, char *tag, gchar **args);
 
+gboolean cmd_omemo_gen(ProfWin *window, const char *const command, gchar **args);
+gboolean cmd_omemo_char(ProfWin *window, const char *const command, gchar **args);
+gboolean cmd_omemo_log(ProfWin *window, const char *const command, gchar **args);
+gboolean cmd_omemo_start(ProfWin *window, const char *const command, gchar **args);
+gboolean cmd_omemo_end(ProfWin *window, const char *const command, gchar **args);
+gboolean cmd_omemo_fingerprint(ProfWin *window, const char *const command, gchar **args);
+gboolean cmd_omemo_trust(ProfWin *window, const char *const command, gchar **args);
+gboolean cmd_omemo_untrust(ProfWin *window, const char *const command, gchar **args);
+gboolean cmd_omemo_clear_device_list(ProfWin *window, const char *const command, gchar **args);
+
 #endif
diff --git a/src/config/files.h b/src/config/files.h
index 1d8d2890..f7dfa29a 100644
--- a/src/config/files.h
+++ b/src/config/files.h
@@ -50,6 +50,7 @@
 #define DIR_CHATLOGS "chatlogs"
 #define DIR_OTR "otr"
 #define DIR_PGP "pgp"
+#define DIR_OMEMO "omemo"
 #define DIR_PLUGINS "plugins"
 
 void files_create_directories(void);
diff --git a/src/config/preferences.c b/src/config/preferences.c
index 265e11db..65e7a64d 100644
--- a/src/config/preferences.c
+++ b/src/config/preferences.c
@@ -58,6 +58,7 @@
 #define PREF_GROUP_ALIAS "alias"
 #define PREF_GROUP_OTR "otr"
 #define PREF_GROUP_PGP "pgp"
+#define PREF_GROUP_OMEMO "omemo"
 #define PREF_GROUP_MUC "muc"
 #define PREF_GROUP_PLUGINS "plugins"
 
@@ -823,6 +824,33 @@ prefs_set_pgp_char(char ch)
 }
 
 char
+prefs_get_omemo_char(void)
+{
+    char result = '~';
+
+    char *resultstr = g_key_file_get_string(prefs, PREF_GROUP_OMEMO, "omemo.char", NULL);
+    if (!resultstr) {
+        result =  '~';
+    } else {
+        result = resultstr[0];
+    }
+    free(resultstr);
+
+    return result;
+}
+
+void
+prefs_set_omemo_char(char ch)
+{
+    char str[2];
+    str[0] = ch;
+    str[1] = '\0';
+
+    g_key_file_set_string(prefs, PREF_GROUP_OMEMO, "omemo.char", str);
+    _save_prefs();
+}
+
+char
 prefs_get_roster_header_char(void)
 {
     char result = 0;
@@ -1657,6 +1685,8 @@ _get_group(preference_t pref)
             return PREF_GROUP_MUC;
         case PREF_PLUGINS_SOURCEPATH:
             return PREF_GROUP_PLUGINS;
+        case PREF_OMEMO_LOG:
+            return PREF_GROUP_OMEMO;
         default:
             return NULL;
     }
@@ -1871,6 +1901,8 @@ _get_key(preference_t pref)
             return "statusbar.chat";
         case PREF_STATUSBAR_ROOM:
             return "statusbar.room";
+        case PREF_OMEMO_LOG:
+            return "log";
         default:
             return NULL;
     }
@@ -1989,6 +2021,8 @@ _get_default_string(preference_t pref)
             return "user";
         case PREF_STATUSBAR_ROOM:
             return "room";
+        case PREF_OMEMO_LOG:
+            return "redact";
         default:
             return NULL;
     }
diff --git a/src/config/preferences.h b/src/config/preferences.h
index 65dee327..a4d82967 100644
--- a/src/config/preferences.h
+++ b/src/config/preferences.h
@@ -148,6 +148,7 @@ typedef enum {
     PREF_STATUSBAR_SELF,
     PREF_STATUSBAR_CHAT,
     PREF_STATUSBAR_ROOM,
+    PREF_OMEMO_LOG,
 } preference_t;
 
 typedef struct prof_alias_t {
@@ -216,6 +217,8 @@ char prefs_get_otr_char(void);
 void prefs_set_otr_char(char ch);
 char prefs_get_pgp_char(void);
 void prefs_set_pgp_char(char ch);
+char prefs_get_omemo_char(void);
+void prefs_set_omemo_char(char ch);
 
 char prefs_get_roster_header_char(void);
 void prefs_set_roster_header_char(char ch);
diff --git a/src/event/client_events.c b/src/event/client_events.c
index 3b6218ea..c2149985 100644
--- a/src/event/client_events.c
+++ b/src/event/client_events.c
@@ -54,6 +54,10 @@
 #include "pgp/gpg.h"
 #endif
 
+#ifdef HAVE_OMEMO
+#include "omemo/omemo.h"
+#endif
+
 jabber_conn_status_t
 cl_ev_connect_jid(const char *const jid, const char *const passwd, const char *const altdomain, const int port, const char *const tls_policy)
 {
@@ -94,6 +98,9 @@ cl_ev_disconnect(void)
 #ifdef HAVE_LIBGPGME
     p_gpg_on_disconnect();
 #endif
+#ifdef HAVE_OMEMO
+    omemo_on_disconnect();
+#endif
 }
 
 void
@@ -141,9 +148,10 @@ cl_ev_send_msg(ProfChatWin *chatwin, const char *const msg, const char *const oo
         return;
     }
 
-// OTR suported, PGP supported
+// OTR suported, PGP supported, OMEMO unsupported
 #ifdef HAVE_LIBOTR
 #ifdef HAVE_LIBGPGME
+#ifndef HAVE_OMEMO
     if (chatwin->pgp_send) {
         char *id = message_send_chat_pgp(chatwin->barejid, plugin_msg, request_receipt);
         chat_log_pgp_msg_out(chatwin->barejid, plugin_msg);
@@ -164,10 +172,12 @@ cl_ev_send_msg(ProfChatWin *chatwin, const char *const msg, const char *const oo
     return;
 #endif
 #endif
+#endif
 
-// OTR supported, PGP unsupported
+// OTR supported, PGP unsupported, OMEMO unsupported
 #ifdef HAVE_LIBOTR
 #ifndef HAVE_LIBGPGME
+#ifndef HAVE_OMEMO
     gboolean handled = otr_on_message_send(chatwin, plugin_msg, request_receipt);
     if (!handled) {
         char *id = message_send_chat(chatwin->barejid, plugin_msg, oob_url, request_receipt);
@@ -181,10 +191,12 @@ cl_ev_send_msg(ProfChatWin *chatwin, const char *const msg, const char *const oo
     return;
 #endif
 #endif
+#endif
 
-// OTR unsupported, PGP supported
+// OTR unsupported, PGP supported, OMEMO unsupported
 #ifndef HAVE_LIBOTR
 #ifdef HAVE_LIBGPGME
+#ifndef HAVE_OMEMO
     if (chatwin->pgp_send) {
         char *id = message_send_chat_pgp(chatwin->barejid, plugin_msg, request_receipt);
         chat_log_pgp_msg_out(chatwin->barejid, plugin_msg);
@@ -202,10 +214,120 @@ cl_ev_send_msg(ProfChatWin *chatwin, const char *const msg, const char *const oo
     return;
 #endif
 #endif
+#endif
+
+// OTR unsupported, PGP unsupported, OMEMO supported
+#ifndef HAVE_LIBOTR
+#ifndef HAVE_LIBGPGME
+#ifdef HAVE_OMEMO
+    if (chatwin->is_omemo) {
+        char *id = omemo_on_message_send((ProfWin *)chatwin, plugin_msg, request_receipt, FALSE);
+        chat_log_omemo_msg_out(chatwin->barejid, plugin_msg);
+        chatwin_outgoing_msg(chatwin, plugin_msg, id, PROF_MSG_OMEMO, request_receipt);
+        free(id);
+    } else {
+        char *id = message_send_chat(chatwin->barejid, plugin_msg, oob_url, request_receipt);
+        chat_log_msg_out(chatwin->barejid, plugin_msg);
+        chatwin_outgoing_msg(chatwin, plugin_msg, id, PROF_MSG_PLAIN, request_receipt);
+        free(id);
+    }
+
+    plugins_post_chat_message_send(chatwin->barejid, plugin_msg);
+    free(plugin_msg);
+    return;
+#endif
+#endif
+#endif
+
+// OTR supported, PGP unsupported, OMEMO supported
+#ifdef HAVE_LIBOTR
+#ifndef HAVE_LIBGPGME
+#ifdef HAVE_OMEMO
+    if (chatwin->is_omemo) {
+        char *id = omemo_on_message_send((ProfWin *)chatwin, plugin_msg, request_receipt, FALSE);
+        chat_log_omemo_msg_out(chatwin->barejid, plugin_msg);
+        chatwin_outgoing_msg(chatwin, plugin_msg, id, PROF_MSG_OMEMO, request_receipt);
+        free(id);
+    } else {
+        gboolean handled = otr_on_message_send(chatwin, plugin_msg, request_receipt);
+        if (!handled) {
+            char *id = message_send_chat(chatwin->barejid, plugin_msg, oob_url, request_receipt);
+            chat_log_msg_out(chatwin->barejid, plugin_msg);
+            chatwin_outgoing_msg(chatwin, plugin_msg, id, PROF_MSG_PLAIN, request_receipt);
+            free(id);
+        }
+    }
+
+    plugins_post_chat_message_send(chatwin->barejid, plugin_msg);
+    free(plugin_msg);
+    return;
+#endif
+#endif
+#endif
+
+// OTR unsupported, PGP supported, OMEMO supported
+#ifndef HAVE_LIBOTR
+#ifdef HAVE_LIBGPGME
+#ifdef HAVE_OMEMO
+    if (chatwin->is_omemo) {
+        char *id = omemo_on_message_send((ProfWin *)chatwin, plugin_msg, request_receipt, FALSE);
+        chat_log_omemo_msg_out(chatwin->barejid, plugin_msg);
+        chatwin_outgoing_msg(chatwin, plugin_msg, id, PROF_MSG_OMEMO, request_receipt);
+        free(id);
+    } else if (chatwin->pgp_send) {
+        char *id = message_send_chat_pgp(chatwin->barejid, plugin_msg, request_receipt);
+        chat_log_pgp_msg_out(chatwin->barejid, plugin_msg);
+        chatwin_outgoing_msg(chatwin, plugin_msg, id, PROF_MSG_PGP, request_receipt);
+        free(id);
+    } else {
+        char *id = message_send_chat(chatwin->barejid, plugin_msg, oob_url, request_receipt);
+        chat_log_msg_out(chatwin->barejid, plugin_msg);
+        chatwin_outgoing_msg(chatwin, plugin_msg, id, PROF_MSG_PLAIN, request_receipt);
+        free(id);
+    }
+
+    plugins_post_chat_message_send(chatwin->barejid, plugin_msg);
+    free(plugin_msg);
+    return;
+#endif
+#endif
+#endif
 
-// OTR unsupported, PGP unsupported
+// OTR supported, PGP supported, OMEMO supported
+#ifdef HAVE_LIBOTR
+#ifdef HAVE_LIBGPGME
+#ifdef HAVE_OMEMO
+    if (chatwin->is_omemo) {
+        char *id = omemo_on_message_send((ProfWin *)chatwin, plugin_msg, request_receipt, FALSE);
+        chat_log_omemo_msg_out(chatwin->barejid, plugin_msg);
+        chatwin_outgoing_msg(chatwin, plugin_msg, id, PROF_MSG_OMEMO, request_receipt);
+        free(id);
+    } else if (chatwin->pgp_send) {
+        char *id = message_send_chat_pgp(chatwin->barejid, plugin_msg, request_receipt);
+        chat_log_pgp_msg_out(chatwin->barejid, plugin_msg);
+        chatwin_outgoing_msg(chatwin, plugin_msg, id, PROF_MSG_PGP, request_receipt);
+        free(id);
+    } else {
+        gboolean handled = otr_on_message_send(chatwin, plugin_msg, request_receipt);
+        if (!handled) {
+            char *id = message_send_chat(chatwin->barejid, plugin_msg, oob_url, request_receipt);
+            chat_log_msg_out(chatwin->barejid, plugin_msg);
+            chatwin_outgoing_msg(chatwin, plugin_msg, id, PROF_MSG_PLAIN, request_receipt);
+            free(id);
+        }
+    }
+
+    plugins_post_chat_message_send(chatwin->barejid, plugin_msg);
+    free(plugin_msg);
+    return;
+#endif
+#endif
+#endif
+
+// OTR unsupported, PGP unsupported, OMEMO unsupported
 #ifndef HAVE_LIBOTR
 #ifndef HAVE_LIBGPGME
+#ifndef HAVE_OMEMO
     char *id = message_send_chat(chatwin->barejid, plugin_msg, oob_url, request_receipt);
     chat_log_msg_out(chatwin->barejid, plugin_msg);
     chatwin_outgoing_msg(chatwin, plugin_msg, id, PROF_MSG_PLAIN, request_receipt);
@@ -216,6 +338,7 @@ cl_ev_send_msg(ProfChatWin *chatwin, const char *const msg, const char *const oo
     return;
 #endif
 #endif
+#endif
 }
 
 void
@@ -226,10 +349,34 @@ cl_ev_send_muc_msg(ProfMucWin *mucwin, const char *const msg, const char *const
         return;
     }
 
-    message_send_groupchat(mucwin->roomjid, plugin_msg, oob_url);
+#ifdef HAVE_OMEMO
+    if (mucwin->is_omemo) {
+        char *id = omemo_on_message_send((ProfWin *)mucwin, plugin_msg, FALSE, TRUE);
+        groupchat_log_omemo_msg_out(mucwin->roomjid, plugin_msg);
+        mucwin_outgoing_msg(mucwin, plugin_msg, id, PROF_MSG_OMEMO);
+        free(id);
+    } else {
+        char *id = message_send_groupchat(mucwin->roomjid, plugin_msg, oob_url);
+        groupchat_log_msg_out(mucwin->roomjid, plugin_msg);
+        mucwin_outgoing_msg(mucwin, plugin_msg, id, PROF_MSG_PLAIN);
+        free(id);
+    }
+
+    plugins_post_room_message_send(mucwin->roomjid, plugin_msg);
+    free(plugin_msg);
+    return;
+#endif
+
+#ifndef HAVE_OMEMO
+    char *id = message_send_groupchat(mucwin->roomjid, plugin_msg, oob_url);
+    groupchat_log_msg_out(mucwin->roomjid, plugin_msg);
+    mucwin_outgoing_msg(mucwin, plugin_msg, id, PROF_MSG_PLAIN);
+    free(id);
 
     plugins_post_room_message_send(mucwin->roomjid, plugin_msg);
     free(plugin_msg);
+    return;
+#endif
 }
 
 void
diff --git a/src/event/server_events.c b/src/event/server_events.c
index 69883141..36db8ebe 100644
--- a/src/event/server_events.c
+++ b/src/event/server_events.c
@@ -59,6 +59,10 @@
 #include "pgp/gpg.h"
 #endif
 
+#ifdef HAVE_OMEMO
+#include "omemo/omemo.h"
+#endif
+
 #include "ui/ui.h"
 
 void
@@ -76,6 +80,10 @@ sv_ev_login_account_success(char *account_name, gboolean secured)
     p_gpg_on_connect(account->jid);
 #endif
 
+#ifdef HAVE_OMEMO
+    omemo_on_connect(account);
+#endif
+
     ui_handle_login_account_success(account, secured);
 
     // attempt to rejoin rooms with passwords
@@ -166,6 +174,18 @@ sv_ev_roster_received(void)
 
     const char *fulljid = connection_get_fulljid();
     plugins_on_connect(account_name, fulljid);
+
+#ifdef HAVE_OMEMO
+    omemo_start_sessions();
+#endif
+}
+
+void
+sv_ev_connection_features_received(void)
+{
+#ifdef HAVE_OMEMO
+    omemo_publish_crypto_materials();
+#endif
 }
 
 void
@@ -252,22 +272,23 @@ sv_ev_room_history(const char *const room_jid, const char *const nick,
 }
 
 void
-sv_ev_room_message(const char *const room_jid, const char *const nick, const char *const message)
+sv_ev_room_message(const char *const room_jid, const char *const nick, const char *const message, const char *const id, gboolean omemo)
 {
-    if (prefs_get_boolean(PREF_GRLOG)) {
-        Jid *jid = jid_create(connection_get_fulljid());
-        groupchat_log_chat(jid->barejid, room_jid, nick, message);
-        jid_destroy(jid);
-    }
-
     ProfMucWin *mucwin = wins_get_muc(room_jid);
     if (!mucwin) {
         return;
     }
 
-    char *new_message = plugins_pre_room_message_display(room_jid, nick, message);
     char *mynick = muc_nick(mucwin->roomjid);
 
+    if (omemo) {
+        groupchat_log_omemo_msg_in(room_jid, nick, message);
+    } else {
+        groupchat_log_msg_in(room_jid, nick, message);
+    }
+
+    char *new_message = plugins_pre_room_message_display(room_jid, nick, message);
+
     gboolean whole_word = prefs_get_boolean(PREF_NOTIFY_MENTION_WHOLE_WORD);
     gboolean case_sensitive = prefs_get_boolean(PREF_NOTIFY_MENTION_CASE_SENSITIVE);
     char *message_search = case_sensitive ? strdup(new_message) : g_utf8_strdown(new_message, -1);
@@ -281,7 +302,11 @@ sv_ev_room_message(const char *const room_jid, const char *const nick, const cha
 
     GList *triggers = prefs_message_get_triggers(new_message);
 
-    mucwin_message(mucwin, nick, new_message, mentions, triggers);
+    if (omemo) {
+        mucwin_incoming_msg(mucwin, nick, new_message, id, mentions, triggers, PROF_MSG_OMEMO);
+    } else {
+        mucwin_incoming_msg(mucwin, nick, new_message, id, mentions, triggers, PROF_MSG_PLAIN);
+    }
 
     g_slist_free(mentions);
 
@@ -370,7 +395,7 @@ sv_ev_delayed_private_message(const char *const fulljid, char *message, GDateTim
 }
 
 void
-sv_ev_outgoing_carbon(char *barejid, char *message, char *pgp_message)
+sv_ev_outgoing_carbon(char *barejid, char *message, char *pgp_message, gboolean omemo)
 {
     ProfChatWin *chatwin = wins_get_chat(barejid);
     if (!chatwin) {
@@ -380,6 +405,7 @@ sv_ev_outgoing_carbon(char *barejid, char *message, char *pgp_message)
     chat_state_active(chatwin->state);
 
 #ifdef HAVE_LIBGPGME
+#ifndef HAVE_OMEMO
     if (pgp_message) {
         char *decrypted = p_gpg_decrypt(pgp_message);
         if (decrypted) {
@@ -390,9 +416,44 @@ sv_ev_outgoing_carbon(char *barejid, char *message, char *pgp_message)
     } else {
         chatwin_outgoing_carbon(chatwin, message, PROF_MSG_PLAIN);
     }
-#else
+    return;
+#endif
+#endif
+
+#ifndef HAVE_LIBGPGME
+#ifdef HAVE_OMEMO
+    if (omemo) {
+        chatwin_outgoing_carbon(chatwin, message, PROF_MSG_OMEMO);
+    } else {
+        chatwin_outgoing_carbon(chatwin, message, PROF_MSG_PLAIN);
+    }
+    return;
+#endif
+#endif
+
+#ifdef HAVE_LIBGPGME
+#ifdef HAVE_OMEMO
+    if (omemo) {
+        chatwin_outgoing_carbon(chatwin, message, PROF_MSG_OMEMO);
+    } else if (pgp_message) {
+        char *decrypted = p_gpg_decrypt(pgp_message);
+        if (decrypted) {
+            chatwin_outgoing_carbon(chatwin, decrypted, PROF_MSG_PGP);
+        } else {
+            chatwin_outgoing_carbon(chatwin, message, PROF_MSG_PLAIN);
+        }
+    } else {
+        chatwin_outgoing_carbon(chatwin, message, PROF_MSG_PLAIN);
+    }
+    return;
+#endif
+#endif
+
+#ifndef HAVE_LIBGPGME
+#ifndef HAVE_OMEMO
     chatwin_outgoing_carbon(chatwin, message, PROF_MSG_PLAIN);
 #endif
+#endif
 }
 
 #ifdef HAVE_LIBGPGME
@@ -433,6 +494,16 @@ _sv_ev_incoming_otr(ProfChatWin *chatwin, gboolean new_win, char *barejid, char
 }
 #endif
 
+#ifdef HAVE_OMEMO
+static void
+_sv_ev_incoming_omemo(ProfChatWin *chatwin, gboolean new_win, char *barejid, char *resource, char *message, GDateTime *timestamp)
+{
+    chatwin_incoming_msg(chatwin, resource, message, timestamp, new_win, PROF_MSG_OMEMO);
+    chat_log_omemo_msg_in(barejid, message, timestamp);
+    chatwin->pgp_recv = FALSE;
+}
+#endif
+
 static void
 _sv_ev_incoming_plain(ProfChatWin *chatwin, gboolean new_win, char *barejid, char *resource, char *message, GDateTime *timestamp)
 {
@@ -442,7 +513,7 @@ _sv_ev_incoming_plain(ProfChatWin *chatwin, gboolean new_win, char *barejid, cha
 }
 
 void
-sv_ev_incoming_message(char *barejid, char *resource, char *message, char *pgp_message, GDateTime *timestamp)
+sv_ev_incoming_message(char *barejid, char *resource, char *message, char *pgp_message, GDateTime *timestamp, gboolean omemo)
 {
     gboolean new_win = FALSE;
     ProfChatWin *chatwin = wins_get_chat(barejid);
@@ -452,9 +523,10 @@ sv_ev_incoming_message(char *barejid, char *resource, char *message, char *pgp_m
         new_win = TRUE;
     }
 
-// OTR suported, PGP supported
+// OTR suported, PGP supported, OMEMO unsupported
 #ifdef HAVE_LIBOTR
 #ifdef HAVE_LIBGPGME
+#ifndef HAVE_OMEMO
     if (pgp_message) {
         if (chatwin->is_otr) {
             win_println((ProfWin*)chatwin, THEME_DEFAULT, '-', "PGP encrypted message received whilst in OTR session.");
@@ -468,19 +540,23 @@ sv_ev_incoming_message(char *barejid, char *resource, char *message, char *pgp_m
     return;
 #endif
 #endif
+#endif
 
-// OTR supported, PGP unsupported
+// OTR supported, PGP unsupported, OMEMO unsupported
 #ifdef HAVE_LIBOTR
 #ifndef HAVE_LIBGPGME
+#ifndef HAVE_OMEMO
     _sv_ev_incoming_otr(chatwin, new_win, barejid, resource, message, timestamp);
     rosterwin_roster();
     return;
 #endif
 #endif
+#endif
 
-// OTR unsupported, PGP supported
+// OTR unsupported, PGP supported, OMEMO unsupported
 #ifndef HAVE_LIBOTR
 #ifdef HAVE_LIBGPGME
+#ifndef HAVE_OMEMO
     if (pgp_message) {
         _sv_ev_incoming_pgp(chatwin, new_win, barejid, resource, message, pgp_message, timestamp);
     } else {
@@ -490,19 +566,90 @@ sv_ev_incoming_message(char *barejid, char *resource, char *message, char *pgp_m
     return;
 #endif
 #endif
+#endif
 
-// OTR unsupported, PGP unsupported
+// OTR suported, PGP supported, OMEMO supported
+#ifdef HAVE_LIBOTR
+#ifdef HAVE_LIBGPGME
+#ifdef HAVE_OMEMO
+    if (pgp_message) {
+        if (chatwin->is_otr) {
+            win_println((ProfWin*)chatwin, THEME_DEFAULT, '-', "PGP encrypted message received whilst in OTR session.");
+        } else { // PROF_ENC_NONE, PROF_ENC_PGP
+            _sv_ev_incoming_pgp(chatwin, new_win, barejid, resource, message, pgp_message, timestamp);
+        }
+    } else if (omemo) {
+        _sv_ev_incoming_omemo(chatwin, new_win, barejid, resource, message, timestamp);
+    } else {
+        _sv_ev_incoming_otr(chatwin, new_win, barejid, resource, message, timestamp);
+    }
+    rosterwin_roster();
+    return;
+#endif
+#endif
+#endif
+
+// OTR supported, PGP unsupported, OMEMO supported
+#ifdef HAVE_LIBOTR
+#ifndef HAVE_LIBGPGME
+#ifdef HAVE_OMEMO
+    if (omemo) {
+        _sv_ev_incoming_omemo(chatwin, new_win, barejid, resource, message, timestamp);
+    } else {
+        _sv_ev_incoming_otr(chatwin, new_win, barejid, resource, message, timestamp);
+    }
+    rosterwin_roster();
+    return;
+#endif
+#endif
+#endif
+
+// OTR unsupported, PGP supported, OMEMO supported
+#ifndef HAVE_LIBOTR
+#ifdef HAVE_LIBGPGME
+#ifdef HAVE_OMEMO
+    if (pgp_message) {
+        _sv_ev_incoming_pgp(chatwin, new_win, barejid, resource, message, pgp_message, timestamp);
+    } else if (omemo) {
+        _sv_ev_incoming_omemo(chatwin, new_win, barejid, resource, message, timestamp);
+    } else {
+        _sv_ev_incoming_plain(chatwin, new_win, barejid, resource, message, timestamp);
+    }
+    rosterwin_roster();
+    return;
+#endif
+#endif
+#endif
+
+// OTR unsupported, PGP unsupported, OMEMO supported
+#ifndef HAVE_LIBOTR
+#ifndef HAVE_LIBGPGME
+#ifdef HAVE_OMEMO
+    if (omemo) {
+        _sv_ev_incoming_omemo(chatwin, new_win, barejid, resource, message, timestamp);
+    } else {
+        _sv_ev_incoming_plain(chatwin, new_win, barejid, resource, message, timestamp);
+    }
+    rosterwin_roster();
+    return;
+#endif
+#endif
+#endif
+
+// OTR unsupported, PGP unsupported, OMEMO unsupported
 #ifndef HAVE_LIBOTR
 #ifndef HAVE_LIBGPGME
+#ifndef HAVE_OMEMO
     _sv_ev_incoming_plain(chatwin, new_win, barejid, resource, message, timestamp);
     rosterwin_roster();
     return;
 #endif
 #endif
+#endif
 }
 
 void
-sv_ev_incoming_carbon(char *barejid, char *resource, char *message, char *pgp_message)
+sv_ev_incoming_carbon(char *barejid, char *resource, char *message, char *pgp_message, gboolean omemo)
 {
     gboolean new_win = FALSE;
     ProfChatWin *chatwin = wins_get_chat(barejid);
@@ -513,15 +660,50 @@ sv_ev_incoming_carbon(char *barejid, char *resource, char *message, char *pgp_me
     }
 
 #ifdef HAVE_LIBGPGME
+#ifndef HAVE_OMEMO
     if (pgp_message) {
         _sv_ev_incoming_pgp(chatwin, new_win, barejid, resource, message, pgp_message, NULL);
     } else {
         _sv_ev_incoming_plain(chatwin, new_win, barejid, resource, message, NULL);
     }
-#else
-    _sv_ev_incoming_plain(chatwin, new_win, barejid, resource, message, NULL);
+    rosterwin_roster();
+    return;
+#endif
 #endif
+
+#ifdef HAVE_LIBGPGME
+#ifdef HAVE_OMEMO
+    if (pgp_message) {
+        _sv_ev_incoming_pgp(chatwin, new_win, barejid, resource, message, pgp_message, NULL);
+    } else if (omemo) {
+        _sv_ev_incoming_omemo(chatwin, new_win, barejid, resource, message, NULL);
+    } else {
+        _sv_ev_incoming_plain(chatwin, new_win, barejid, resource, message, NULL);
+    }
     rosterwin_roster();
+    return;
+#endif
+#endif
+
+#ifndef HAVE_LIBGPGME
+#ifdef HAVE_OMEMO
+    if (omemo) {
+        _sv_ev_incoming_omemo(chatwin, new_win, barejid, resource, message, NULL);
+    } else {
+        _sv_ev_incoming_plain(chatwin, new_win, barejid, resource, message, NULL);
+    }
+    rosterwin_roster();
+    return;
+#endif
+#endif
+
+#ifndef HAVE_LIBGPGME
+#ifndef HAVE_OMEMO
+    _sv_ev_incoming_plain(chatwin, new_win, barejid, resource, message, NULL);
+    rosterwin_roster();
+    return;
+#endif
+#endif
 }
 
 void
diff --git a/src/event/server_events.h b/src/event/server_events.h
index cc261487..74016ceb 100644
--- a/src/event/server_events.h
+++ b/src/event/server_events.h
@@ -46,10 +46,10 @@ void sv_ev_room_invite(jabber_invite_t invite_type,
 void sv_ev_room_broadcast(const char *const room_jid, const char *const message);
 void sv_ev_room_subject(const char *const room, const char *const nick, const char *const subject);
 void sv_ev_room_history(const char *const room_jid, const char *const nick,
-    GDateTime *timestamp, const char *const message);
+    GDateTime *timestamp, const char *const message, gboolean omemo);
 void sv_ev_room_message(const char *const room_jid, const char *const nick,
-    const char *const message);
-void sv_ev_incoming_message(char *barejid, char *resource, char *message, char *pgp_message, GDateTime *timestamp);
+    const char *const message, const char *const id, gboolean omemo);
+void sv_ev_incoming_message(char *barejid, char *resource, char *message, char *pgp_message, GDateTime *timestamp, gboolean omemo);
 void sv_ev_incoming_private_message(const char *const fulljid, char *message);
 void sv_ev_delayed_private_message(const char *const fulljid, char *message, GDateTime *timestamp);
 void sv_ev_typing(char *barejid, char *resource);
@@ -73,8 +73,8 @@ void sv_ev_room_occupent_kicked(const char *const room, const char *const nick,
 void sv_ev_room_banned(const char *const room, const char *const actor, const char *const reason);
 void sv_ev_room_occupent_banned(const char *const room, const char *const nick, const char *const actor,
     const char *const reason);
-void sv_ev_outgoing_carbon(char *barejid, char *message, char *pgp_message);
-void sv_ev_incoming_carbon(char *barejid, char *resource, char *message, char *pgp_message);
+void sv_ev_outgoing_carbon(char *barejid, char *message, char *pgp_message, gboolean omemo);
+void sv_ev_incoming_carbon(char *barejid, char *resource, char *message, char *pgp_message, gboolean omemo);
 void sv_ev_xmpp_stanza(const char *const msg);
 void sv_ev_muc_self_online(const char *const room, const char *const nick, gboolean config_required,
     const char *const role, const char *const affiliation, const char *const actor, const char *const reason,
@@ -85,6 +85,7 @@ void sv_ev_muc_occupant_online(const char *const room, const char *const nick, c
 void sv_ev_roster_update(const char *const barejid, const char *const name,
     GSList *groups, const char *const subscription, gboolean pending_out);
 void sv_ev_roster_received(void);
+void sv_ev_connection_features_received(void);
 int sv_ev_certfail(const char *const errormsg, TLSCertificate *cert);
 void sv_ev_lastactivity_response(const char *const from, const int seconds, const char *const msg);
 void sv_ev_bookmark_autojoin(Bookmark *bookmark);
diff --git a/src/log.c b/src/log.c
index 0133a6cf..b679eb1a 100644
--- a/src/log.c
+++ b/src/log.c
@@ -48,6 +48,7 @@
 #include "config/files.h"
 #include "config/preferences.h"
 #include "xmpp/xmpp.h"
+#include "xmpp/muc.h"
 
 #define PROF "prof"
 
@@ -89,6 +90,8 @@ static void _rotate_log_file(void);
 static char* _log_string_from_level(log_level_t level);
 static void _chat_log_chat(const char *const login, const char *const other, const gchar *const msg,
     chat_log_direction_t direction, GDateTime *timestamp);
+static void _groupchat_log_chat(const gchar *const login, const gchar *const room, const gchar *const nick,
+    const gchar *const msg);
 
 void
 log_debug(const char *const msg, ...)
@@ -305,6 +308,23 @@ chat_log_pgp_msg_out(const char *const barejid, const char *const msg)
 }
 
 void
+chat_log_omemo_msg_out(const char *const barejid, const char *const msg)
+{
+    if (prefs_get_boolean(PREF_CHLOG)) {
+        const char *jid = connection_get_fulljid();
+        Jid *jidp = jid_create(jid);
+        char *pref_omemo_log = prefs_get_string(PREF_OMEMO_LOG);
+        if (strcmp(pref_omemo_log, "on") == 0) {
+            _chat_log_chat(jidp->barejid, barejid, msg, PROF_OUT_LOG, NULL);
+        } else if (strcmp(pref_omemo_log, "redact") == 0) {
+            _chat_log_chat(jidp->barejid, barejid, "[redacted]", PROF_OUT_LOG, NULL);
+        }
+        prefs_free_string(pref_omemo_log);
+        jid_destroy(jidp);
+    }
+}
+
+void
 chat_log_otr_msg_in(const char *const barejid, const char *const msg, gboolean was_decrypted, GDateTime *timestamp)
 {
     if (prefs_get_boolean(PREF_CHLOG)) {
@@ -339,6 +359,23 @@ chat_log_pgp_msg_in(const char *const barejid, const char *const msg, GDateTime
 }
 
 void
+chat_log_omemo_msg_in(const char *const barejid, const char *const msg, GDateTime *timestamp)
+{
+    if (prefs_get_boolean(PREF_CHLOG)) {
+        const char *jid = connection_get_fulljid();
+        Jid *jidp = jid_create(jid);
+        char *pref_omemo_log = prefs_get_string(PREF_OMEMO_LOG);
+        if (strcmp(pref_omemo_log, "on") == 0) {
+            _chat_log_chat(jidp->barejid, barejid, msg, PROF_IN_LOG, timestamp);
+        } else if (strcmp(pref_omemo_log, "redact") == 0) {
+            _chat_log_chat(jidp->barejid, barejid, "[redacted]", PROF_IN_LOG, timestamp);
+        }
+        prefs_free_string(pref_omemo_log);
+        jid_destroy(jidp);
+    }
+}
+
+void
 chat_log_msg_in(const char *const barejid, const char *const msg, GDateTime *timestamp)
 {
     if (prefs_get_boolean(PREF_CHLOG)) {
@@ -406,7 +443,66 @@ _chat_log_chat(const char *const login, const char *const other, const char *con
 }
 
 void
-groupchat_log_chat(const gchar *const login, const gchar *const room, const gchar *const nick, const gchar *const msg)
+groupchat_log_msg_out(const gchar *const room, const gchar *const msg)
+{
+    if (prefs_get_boolean(PREF_GRLOG)) {
+        const char *jid = connection_get_fulljid();
+        Jid *jidp = jid_create(jid);
+        char *mynick = muc_nick(room);
+        _groupchat_log_chat(jidp->barejid, room, mynick, msg);
+        jid_destroy(jidp);
+    }
+}
+
+void
+groupchat_log_msg_in(const gchar *const room, const gchar *const nick, const gchar *const msg)
+{
+    if (prefs_get_boolean(PREF_GRLOG)) {
+        const char *jid = connection_get_fulljid();
+        Jid *jidp = jid_create(jid);
+        _groupchat_log_chat(jidp->barejid, room, nick, msg);
+        jid_destroy(jidp);
+    }
+}
+
+void
+groupchat_log_omemo_msg_out(const gchar *const room, const gchar *const msg)
+{
+    if (prefs_get_boolean(PREF_CHLOG)) {
+        const char *jid = connection_get_fulljid();
+        Jid *jidp = jid_create(jid);
+        char *pref_omemo_log = prefs_get_string(PREF_OMEMO_LOG);
+        char *mynick = muc_nick(room);
+        if (strcmp(pref_omemo_log, "on") == 0) {
+            _groupchat_log_chat(jidp->barejid, room, mynick, msg);
+        } else if (strcmp(pref_omemo_log, "redact") == 0) {
+            _groupchat_log_chat(jidp->barejid, room, mynick, "[redacted]");
+        }
+        prefs_free_string(pref_omemo_log);
+        jid_destroy(jidp);
+    }
+}
+
+void
+groupchat_log_omemo_msg_in(const gchar *const room, const gchar *const nick, const gchar *const msg)
+{
+    if (prefs_get_boolean(PREF_CHLOG)) {
+        const char *jid = connection_get_fulljid();
+        Jid *jidp = jid_create(jid);
+        char *pref_omemo_log = prefs_get_string(PREF_OMEMO_LOG);
+        if (strcmp(pref_omemo_log, "on") == 0) {
+            _groupchat_log_chat(jidp->barejid, room, nick, msg);
+        } else if (strcmp(pref_omemo_log, "redact") == 0) {
+            _groupchat_log_chat(jidp->barejid, room, nick, "[redacted]");
+        }
+        prefs_free_string(pref_omemo_log);
+        jid_destroy(jidp);
+    }
+}
+
+void
+_groupchat_log_chat(const gchar *const login, const gchar *const room, const gchar *const nick,
+    const gchar *const msg)
 {
     struct dated_chat_log *dated_log = g_hash_table_lookup(groupchat_logs, room);
 
diff --git a/src/log.h b/src/log.h
index 43a34ca1..1f45545c 100644
--- a/src/log.h
+++ b/src/log.h
@@ -71,16 +71,21 @@ void chat_log_init(void);
 void chat_log_msg_out(const char *const barejid, const char *const msg);
 void chat_log_otr_msg_out(const char *const barejid, const char *const msg);
 void chat_log_pgp_msg_out(const char *const barejid, const char *const msg);
+void chat_log_omemo_msg_out(const char *const barejid, const char *const msg);
 
 void chat_log_msg_in(const char *const barejid, const char *const msg, GDateTime *timestamp);
 void chat_log_otr_msg_in(const char *const barejid, const char *const msg, gboolean was_decrypted, GDateTime *timestamp);
 void chat_log_pgp_msg_in(const char *const barejid, const char *const msg, GDateTime *timestamp);
+void chat_log_omemo_msg_in(const char *const barejid, const char *const msg, GDateTime *timestamp);
 
 void chat_log_close(void);
 GSList* chat_log_get_previous(const gchar *const login, const gchar *const recipient);
 
 void groupchat_log_init(void);
-void groupchat_log_chat(const gchar *const login, const gchar *const room, const gchar *const nick,
-    const gchar *const msg);
+
+void groupchat_log_msg_out(const gchar *const room, const gchar *const msg);
+void groupchat_log_msg_in(const gchar *const room, const gchar *const nick, const gchar *const msg);
+void groupchat_log_omemo_msg_out(const gchar *const room, const gchar *const msg);
+void groupchat_log_omemo_msg_in(const gchar *const room, const gchar *const nick, const gchar *const msg);
 
 #endif
diff --git a/src/main.c b/src/main.c
index d2392a2b..6060ca27 100644
--- a/src/main.c
+++ b/src/main.c
@@ -138,6 +138,12 @@ main(int argc, char **argv)
         g_print("PGP support: Disabled\n");
 #endif
 
+#ifdef HAVE_OMEMO
+        g_print("OMEMO support: Enabled\n");
+#else
+        g_print("OMEMO support: Disabled\n");
+#endif
+
 #ifdef HAVE_C
         g_print("C plugins: Enabled\n");
 #else
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);
diff --git a/src/plugins/api.c b/src/plugins/api.c
index 4d8434e1..fc47f193 100644
--- a/src/plugins/api.c
+++ b/src/plugins/api.c
@@ -473,7 +473,7 @@ api_settings_int_set(const char *const group, const char *const key, int value)
 void
 api_incoming_message(const char *const barejid, const char *const resource, const char *const message)
 {
-    sv_ev_incoming_message((char*)barejid, (char*)resource, (char*)message, NULL, NULL);
+    sv_ev_incoming_message((char*)barejid, (char*)resource, (char*)message, NULL, NULL, FALSE);
 
     // TODO handle all states
     sv_ev_activity((char*)barejid, (char*)resource, FALSE);
diff --git a/src/profanity.c b/src/profanity.c
index 1d4a2c35..f21f02c0 100644
--- a/src/profanity.c
+++ b/src/profanity.c
@@ -80,6 +80,10 @@
 #include "pgp/gpg.h"
 #endif
 
+#ifdef HAVE_OMEMO
+#include "omemo/omemo.h"
+#endif
+
 static void _init(char *log_level);
 static void _shutdown(void);
 static void _connect_default(const char * const account);
@@ -197,6 +201,9 @@ _init(char *log_level)
 #ifdef HAVE_LIBGPGME
     p_gpg_init();
 #endif
+#ifdef HAVE_OMEMO
+    omemo_init();
+#endif
     atexit(_shutdown);
     plugins_init();
 #ifdef HAVE_GTK
diff --git a/src/ui/chatwin.c b/src/ui/chatwin.c
index 98431a60..5064b194 100644
--- a/src/ui/chatwin.c
+++ b/src/ui/chatwin.c
@@ -305,6 +305,8 @@ chatwin_outgoing_msg(ProfChatWin *chatwin, const char *const message, char *id,
         enc_char = prefs_get_otr_char();
     } else if (enc_mode == PROF_MSG_PGP) {
         enc_char = prefs_get_pgp_char();
+    } else if (enc_mode == PROF_MSG_OMEMO) {
+        enc_char = prefs_get_omemo_char();
     }
 
     if (request_receipt && id) {
@@ -322,6 +324,8 @@ chatwin_outgoing_carbon(ProfChatWin *chatwin, const char *const message, prof_en
     char enc_char = '-';
     if (enc_mode == PROF_MSG_PGP) {
         enc_char = prefs_get_pgp_char();
+    } else if (enc_mode == PROF_MSG_OMEMO) {
+        enc_char = prefs_get_omemo_char();
     }
 
     ProfWin *window = (ProfWin*)chatwin;
diff --git a/src/ui/console.c b/src/ui/console.c
index e5c12158..260658c8 100644
--- a/src/ui/console.c
+++ b/src/ui/console.c
@@ -1999,6 +1999,28 @@ cons_show_pgp_prefs(void)
 }
 
 void
+cons_show_omemo_prefs(void)
+{
+    cons_show("OMEMO preferences:");
+    cons_show("");
+
+    char *log_value = prefs_get_string(PREF_OMEMO_LOG);
+    if (strcmp(log_value, "on") == 0) {
+        cons_show("OMEMO logging (/omemo log)   : ON");
+    } else if (strcmp(log_value, "off") == 0) {
+        cons_show("OMEMO logging (/omemo log)   : OFF");
+    } else {
+        cons_show("OMEMO logging (/omemo log)   : Redacted");
+    }
+    prefs_free_string(log_value);
+
+    char ch = prefs_get_omemo_char();
+    cons_show("OMEMO char (/omemo char)     : %c", ch);
+
+    cons_alert();
+}
+
+void
 cons_show_themes(GSList *themes)
 {
     cons_show("");
@@ -2072,6 +2094,8 @@ cons_prefs(void)
     cons_show("");
     cons_show_pgp_prefs();
     cons_show("");
+    cons_show_omemo_prefs();
+    cons_show("");
 
     cons_alert();
 }
diff --git a/src/ui/mucwin.c b/src/ui/mucwin.c
index 0f9f4f2b..0122950a 100644
--- a/src/ui/mucwin.c
+++ b/src/ui/mucwin.c
@@ -478,29 +478,60 @@ _mucwin_print_triggers(ProfWin *window, const char *const message, GList *trigge
 }
 
 void
-mucwin_message(ProfMucWin *mucwin, const char *const nick, const char *const message, GSList *mentions, GList *triggers)
+mucwin_outgoing_msg(ProfMucWin *mucwin, const char *const message, const char *const id, prof_enc_t enc_mode)
 {
     assert(mucwin != NULL);
 
+    g_hash_table_insert(mucwin->sent_messages, strdup(id), NULL);
+
     ProfWin *window = (ProfWin*)mucwin;
     char *mynick = muc_nick(mucwin->roomjid);
 
     char ch = '-';
     if (mucwin->message_char) {
         ch = mucwin->message_char[0];
+    } else if (enc_mode == PROF_MSG_OTR) {
+        ch = prefs_get_otr_char();
+    } else if (enc_mode == PROF_MSG_PGP) {
+        ch = prefs_get_pgp_char();
+    } else if (enc_mode == PROF_MSG_OMEMO) {
+        ch = prefs_get_omemo_char();
     }
 
-    if (g_strcmp0(nick, mynick) != 0) {
-        if (g_slist_length(mentions) > 0) {
-            _mucwin_print_mention(window, message, nick, mynick, mentions, &ch);
-        } else if (triggers) {
-            win_print_them(window, THEME_ROOMTRIGGER, ch, nick);
-            _mucwin_print_triggers(window, message, triggers);
-        } else {
-            win_println_them_message(window, ch, nick, "%s", message);
-        }
+    win_println_me_message(window, ch, mynick, "%s", message);
+}
+
+void
+mucwin_incoming_msg(ProfMucWin *mucwin, const char *const nick, const char *const message, const char *const id, GSList *mentions, GList *triggers, prof_enc_t enc_mode)
+{
+    assert(mucwin != NULL);
+
+    if (g_hash_table_remove(mucwin->sent_messages, id)) {
+        /* Ignore reflection messages */
+        return;
+    }
+
+    ProfWin *window = (ProfWin*)mucwin;
+    char *mynick = muc_nick(mucwin->roomjid);
+
+    char ch = '-';
+    if (mucwin->message_char) {
+        ch = mucwin->message_char[0];
+    } else if (enc_mode == PROF_MSG_OTR) {
+        ch = prefs_get_otr_char();
+    } else if (enc_mode == PROF_MSG_PGP) {
+        ch = prefs_get_pgp_char();
+    } else if (enc_mode == PROF_MSG_OMEMO) {
+        ch = prefs_get_omemo_char();
+    }
+
+    if (g_slist_length(mentions) > 0) {
+        _mucwin_print_mention(window, message, nick, mynick, mentions, &ch);
+    } else if (triggers) {
+        win_print_them(window, THEME_ROOMTRIGGER, ch, nick);
+        _mucwin_print_triggers(window, message, triggers);
     } else {
-        win_println_me_message(window, ch, mynick, "%s", message);
+        win_println_them_message(window, ch, nick, "%s", message);
     }
 }
 
diff --git a/src/ui/titlebar.c b/src/ui/titlebar.c
index f519fdd2..e1758d81 100644
--- a/src/ui/titlebar.c
+++ b/src/ui/titlebar.c
@@ -321,6 +321,21 @@ _show_muc_privacy(ProfMucWin *mucwin)
     int bracket_attrs = theme_attrs(THEME_TITLE_BRACKET);
     int encrypted_attrs = theme_attrs(THEME_TITLE_ENCRYPTED);
 
+    if (mucwin->is_omemo) {
+        wprintw(win, " ");
+        wattron(win, bracket_attrs);
+        wprintw(win, "[");
+        wattroff(win, bracket_attrs);
+        wattron(win, encrypted_attrs);
+        wprintw(win, "OMEMO");
+        wattroff(win, encrypted_attrs);
+        wattron(win, bracket_attrs);
+        wprintw(win, "]");
+        wattroff(win, bracket_attrs);
+
+        return;
+    }
+
     if (mucwin->enctext) {
         wprintw(win, " ");
         wattron(win, bracket_attrs);
@@ -421,6 +436,21 @@ _show_privacy(ProfChatWin *chatwin)
         return;
     }
 
+    if (chatwin->is_omemo) {
+        wprintw(win, " ");
+        wattron(win, bracket_attrs);
+        wprintw(win, "[");
+        wattroff(win, bracket_attrs);
+        wattron(win, encrypted_attrs);
+        wprintw(win, "OMEMO");
+        wattroff(win, encrypted_attrs);
+        wattron(win, bracket_attrs);
+        wprintw(win, "]");
+        wattroff(win, bracket_attrs);
+
+        return;
+    }
+
     if (prefs_get_boolean(PREF_ENC_WARN)) {
         wprintw(win, " ");
         wattron(win, bracket_attrs);
diff --git a/src/ui/ui.h b/src/ui/ui.h
index ad5a1216..b94fe475 100644
--- a/src/ui/ui.h
+++ b/src/ui/ui.h
@@ -56,7 +56,8 @@
 typedef enum {
     PROF_MSG_PLAIN,
     PROF_MSG_OTR,
-    PROF_MSG_PGP
+    PROF_MSG_PGP,
+    PROF_MSG_OMEMO
 } prof_enc_t;
 
 // core UI
@@ -161,7 +162,8 @@ void mucwin_occupant_role_and_affiliation_change(ProfMucWin *mucwin, const char
     const char *const role, const char *const affiliation, const char *const actor, const char *const reason);
 void mucwin_roster(ProfMucWin *mucwin, GList *occupants, const char *const presence);
 void mucwin_history(ProfMucWin *mucwin, const char *const nick, GDateTime *timestamp, const char *const message);
-void mucwin_message(ProfMucWin *mucwin, const char *const nick, const char *const message, GSList *mentions, GList *triggers);
+void mucwin_outgoing_msg(ProfMucWin *mucwin, const char *const message, const char *const id, prof_enc_t enc_mode);
+void mucwin_incoming_msg(ProfMucWin *mucwin, const char *const nick, const char *const message, const char *const id, GSList *mentions, GList *triggers, prof_enc_t enc_mode);
 void mucwin_subject(ProfMucWin *mucwin, const char *const nick, const char *const subject);
 void mucwin_requires_config(ProfMucWin *mucwin);
 void mucwin_info(ProfMucWin *mucwin);
@@ -250,6 +252,7 @@ void cons_show_presence_prefs(void);
 void cons_show_connection_prefs(void);
 void cons_show_otr_prefs(void);
 void cons_show_pgp_prefs(void);
+void cons_show_omemo_prefs(void);
 void cons_show_account(ProfAccount *account);
 void cons_debug(const char *const msg, ...);
 void cons_show_error(const char *const cmd, ...);
diff --git a/src/ui/win_types.h b/src/ui/win_types.h
index 92618a36..e1e64bf9 100644
--- a/src/ui/win_types.h
+++ b/src/ui/win_types.h
@@ -152,6 +152,7 @@ typedef struct prof_chat_win_t {
     gboolean otr_is_trusted;
     gboolean pgp_send;
     gboolean pgp_recv;
+    gboolean is_omemo;
     char *resource_override;
     gboolean history_shown;
     unsigned long memcheck;
@@ -167,9 +168,11 @@ typedef struct prof_muc_win_t {
     gboolean unread_mentions;
     gboolean unread_triggers;
     gboolean showjid;
+    gboolean is_omemo;
     unsigned long memcheck;
     char *enctext;
     char *message_char;
+    GHashTable *sent_messages;
 } ProfMucWin;
 
 typedef struct prof_conf_win_t ProfConfWin;
diff --git a/src/ui/window.c b/src/ui/window.c
index cc2c2062..12b6c15b 100644
--- a/src/ui/window.c
+++ b/src/ui/window.c
@@ -143,6 +143,7 @@ win_create_chat(const char *const barejid)
     new_win->otr_is_trusted = FALSE;
     new_win->pgp_recv = FALSE;
     new_win->pgp_send = FALSE;
+    new_win->is_omemo = FALSE;
     new_win->history_shown = FALSE;
     new_win->unread = 0;
     new_win->state = chat_state_new();
@@ -196,6 +197,8 @@ win_create_muc(const char *const roomjid)
     }
     new_win->enctext = NULL;
     new_win->message_char = NULL;
+    new_win->is_omemo = FALSE;
+    new_win->sent_messages = g_hash_table_new_full(g_str_hash, g_str_equal, free, NULL);
 
     new_win->memcheck = PROFMUCWIN_MEMCHECK;
 
@@ -1057,6 +1060,8 @@ win_print_incoming(ProfWin *window, GDateTime *timestamp,
                 enc_char = prefs_get_otr_char();
             } else if (enc_mode == PROF_MSG_PGP) {
                 enc_char = prefs_get_pgp_char();
+            } else if (enc_mode == PROF_MSG_OMEMO) {
+                enc_char = prefs_get_omemo_char();
             }
             _win_printf(window, enc_char, 0, timestamp, NO_ME, THEME_TEXT_THEM, from, "%s", message);
             break;
diff --git a/src/ui/window_list.c b/src/ui/window_list.c
index 5ce68d63..43230b57 100644
--- a/src/ui/window_list.c
+++ b/src/ui/window_list.c
@@ -561,6 +561,7 @@ wins_close_by_num(int i)
                 ProfMucWin *mucwin = (ProfMucWin*)window;
                 autocomplete_remove(wins_ac, mucwin->roomjid);
                 autocomplete_remove(wins_close_ac, mucwin->roomjid);
+                g_hash_table_remove_all(mucwin->sent_messages);
                 break;
             }
             case WIN_PRIVATE:
diff --git a/src/xmpp/connection.c b/src/xmpp/connection.c
index 2adda46e..afcd8199 100644
--- a/src/xmpp/connection.c
+++ b/src/xmpp/connection.c
@@ -63,6 +63,7 @@ typedef struct prof_conn_t {
     char *domain;
     GHashTable *available_resources;
     GHashTable *features_by_jid;
+    GHashTable *requested_features;
 } ProfConnection;
 
 static ProfConnection conn;
@@ -89,6 +90,7 @@ connection_init(void)
     conn.domain = NULL;
     conn.features_by_jid = NULL;
     conn.available_resources = g_hash_table_new_full(g_str_hash, g_str_equal, free, (GDestroyNotify)resource_destroy);
+    conn.requested_features = g_hash_table_new_full(g_str_hash, g_str_equal, free, NULL);
 }
 
 void
@@ -231,6 +233,10 @@ connection_clear_data(void)
     if (conn.available_resources) {
         g_hash_table_remove_all(conn.available_resources);
     }
+
+    if (conn.requested_features) {
+        g_hash_table_remove_all(conn.requested_features);
+    }
 }
 
 #ifdef HAVE_LIBMESODE
@@ -314,11 +320,20 @@ connection_jid_for_feature(const char *const feature)
 }
 
 void
+connection_request_features(void)
+{
+    /* We don't record it as a requested feature to avoid triggering th
+     * sv_ev_connection_features_received too soon */
+    iq_disco_info_request_onconnect(conn.domain);
+}
+
+void
 connection_set_disco_items(GSList *items)
 {
     GSList *curr = items;
     while (curr) {
         DiscoItem *item = curr->data;
+        g_hash_table_insert(conn.requested_features, strdup(item->jid), NULL);
         g_hash_table_insert(conn.features_by_jid, strdup(item->jid),
             g_hash_table_new_full(g_str_hash, g_str_equal, free, NULL));
 
@@ -357,6 +372,14 @@ connection_get_fulljid(void)
     }
 }
 
+void
+connection_features_received(const char *const jid)
+{
+    if (g_hash_table_remove(conn.requested_features, jid) && g_hash_table_size(conn.requested_features) == 0) {
+        sv_ev_connection_features_received();
+    }
+}
+
 GHashTable*
 connection_get_features(const char *const jid)
 {
diff --git a/src/xmpp/connection.h b/src/xmpp/connection.h
index 170bc2bf..044cf368 100644
--- a/src/xmpp/connection.h
+++ b/src/xmpp/connection.h
@@ -53,6 +53,8 @@ void connection_set_disco_items(GSList *items);
 xmpp_conn_t* connection_get_conn(void);
 xmpp_ctx_t* connection_get_ctx(void);
 char *connection_get_domain(void);
+void connection_request_features(void);
+void connection_features_received(const char *const jid);
 GHashTable* connection_get_features(const char *const jid);
 
 void connection_clear_data(void);
diff --git a/src/xmpp/iq.c b/src/xmpp/iq.c
index a77ef59b..d6e4c153 100644
--- a/src/xmpp/iq.c
+++ b/src/xmpp/iq.c
@@ -77,11 +77,11 @@ typedef struct p_room_info_data_t {
     gboolean display;
 } ProfRoomInfoData;
 
-typedef struct p_id_handle_t {
-    ProfIdCallback func;
-    ProfIdFreeCallback free_func;
+typedef struct p_iq_handle_t {
+    ProfIqCallback func;
+    ProfIqFreeCallback free_func;
     void *userdata;
-} ProfIdHandler;
+} ProfIqHandler;
 
 typedef struct privilege_set_t {
     char *item;
@@ -205,7 +205,7 @@ _iq_handler(xmpp_conn_t *const conn, xmpp_stanza_t *const stanza, void *const us
 
     const char *id = xmpp_stanza_get_id(stanza);
     if (id) {
-        ProfIdHandler *handler = g_hash_table_lookup(id_handlers, id);
+        ProfIqHandler *handler = g_hash_table_lookup(id_handlers, id);
         if (handler) {
             int keep = handler->func(stanza, handler->userdata);
             if (!keep) {
@@ -234,7 +234,7 @@ iq_handlers_init(void)
         GList *keys = g_hash_table_get_keys(id_handlers);
         GList *curr = keys;
         while (curr) {
-            ProfIdHandler *handler = g_hash_table_lookup(id_handlers, curr->data);
+            ProfIqHandler *handler = g_hash_table_lookup(id_handlers, curr->data);
             if (handler->free_func && handler->userdata) {
                 handler->free_func(handler->userdata);
             }
@@ -248,9 +248,9 @@ iq_handlers_init(void)
 }
 
 void
-iq_id_handler_add(const char *const id, ProfIdCallback func, ProfIdFreeCallback free_func, void *userdata)
+iq_id_handler_add(const char *const id, ProfIqCallback func, ProfIqFreeCallback free_func, void *userdata)
 {
-    ProfIdHandler *handler = malloc(sizeof(ProfIdHandler));
+    ProfIqHandler *handler = malloc(sizeof(ProfIqHandler));
     handler->func = func;
     handler->free_func = free_func;
     handler->userdata = userdata;
@@ -438,7 +438,7 @@ iq_room_info_request(const char *const room, gboolean display_result)
     cb_data->room = strdup(room);
     cb_data->display = display_result;
 
-    iq_id_handler_add(id, _room_info_response_id_handler, (ProfIdFreeCallback)_iq_free_room_data, cb_data);
+    iq_id_handler_add(id, _room_info_response_id_handler, (ProfIqFreeCallback)_iq_free_room_data, cb_data);
 
     free(id);
 
@@ -651,7 +651,7 @@ iq_room_affiliation_set(const char *const room, const char *const jid, char *aff
     affiliation_set->item = strdup(jid);
     affiliation_set->privilege = strdup(affiliation);
 
-    iq_id_handler_add(id, _room_affiliation_set_result_id_handler, (ProfIdFreeCallback)_iq_free_affiliation_set, affiliation_set);
+    iq_id_handler_add(id, _room_affiliation_set_result_id_handler, (ProfIqFreeCallback)_iq_free_affiliation_set, affiliation_set);
 
     iq_send_stanza(iq);
     xmpp_stanza_release(iq);
@@ -670,7 +670,7 @@ iq_room_role_set(const char *const room, const char *const nick, char *role,
     role_set->item = strdup(nick);
     role_set->privilege = strdup(role);
 
-    iq_id_handler_add(id, _room_role_set_result_id_handler, (ProfIdFreeCallback)_iq_free_affiliation_set, role_set);
+    iq_id_handler_add(id, _room_role_set_result_id_handler, (ProfIqFreeCallback)_iq_free_affiliation_set, role_set);
 
     iq_send_stanza(iq);
     xmpp_stanza_release(iq);
@@ -697,7 +697,7 @@ iq_send_ping(const char *const target)
     const char *id = xmpp_stanza_get_id(iq);
 
     GDateTime *now = g_date_time_new_now_local();
-    iq_id_handler_add(id, _manual_pong_id_handler, (ProfIdFreeCallback)g_date_time_unref, now);
+    iq_id_handler_add(id, _manual_pong_id_handler, (ProfIqFreeCallback)g_date_time_unref, now);
 
     iq_send_stanza(iq);
     xmpp_stanza_release(iq);
@@ -2291,6 +2291,8 @@ _disco_info_response_id_handler_onconnect(xmpp_stanza_t *const stanza, void *con
         }
     }
 
+    connection_features_received(from);
+
     return 0;
 }
 
diff --git a/src/xmpp/iq.h b/src/xmpp/iq.h
index 025d5e9f..bc273db4 100644
--- a/src/xmpp/iq.h
+++ b/src/xmpp/iq.h
@@ -35,12 +35,12 @@
 #ifndef XMPP_IQ_H
 #define XMPP_IQ_H
 
-typedef int(*ProfIdCallback)(xmpp_stanza_t *const stanza, void *const userdata);
-typedef void(*ProfIdFreeCallback)(void *userdata);
+typedef int(*ProfIqCallback)(xmpp_stanza_t *const stanza, void *const userdata);
+typedef void(*ProfIqFreeCallback)(void *userdata);
 
 void iq_handlers_init(void);
 void iq_send_stanza(xmpp_stanza_t *const stanza);
-void iq_id_handler_add(const char *const id, ProfIdCallback func, ProfIdFreeCallback free_func, void *userdata);
+void iq_id_handler_add(const char *const id, ProfIqCallback func, ProfIqFreeCallback free_func, void *userdata);
 void iq_disco_info_request_onconnect(gchar *jid);
 void iq_disco_items_request_onconnect(gchar *jid);
 void iq_send_caps_request(const char *const to, const char *const id, const char *const node, const char *const ver);
diff --git a/src/xmpp/message.c b/src/xmpp/message.c
index adea5c10..47cf35d7 100644
--- a/src/xmpp/message.c
+++ b/src/xmpp/message.c
@@ -52,6 +52,7 @@
 #include "pgp/gpg.h"
 #include "plugins/plugins.h"
 #include "ui/ui.h"
+#include "ui/window_list.h"
 #include "xmpp/chat_session.h"
 #include "xmpp/muc.h"
 #include "xmpp/session.h"
@@ -62,6 +63,17 @@
 #include "xmpp/connection.h"
 #include "xmpp/xmpp.h"
 
+#ifdef HAVE_OMEMO
+#include "xmpp/omemo.h"
+#include "omemo/omemo.h"
+#endif
+
+typedef struct p_message_handle_t {
+    ProfMessageCallback func;
+    ProfMessageFreeCallback free_func;
+    void *userdata;
+} ProfMessageHandler;
+
 static int _message_handler(xmpp_conn_t *const conn, xmpp_stanza_t *const stanza, void *const userdata);
 
 static void _handle_error(xmpp_stanza_t *const stanza);
@@ -74,6 +86,8 @@ static void _handle_chat(xmpp_stanza_t *const stanza);
 
 static void _send_message_stanza(xmpp_stanza_t *const stanza);
 
+static GHashTable *pubsub_event_handlers;
+
 static int
 _message_handler(xmpp_conn_t *const conn, xmpp_stanza_t *const stanza, void *const userdata)
 {
@@ -118,6 +132,23 @@ _message_handler(xmpp_conn_t *const conn, xmpp_stanza_t *const stanza, void *con
         _handle_receipt_received(stanza);
     }
 
+    xmpp_stanza_t *event = xmpp_stanza_get_child_by_ns(stanza, STANZA_NS_PUBSUB_EVENT);
+    if (event) {
+        xmpp_stanza_t *child = xmpp_stanza_get_children(event);
+        if (child) {
+            const char *node = xmpp_stanza_get_attribute(child, STANZA_ATTR_NODE);
+            if (node) {
+                ProfMessageHandler *handler = g_hash_table_lookup(pubsub_event_handlers, node);
+                if (handler) {
+                    int keep = handler->func(stanza, handler->userdata);
+                    if (!keep) {
+                        g_hash_table_remove(pubsub_event_handlers, node);
+                    }
+                }
+            }
+        }
+    }
+
     _handle_chat(stanza);
 
     return 1;
@@ -129,6 +160,33 @@ message_handlers_init(void)
     xmpp_conn_t * const conn = connection_get_conn();
     xmpp_ctx_t * const ctx = connection_get_ctx();
     xmpp_handler_add(conn, _message_handler, NULL, STANZA_NAME_MESSAGE, NULL, ctx);
+
+    if (pubsub_event_handlers) {
+        GList *keys = g_hash_table_get_keys(pubsub_event_handlers);
+        GList *curr = keys;
+        while (curr) {
+            ProfMessageHandler *handler = g_hash_table_lookup(pubsub_event_handlers, curr->data);
+            if (handler->free_func && handler->userdata) {
+                handler->free_func(handler->userdata);
+            }
+            curr = g_list_next(curr);
+        }
+        g_list_free(keys);
+        g_hash_table_destroy(pubsub_event_handlers);
+    }
+
+    pubsub_event_handlers = g_hash_table_new_full(g_str_hash, g_str_equal, free, free);
+}
+
+void
+message_pubsub_event_handler_add(const char *const node, ProfMessageCallback func, ProfMessageFreeCallback free_func, void *userdata)
+{
+    ProfMessageHandler *handler = malloc(sizeof(ProfMessageHandler));
+    handler->func = func;
+    handler->free_func = free_func;
+    handler->userdata = userdata;
+
+    g_hash_table_insert(pubsub_event_handlers, strdup(node), handler);
 }
 
 char*
@@ -254,6 +312,118 @@ message_send_chat_otr(const char *const barejid, const char *const msg, gboolean
     return id;
 }
 
+#ifdef HAVE_OMEMO
+char*
+message_send_chat_omemo(const char *const jid, uint32_t sid, GList *keys,
+    const unsigned char *const iv, size_t iv_len,
+    const unsigned char *const ciphertext, size_t ciphertext_len,
+    gboolean request_receipt, gboolean muc)
+{
+    char *state = chat_session_get_state(jid);
+    xmpp_ctx_t * const ctx = connection_get_ctx();
+    char *id;
+    xmpp_stanza_t *message;
+    if (muc) {
+        id = connection_create_stanza_id("muc");
+        message = xmpp_message_new(ctx, STANZA_TYPE_GROUPCHAT, jid, id);
+        stanza_attach_origin_id(ctx, message, id);
+    } else {
+        id = connection_create_stanza_id("msg");
+        message = xmpp_message_new(ctx, STANZA_TYPE_CHAT, jid, id);
+    }
+
+    xmpp_stanza_t *encrypted = xmpp_stanza_new(ctx);
+    xmpp_stanza_set_name(encrypted, "encrypted");
+    xmpp_stanza_set_ns(encrypted, STANZA_NS_OMEMO);
+
+    xmpp_stanza_t *header = xmpp_stanza_new(ctx);
+    xmpp_stanza_set_name(header, "header");
+    char *sid_text = g_strdup_printf("%d", sid);
+    xmpp_stanza_set_attribute(header, "sid", sid_text);
+    g_free(sid_text);
+
+    GList *key_iter;
+    for (key_iter = keys; key_iter != NULL; key_iter = key_iter->next) {
+        omemo_key_t *key = (omemo_key_t *)key_iter->data;
+
+        xmpp_stanza_t *key_stanza = xmpp_stanza_new(ctx);
+        xmpp_stanza_set_name(key_stanza, "key");
+        char *rid = g_strdup_printf("%d", key->device_id);
+        xmpp_stanza_set_attribute(key_stanza, "rid", rid);
+        g_free(rid);
+        if (key->prekey) {
+            xmpp_stanza_set_attribute(key_stanza, "prekey", "true");
+        }
+
+        gchar *key_raw = g_base64_encode(key->data, key->length);
+        xmpp_stanza_t *key_text = xmpp_stanza_new(ctx);
+        xmpp_stanza_set_text(key_text, key_raw);
+        g_free(key_raw);
+
+        xmpp_stanza_add_child(key_stanza, key_text);
+        xmpp_stanza_add_child(header, key_stanza);
+        xmpp_stanza_release(key_text);
+        xmpp_stanza_release(key_stanza);
+    }
+
+    xmpp_stanza_t *iv_stanza = xmpp_stanza_new(ctx);
+    xmpp_stanza_set_name(iv_stanza, "iv");
+
+    gchar *iv_raw = g_base64_encode(iv, iv_len);
+    xmpp_stanza_t *iv_text = xmpp_stanza_new(ctx);
+    xmpp_stanza_set_text(iv_text, iv_raw);
+    g_free(iv_raw);
+
+    xmpp_stanza_add_child(iv_stanza, iv_text);
+    xmpp_stanza_add_child(header, iv_stanza);
+    xmpp_stanza_release(iv_text);
+    xmpp_stanza_release(iv_stanza);
+
+    xmpp_stanza_add_child(encrypted, header);
+    xmpp_stanza_release(header);
+
+    xmpp_stanza_t *payload = xmpp_stanza_new(ctx);
+    xmpp_stanza_set_name(payload, "payload");
+
+    gchar *ciphertext_raw = g_base64_encode(ciphertext, ciphertext_len);
+    xmpp_stanza_t *payload_text = xmpp_stanza_new(ctx);
+    xmpp_stanza_set_text(payload_text, ciphertext_raw);
+    g_free(ciphertext_raw);
+
+    xmpp_stanza_add_child(payload, payload_text);
+    xmpp_stanza_add_child(encrypted, payload);
+    xmpp_stanza_release(payload_text);
+    xmpp_stanza_release(payload);
+
+    xmpp_stanza_add_child(message, encrypted);
+    xmpp_stanza_release(encrypted);
+
+    xmpp_stanza_t *body = xmpp_stanza_new(ctx);
+    xmpp_stanza_set_name(body, "body");
+    xmpp_stanza_t *body_text = xmpp_stanza_new(ctx);
+    xmpp_stanza_set_text(body_text, "You received a message encrypted with OMEMO but your client doesn't support OMEMO.");
+    xmpp_stanza_add_child(body, body_text);
+    xmpp_stanza_release(body_text);
+    xmpp_stanza_add_child(message, body);
+    xmpp_stanza_release(body);
+
+    if (state) {
+        stanza_attach_state(ctx, message, state);
+    }
+
+    stanza_attach_hints_store(ctx, message);
+
+    if (request_receipt) {
+        stanza_attach_receipt_request(ctx, message);
+    }
+
+    _send_message_stanza(message);
+    xmpp_stanza_release(message);
+
+    return id;
+}
+#endif
+
 void
 message_send_private(const char *const fulljid, const char *const msg, const char *const oob_url)
 {
@@ -273,23 +443,24 @@ message_send_private(const char *const fulljid, const char *const msg, const cha
     xmpp_stanza_release(message);
 }
 
-void
+char*
 message_send_groupchat(const char *const roomjid, const char *const msg, const char *const oob_url)
 {
     xmpp_ctx_t * const ctx = connection_get_ctx();
     char *id = connection_create_stanza_id("muc");
 
     xmpp_stanza_t *message = xmpp_message_new(ctx, STANZA_TYPE_GROUPCHAT, roomjid, id);
+    stanza_attach_origin_id(ctx, message, id);
     xmpp_message_set_body(message, msg);
 
-    free(id);
-
     if (oob_url) {
         stanza_attach_x_oob_url(ctx, message, oob_url);
     }
 
     _send_message_stanza(message);
     xmpp_stanza_release(message);
+
+    return id;
 }
 
 void
@@ -518,6 +689,14 @@ _handle_groupchat(xmpp_stanza_t *const stanza)
 {
     xmpp_ctx_t *ctx = connection_get_ctx();
     char *message = NULL;
+
+    const char *id = xmpp_stanza_get_id(stanza);
+
+    xmpp_stanza_t *origin = xmpp_stanza_get_child_by_ns(stanza, STANZA_NS_STABLE_ID);
+    if (origin && g_strcmp0(xmpp_stanza_get_name(origin), STANZA_NAME_ORIGIN_ID) == 0) {
+        id = xmpp_stanza_get_attribute(origin, STANZA_ATTR_ID);
+    }
+
     const char *room_jid = xmpp_stanza_get_from(stanza);
     Jid *jid = jid_create(room_jid);
 
@@ -560,19 +739,28 @@ _handle_groupchat(xmpp_stanza_t *const stanza)
         return;
     }
 
-    message = xmpp_message_get_body(stanza);
+    // check omemo encryption
+    gboolean omemo = FALSE;
+#ifdef HAVE_OMEMO
+    message = omemo_receive_message(stanza);
+    omemo = message != NULL;
+#endif
+
     if (!message) {
-        jid_destroy(jid);
-        return;
+        message = xmpp_message_get_body(stanza);
+        if (!message) {
+            jid_destroy(jid);
+            return;
+        }
     }
 
     // determine if the notifications happened whilst offline
     GDateTime *timestamp = stanza_get_delay(stanza);
     if (timestamp) {
-        sv_ev_room_history(jid->barejid, jid->resourcepart, timestamp, message);
+        sv_ev_room_history(jid->barejid, jid->resourcepart, timestamp, message, omemo);
         g_date_time_unref(timestamp);
     } else {
-        sv_ev_room_message(jid->barejid, jid->resourcepart, message);
+        sv_ev_room_message(jid->barejid, jid->resourcepart, message, id, omemo);
     }
 
     xmpp_free(ctx, message);
@@ -675,6 +863,7 @@ _private_chat_handler(xmpp_stanza_t *const stanza, const char *const fulljid)
 static gboolean
 _handle_carbons(xmpp_stanza_t *const stanza)
 {
+    char *message_txt = NULL;
     xmpp_stanza_t *carbons = xmpp_stanza_get_child_by_ns(stanza, STANZA_NS_CARBONS);
     if (!carbons) {
         return FALSE;
@@ -708,10 +897,19 @@ _handle_carbons(xmpp_stanza_t *const stanza)
         return TRUE;
     }
 
-    char *message_txt = xmpp_message_get_body(message);
+    // check omemo encryption
+    gboolean omemo = FALSE;
+#ifdef HAVE_OMEMO
+    message_txt = omemo_receive_message(message);
+    omemo = message_txt != NULL;
+#endif
+
     if (!message_txt) {
-        log_warning("Carbon received with no message.");
-        return TRUE;
+        message_txt = xmpp_message_get_body(message);
+        if (!message_txt) {
+            log_warning("Carbon received with no message.");
+            return TRUE;
+        }
     }
 
     Jid *my_jid = jid_create(connection_get_fulljid());
@@ -739,11 +937,11 @@ _handle_carbons(xmpp_stanza_t *const stanza)
 
     // if we are the recipient, treat as standard incoming message
     if (g_strcmp0(my_jid->barejid, jid_to->barejid) == 0) {
-        sv_ev_incoming_carbon(jid_from->barejid, jid_from->resourcepart, message_txt, enc_message);
+        sv_ev_incoming_carbon(jid_from->barejid, jid_from->resourcepart, message_txt, enc_message, omemo);
 
     // else treat as a sent message
     } else {
-        sv_ev_outgoing_carbon(jid_to->barejid, message_txt, enc_message);
+        sv_ev_outgoing_carbon(jid_to->barejid, message_txt, enc_message, omemo);
     }
 
     xmpp_ctx_t *ctx = connection_get_ctx();
@@ -760,6 +958,7 @@ _handle_carbons(xmpp_stanza_t *const stanza)
 static void
 _handle_chat(xmpp_stanza_t *const stanza)
 {
+    char *message = NULL;
     // ignore if type not chat or absent
     const char *type = xmpp_stanza_get_type(stanza);
     if (!(g_strcmp0(type, "chat") == 0 || type == NULL)) {
@@ -772,6 +971,13 @@ _handle_chat(xmpp_stanza_t *const stanza)
         return;
     }
 
+    // check omemo encryption
+    gboolean omemo = FALSE;
+#ifdef HAVE_OMEMO
+    message = omemo_receive_message(stanza);
+    omemo = message != NULL;
+#endif
+
     // ignore handled namespaces
     xmpp_stanza_t *conf = xmpp_stanza_get_child_by_ns(stanza, STANZA_NS_CONFERENCE);
     xmpp_stanza_t *captcha = xmpp_stanza_get_child_by_ns(stanza, STANZA_NS_CAPTCHA);
@@ -801,19 +1007,24 @@ _handle_chat(xmpp_stanza_t *const stanza)
     // standard chat message, use jid without resource
     xmpp_ctx_t *ctx = connection_get_ctx();
     GDateTime *timestamp = stanza_get_delay(stanza);
-    if (body) {
-        char *message = xmpp_stanza_get_text(body);
-        if (message) {
-            char *enc_message = NULL;
-            xmpp_stanza_t *x = xmpp_stanza_get_child_by_ns(stanza, STANZA_NS_ENCRYPTED);
-            if (x) {
-                enc_message = xmpp_stanza_get_text(x);
-            }
-            sv_ev_incoming_message(jid->barejid, jid->resourcepart, message, enc_message, timestamp);
-            xmpp_free(ctx, enc_message);
+    if (!message && body) {
+        message = xmpp_stanza_get_text(body);
+    }
 
-            _receipt_request_handler(stanza);
+    if (message) {
+        char *enc_message = NULL;
+        xmpp_stanza_t *x = xmpp_stanza_get_child_by_ns(stanza, STANZA_NS_ENCRYPTED);
+        if (x) {
+            enc_message = xmpp_stanza_get_text(x);
+        }
+        sv_ev_incoming_message(jid->barejid, jid->resourcepart, message, enc_message, timestamp, omemo);
+        xmpp_free(ctx, enc_message);
 
+        _receipt_request_handler(stanza);
+
+        if (omemo) {
+            free(message);
+        } else {
             xmpp_free(ctx, message);
         }
     }
diff --git a/src/xmpp/message.h b/src/xmpp/message.h
index dee9be2d..0c81ca39 100644
--- a/src/xmpp/message.h
+++ b/src/xmpp/message.h
@@ -35,6 +35,10 @@
 #ifndef XMPP_MESSAGE_H
 #define XMPP_MESSAGE_H
 
+typedef int(*ProfMessageCallback)(xmpp_stanza_t *const stanza, void *const userdata);
+typedef void(*ProfMessageFreeCallback)(void *userdata);
+
 void message_handlers_init(void);
+void message_pubsub_event_handler_add(const char *const node, ProfMessageCallback func, ProfMessageFreeCallback free_func, void *userdata);
 
 #endif
diff --git a/src/xmpp/omemo.c b/src/xmpp/omemo.c
new file mode 100644
index 00000000..4b77ef23
--- /dev/null
+++ b/src/xmpp/omemo.c
@@ -0,0 +1,448 @@
+#include <glib.h>
+
+#include "log.h"
+#include "xmpp/connection.h"
+#include "xmpp/form.h"
+#include "xmpp/iq.h"
+#include "xmpp/message.h"
+#include "xmpp/stanza.h"
+
+#include "omemo/omemo.h"
+
+static int _omemo_receive_devicelist(xmpp_stanza_t *const stanza, void *const userdata);
+static int _omemo_bundle_publish_result(xmpp_stanza_t *const stanza, void *const userdata);
+static int _omemo_bundle_publish_configure(xmpp_stanza_t *const stanza, void *const userdata);
+static int _omemo_bundle_publish_configure_result(xmpp_stanza_t *const stanza, void *const userdata);
+
+void
+omemo_devicelist_subscribe(void)
+{
+    message_pubsub_event_handler_add(STANZA_NS_OMEMO_DEVICELIST, _omemo_receive_devicelist, NULL, NULL);
+
+    caps_add_feature(XMPP_FEATURE_OMEMO_DEVICELIST_NOTIFY);
+}
+
+void
+omemo_devicelist_publish(GList *device_list)
+{
+    xmpp_ctx_t * const ctx = connection_get_ctx();
+    xmpp_stanza_t *iq = stanza_create_omemo_devicelist_publish(ctx, device_list);
+
+    if (connection_supports(XMPP_FEATURE_PUBSUB_PUBLISH_OPTIONS)) {
+        stanza_attach_publish_options(ctx, iq, "pubsub#access_model", "open");
+    }
+
+    iq_send_stanza(iq);
+    xmpp_stanza_release(iq);
+}
+
+void
+omemo_devicelist_request(const char * const jid)
+{
+    xmpp_ctx_t * const ctx = connection_get_ctx();
+    char *id = connection_create_stanza_id("devicelist_request");
+
+    xmpp_stanza_t *iq = stanza_create_omemo_devicelist_request(ctx, id, jid);
+    iq_id_handler_add(id, _omemo_receive_devicelist, NULL, NULL);
+
+    iq_send_stanza(iq);
+
+    free(id);
+    xmpp_stanza_release(iq);
+}
+
+void
+omemo_bundle_publish(gboolean first)
+{
+    xmpp_ctx_t * const ctx = connection_get_ctx();
+    unsigned char *identity_key = NULL;
+    size_t identity_key_length;
+    unsigned char *signed_prekey = NULL;
+    size_t signed_prekey_length;
+    unsigned char *signed_prekey_signature = NULL;
+    size_t signed_prekey_signature_length;
+    GList *prekeys = NULL, *ids = NULL, *lengths = NULL;
+
+    omemo_identity_key(&identity_key, &identity_key_length);
+    omemo_signed_prekey(&signed_prekey, &signed_prekey_length);
+    omemo_signed_prekey_signature(&signed_prekey_signature, &signed_prekey_signature_length);
+    omemo_prekeys(&prekeys, &ids, &lengths);
+
+    char *id = connection_create_stanza_id("omemo_bundle_publish");
+    xmpp_stanza_t *iq = stanza_create_omemo_bundle_publish(ctx, id,
+        omemo_device_id(), identity_key, identity_key_length, signed_prekey,
+        signed_prekey_length, signed_prekey_signature,
+        signed_prekey_signature_length, prekeys, ids, lengths);
+
+    g_list_free_full(prekeys, free);
+    g_list_free(lengths);
+    g_list_free(ids);
+
+    if (connection_supports(XMPP_FEATURE_PUBSUB_PUBLISH_OPTIONS)) {
+        stanza_attach_publish_options(ctx, iq, "pubsub#access_model", "open");
+    }
+
+    iq_id_handler_add(id, _omemo_bundle_publish_result, NULL, GINT_TO_POINTER(first));
+
+    iq_send_stanza(iq);
+
+    xmpp_stanza_release(iq);
+    free(identity_key);
+    free(signed_prekey);
+    free(signed_prekey_signature);
+    free(id);
+}
+
+void
+omemo_bundle_request(const char * const jid, uint32_t device_id, ProfIqCallback func, ProfIqFreeCallback free_func, void *userdata)
+{
+    xmpp_ctx_t * const ctx = connection_get_ctx();
+    char *id = connection_create_stanza_id("bundle_request");
+
+    xmpp_stanza_t *iq = stanza_create_omemo_bundle_request(ctx, id, jid, device_id);
+    iq_id_handler_add(id, func, free_func, userdata);
+
+    iq_send_stanza(iq);
+
+    free(id);
+    xmpp_stanza_release(iq);
+}
+
+int
+omemo_start_device_session_handle_bundle(xmpp_stanza_t *const stanza, void *const userdata)
+{
+    char *from = NULL;
+    const char *from_attr = xmpp_stanza_get_attribute(stanza, STANZA_ATTR_FROM);
+    if (!from_attr) {
+        Jid *jid = jid_create(connection_get_fulljid());
+        from = strdup(jid->barejid);
+        jid_destroy(jid);
+    } else {
+        from = strdup(from_attr);
+    }
+
+    if (g_strcmp0(from, userdata) != 0) {
+        return 1;
+    }
+
+    xmpp_stanza_t *pubsub = xmpp_stanza_get_child_by_ns(stanza, STANZA_NS_PUBSUB);
+    if (!pubsub) {
+        return 1;
+    }
+
+    xmpp_stanza_t *items = xmpp_stanza_get_child_by_name(pubsub, "items");
+    if (!items) {
+        return 1;
+    }
+    const char *node = xmpp_stanza_get_attribute(items, "node");
+    char *device_id_str = strstr(node, ":");
+    if (!device_id_str) {
+        return 1;
+    }
+
+    uint32_t device_id = strtoul(++device_id_str, NULL, 10);
+
+    xmpp_stanza_t *item = xmpp_stanza_get_child_by_name(items, "item");
+    if (!item) {
+        return 1;
+    }
+
+    xmpp_stanza_t *bundle = xmpp_stanza_get_child_by_ns(item, STANZA_NS_OMEMO);
+    if (!bundle) {
+        return 1;
+    }
+
+    xmpp_stanza_t *prekeys = xmpp_stanza_get_child_by_name(bundle, "prekeys");
+    if (!prekeys) {
+        return 1;
+    }
+
+    GList *prekeys_list = NULL;
+    xmpp_stanza_t *prekey;
+    for (prekey = xmpp_stanza_get_children(prekeys); prekey != NULL; prekey = xmpp_stanza_get_next(prekey)) {
+        omemo_key_t *key = malloc(sizeof(omemo_key_t));
+
+        const char *prekey_id_text = xmpp_stanza_get_attribute(prekey, "preKeyId");
+        if (!prekey_id_text) {
+            return 1;
+        }
+        key->id = strtoul(prekey_id_text, NULL, 10);
+        xmpp_stanza_t *prekey_text = xmpp_stanza_get_children(prekey);
+        if (!prekey_text) {
+            return 1;
+        }
+        char *prekey_b64 = xmpp_stanza_get_text(prekey_text);
+        key->data = g_base64_decode(prekey_b64, &key->length);
+        free(prekey_b64);
+        key->prekey = TRUE;
+        key->device_id = device_id;
+
+        prekeys_list = g_list_append(prekeys_list, key);
+    }
+
+    xmpp_stanza_t *signed_prekey = xmpp_stanza_get_child_by_name(bundle, "signedPreKeyPublic");
+    if (!signed_prekey) {
+        return 1;
+    }
+    const char *signed_prekey_id_text = xmpp_stanza_get_attribute(signed_prekey, "signedPreKeyId");
+    if (!signed_prekey_id_text) {
+        return 1;
+    }
+    uint32_t signed_prekey_id = strtoul(signed_prekey_id_text, NULL, 10);
+    xmpp_stanza_t *signed_prekey_text = xmpp_stanza_get_children(signed_prekey);
+    if (!signed_prekey_text) {
+        return 1;
+    }
+    size_t signed_prekey_len;
+    char *signed_prekey_b64 = xmpp_stanza_get_text(signed_prekey_text);
+    unsigned char *signed_prekey_raw = g_base64_decode(signed_prekey_b64, &signed_prekey_len);
+    free(signed_prekey_b64);
+
+    xmpp_stanza_t *signed_prekey_signature = xmpp_stanza_get_child_by_name(bundle, "signedPreKeySignature");
+    if (!signed_prekey_signature) {
+        return 1;
+    }
+    xmpp_stanza_t *signed_prekey_signature_text = xmpp_stanza_get_children(signed_prekey_signature);
+    if (!signed_prekey_signature_text) {
+        return 1;
+    }
+    size_t signed_prekey_signature_len;
+    char *signed_prekey_signature_b64 = xmpp_stanza_get_text(signed_prekey_signature_text);
+    unsigned char *signed_prekey_signature_raw = g_base64_decode(signed_prekey_signature_b64, &signed_prekey_signature_len);
+    free(signed_prekey_signature_b64);
+
+    xmpp_stanza_t *identity_key = xmpp_stanza_get_child_by_name(bundle, "identityKey");
+    if (!identity_key) {
+        return 1;
+    }
+    xmpp_stanza_t *identity_key_text = xmpp_stanza_get_children(identity_key);
+    if (!identity_key_text) {
+        return 1;
+    }
+    size_t identity_key_len;
+    char *identity_key_b64 = xmpp_stanza_get_text(identity_key_text);
+    unsigned char *identity_key_raw = g_base64_decode(identity_key_b64, &identity_key_len);
+    free(identity_key_b64);
+
+    omemo_start_device_session(from, device_id, prekeys_list, signed_prekey_id,
+        signed_prekey_raw, signed_prekey_len, signed_prekey_signature_raw,
+        signed_prekey_signature_len, identity_key_raw, identity_key_len);
+
+    free(from);
+    g_list_free_full(prekeys_list, (GDestroyNotify)omemo_key_free);
+    g_free(signed_prekey_raw);
+    g_free(identity_key_raw);
+    g_free(signed_prekey_signature_raw);
+    return 1;
+}
+
+char *
+omemo_receive_message(xmpp_stanza_t *const stanza)
+{
+    const char *type = xmpp_stanza_get_type(stanza);
+
+    xmpp_stanza_t *encrypted = xmpp_stanza_get_child_by_ns(stanza, STANZA_NS_OMEMO);
+    if (!encrypted) {
+        return NULL;
+    }
+
+    xmpp_stanza_t *header = xmpp_stanza_get_child_by_name(encrypted, "header");
+    if (!header) {
+        return NULL;
+    }
+
+    const char *sid_text = xmpp_stanza_get_attribute(header, "sid");
+    if (!sid_text) {
+        return NULL;
+    }
+    uint32_t sid = strtoul(sid_text, NULL, 10);
+
+    xmpp_stanza_t *iv = xmpp_stanza_get_child_by_name(header, "iv");
+    if (!iv) {
+        return NULL;
+    }
+    char *iv_text = xmpp_stanza_get_text(iv);
+    if (!iv_text) {
+        return NULL;
+    }
+    size_t iv_len;
+    unsigned char *iv_raw = g_base64_decode(iv_text, &iv_len);
+
+    xmpp_stanza_t *payload = xmpp_stanza_get_child_by_name(encrypted, "payload");
+    if (!payload) {
+        return NULL;
+    }
+    char *payload_text = xmpp_stanza_get_text(payload);
+    if (!payload_text) {
+        return NULL;
+    }
+    size_t payload_len;
+    unsigned char *payload_raw = g_base64_decode(payload_text, &payload_len);
+
+    GList *keys = NULL;
+    xmpp_stanza_t *key_stanza;
+    for (key_stanza = xmpp_stanza_get_children(header); key_stanza != NULL; key_stanza = xmpp_stanza_get_next(key_stanza)) {
+        if (g_strcmp0(xmpp_stanza_get_name(key_stanza), "key") != 0) {
+            continue;
+        }
+
+        omemo_key_t *key = malloc(sizeof(omemo_key_t));
+        char *key_text = xmpp_stanza_get_text(key_stanza);
+        if (!key_text) {
+            goto skip;
+        }
+
+
+        const char *rid_text = xmpp_stanza_get_attribute(key_stanza, "rid");
+        key->device_id = strtoul(rid_text, NULL, 10);
+        if (!key->device_id) {
+            goto skip;
+        }
+        key->data = g_base64_decode(key_text, &key->length);
+        free(key_text);
+        key->prekey = g_strcmp0(xmpp_stanza_get_attribute(key_stanza, "prekey"), "true") == 0;
+        keys = g_list_append(keys, key);
+        continue;
+
+skip:
+        free(key);
+    }
+
+    const char *from = xmpp_stanza_get_from(stanza);
+
+    char *plaintext = omemo_on_message_recv(from, sid, iv_raw, iv_len,
+        keys, payload_raw, payload_len,
+        g_strcmp0(type, STANZA_TYPE_GROUPCHAT) == 0);
+
+    g_list_free_full(keys, (GDestroyNotify)omemo_key_free);
+    g_free(iv_raw);
+    g_free(payload_raw);
+    g_free(iv_text);
+    g_free(payload_text);
+
+    return plaintext;
+}
+
+static int
+_omemo_receive_devicelist(xmpp_stanza_t *const stanza, void *const userdata)
+{
+    GList *device_list = NULL;
+    const char *from = xmpp_stanza_get_attribute(stanza, STANZA_ATTR_FROM);
+
+    xmpp_stanza_t *root = NULL;
+    xmpp_stanza_t *event = xmpp_stanza_get_child_by_ns(stanza, STANZA_NS_PUBSUB_EVENT);
+    if (event) {
+        root = event;
+    }
+
+    xmpp_stanza_t *pubsub = xmpp_stanza_get_child_by_ns(stanza, STANZA_NS_PUBSUB);
+    if (pubsub) {
+        root = pubsub;
+    }
+
+    if (!root) {
+        return 1;
+    }
+
+    xmpp_stanza_t *items = xmpp_stanza_get_child_by_name(root, "items");
+    if (!items) {
+        return 1;
+    }
+
+    xmpp_stanza_t *item = xmpp_stanza_get_child_by_name(items, "item");
+    if (item) {
+        xmpp_stanza_t *list = xmpp_stanza_get_child_by_ns(item, STANZA_NS_OMEMO);
+        if (!list) {
+            return 1;
+        }
+
+        xmpp_stanza_t *device;
+        for (device = xmpp_stanza_get_children(list); device != NULL; device = xmpp_stanza_get_next(device)) {
+            const char *id = xmpp_stanza_get_id(device);
+            device_list = g_list_append(device_list, GINT_TO_POINTER(strtoul(id, NULL, 10)));
+        }
+    }
+    omemo_set_device_list(from, device_list);
+
+    return 1;
+}
+
+static int
+_omemo_bundle_publish_result(xmpp_stanza_t *const stanza, void *const userdata)
+{
+    const char *type = xmpp_stanza_get_type(stanza);
+
+    if (g_strcmp0(type, STANZA_TYPE_ERROR) != 0) {
+        return 0;
+    }
+
+    if (!GPOINTER_TO_INT(userdata)) {
+        log_error("OMEMO: definitely cannot publish bundle with an open access model");
+        return 0;
+    }
+
+    log_info("OMEMO: cannot publish bundle with open access model, trying to configure node");
+    xmpp_ctx_t * const ctx = connection_get_ctx();
+    Jid *jid = jid_create(connection_get_fulljid());
+    char *id = connection_create_stanza_id("omemo_bundle_node_configure_request");
+    char *node = g_strdup_printf("%s:%d", STANZA_NS_OMEMO_BUNDLES, omemo_device_id());
+    xmpp_stanza_t *iq = stanza_create_pubsub_configure_request(ctx, id, jid->barejid, node);
+    g_free(node);
+
+    iq_id_handler_add(id, _omemo_bundle_publish_configure, NULL, userdata);
+
+    iq_send_stanza(iq);
+
+    xmpp_stanza_release(iq);
+    free(id);
+    jid_destroy(jid);
+    return 0;
+}
+
+static int
+_omemo_bundle_publish_configure(xmpp_stanza_t *const stanza, void *const userdata)
+{
+    /* TODO handle error */
+    xmpp_stanza_t *pubsub = xmpp_stanza_get_child_by_name(stanza, "pubsub");
+    xmpp_stanza_t *configure = xmpp_stanza_get_child_by_name(pubsub, STANZA_NAME_CONFIGURE);
+    xmpp_stanza_t *x = xmpp_stanza_get_child_by_name(configure, "x");
+
+    DataForm* form = form_create(x);
+    char *tag = g_hash_table_lookup(form->var_to_tag, "pubsub#access_model");
+    if (!tag) {
+        log_info("OMEMO: cannot configure bundle to an open access model");
+        return 0;
+    }
+    form_set_value(form, tag, "open");
+
+    xmpp_ctx_t * const ctx = connection_get_ctx();
+    Jid *jid = jid_create(connection_get_fulljid());
+    char *id = connection_create_stanza_id("omemo_bundle_node_configure_submit");
+    char *node = g_strdup_printf("%s:%d", STANZA_NS_OMEMO_BUNDLES, omemo_device_id());
+    xmpp_stanza_t *iq = stanza_create_pubsub_configure_submit(ctx, id, jid->barejid, node, form);
+    g_free(node);
+
+    iq_id_handler_add(id, _omemo_bundle_publish_configure_result, NULL, userdata);
+
+    iq_send_stanza(iq);
+
+    xmpp_stanza_release(iq);
+    free(id);
+    jid_destroy(jid);
+    return 0;
+}
+
+static int
+_omemo_bundle_publish_configure_result(xmpp_stanza_t *const stanza, void *const userdata)
+{
+    const char *type = xmpp_stanza_get_type(stanza);
+
+    if (g_strcmp0(type, STANZA_TYPE_ERROR) == 0) {
+        log_error("OMEMO: cannot configure bundle to an open access model");
+        return 0;
+    }
+
+    omemo_bundle_publish(TRUE);
+
+    return 0;
+}
diff --git a/src/xmpp/omemo.h b/src/xmpp/omemo.h
new file mode 100644
index 00000000..f1fff7b7
--- /dev/null
+++ b/src/xmpp/omemo.h
@@ -0,0 +1,11 @@
+#include <glib.h>
+
+#include "xmpp/iq.h"
+
+void omemo_devicelist_subscribe(void);
+void omemo_devicelist_publish(GList *device_list);
+void omemo_devicelist_request(const char * const jid);
+void omemo_bundle_publish(gboolean first);
+void omemo_bundle_request(const char * const jid, uint32_t device_id, ProfIqCallback func, ProfIqFreeCallback free_func, void *userdata);
+int omemo_start_device_session_handle_bundle(xmpp_stanza_t *const stanza, void *const userdata);
+char * omemo_receive_message(xmpp_stanza_t *const stanza);
diff --git a/src/xmpp/roster.c b/src/xmpp/roster.c
index 9be154e7..fe15515f 100644
--- a/src/xmpp/roster.c
+++ b/src/xmpp/roster.c
@@ -137,7 +137,7 @@ roster_send_add_to_group(const char *const group, PContact contact)
     }
 
     xmpp_ctx_t * const ctx = connection_get_ctx();
-    iq_id_handler_add(unique_id, _group_add_id_handler, (ProfIdFreeCallback)_free_group_data, data);
+    iq_id_handler_add(unique_id, _group_add_id_handler, (ProfIqFreeCallback)_free_group_data, data);
     xmpp_stanza_t *iq = stanza_create_roster_set(ctx, unique_id, p_contact_barejid(contact),
         p_contact_name(contact), new_groups);
     iq_send_stanza(iq);
@@ -180,7 +180,7 @@ roster_send_remove_from_group(const char *const group, PContact contact)
         data->name = strdup(p_contact_barejid(contact));
     }
 
-    iq_id_handler_add(unique_id, _group_remove_id_handler, (ProfIdFreeCallback)_free_group_data, data);
+    iq_id_handler_add(unique_id, _group_remove_id_handler, (ProfIqFreeCallback)_free_group_data, data);
     xmpp_stanza_t *iq = stanza_create_roster_set(ctx, unique_id, p_contact_barejid(contact),
         p_contact_name(contact), new_groups);
     iq_send_stanza(iq);
diff --git a/src/xmpp/session.c b/src/xmpp/session.c
index de7fb7ac..675f23af 100644
--- a/src/xmpp/session.c
+++ b/src/xmpp/session.c
@@ -60,6 +60,11 @@
 #include "xmpp/chat_session.h"
 #include "xmpp/jid.h"
 
+#ifdef HAVE_OMEMO
+#include "omemo/omemo.h"
+#include "xmpp/omemo.h"
+#endif
+
 // for auto reconnect
 static struct {
     char *name;
@@ -286,6 +291,12 @@ session_get_account_name(void)
 void
 session_login_success(gboolean secured)
 {
+    chat_sessions_init();
+
+    message_handlers_init();
+    presence_handlers_init();
+    iq_handlers_init();
+
     // logged in with account
     if (saved_account.name) {
         log_debug("Connection handler: logged in with account name: %s", saved_account.name);
@@ -297,26 +308,20 @@ session_login_success(gboolean secured)
         accounts_add(saved_details.name, saved_details.altdomain, saved_details.port, saved_details.tls_policy);
         accounts_set_jid(saved_details.name, saved_details.jid);
 
-        sv_ev_login_account_success(saved_details.name, secured);
         saved_account.name = strdup(saved_details.name);
         saved_account.passwd = strdup(saved_details.passwd);
 
         _session_free_saved_details();
+        sv_ev_login_account_success(saved_account.name, secured);
     }
 
-    chat_sessions_init();
-
-    message_handlers_init();
-    presence_handlers_init();
-    iq_handlers_init();
-
     roster_request();
     bookmark_request();
     blocking_request();
 
     // items discovery
+    connection_request_features();
     char *domain = connection_get_domain();
-    iq_disco_info_request_onconnect(domain);
     iq_disco_items_request_onconnect(domain);
 
     if (prefs_get_boolean(PREF_CARBONS)){
diff --git a/src/xmpp/stanza.c b/src/xmpp/stanza.c
index 534ee06b..615de44f 100644
--- a/src/xmpp/stanza.c
+++ b/src/xmpp/stanza.c
@@ -396,6 +396,18 @@ stanza_attach_hints_no_store(xmpp_ctx_t *ctx, xmpp_stanza_t *stanza)
 }
 
 xmpp_stanza_t*
+stanza_attach_hints_store(xmpp_ctx_t *ctx, xmpp_stanza_t *stanza)
+{
+    xmpp_stanza_t *store = xmpp_stanza_new(ctx);
+    xmpp_stanza_set_name(store, "store");
+    xmpp_stanza_set_ns(store, STANZA_NS_HINTS);
+    xmpp_stanza_add_child(stanza, store);
+    xmpp_stanza_release(store);
+
+    return stanza;
+}
+
+xmpp_stanza_t*
 stanza_attach_receipt_request(xmpp_ctx_t *ctx, xmpp_stanza_t *stanza)
 {
     xmpp_stanza_t *receipet_request = xmpp_stanza_new(ctx);
@@ -1821,6 +1833,45 @@ stanza_get_error_message(xmpp_stanza_t *stanza)
 }
 
 void
+stanza_attach_publish_options(xmpp_ctx_t *const ctx, xmpp_stanza_t *const iq, const char *const option, const char *const value)
+{
+    xmpp_stanza_t *publish_options = xmpp_stanza_new(ctx);
+    xmpp_stanza_set_name(publish_options, STANZA_NAME_PUBLISH_OPTIONS);
+
+    xmpp_stanza_t *x = xmpp_stanza_new(ctx);
+    xmpp_stanza_set_name(x, STANZA_NAME_X);
+    xmpp_stanza_set_ns(x, STANZA_NS_DATA);
+    xmpp_stanza_set_type(x, "submit");
+    xmpp_stanza_add_child(publish_options, x);
+
+    xmpp_stanza_t *form_type = xmpp_stanza_new(ctx);
+    xmpp_stanza_set_name(form_type, STANZA_NAME_FIELD);
+    xmpp_stanza_set_attribute(form_type, STANZA_ATTR_VAR, "FORM_TYPE");
+    xmpp_stanza_set_type(form_type, "hidden");
+    xmpp_stanza_t *form_type_value = xmpp_stanza_new(ctx);
+    xmpp_stanza_set_name(form_type_value, STANZA_NAME_VALUE);
+    xmpp_stanza_t *form_type_value_text = xmpp_stanza_new(ctx);
+    xmpp_stanza_set_text(form_type_value_text, XMPP_FEATURE_PUBSUB_PUBLISH_OPTIONS);
+    xmpp_stanza_add_child(form_type_value, form_type_value_text);
+    xmpp_stanza_add_child(form_type, form_type_value);
+    xmpp_stanza_add_child(x, form_type);
+
+    xmpp_stanza_t *access_model = xmpp_stanza_new(ctx);
+    xmpp_stanza_set_name(access_model, STANZA_NAME_FIELD);
+    xmpp_stanza_set_attribute(access_model, STANZA_ATTR_VAR, option);
+    xmpp_stanza_t *access_model_value = xmpp_stanza_new(ctx);
+    xmpp_stanza_set_name(access_model_value, STANZA_NAME_VALUE);
+    xmpp_stanza_t *access_model_value_text = xmpp_stanza_new(ctx);
+    xmpp_stanza_set_text(access_model_value_text, value);
+    xmpp_stanza_add_child(access_model_value, access_model_value_text);
+    xmpp_stanza_add_child(access_model, access_model_value);
+    xmpp_stanza_add_child(x, access_model);
+
+    xmpp_stanza_t *pubsub = xmpp_stanza_get_child_by_ns(iq, STANZA_NS_PUBSUB);
+    xmpp_stanza_add_child(pubsub, publish_options);
+}
+
+void
 stanza_attach_priority(xmpp_ctx_t *const ctx, xmpp_stanza_t *const presence, const int pri)
 {
     if (pri == 0) {
@@ -2092,6 +2143,295 @@ stanza_create_command_config_submit_iq(xmpp_ctx_t *ctx, const char *const room,
     return iq;
 }
 
+xmpp_stanza_t*
+stanza_create_omemo_devicelist_request(xmpp_ctx_t *ctx, const char *const id,
+    const char *const jid)
+{
+    xmpp_stanza_t *iq = xmpp_iq_new(ctx, STANZA_TYPE_GET, id);
+    xmpp_stanza_set_to(iq, jid);
+
+    xmpp_stanza_t *pubsub = xmpp_stanza_new(ctx);
+    xmpp_stanza_set_name(pubsub, STANZA_NAME_PUBSUB);
+    xmpp_stanza_set_ns(pubsub, STANZA_NS_PUBSUB);
+
+    xmpp_stanza_t *items = xmpp_stanza_new(ctx);
+    xmpp_stanza_set_name(items, "items");
+    xmpp_stanza_set_attribute(items, STANZA_ATTR_NODE, STANZA_NS_OMEMO_DEVICELIST);
+
+    xmpp_stanza_add_child(pubsub, items);
+    xmpp_stanza_add_child(iq, pubsub);
+
+    xmpp_stanza_release(items);
+    xmpp_stanza_release(pubsub);
+
+    return iq;
+}
+
+xmpp_stanza_t*
+stanza_create_omemo_devicelist_subscribe(xmpp_ctx_t *ctx, const char *const jid)
+{
+    char *id = connection_create_stanza_id("omemo_devicelist_subscribe");
+    xmpp_stanza_t *iq = xmpp_iq_new(ctx, STANZA_TYPE_SET, id);
+    free(id);
+
+    xmpp_stanza_t *pubsub = xmpp_stanza_new(ctx);
+    xmpp_stanza_set_name(pubsub, STANZA_NAME_PUBSUB);
+    xmpp_stanza_set_ns(pubsub, STANZA_NS_PUBSUB);
+
+    xmpp_stanza_t *subscribe = xmpp_stanza_new(ctx);
+    xmpp_stanza_set_name(subscribe, STANZA_NAME_SUBSCRIBE);
+    xmpp_stanza_set_attribute(subscribe, STANZA_ATTR_NODE, STANZA_NS_OMEMO_DEVICELIST);
+    xmpp_stanza_set_attribute(subscribe, "jid", jid);
+
+    xmpp_stanza_add_child(pubsub, subscribe);
+    xmpp_stanza_add_child(iq, pubsub);
+
+    xmpp_stanza_release(subscribe);
+    xmpp_stanza_release(pubsub);
+
+    return iq;
+}
+
+xmpp_stanza_t*
+stanza_create_omemo_devicelist_publish(xmpp_ctx_t *ctx, GList *const ids)
+{
+    char *id = connection_create_stanza_id("omemo_devicelist_publish");
+    xmpp_stanza_t *iq = xmpp_iq_new(ctx, STANZA_TYPE_SET, id);
+    free(id);
+
+    xmpp_stanza_t *pubsub = xmpp_stanza_new(ctx);
+    xmpp_stanza_set_name(pubsub, STANZA_NAME_PUBSUB);
+    xmpp_stanza_set_ns(pubsub, STANZA_NS_PUBSUB);
+
+    xmpp_stanza_t *publish = xmpp_stanza_new(ctx);
+    xmpp_stanza_set_name(publish, STANZA_NAME_PUBLISH);
+    xmpp_stanza_set_attribute(publish, STANZA_ATTR_NODE, STANZA_NS_OMEMO_DEVICELIST);
+
+    xmpp_stanza_t *item = xmpp_stanza_new(ctx);
+    xmpp_stanza_set_name(item, STANZA_NAME_ITEM);
+    xmpp_stanza_set_attribute(item, "id", "current");
+
+    xmpp_stanza_t *list = xmpp_stanza_new(ctx);
+    xmpp_stanza_set_name(list, "list");
+    xmpp_stanza_set_ns(list, "eu.siacs.conversations.axolotl");
+
+    GList *i;
+    for (i = ids; i != NULL; i = i->next) {
+        xmpp_stanza_t *device = xmpp_stanza_new(ctx);
+        xmpp_stanza_set_name(device, "device");
+        char *id = g_strdup_printf("%d", GPOINTER_TO_INT(i->data));
+        xmpp_stanza_set_attribute(device, "id", id);
+        g_free(id);
+
+        xmpp_stanza_add_child(list, device);
+        xmpp_stanza_release(device);
+    }
+
+    xmpp_stanza_add_child(item, list);
+    xmpp_stanza_add_child(publish, item);
+    xmpp_stanza_add_child(pubsub, publish);
+    xmpp_stanza_add_child(iq, pubsub);
+
+    xmpp_stanza_release(list);
+    xmpp_stanza_release(item);
+    xmpp_stanza_release(publish);
+    xmpp_stanza_release(pubsub);
+
+    return iq;
+}
+
+xmpp_stanza_t*
+stanza_create_omemo_bundle_publish(xmpp_ctx_t *ctx, const char *const id,
+    uint32_t device_id,
+    const unsigned char * const identity_key, size_t identity_key_length,
+    const unsigned char * const signed_prekey, size_t signed_prekey_length,
+    const unsigned char * const signed_prekey_signature, size_t signed_prekey_signature_length,
+    GList *const prekeys, GList *const prekeys_id, GList *const prekeys_length)
+{
+    xmpp_stanza_t *iq = xmpp_iq_new(ctx, STANZA_TYPE_SET, id);
+
+    xmpp_stanza_t *pubsub = xmpp_stanza_new(ctx);
+    xmpp_stanza_set_name(pubsub, STANZA_NAME_PUBSUB);
+    xmpp_stanza_set_ns(pubsub, STANZA_NS_PUBSUB);
+
+    xmpp_stanza_t *publish = xmpp_stanza_new(ctx);
+    xmpp_stanza_set_name(publish, STANZA_NAME_PUBLISH);
+    char *node = g_strdup_printf("%s:%d", "eu.siacs.conversations.axolotl.bundles", device_id);
+    xmpp_stanza_set_attribute(publish, STANZA_ATTR_NODE, node);
+    g_free(node);
+
+    xmpp_stanza_t *item = xmpp_stanza_new(ctx);
+    xmpp_stanza_set_name(item, STANZA_NAME_ITEM);
+    xmpp_stanza_set_attribute(item, "id", "current");
+
+    xmpp_stanza_t *bundle = xmpp_stanza_new(ctx);
+    xmpp_stanza_set_name(bundle, "bundle");
+    xmpp_stanza_set_ns(bundle, "eu.siacs.conversations.axolotl");
+
+    xmpp_stanza_t *signed_prekey_public_stanza = xmpp_stanza_new(ctx);
+    xmpp_stanza_set_name(signed_prekey_public_stanza , "signedPreKeyPublic");
+    xmpp_stanza_set_attribute(signed_prekey_public_stanza, "signedPreKeyId", "1");
+
+    xmpp_stanza_t *signed_prekey_public_stanza_text= xmpp_stanza_new(ctx);
+    char *signed_prekey_b64 = g_base64_encode(signed_prekey, signed_prekey_length);
+    xmpp_stanza_set_text(signed_prekey_public_stanza_text, signed_prekey_b64);
+    g_free(signed_prekey_b64);
+    xmpp_stanza_add_child(signed_prekey_public_stanza, signed_prekey_public_stanza_text);
+    xmpp_stanza_release(signed_prekey_public_stanza_text);
+
+    xmpp_stanza_t *signed_prekey_signature_stanza = xmpp_stanza_new(ctx);
+    xmpp_stanza_set_name(signed_prekey_signature_stanza , "signedPreKeySignature");
+
+    xmpp_stanza_t *signed_prekey_signature_stanza_text= xmpp_stanza_new(ctx);
+    char *signed_prekey_signature_b64 = g_base64_encode(signed_prekey_signature, signed_prekey_signature_length);
+    xmpp_stanza_set_text(signed_prekey_signature_stanza_text, signed_prekey_signature_b64);
+    g_free(signed_prekey_signature_b64);
+    xmpp_stanza_add_child(signed_prekey_signature_stanza, signed_prekey_signature_stanza_text);
+    xmpp_stanza_release(signed_prekey_signature_stanza_text);
+
+    xmpp_stanza_t *identity_key_stanza = xmpp_stanza_new(ctx);
+    xmpp_stanza_set_name(identity_key_stanza , "identityKey");
+
+    xmpp_stanza_t *identity_key_stanza_text= xmpp_stanza_new(ctx);
+    char *identity_key_b64 = g_base64_encode(identity_key, identity_key_length);
+    xmpp_stanza_set_text(identity_key_stanza_text, identity_key_b64);
+    g_free(identity_key_b64);
+    xmpp_stanza_add_child(identity_key_stanza, identity_key_stanza_text);
+    xmpp_stanza_release(identity_key_stanza_text);
+
+    xmpp_stanza_t *prekeys_stanza = xmpp_stanza_new(ctx);
+    xmpp_stanza_set_name(prekeys_stanza, "prekeys");
+
+    GList *p, *i, *l;
+    for (p = prekeys, i = prekeys_id, l = prekeys_length; p != NULL; p = p->next, i = i->next, l = l->next) {
+        xmpp_stanza_t *prekey = xmpp_stanza_new(ctx);
+        xmpp_stanza_set_name(prekey, "preKeyPublic");
+        char *id = g_strdup_printf("%d", GPOINTER_TO_INT(i->data));
+        xmpp_stanza_set_attribute(prekey, "preKeyId", id);
+        g_free(id);
+
+        xmpp_stanza_t *prekey_text = xmpp_stanza_new(ctx);
+        char *prekey_b64 = g_base64_encode(p->data, GPOINTER_TO_INT(l->data));
+        xmpp_stanza_set_text(prekey_text, prekey_b64);
+        g_free(prekey_b64);
+
+        xmpp_stanza_add_child(prekey, prekey_text);
+        xmpp_stanza_add_child(prekeys_stanza, prekey);
+        xmpp_stanza_release(prekey_text);
+        xmpp_stanza_release(prekey);
+    }
+
+    xmpp_stanza_add_child(bundle, signed_prekey_public_stanza);
+    xmpp_stanza_add_child(bundle, signed_prekey_signature_stanza);
+    xmpp_stanza_add_child(bundle, identity_key_stanza);
+    xmpp_stanza_add_child(bundle, prekeys_stanza);
+    xmpp_stanza_add_child(item, bundle);
+    xmpp_stanza_add_child(publish, item);
+    xmpp_stanza_add_child(pubsub, publish);
+    xmpp_stanza_add_child(iq, pubsub);
+
+    xmpp_stanza_release(signed_prekey_public_stanza);
+    xmpp_stanza_release(signed_prekey_signature_stanza);
+    xmpp_stanza_release(identity_key_stanza);
+    xmpp_stanza_release(prekeys_stanza);
+    xmpp_stanza_release(bundle);
+    xmpp_stanza_release(item);
+    xmpp_stanza_release(publish);
+    xmpp_stanza_release(pubsub);
+
+    return iq;
+}
+
+xmpp_stanza_t*
+stanza_create_omemo_bundle_request(xmpp_ctx_t *ctx, const char *const id, const char *const jid, uint32_t device_id)
+{
+    xmpp_stanza_t *iq = xmpp_iq_new(ctx, STANZA_TYPE_GET, id);
+    xmpp_stanza_set_to(iq, jid);
+
+    xmpp_stanza_t *pubsub = xmpp_stanza_new(ctx);
+    xmpp_stanza_set_name(pubsub, STANZA_NAME_PUBSUB);
+    xmpp_stanza_set_ns(pubsub, STANZA_NS_PUBSUB);
+
+    xmpp_stanza_t *items = xmpp_stanza_new(ctx);
+    xmpp_stanza_set_name(items, "items");
+    char *node = g_strdup_printf("%s:%d", STANZA_NS_OMEMO_BUNDLES, device_id);
+    xmpp_stanza_set_attribute(items, STANZA_ATTR_NODE, node);
+    g_free(node);
+
+    xmpp_stanza_add_child(pubsub, items);
+    xmpp_stanza_add_child(iq, pubsub);
+
+    xmpp_stanza_release(items);
+    xmpp_stanza_release(pubsub);
+
+    return iq;
+}
+
+xmpp_stanza_t*
+stanza_create_pubsub_configure_request(xmpp_ctx_t *ctx, const char *const id, const char *const jid, const char *const node)
+{
+    xmpp_stanza_t *iq = xmpp_iq_new(ctx, STANZA_TYPE_GET, id);
+    xmpp_stanza_set_to(iq, jid);
+
+    xmpp_stanza_t *pubsub = xmpp_stanza_new(ctx);
+    xmpp_stanza_set_name(pubsub, STANZA_NAME_PUBSUB);
+    xmpp_stanza_set_ns(pubsub, STANZA_NS_PUBSUB_OWNER);
+
+    xmpp_stanza_t *configure = xmpp_stanza_new(ctx);
+    xmpp_stanza_set_name(configure, STANZA_NAME_CONFIGURE);
+    xmpp_stanza_set_attribute(configure, STANZA_ATTR_NODE, node);
+
+    xmpp_stanza_add_child(pubsub, configure);
+    xmpp_stanza_add_child(iq, pubsub);
+
+    xmpp_stanza_release(configure);
+    xmpp_stanza_release(pubsub);
+
+    return iq;
+}
+
+xmpp_stanza_t*
+stanza_create_pubsub_configure_submit(xmpp_ctx_t *ctx, const char *const id, const char *const jid, const char *const node, DataForm *form)
+{
+    xmpp_stanza_t *iq = xmpp_iq_new(ctx, STANZA_TYPE_SET, id);
+    xmpp_stanza_set_to(iq, jid);
+
+    xmpp_stanza_t *pubsub = xmpp_stanza_new(ctx);
+    xmpp_stanza_set_name(pubsub, STANZA_NAME_PUBSUB);
+    xmpp_stanza_set_ns(pubsub, STANZA_NS_PUBSUB_OWNER);
+
+    xmpp_stanza_t *configure = xmpp_stanza_new(ctx);
+    xmpp_stanza_set_name(configure, STANZA_NAME_CONFIGURE);
+    xmpp_stanza_set_attribute(configure, STANZA_ATTR_NODE, node);
+
+    xmpp_stanza_t *x = form_create_submission(form);
+
+    xmpp_stanza_add_child(configure, x);
+    xmpp_stanza_add_child(pubsub, configure);
+    xmpp_stanza_add_child(iq, pubsub);
+
+    xmpp_stanza_release(x);
+    xmpp_stanza_release(configure);
+    xmpp_stanza_release(pubsub);
+
+    return iq;
+}
+
+xmpp_stanza_t*
+stanza_attach_origin_id(xmpp_ctx_t *ctx, xmpp_stanza_t *stanza, const char *const id)
+{
+    xmpp_stanza_t *origin_id = xmpp_stanza_new(ctx);
+    xmpp_stanza_set_name(origin_id, STANZA_NAME_ORIGIN_ID);
+    xmpp_stanza_set_ns(origin_id, STANZA_NS_STABLE_ID);
+    xmpp_stanza_set_attribute(origin_id, STANZA_ATTR_ID, id);
+
+    xmpp_stanza_add_child(stanza, origin_id);
+
+    xmpp_stanza_release(origin_id);
+
+    return stanza;
+}
+
 static void
 _stanza_add_unique_id(xmpp_stanza_t *stanza, char *prefix)
 {
diff --git a/src/xmpp/stanza.h b/src/xmpp/stanza.h
index d3c3c9dc..e5e17ba4 100644
--- a/src/xmpp/stanza.h
+++ b/src/xmpp/stanza.h
@@ -82,6 +82,7 @@
 #define STANZA_NAME_PUBSUB "pubsub"
 #define STANZA_NAME_PUBLISH "publish"
 #define STANZA_NAME_PUBLISH_OPTIONS "publish-options"
+#define STANZA_NAME_SUBSCRIBE "subscribe"
 #define STANZA_NAME_FIELD "field"
 #define STANZA_NAME_STORAGE "storage"
 #define STANZA_NAME_NICK "nick"
@@ -100,6 +101,8 @@
 #define STANZA_NAME_GET "get"
 #define STANZA_NAME_URL "url"
 #define STANZA_NAME_COMMAND "command"
+#define STANZA_NAME_CONFIGURE "configure"
+#define STANZA_NAME_ORIGIN_ID "origin-id"
 
 // error conditions
 #define STANZA_NAME_BAD_REQUEST "bad-request"
@@ -179,6 +182,8 @@
 #define STANZA_NS_CONFERENCE "jabber:x:conference"
 #define STANZA_NS_CAPTCHA "urn:xmpp:captcha"
 #define STANZA_NS_PUBSUB "http://jabber.org/protocol/pubsub"
+#define STANZA_NS_PUBSUB_OWNER "http://jabber.org/protocol/pubsub#owner"
+#define STANZA_NS_PUBSUB_EVENT "http://jabber.org/protocol/pubsub#event"
 #define STANZA_NS_CARBONS "urn:xmpp:carbons:2"
 #define STANZA_NS_HINTS "urn:xmpp:hints"
 #define STANZA_NS_FORWARD "urn:xmpp:forward:0"
@@ -189,6 +194,10 @@
 #define STANZA_NS_X_OOB "jabber:x:oob"
 #define STANZA_NS_BLOCKING "urn:xmpp:blocking"
 #define STANZA_NS_COMMAND "http://jabber.org/protocol/commands"
+#define STANZA_NS_OMEMO "eu.siacs.conversations.axolotl"
+#define STANZA_NS_OMEMO_DEVICELIST "eu.siacs.conversations.axolotl.devicelist"
+#define STANZA_NS_OMEMO_BUNDLES "eu.siacs.conversations.axolotl.bundles"
+#define STANZA_NS_STABLE_ID "urn:xmpp:sid:0"
 
 #define STANZA_DATAFORM_SOFTWARE "urn:xmpp:dataforms:softwareinfo"
 
@@ -228,8 +237,10 @@ xmpp_stanza_t* stanza_attach_state(xmpp_ctx_t *ctx, xmpp_stanza_t *stanza, const
 xmpp_stanza_t* stanza_attach_carbons_private(xmpp_ctx_t *ctx, xmpp_stanza_t *stanza);
 xmpp_stanza_t* stanza_attach_hints_no_copy(xmpp_ctx_t *ctx, xmpp_stanza_t *stanza);
 xmpp_stanza_t* stanza_attach_hints_no_store(xmpp_ctx_t *ctx, xmpp_stanza_t *stanza);
+xmpp_stanza_t* stanza_attach_hints_store(xmpp_ctx_t *ctx, xmpp_stanza_t *stanza);
 xmpp_stanza_t* stanza_attach_receipt_request(xmpp_ctx_t *ctx, xmpp_stanza_t *stanza);
 xmpp_stanza_t* stanza_attach_x_oob_url(xmpp_ctx_t *ctx, xmpp_stanza_t *stanza, const char *const url);
+xmpp_stanza_t* stanza_attach_origin_id(xmpp_ctx_t *ctx, xmpp_stanza_t *stanza, const char *const id);
 
 xmpp_stanza_t* stanza_create_room_join_presence(xmpp_ctx_t *const ctx,
     const char *const full_room_jid, const char *const passwd);
@@ -284,6 +295,17 @@ xmpp_stanza_t* stanza_create_room_kick_iq(xmpp_ctx_t *const ctx, const char *con
 xmpp_stanza_t* stanza_create_command_exec_iq(xmpp_ctx_t *ctx, const char *const target, const char *const node);
 xmpp_stanza_t* stanza_create_command_config_submit_iq(xmpp_ctx_t *ctx, const char *const room, const char *const node, const char *const sessionid, DataForm *form);
 
+void stanza_attach_publish_options(xmpp_ctx_t *const ctx, xmpp_stanza_t *const publish, const char *const option, const char *const value);
+
+xmpp_stanza_t* stanza_create_omemo_devicelist_request(xmpp_ctx_t *ctx, const char *const id, const char *const jid);
+xmpp_stanza_t* stanza_create_omemo_devicelist_subscribe(xmpp_ctx_t *ctx, const char *const jid);
+xmpp_stanza_t* stanza_create_omemo_devicelist_publish(xmpp_ctx_t *ctx, GList *const ids);
+xmpp_stanza_t* stanza_create_omemo_bundle_publish(xmpp_ctx_t *ctx, const char *const id, uint32_t device_id, const unsigned char * const identity_key, size_t identity_key_length, const unsigned char * const signed_prekey, size_t signed_prekey_length, const unsigned char * const signed_prekey_signature, size_t signed_prekey_signature_length, GList *const prekeys, GList *const prekeys_id, GList *const prekeys_length);
+xmpp_stanza_t* stanza_create_omemo_bundle_request(xmpp_ctx_t *ctx, const char *const id, const char *const jid, uint32_t device_id);
+
+xmpp_stanza_t* stanza_create_pubsub_configure_request(xmpp_ctx_t *ctx, const char *const id, const char *const jid, const char *const node);
+xmpp_stanza_t* stanza_create_pubsub_configure_submit(xmpp_ctx_t *ctx, const char *const id, const char *const jid, const char *const node, DataForm *form);
+
 int stanza_get_idle_time(xmpp_stanza_t *const stanza);
 
 void stanza_attach_priority(xmpp_ctx_t *const ctx, xmpp_stanza_t *const presence, const int pri);
diff --git a/src/xmpp/xmpp.h b/src/xmpp/xmpp.h
index c9403090..d5330599 100644
--- a/src/xmpp/xmpp.h
+++ b/src/xmpp/xmpp.h
@@ -35,6 +35,8 @@
 #ifndef XMPP_XMPP_H
 #define XMPP_XMPP_H
 
+#include <stdint.h>
+
 #include "config.h"
 
 #ifdef HAVE_LIBMESODE
@@ -61,6 +63,9 @@
 #define XMPP_FEATURE_LASTACTIVITY "jabber:iq:last"
 #define XMPP_FEATURE_MUC "http://jabber.org/protocol/muc"
 #define XMPP_FEATURE_COMMANDS "http://jabber.org/protocol/commands"
+#define XMPP_FEATURE_OMEMO_DEVICELIST_NOTIFY "eu.siacs.conversations.axolotl.devicelist+notify"
+#define XMPP_FEATURE_PUBSUB "http://jabber.org/protocol/pubsub"
+#define XMPP_FEATURE_PUBSUB_PUBLISH_OPTIONS "http://jabber.org/protocol/pubsub#publish-options"
 
 typedef enum {
     JABBER_CONNECTING,
@@ -139,8 +144,9 @@ char* message_send_chat(const char *const barejid, const char *const msg, const
     gboolean request_receipt);
 char* message_send_chat_otr(const char *const barejid, const char *const msg, gboolean request_receipt);
 char* message_send_chat_pgp(const char *const barejid, const char *const msg, gboolean request_receipt);
+char* message_send_chat_omemo(const char *const jid, uint32_t sid, GList *keys, const unsigned char *const iv, size_t iv_len, const unsigned char *const ciphertext, size_t ciphertext_len, gboolean request_receipt, gboolean muc);
 void message_send_private(const char *const fulljid, const char *const msg, const char *const oob_url);
-void message_send_groupchat(const char *const roomjid, const char *const msg, const char *const oob_url);
+char* message_send_groupchat(const char *const roomjid, const char *const msg, const char *const oob_url);
 void message_send_groupchat_subject(const char *const roomjid, const char *const subject);
 void message_send_inactive(const char *const jid);
 void message_send_composing(const char *const jid);
diff --git a/tests/unittests/log/stub_log.c b/tests/unittests/log/stub_log.c
index e7d744b3..dfbec2bd 100644
--- a/tests/unittests/log/stub_log.c
+++ b/tests/unittests/log/stub_log.c
@@ -58,10 +58,12 @@ void chat_log_init(void) {}
 void chat_log_msg_out(const char * const barejid, const char * const msg) {}
 void chat_log_otr_msg_out(const char * const barejid, const char * const msg) {}
 void chat_log_pgp_msg_out(const char * const barejid, const char * const msg) {}
+void chat_log_omemo_msg_out(const char *const barejid, const char *const msg) {}
 
 void chat_log_msg_in(const char * const barejid, const char * const msg, GDateTime *timestamp) {}
 void chat_log_otr_msg_in(const char * const barejid, const char * const msg, gboolean was_decrypted, GDateTime *timestamp) {}
 void chat_log_pgp_msg_in(const char * const barejid, const char * const msg, GDateTime *timestamp) {}
+void chat_log_omemo_msg_in(const char *const barejid, const char *const msg, GDateTime *timestamp) {}
 
 void chat_log_close(void) {}
 GSList * chat_log_get_previous(const gchar * const login,
@@ -71,5 +73,7 @@ GSList * chat_log_get_previous(const gchar * const login,
 }
 
 void groupchat_log_init(void) {}
-void groupchat_log_chat(const gchar * const login, const gchar * const room,
-    const gchar * const nick, const gchar * const msg) {}
+void groupchat_log_msg_in(const gchar *const room, const gchar *const nick, const gchar *const msg) {}
+void groupchat_log_msg_out(const gchar *const room, const gchar *const msg) {}
+void groupchat_log_omemo_msg_in(const gchar *const room, const gchar *const nick, const gchar *const msg) {}
+void groupchat_log_omemo_msg_out(const gchar *const room, const gchar *const msg) {}
diff --git a/tests/unittests/omemo/stub_omemo.c b/tests/unittests/omemo/stub_omemo.c
new file mode 100644
index 00000000..5b1d02df
--- /dev/null
+++ b/tests/unittests/omemo/stub_omemo.c
@@ -0,0 +1,68 @@
+#include <glib.h>
+
+#include "config/account.h"
+#include "ui/ui.h"
+
+void omemo_init(void) {}
+
+char*
+omemo_fingerprint_autocomplete(const char *const search_str, gboolean previous)
+{
+    return NULL;
+}
+
+void omemo_fingerprint_autocomplete_reset(void) {}
+
+char *
+omemo_format_fingerprint(const char *const fingerprint)
+{
+    return NULL;
+}
+
+void omemo_generate_crypto_materials(ProfAccount *account) {}
+
+gboolean
+omemo_is_trusted_jid(const char *const jid)
+{
+    return TRUE;
+}
+
+gboolean
+omemo_is_trusted_identity(const char *const jid, const char *const fingerprint)
+{
+    return TRUE;
+}
+
+GList *
+omemo_known_device_identities(const char *const jid)
+{
+    return NULL;
+}
+
+gboolean
+omemo_loaded(void)
+{
+    return TRUE;
+}
+
+void omemo_on_connect(ProfAccount *account) {}
+void omemo_on_disconnect(void) {}
+
+char *
+omemo_on_message_send(ProfWin *win, const char *const message, gboolean request_receipt, gboolean muc)
+{
+    return NULL;
+}
+
+char *
+omemo_own_fingerprint(gboolean formatted)
+{
+    return NULL;
+}
+
+void omemo_start_muc_sessions(const char *const roomjid) {}
+void omemo_start_session(const char *const barejid) {}
+void omemo_trust(const char *const jid, const char *const fingerprint_formatted) {}
+void omemo_untrust(const char *const jid, const char *const fingerprint_formatted) {}
+void omemo_devicelist_publish(GList *device_list) {}
+void omemo_publish_crypto_materials(void) {}
diff --git a/tests/unittests/ui/stub_ui.c b/tests/unittests/ui/stub_ui.c
index 855dca22..361a42e2 100644
--- a/tests/unittests/ui/stub_ui.c
+++ b/tests/unittests/ui/stub_ui.c
@@ -190,7 +190,8 @@ void mucwin_occupant_role_and_affiliation_change(ProfMucWin *mucwin, const char
     const char * const affiliation, const char * const actor, const char * const reason) {}
 void mucwin_roster(ProfMucWin *mucwin, GList *occupants, const char * const presence) {}
 void mucwin_history(ProfMucWin *mucwin, const char * const nick, GDateTime *timestamp, const char * const message) {}
-void mucwin_message(ProfMucWin *mucwin, const char *const nick, const char *const message, GSList *mentions, GList *triggers) {}
+void mucwin_incoming_msg(ProfMucWin *mucwin, const char *const nick, const char *const message, const char *const id, GSList *mentions, GList *triggers, prof_enc_t enc_mode) {}
+void mucwin_outgoing_msg(ProfMucWin *mucwin, const char *const message, const char *const id, prof_enc_t enc_mode) {}
 void mucwin_subject(ProfMucWin *mucwin, const char * const nick, const char * const subject) {}
 void mucwin_requires_config(ProfMucWin *mucwin) {}
 void ui_room_destroy(const char * const roomjid) {}
@@ -337,6 +338,7 @@ void cons_show_presence_prefs(void) {}
 void cons_show_connection_prefs(void) {}
 void cons_show_otr_prefs(void) {}
 void cons_show_pgp_prefs(void) {}
+void cons_show_omemo_prefs(void) {}
 
 void cons_show_account(ProfAccount *account)
 {
diff --git a/tests/unittests/xmpp/stub_xmpp.c b/tests/unittests/xmpp/stub_xmpp.c
index bc2c50db..53a729e6 100644
--- a/tests/unittests/xmpp/stub_xmpp.c
+++ b/tests/unittests/xmpp/stub_xmpp.c
@@ -114,7 +114,10 @@ char* message_send_chat_pgp(const char * const barejid, const char * const msg,
 }
 
 void message_send_private(const char * const fulljid, const char * const msg, const char *const oob_url) {}
-void message_send_groupchat(const char * const roomjid, const char * const msg, const char *const oob_url) {}
+char* message_send_groupchat(const char * const roomjid, const char * const msg, const char *const oob_url)
+{
+    return NULL;
+}
 void message_send_groupchat_subject(const char * const roomjid, const char * const subject) {}
 
 void message_send_inactive(const char * const barejid) {}