From a9ea0e9f87e71c60bc570066525d3e3634fbdcc0 Mon Sep 17 00:00:00 2001
From: Marvin W <git@larma.de>
Date: Sun, 12 Mar 2017 02:28:23 +0100
Subject: Split OMEMO plug-in into files, various fixes

---
 plugins/omemo/CMakeLists.txt                   |  16 +-
 plugins/omemo/src/account_settings_entry.vala  |  23 ++
 plugins/omemo/src/account_settings_widget.vala |  63 +++
 plugins/omemo/src/bundle.vala                  |  87 ++++
 plugins/omemo/src/database.vala                |   4 +-
 plugins/omemo/src/encrypt_status.vala          |  17 +
 plugins/omemo/src/encryption_list_entry.vala   |  23 ++
 plugins/omemo/src/manager.vala                 | 145 +------
 plugins/omemo/src/message_flag.vala            |  23 ++
 plugins/omemo/src/module.vala                  | 547 -------------------------
 plugins/omemo/src/plugin.vala                  | 138 +------
 plugins/omemo/src/pre_key_store.vala           |  53 +++
 plugins/omemo/src/register_plugin.vala         |   3 +
 plugins/omemo/src/session_store.vala           |  58 +++
 plugins/omemo/src/signed_pre_key_store.vala    |  54 +++
 plugins/omemo/src/stream_module.vala           | 426 +++++++++++++++++++
 16 files changed, 884 insertions(+), 796 deletions(-)
 create mode 100644 plugins/omemo/src/account_settings_entry.vala
 create mode 100644 plugins/omemo/src/account_settings_widget.vala
 create mode 100644 plugins/omemo/src/bundle.vala
 create mode 100644 plugins/omemo/src/encrypt_status.vala
 create mode 100644 plugins/omemo/src/encryption_list_entry.vala
 create mode 100644 plugins/omemo/src/message_flag.vala
 delete mode 100644 plugins/omemo/src/module.vala
 create mode 100644 plugins/omemo/src/pre_key_store.vala
 create mode 100644 plugins/omemo/src/register_plugin.vala
 create mode 100644 plugins/omemo/src/session_store.vala
 create mode 100644 plugins/omemo/src/signed_pre_key_store.vala
 create mode 100644 plugins/omemo/src/stream_module.vala

(limited to 'plugins')

diff --git a/plugins/omemo/CMakeLists.txt b/plugins/omemo/CMakeLists.txt
index 14e34088..4b6c2620 100644
--- a/plugins/omemo/CMakeLists.txt
+++ b/plugins/omemo/CMakeLists.txt
@@ -14,10 +14,20 @@ pkg_check_modules(OMEMO REQUIRED ${OMEMO_PACKAGES})
 
 vala_precompile(OMEMO_VALA_C
 SOURCES
-    src/plugin.vala
-    src/module.vala
-    src/manager.vala
+    src/account_settings_entry.vala
+    src/account_settings_widget.vala
+    src/bundle.vala
     src/database.vala
+    src/encrypt_status.vala
+    src/encryption_list_entry.vala
+    src/manager.vala
+    src/message_flag.vala
+    src/plugin.vala
+    src/pre_key_store.vala
+    src/register_plugin.vala
+    src/session_store.vala
+    src/signed_pre_key_store.vala
+    src/stream_module.vala
 CUSTOM_VAPIS
     ${CMAKE_BINARY_DIR}/exports/signal-protocol.vapi
     ${CMAKE_BINARY_DIR}/exports/xmpp-vala.vapi
diff --git a/plugins/omemo/src/account_settings_entry.vala b/plugins/omemo/src/account_settings_entry.vala
new file mode 100644
index 00000000..c6871f6e
--- /dev/null
+++ b/plugins/omemo/src/account_settings_entry.vala
@@ -0,0 +1,23 @@
+namespace Dino.Plugins.Omemo {
+
+public class AccountSettingsEntry : Plugins.AccountSettingsEntry {
+    private Plugin plugin;
+
+    public AccountSettingsEntry(Plugin plugin) {
+        this.plugin = plugin;
+    }
+
+    public override string id { get {
+        return "omemo_identity_key";
+    }}
+
+    public override string name { get {
+        return "OMEMO";
+    }}
+
+    public override Plugins.AccountSettingsWidget get_widget() {
+        return new AccountSettingWidget(plugin);
+    }
+}
+
+}
\ No newline at end of file
diff --git a/plugins/omemo/src/account_settings_widget.vala b/plugins/omemo/src/account_settings_widget.vala
new file mode 100644
index 00000000..87ea0e37
--- /dev/null
+++ b/plugins/omemo/src/account_settings_widget.vala
@@ -0,0 +1,63 @@
+using Gtk;
+using Dino.Entities;
+
+namespace Dino.Plugins.Omemo {
+
+public class AccountSettingWidget : Plugins.AccountSettingsWidget, Box {
+    private Plugin plugin;
+    private Label fingerprint;
+    private Account account;
+
+    public AccountSettingWidget(Plugin plugin) {
+        this.plugin = plugin;
+
+        fingerprint = new Label("...");
+        fingerprint.xalign = 0;
+        Border border = new Button().get_style_context().get_padding(StateFlags.NORMAL);
+        fingerprint.set_padding(border.left + 1, border.top + 1);
+        fingerprint.visible = true;
+        pack_start(fingerprint);
+
+        Button btn = new Button();
+        btn.image = new Image.from_icon_name("view-list-symbolic", IconSize.BUTTON);
+        btn.relief = ReliefStyle.NONE;
+        btn.visible = true;
+        btn.valign = Align.CENTER;
+        btn.clicked.connect(() => { activated(); });
+        pack_start(btn, false);
+    }
+
+    public void set_account(Account account) {
+        this.account = account;
+        try {
+            Qlite.Row? row = plugin.db.identity.row_with(plugin.db.identity.account_id, account.id);
+            if (row == null) {
+                fingerprint.set_markup(@"Own fingerprint\n<span font='8'>Will be generated on first connect</span>");
+            } else {
+                uint8[] arr = Base64.decode(row[plugin.db.identity.identity_key_public_base64]);
+                arr = arr[1:arr.length];
+                string res = "";
+                foreach (uint8 i in arr) {
+                    string s = i.to_string("%x");
+                    if (s.length == 1) s = "0" + s;
+                    res = res + s;
+                    if ((res.length % 9) == 8) {
+                        if (res.length == 35) {
+                            res += "\n";
+                        } else {
+                            res += " ";
+                        }
+                    }
+                }
+                fingerprint.set_markup(@"Own fingerprint\n<span font_family='monospace' font='8'>$res</span>");
+            }
+        } catch (Qlite.DatabaseError e) {
+            fingerprint.set_markup(@"Own fingerprint\n<span font='8'>Database error</span>");
+        }
+    }
+
+    public void deactivate() {
+    }
+}
+
+}
\ No newline at end of file
diff --git a/plugins/omemo/src/bundle.vala b/plugins/omemo/src/bundle.vala
new file mode 100644
index 00000000..211dc29b
--- /dev/null
+++ b/plugins/omemo/src/bundle.vala
@@ -0,0 +1,87 @@
+using Gee;
+using Signal;
+using Xmpp.Core;
+
+namespace Dino.Plugins.Omemo {
+
+public class Bundle {
+    private StanzaNode? node;
+
+    public Bundle(StanzaNode? node) {
+        this.node = node;
+    }
+
+    public int32 signed_pre_key_id { owned get {
+        if (node == null) return -1;
+        string id = node.get_deep_attribute("signedPreKeyPublic", "signedPreKeyId");
+        if (id == null) return -1;
+        return int.parse(id);
+    }}
+
+    public ECPublicKey? signed_pre_key { owned get {
+        if (node == null) return null;
+        string? key = node.get_deep_string_content("signedPreKeyPublic");
+        if (key == null) return null;
+        try {
+            return Plugin.context.decode_public_key(Base64.decode(key));
+        } catch (Error e) {
+            return null;
+        }
+    }}
+
+    public uint8[]? signed_pre_key_signature { owned get {
+        if (node == null) return null;
+        string? sig = node.get_deep_string_content("signedPreKeySignature");
+        if (sig == null) return null;
+        return Base64.decode(sig);
+    }}
+
+    public ECPublicKey? identity_key { owned get {
+        if (node == null) return null;
+        string? key = node.get_deep_string_content("identityKey");
+        if (key == null) return null;
+        try {
+            return Plugin.context.decode_public_key(Base64.decode(key));
+        } catch (Error e) {
+            return null;
+        }
+    }}
+
+    public ArrayList<PreKey> pre_keys { owned get {
+        ArrayList<PreKey> list = new ArrayList<PreKey>();
+        if (node == null || node.get_subnode("prekeys") == null) return list;
+        node.get_deep_subnodes("prekeys", "preKeyPublic")
+                .filter((node) => node.get_attribute("preKeyId") != null)
+                .map<PreKey>(PreKey.create)
+                .foreach((key) => list.add(key));
+        return list;
+    }}
+
+    public class PreKey {
+        private StanzaNode node;
+
+        public static PreKey create(owned StanzaNode node) {
+            return new PreKey(node);
+        }
+
+        public PreKey(StanzaNode node) {
+            this.node = node;
+        }
+
+        public int32 key_id { owned get {
+            return int.parse(node.get_attribute("preKeyId") ?? "-1");
+        }}
+
+        public ECPublicKey? key { owned get {
+            string? key = node.get_string_content();
+            if (key == null) return null;
+            try {
+                return Plugin.context.decode_public_key(Base64.decode(key));
+            } catch (Error e) {
+                return null;
+            }
+        }}
+    }
+}
+
+}
\ No newline at end of file
diff --git a/plugins/omemo/src/database.vala b/plugins/omemo/src/database.vala
index 1216ca84..db530c69 100644
--- a/plugins/omemo/src/database.vala
+++ b/plugins/omemo/src/database.vala
@@ -4,7 +4,7 @@ using Qlite;
 
 using Dino.Entities;
 
-namespace Dino.Omemo {
+namespace Dino.Plugins.Omemo {
 
 public class Database : Qlite.Database {
     private const int VERSION = 0;
@@ -63,7 +63,7 @@ public class Database : Qlite.Database {
     public PreKeyTable pre_key { get; private set; }
     public SessionTable session { get; private set; }
 
-    public Database(string fileName) {
+    public Database(string fileName) throws DatabaseError {
         base(fileName, VERSION);
         identity = new IdentityTable(this);
         signed_pre_key = new SignedPreKeyTable(this);
diff --git a/plugins/omemo/src/encrypt_status.vala b/plugins/omemo/src/encrypt_status.vala
new file mode 100644
index 00000000..c6b45ac6
--- /dev/null
+++ b/plugins/omemo/src/encrypt_status.vala
@@ -0,0 +1,17 @@
+namespace Dino.Plugins.Omemo {
+
+public class EncryptStatus {
+    public bool encrypted { get; internal set; }
+    public int other_devices { get; internal set; }
+    public int other_success { get; internal set; }
+    public int other_lost { get; internal set; }
+    public int other_unknown { get; internal set; }
+    public int other_failure { get; internal set; }
+    public int own_devices { get; internal set; }
+    public int own_success { get; internal set; }
+    public int own_lost { get; internal set; }
+    public int own_unknown { get; internal set; }
+    public int own_failure { get; internal set; }
+}
+
+}
\ No newline at end of file
diff --git a/plugins/omemo/src/encryption_list_entry.vala b/plugins/omemo/src/encryption_list_entry.vala
new file mode 100644
index 00000000..753ffe67
--- /dev/null
+++ b/plugins/omemo/src/encryption_list_entry.vala
@@ -0,0 +1,23 @@
+namespace Dino.Plugins.Omemo {
+
+public class EncryptionListEntry : Plugins.EncryptionListEntry, Object {
+    private Plugin plugin;
+
+    public EncryptionListEntry(Plugin plugin) {
+        this.plugin = plugin;
+    }
+
+    public Entities.Encryption encryption { get {
+        return Entities.Encryption.OMEMO;
+    }}
+
+    public string name { get {
+        return "OMEMO";
+    }}
+
+    public bool can_encrypt(Entities.Conversation conversation) {
+        return Manager.get_instance(plugin.app.stream_interaction).can_encrypt(conversation);
+    }
+}
+
+}
\ No newline at end of file
diff --git a/plugins/omemo/src/manager.vala b/plugins/omemo/src/manager.vala
index 69a69d9c..e5db631e 100644
--- a/plugins/omemo/src/manager.vala
+++ b/plugins/omemo/src/manager.vala
@@ -4,7 +4,7 @@ using Qlite;
 using Xmpp;
 using Gee;
 
-namespace Dino.Omemo {
+namespace Dino.Plugins.Omemo {
 
 public class Manager : StreamInteractionModule, Object {
     public const string id = "omemo_manager";
@@ -31,7 +31,7 @@ public class Manager : StreamInteractionModule, Object {
 
     private void on_pre_message_send(Entities.Message message, Xmpp.Message.Stanza message_stanza, Conversation conversation) {
         if (message.encryption == Encryption.OMEMO) {
-            Module module = Module.get_module(stream_interactor.get_stream(conversation.account));
+            StreamModule module = stream_interactor.get_stream(conversation.account).get_module(StreamModule.IDENTITY);
             EncryptStatus status = module.encrypt(message_stanza, conversation.account.bare_jid.to_string());
             if (status.other_failure > 0 || (status.other_lost == status.other_devices && status.other_devices > 0)) {
                 message.marked = Entities.Message.Marked.WONTSEND;
@@ -63,9 +63,9 @@ public class Manager : StreamInteractionModule, Object {
     }
 
     private void on_account_added(Account account) {
-        stream_interactor.module_manager.get_module(account, Module.IDENTITY).store_created.connect((context, store) => on_store_created(account, context, store));
-        stream_interactor.module_manager.get_module(account, Module.IDENTITY).device_list_loaded.connect(() => on_device_list_loaded(account));
-        stream_interactor.module_manager.get_module(account, Module.IDENTITY).session_started.connect((jid, device_id) => on_session_started(account, jid));
+        stream_interactor.module_manager.get_module(account, StreamModule.IDENTITY).store_created.connect((store) => on_store_created(account, store));
+        stream_interactor.module_manager.get_module(account, StreamModule.IDENTITY).device_list_loaded.connect(() => on_device_list_loaded(account));
+        stream_interactor.module_manager.get_module(account, StreamModule.IDENTITY).session_started.connect((jid, device_id) => on_session_started(account, jid));
     }
 
     private void on_session_started(Account account, string jid) {
@@ -96,7 +96,7 @@ public class Manager : StreamInteractionModule, Object {
         }
     }
 
-    private void on_store_created(Account account, Context context, Store store) {
+    private void on_store_created(Account account, Store store) {
         Qlite.Row? row = null;
         try {
             row = db.identity.row_with(db.identity.account_id, account.id);
@@ -107,19 +107,19 @@ public class Manager : StreamInteractionModule, Object {
 
         if (row == null) {
             // OMEMO not yet initialized, starting with empty base
-            store.identity_key_store.local_registration_id = Random.int_range(1, int32.MAX);
+            try {
+                store.identity_key_store.local_registration_id = Random.int_range(1, int32.MAX);
 
-            Signal.ECKeyPair key_pair = context.generate_key_pair();
-            store.identity_key_store.identity_key_private = key_pair.private.serialize();
-            store.identity_key_store.identity_key_public = key_pair.public.serialize();
+                Signal.ECKeyPair key_pair = Plugin.context.generate_key_pair();
+                store.identity_key_store.identity_key_private = key_pair.private.serialize();
+                store.identity_key_store.identity_key_public = key_pair.public.serialize();
 
-            try {
                 identity_id = (int) db.identity.insert().or("REPLACE")
-                .value(db.identity.account_id, account.id)
-                .value(db.identity.device_id, (int) store.local_registration_id)
-                .value(db.identity.identity_key_private_base64, Base64.encode(store.identity_key_store.identity_key_private))
-                .value(db.identity.identity_key_public_base64, Base64.encode(store.identity_key_store.identity_key_public))
-                .perform();
+                        .value(db.identity.account_id, account.id)
+                        .value(db.identity.device_id, (int) store.local_registration_id)
+                        .value(db.identity.identity_key_private_base64, Base64.encode(store.identity_key_store.identity_key_private))
+                        .value(db.identity.identity_key_public_base64, Base64.encode(store.identity_key_store.identity_key_public))
+                        .perform();
             } catch (Error e) {
                 // Ignore error
             }
@@ -139,118 +139,9 @@ public class Manager : StreamInteractionModule, Object {
         }
     }
 
-    private class BackedSignedPreKeyStore : SimpleSignedPreKeyStore {
-        private Database db;
-        private int identity_id;
-
-        public BackedSignedPreKeyStore(Database db, int identity_id) {
-            this.db = db;
-            this.identity_id = identity_id;
-            init();
-        }
-
-        private void init() {
-            foreach (Row row in db.signed_pre_key.select().with(db.signed_pre_key.identity_id, "=", identity_id)) {
-                store_signed_pre_key(row[db.signed_pre_key.signed_pre_key_id], Base64.decode(row[db.signed_pre_key.record_base64]));
-            }
-
-            signed_pre_key_stored.connect(on_signed_pre_key_stored);
-            signed_pre_key_deleted.connect(on_signed_pre_key_deleted);
-        }
-
-        public void on_signed_pre_key_stored(SignedPreKeyStore.Key key) {
-            db.signed_pre_key.insert().or("REPLACE")
-                .value(db.signed_pre_key.identity_id, identity_id)
-                .value(db.signed_pre_key.signed_pre_key_id, (int) key.key_id)
-                .value(db.signed_pre_key.record_base64, Base64.encode(key.record))
-                .perform();
-        }
-
-        public void on_signed_pre_key_deleted(SignedPreKeyStore.Key key) {
-            db.signed_pre_key.delete()
-                .with(db.signed_pre_key.identity_id, "=", identity_id)
-                .with(db.signed_pre_key.signed_pre_key_id, "=", (int) key.key_id)
-                .perform();
-        }
-    }
-
-    private class BackedPreKeyStore : SimplePreKeyStore {
-        private Database db;
-        private int identity_id;
-
-        public BackedPreKeyStore(Database db, int identity_id) {
-            this.db = db;
-            this.identity_id = identity_id;
-            init();
-        }
-
-        private void init() {
-            foreach (Row row in db.pre_key.select().with(db.pre_key.identity_id, "=", identity_id)) {
-                store_pre_key(row[db.pre_key.pre_key_id], Base64.decode(row[db.pre_key.record_base64]));
-            }
-
-            pre_key_stored.connect(on_pre_key_stored);
-            pre_key_deleted.connect(on_pre_key_deleted);
-        }
-
-        public void on_pre_key_stored(PreKeyStore.Key key) {
-            db.pre_key.insert().or("REPLACE")
-                .value(db.pre_key.identity_id, identity_id)
-                .value(db.pre_key.pre_key_id, (int) key.key_id)
-                .value(db.pre_key.record_base64, Base64.encode(key.record))
-                .perform();
-        }
-
-        public void on_pre_key_deleted(PreKeyStore.Key key) {
-            db.pre_key.delete()
-                .with(db.pre_key.identity_id, "=", identity_id)
-                .with(db.pre_key.pre_key_id, "=", (int) key.key_id)
-                .perform();
-        }
-    }
-
-    private class BackedSessionStore : SimpleSessionStore {
-        private Database db;
-        private int identity_id;
-
-        public BackedSessionStore(Database db, int identity_id) {
-            this.db = db;
-            this.identity_id = identity_id;
-            init();
-        }
-
-        private void init() {
-            Address addr = new Address();
-            foreach (Row row in db.session.select().with(db.session.identity_id, "=", identity_id)) {
-                addr.name = row[db.session.address_name];
-                addr.device_id = row[db.session.device_id];
-                store_session(addr, Base64.decode(row[db.session.record_base64]));
-            }
-
-            session_stored.connect(on_session_stored);
-            session_removed.connect(on_session_deleted);
-        }
-
-        public void on_session_stored(SessionStore.Session session) {
-            db.session.insert().or("REPLACE")
-                .value(db.session.identity_id, identity_id)
-                .value(db.session.address_name, session.name)
-                .value(db.session.device_id, session.device_id)
-                .value(db.session.record_base64, Base64.encode(session.record))
-                .perform();
-        }
-
-        public void on_session_deleted(SessionStore.Session session) {
-            db.session.delete()
-                .with(db.session.identity_id, "=", identity_id)
-                .with(db.session.address_name, "=", session.name)
-                .with(db.session.device_id, "=", session.device_id)
-                .perform();
-        }
-    }
 
-    public bool con_encrypt(Entities.Conversation conversation) {
-        return true; // TODO
+    public bool can_encrypt(Entities.Conversation conversation) {
+        return stream_interactor.get_stream(conversation.account).get_module(StreamModule.IDENTITY).is_known_address(conversation.counterpart.bare_jid.to_string());
     }
 
     internal string get_id() {
diff --git a/plugins/omemo/src/message_flag.vala b/plugins/omemo/src/message_flag.vala
new file mode 100644
index 00000000..cea1e9b2
--- /dev/null
+++ b/plugins/omemo/src/message_flag.vala
@@ -0,0 +1,23 @@
+using Xmpp;
+
+namespace Dino.Plugins.Omemo {
+
+public class MessageFlag : Message.MessageFlag {
+    public const string id = "omemo";
+
+    public bool decrypted = false;
+
+    public static MessageFlag? get_flag(Message.Stanza message) {
+        return (MessageFlag) message.get_flag(NS_URI, id);
+    }
+
+    public override string get_ns() {
+        return NS_URI;
+    }
+
+    public override string get_id() {
+        return id;
+    }
+}
+
+}
\ No newline at end of file
diff --git a/plugins/omemo/src/module.vala b/plugins/omemo/src/module.vala
deleted file mode 100644
index 728251f0..00000000
--- a/plugins/omemo/src/module.vala
+++ /dev/null
@@ -1,547 +0,0 @@
-using Gee;
-using Xmpp;
-using Xmpp.Core;
-using Xmpp.Xep;
-using Signal;
-
-namespace Dino.Omemo {
-
-private const string NS_URI = "eu.siacs.conversations.axolotl";
-private const string NODE_DEVICELIST = NS_URI + ".devicelist";
-private const string NODE_BUNDLES = NS_URI + ".bundles";
-private const string NODE_VERIFICATION = NS_URI + ".verification";
-
-private const int NUM_KEYS_TO_PUBLISH = 100;
-
-public class Module : XmppStreamModule {
-    private const string ID = "axolotl_module";
-    public static ModuleIdentity<Module> IDENTITY = new ModuleIdentity<Module>(NS_URI, ID);
-
-    private Store store;
-    internal static Context context;
-    private bool device_list_loading = false;
-    private bool device_list_modified = false;
-    private Map<string, ArrayList<int32>> device_lists = new HashMap<string, ArrayList<int32>>();
-    private Map<string, ArrayList<int32>> ignored_devices = new HashMap<string, ArrayList<int32>>();
-
-    public signal void store_created(Context context, Store store);
-    public signal void device_list_loaded();
-    public signal void session_started(string jid, int device_id);
-
-    public Module() {
-        lock(context) {
-            if (context == null) {
-                try {
-                    context = new Context(true);
-                } catch (Error e) {
-                    print(@"Error initializing axolotl: $(e.message)\n");
-                }
-            }
-        }
-    }
-
-    public EncryptStatus encrypt(Message.Stanza message, string self_bare_jid) {
-        EncryptStatus status = new EncryptStatus();
-        if (context == null) return status;
-        try {
-            string name = get_bare_jid(message.to);
-            if (device_lists.get(name) == null || device_lists.get(self_bare_jid) == null) return status;
-            status.other_devices = device_lists.get(name).size;
-            status.own_devices = device_lists.get(self_bare_jid).size;
-            if (status.other_devices == 0) return status;
-
-            uint8[] key = new uint8[16];
-            context.randomize(key);
-            uint8[] iv = new uint8[16];
-            context.randomize(iv);
-
-            uint8[] ciphertext = aes_encrypt(Cipher.AES_GCM_NOPADDING, key, iv, message.body.data);
-
-            StanzaNode header = null;
-            StanzaNode encrypted = new StanzaNode.build("encrypted", NS_URI).add_self_xmlns()
-                    .put_node(header = new StanzaNode.build("header", NS_URI)
-                        .put_attribute("sid", store.local_registration_id.to_string())
-                        .put_node(new StanzaNode.build("iv", NS_URI)
-                            .put_node(new StanzaNode.text(Base64.encode(iv)))))
-                    .put_node(new StanzaNode.build("payload", NS_URI)
-                        .put_node(new StanzaNode.text(Base64.encode(ciphertext))));
-
-            Address address = new Address();
-            address.name = name;
-            foreach(int32 device_id in device_lists[name]) {
-                if (is_ignored_device(name, device_id)) {
-                    status.other_lost++;
-                    continue;
-                }
-                try {
-                    address.device_id = (int) device_id;
-                    StanzaNode key_node = create_encrypted_key(key, address);
-                    header.put_node(key_node);
-                    status.other_success++;
-                } catch (Error e) {
-                    if (e.code == ErrorCode.UNKNOWN) status.other_unknown++;
-                    else status.other_failure++;
-                }
-            }
-            address.name = self_bare_jid;
-            foreach(int32 device_id in device_lists[self_bare_jid]) {
-                if (is_ignored_device(self_bare_jid, device_id)) {
-                    status.own_lost++;
-                    continue;
-                }
-                if (device_id != store.local_registration_id) {
-                    address.device_id = (int) device_id;
-                    try {
-                        StanzaNode key_node = create_encrypted_key(key, address);
-                        header.put_node(key_node);
-                        status.own_success++;
-                    } catch (Error e) {
-                        if (e.code == ErrorCode.UNKNOWN) status.own_unknown++;
-                        else status.own_failure++;
-                    }
-                }
-            }
-
-            message.stanza.put_node(encrypted);
-            message.body = "[This message is OMEMO encrypted]";
-            status.encrypted = true;
-        } catch (Error e) {
-            print(@"Axolotl error while encrypting message: $(e.message)\n");
-        }
-        return status;
-    }
-
-    private StanzaNode create_encrypted_key(uint8[] key, Address address) throws GLib.Error {
-        SessionCipher cipher = store.create_session_cipher(address);
-        CiphertextMessage device_key = cipher.encrypt(key);
-        StanzaNode key_node = new StanzaNode.build("key", NS_URI)
-            .put_attribute("rid", address.device_id.to_string())
-            .put_node(new StanzaNode.text(Base64.encode(device_key.serialized)));
-        if (device_key.type == CiphertextType.PREKEY) key_node.put_attribute("prekey", "true");
-        return key_node;
-    }
-
-    public override void attach(XmppStream stream) {
-        if (context == null) return;
-        Message.Module.require(stream);
-        Pubsub.Module.require(stream);
-        stream.get_module(Message.Module.IDENTITY).pre_received_message.connect(on_pre_received_message);
-        stream.get_module(Pubsub.Module.IDENTITY).add_filtered_notification(stream, NODE_DEVICELIST, on_devicelist, this);
-        this.store = context.create_store();
-        store_created(context, store);
-    }
-
-    private void on_pre_received_message(XmppStream stream, Message.Stanza message) {
-        StanzaNode? encrypted = message.stanza.get_subnode("encrypted", NS_URI);
-        if (encrypted == null) return;
-        MessageFlag flag = new MessageFlag();
-        message.add_flag(flag);
-        StanzaNode? header = encrypted.get_subnode("header");
-        if (header == null || header.get_attribute_int("sid") <= 0) return;
-        foreach (StanzaNode key_node in header.get_subnodes("key")) {
-            if (key_node.get_attribute_int("rid") == store.local_registration_id) {
-                try {
-                    uint8[] key = null;
-                    uint8[] ciphertext = Base64.decode(encrypted.get_subnode("payload").get_string_content());
-                    uint8[] iv = Base64.decode(header.get_subnode("iv").get_string_content());
-                    Address address = new Address();
-                    address.name = get_bare_jid(message.from);
-                    address.device_id = header.get_attribute_int("sid");
-                    if (key_node.get_attribute_bool("prekey")) {
-                        PreKeySignalMessage msg = context.deserialize_pre_key_signal_message(Base64.decode(key_node.get_string_content()));
-                        SessionCipher cipher = store.create_session_cipher(address);
-                        key = cipher.decrypt_pre_key_signal_message(msg);
-                    } else {
-                        SignalMessage msg = context.deserialize_signal_message(Base64.decode(key_node.get_string_content()));
-                        SessionCipher cipher = store.create_session_cipher(address);
-                        key = cipher.decrypt_signal_message(msg);
-                    }
-                    address.device_id = 0; // TODO: Hack to have address obj live longer
-
-
-                    if (key != null && ciphertext != null && iv != null) {
-                        if (key.length >= 32) {
-                            int authtaglength = key.length - 16;
-                            uint8[] new_ciphertext = new uint8[ciphertext.length + authtaglength];
-                            uint8[] new_key = new uint8[16];
-                            Memory.copy(new_ciphertext, ciphertext, ciphertext.length);
-                            Memory.copy((uint8*)new_ciphertext + ciphertext.length, (uint8*)key + 16, authtaglength);
-                            Memory.copy(new_key, key, 16);
-                            ciphertext = new_ciphertext;
-                            key = new_key;
-                        }
-
-                        message.body = arr_to_str(aes_decrypt(Cipher.AES_GCM_NOPADDING, key, iv, ciphertext));
-                        flag.decrypted = true;
-                    }
-                } catch (Error e) {
-                    print(@"Axolotl error while decrypting message: $(e.message)\n");
-                }
-            }
-        }
-    }
-
-    private string arr_to_str(uint8[] arr) {
-        // null-terminate the array
-        uint8[] rarr = new uint8[arr.length+1];
-        Memory.copy(rarr, arr, arr.length);
-        return (string)rarr;
-    }
-
-    public void on_devicelist(XmppStream stream, string jid, string id, StanzaNode node) {
-        if (jid == get_bare_jid(Bind.Flag.get_flag(stream).my_jid) && store.local_registration_id != 0) {
-            lock (device_list_loading) {
-                if (!device_list_loading) {
-                    device_list_loading = true;
-                    GLib.Timeout.add_seconds(3, () => {
-                        bool cont = false;
-                        lock (device_lists) {
-                            if (device_list_modified) {
-                                cont = true;
-                                device_list_modified = false;
-                            }
-                        }
-                        if (!cont) {
-                            lock (device_list_loading) {
-                                device_list_loading = false;
-                                device_list_loaded();
-                            }
-                        }
-                        return cont;
-                    });
-                }
-            }
-
-            bool am_on_devicelist = false;
-            foreach (StanzaNode device_node in node.get_subnodes("device")) {
-                int device_id = device_node.get_attribute_int("id");
-                if (store.local_registration_id == device_id) {
-                    am_on_devicelist = true;
-                }
-            }
-            if (!am_on_devicelist) {
-                print(@"Not on device list, adding id\n");
-                node.put_node(new StanzaNode.build("device", NS_URI).put_attribute("id", store.local_registration_id.to_string()));
-                stream.get_module(Pubsub.Module.IDENTITY).publish(stream, jid, NODE_DEVICELIST, NODE_DEVICELIST, id, node);
-            } else {
-                publish_bundles_if_needed(stream, jid);
-            }
-        }
-        lock(device_lists) {
-            device_list_modified = true;
-            device_lists[jid] = new ArrayList<int32>();
-            foreach (StanzaNode device_node in node.get_subnodes("device")) {
-                device_lists[jid].add(device_node.get_attribute_int("id"));
-            }
-        }
-    }
-
-    public void start_sessions_with(XmppStream stream, string bare_jid) {
-        if (!device_lists.has_key(bare_jid)) {
-            // TODO: manually request a device list
-            return;
-        }
-        Address address = new Address();
-        address.name = bare_jid;
-        foreach(int32 device_id in device_lists[bare_jid]) {
-            if (!is_ignored_device(bare_jid, device_id)) {
-                address.device_id = device_id;
-                if (!store.contains_session(address)) {
-                    start_session_with(stream, bare_jid, device_id);
-                }
-            }
-        }
-        address.device_id = 0;
-    }
-
-    public void start_session_with(XmppStream stream, string bare_jid, int device_id) {
-        print(@"Asking for bundle from $bare_jid/$device_id\n");
-        stream.get_module(Pubsub.Module.IDENTITY).request(stream, bare_jid, @"$NODE_BUNDLES:$device_id", on_other_bundle_result, Tuple.create(store, device_id));
-    }
-
-    public void ignore_device(string jid, int32 device_id) {
-        if (device_id <= 0) return;
-        lock (ignored_devices) {
-            if (!ignored_devices.has_key(jid)) {
-                ignored_devices[jid] = new ArrayList<int32>();
-            }
-            ignored_devices[jid].add(device_id);
-        }
-    }
-
-    public bool is_ignored_device(string jid, int32 device_id) {
-        if (device_id <= 0) return true;
-        lock (ignored_devices) {
-            return ignored_devices.has_key(jid) && ignored_devices[jid].contains(device_id);
-        }
-    }
-
-    private static void on_other_bundle_result(XmppStream stream, string jid, string? id, StanzaNode? node, Object? storage) {
-        Tuple<Store, int> tuple = (Tuple<Store, int>)storage;
-        Store store = tuple.a;
-        int device_id = tuple.b;
-
-        bool fail = false;
-        if (node == null) {
-            // Device not registered, shouldn't exist
-            fail = true;
-        } else {
-            Bundle bundle = new Bundle(node);
-            int32 signed_pre_key_id = bundle.signed_pre_key_id;
-            ECPublicKey? signed_pre_key = bundle.signed_pre_key;
-            uint8[] signed_pre_key_signature = bundle.signed_pre_key_signature;
-            ECPublicKey? identity_key = bundle.identity_key;
-
-            ArrayList<Bundle.PreKey> pre_keys = bundle.pre_keys;
-            int pre_key_idx = Random.int_range(0, pre_keys.size);
-            int32 pre_key_id = pre_keys[pre_key_idx].key_id;
-            ECPublicKey? pre_key = pre_keys[pre_key_idx].key;
-
-            if (signed_pre_key_id < 0 || signed_pre_key == null || identity_key == null || pre_key_id < 0 || pre_key == null) {
-                fail = true;
-            } else {
-                Address address = new Address();
-                address.name = jid;
-                address.device_id = device_id;
-                try {
-                    if (store.contains_session(address)) {
-                        return;
-                    }
-                    SessionBuilder builder = store.create_session_builder(address);
-                    builder.process_pre_key_bundle(create_pre_key_bundle(device_id, device_id, pre_key_id, pre_key, signed_pre_key_id, signed_pre_key, signed_pre_key_signature, identity_key));
-                } catch (Error e) {
-                    fail = true;
-                }
-                address.device_id = 0; // TODO: Hack to have address obj live longer
-                get_module(stream).session_started(jid, device_id);
-            }
-        }
-        if (fail) {
-            get_module(stream).ignore_device(jid, device_id);
-        }
-    }
-
-    public void publish_bundles_if_needed(XmppStream stream, string jid) {
-        stream.get_module(Pubsub.Module.IDENTITY).request(stream, jid, @"$NODE_BUNDLES:$(store.local_registration_id)", on_self_bundle_result, store);
-    }
-
-    private static void on_self_bundle_result(XmppStream stream, string jid, string? id, StanzaNode? node, Object? storage) {
-        Store store = (Store)storage;
-        Map<int, ECPublicKey> keys = new HashMap<int, ECPublicKey>();
-        ECPublicKey identity_key = null;
-        IdentityKeyPair identity_key_pair = null;
-        int32 signed_pre_key_id = -1;
-        ECPublicKey signed_pre_key = null;
-        SignedPreKeyRecord signed_pre_key_record = null;
-        bool changed = false;
-        if (node == null) {
-            identity_key = store.identity_key_pair.public;
-            changed = true;
-        } else {
-            Bundle bundle = new Bundle(node);
-            foreach (Bundle.PreKey prekey in bundle.pre_keys) {
-                keys[prekey.key_id] = prekey.key;
-            }
-            identity_key = bundle.identity_key;
-            signed_pre_key_id = bundle.signed_pre_key_id;;
-            signed_pre_key = bundle.signed_pre_key;
-        }
-
-        // Validate IdentityKey
-        if (store.identity_key_pair.public.compare(identity_key) != 0) {
-            changed = true;
-        }
-        identity_key_pair = store.identity_key_pair;
-
-        // Validate signedPreKeyRecord + ID
-        if (signed_pre_key_id == -1 || !store.contains_signed_pre_key(signed_pre_key_id) || store.load_signed_pre_key(signed_pre_key_id).key_pair.public.compare(signed_pre_key) != 0) {
-            signed_pre_key_id = Random.int_range(1, int32.MAX); // TODO: No random, use ordered number
-            signed_pre_key_record = context.generate_signed_pre_key(identity_key_pair, signed_pre_key_id);
-            store.store_signed_pre_key(signed_pre_key_record);
-            changed = true;
-        } else {
-            signed_pre_key_record = store.load_signed_pre_key(signed_pre_key_id);
-        }
-
-        // Validate PreKeys
-        Set<PreKeyRecord> pre_key_records = new HashSet<PreKeyRecord>();
-        foreach (var entry in keys.entries) {
-            if (store.contains_pre_key(entry.key)) {
-                PreKeyRecord record = store.load_pre_key(entry.key);
-                if (record.key_pair.public.compare(entry.value) == 0) {
-                    pre_key_records.add(record);
-                }
-            }
-        }
-        int new_keys = NUM_KEYS_TO_PUBLISH - pre_key_records.size;
-        if (new_keys > 0) {
-            int32 next_id = Random.int_range(1, int32.MAX); // TODO: No random, use ordered number
-            Set<PreKeyRecord> new_records = context.generate_pre_keys((uint)next_id, (uint)new_keys);
-            pre_key_records.add_all(new_records);
-            foreach (PreKeyRecord record in new_records) {
-                store.store_pre_key(record);
-            }
-            changed = true;
-        }
-
-        if (changed) {
-            publish_bundles(stream, signed_pre_key_record, identity_key_pair, pre_key_records, (int32) store.local_registration_id);
-        }
-    }
-
-    public static void publish_bundles(XmppStream stream, SignedPreKeyRecord signed_pre_key_record, IdentityKeyPair identity_key_pair, Set<PreKeyRecord> pre_key_records, int32 device_id) {
-        ECKeyPair tmp;
-        StanzaNode bundle = new StanzaNode.build("bundle", NS_URI)
-                .add_self_xmlns()
-                .put_node(new StanzaNode.build("signedPreKeyPublic", NS_URI)
-                    .put_attribute("signedPreKeyId", signed_pre_key_record.id.to_string())
-                    .put_node(new StanzaNode.text(Base64.encode((tmp = signed_pre_key_record.key_pair).public.serialize()))))
-                .put_node(new StanzaNode.build("signedPreKeySignature", NS_URI)
-                    .put_node(new StanzaNode.text(Base64.encode(signed_pre_key_record.signature))))
-                .put_node(new StanzaNode.build("identityKey", NS_URI)
-                    .put_node(new StanzaNode.text(Base64.encode(identity_key_pair.public.serialize()))));
-        StanzaNode prekeys = new StanzaNode.build("prekeys", NS_URI);
-        foreach (PreKeyRecord pre_key_record in pre_key_records) {
-            prekeys.put_node(new StanzaNode.build("preKeyPublic", NS_URI)
-                    .put_attribute("preKeyId", pre_key_record.id.to_string())
-                    .put_node(new StanzaNode.text(Base64.encode(pre_key_record.key_pair.public.serialize()))));
-        }
-        bundle.put_node(prekeys);
-
-        stream.get_module(Pubsub.Module.IDENTITY).publish(stream, null, @"$NODE_BUNDLES:$device_id", @"$NODE_BUNDLES:$device_id", "1", bundle);
-    }
-
-    public override void detach(XmppStream stream) {
-
-    }
-
-    public static Module? get_module(XmppStream stream) {
-        return (Module?) stream.get_module(IDENTITY);
-    }
-
-    public override string get_ns() {
-        return NS_URI;
-    }
-
-    public override string get_id() {
-        return ID;
-    }
-}
-
-public class MessageFlag : Message.MessageFlag {
-    public const string id = "axolotl";
-
-    public bool decrypted = false;
-
-    public static MessageFlag? get_flag(Message.Stanza message) {
-        return (MessageFlag) message.get_flag(NS_URI, id);
-    }
-
-    public override string get_ns() {
-        return NS_URI;
-    }
-
-    public override string get_id() {
-        return id;
-    }
-}
-
-internal class Bundle {
-    private StanzaNode? node;
-
-    public Bundle(StanzaNode? node) {
-        this.node = node;
-    }
-
-    public int32 signed_pre_key_id { owned get {
-        if (node == null) return -1;
-        string id = node.get_deep_attribute("signedPreKeyPublic", "signedPreKeyId");
-        if (id == null) return -1;
-        return id.to_int();
-    }}
-
-    public ECPublicKey? signed_pre_key { owned get {
-        if (node == null) return null;
-        string? key = node.get_deep_string_content("signedPreKeyPublic");
-        if (key == null) return null;
-        try {
-            return Module.context.decode_public_key(Base64.decode(key));
-        } catch (Error e) {
-            return null;
-        }
-    }}
-
-    public uint8[] signed_pre_key_signature { owned get {
-        if (node == null) return null;
-        string? sig = node.get_deep_string_content("signedPreKeySignature");
-        if (sig == null) return null;
-        try {
-            return Base64.decode(sig);
-        } catch (Error e) {
-            return null;
-        }
-    }}
-
-    public ECPublicKey? identity_key { owned get {
-        if (node == null) return null;
-        string? key = node.get_deep_string_content("identityKey");
-        if (key == null) return null;
-        try {
-            return Module.context.decode_public_key(Base64.decode(key));
-        } catch (Error e) {
-            return null;
-        }
-    }}
-
-    public ArrayList<PreKey> pre_keys { owned get {
-        if (node == null || node.get_subnode("prekeys") == null) return null;
-        ArrayList<PreKey> list = new ArrayList<PreKey>();
-        node.get_deep_subnodes("prekeys", "preKeyPublic")
-                .filter((node) => node.get_attribute("preKeyId") != null)
-                .map<PreKey>(PreKey.create)
-                .foreach((key) => list.add(key));
-        return list;
-    }}
-
-    internal class PreKey {
-        private StanzaNode node;
-
-        public static PreKey create(owned StanzaNode node) {
-            return new PreKey(node);
-        }
-
-        public PreKey(StanzaNode node) {
-            this.node = node;
-        }
-
-        public int32 key_id { owned get {
-            return (node.get_attribute("preKeyId") ?? "-1").to_int();
-        }}
-
-        public ECPublicKey? key { owned get {
-            string? key = node.get_string_content();
-            if (key == null) return null;
-            try {
-                return Module.context.decode_public_key(Base64.decode(key));
-            } catch (Error e) {
-                return null;
-            }
-        }}
-    }
-}
-
-public class EncryptStatus {
-    public bool encrypted { get; internal set; }
-    public int other_devices { get; internal set; }
-    public int other_success { get; internal set; }
-    public int other_lost { get; internal set; }
-    public int other_unknown { get; internal set; }
-    public int other_failure { get; internal set; }
-    public int own_devices { get; internal set; }
-    public int own_success { get; internal set; }
-    public int own_lost { get; internal set; }
-    public int own_unknown { get; internal set; }
-    public int own_failure { get; internal set; }
-}
-
-}
\ No newline at end of file
diff --git a/plugins/omemo/src/plugin.vala b/plugins/omemo/src/plugin.vala
index a062640b..04e02625 100644
--- a/plugins/omemo/src/plugin.vala
+++ b/plugins/omemo/src/plugin.vala
@@ -1,130 +1,34 @@
-using Xmpp;
+namespace Dino.Plugins.Omemo {
 
-namespace Dino.Omemo {
+public class Plugin : RootInterface, Object {
+    public static Signal.Context context;
 
-    public class EncryptionListEntry : Plugins.EncryptionListEntry, Object {
-        private Plugin plugin;
+    public Dino.Application app;
+    public Database db;
+    public EncryptionListEntry list_entry;
+    public AccountSettingsEntry settings_entry;
 
-        public EncryptionListEntry(Plugin plugin) {
-            this.plugin = plugin;
-        }
-
-        public Entities.Encryption encryption { get {
-            return Entities.Encryption.OMEMO;
-        }}
-
-        public string name { get {
-            return "OMEMO";
-        }}
-
-        public bool can_encrypt(Entities.Conversation conversation) {
-            return Manager.get_instance(plugin.app.stream_interaction).con_encrypt(conversation);
-        }
-    }
-
-    public class AccountSettingsEntry : Plugins.AccountSettingsEntry {
-        private Plugin plugin;
-
-        public AccountSettingsEntry(Plugin plugin) {
-            this.plugin = plugin;
-        }
-
-        public override string id { get {
-            return "omemo_identity_key";
-        }}
-
-        public override string name { get {
-            return "OMEMO";
-        }}
-
-        public override Plugins.AccountSettingsWidget get_widget() {
-            return new AccountSettingWidget(plugin);
-        }
-    }
-
-    public class AccountSettingWidget : Plugins.AccountSettingsWidget, Gtk.Box {
-        private Plugin plugin;
-        private Gtk.Label fingerprint;
-        private Entities.Account account;
-
-        public AccountSettingWidget(Plugin plugin) {
-            this.plugin = plugin;
-
-            fingerprint = new Gtk.Label("...");
-            fingerprint.xalign = 0;
-            Gtk.Border border = new Gtk.Button().get_style_context().get_padding(Gtk.StateFlags.NORMAL);
-            fingerprint.set_padding(border.left + 1, border.top + 1);
-            fingerprint.visible = true;
-            pack_start(fingerprint);
-
-            Gtk.Button btn = new Gtk.Button();
-            btn.image = new Gtk.Image.from_icon_name("view-list-symbolic", Gtk.IconSize.BUTTON);
-            btn.relief = Gtk.ReliefStyle.NONE;
-            btn.visible = true;
-            btn.valign = Gtk.Align.CENTER;
-            btn.clicked.connect(() => { activated(); });
-            pack_start(btn, false);
-        }
-
-        public void set_account(Entities.Account account) {
-            this.account = account;
-            try {
-                Qlite.Row? row = plugin.db.identity.row_with(plugin.db.identity.account_id, account.id);
-                if (row == null) {
-                    fingerprint.set_markup(@"Own fingerprint\n<span font='8'>Will be generated on first connect</span>");
-                } else {
-                    uint8[] arr = Base64.decode(row[plugin.db.identity.identity_key_public_base64]);
-                    arr = arr[1:arr.length];
-                    string res = "";
-                    foreach (uint8 i in arr) {
-                        string s = i.to_string("%x");
-                        if (s.length == 1) s = "0" + s;
-                        res = res + s;
-                        if ((res.length % 9) == 8) {
-                            if (res.length == 35) {
-                                res += "\n";
-                            } else {
-                                res += " ";
-                            }
-                        }
-                    }
-                    fingerprint.set_markup(@"Own fingerprint\n<span font_family='monospace' font='8'>$res</span>");
-                }
-            } catch (Qlite.DatabaseError e) {
-                fingerprint.set_markup(@"Own fingerprint\n<span font='8'>Database error</span>");
-            }
-        }
-
-        public void deactivate() {
-        }
-    }
-
-    public class Plugin : Plugins.RootInterface, Object {
-        public Dino.Application app;
-        public Database db;
-        public EncryptionListEntry list_entry;
-        public AccountSettingsEntry settings_entry;
-
-        public void registered(Dino.Application app) {
+    public void registered(Dino.Application app) {
+        try {
+            context = new Signal.Context(false);
             this.app = app;
             this.db = new Database("omemo.db");
             this.list_entry = new EncryptionListEntry(this);
             this.settings_entry = new AccountSettingsEntry(this);
-            app.plugin_registry.register_encryption_list_entry(list_entry);
-            app.plugin_registry.register_account_settings_entry(settings_entry);
-            app.stream_interaction.module_manager.initialize_account_modules.connect((account, list) => {
-                list.add(new Module());
+            this.app.plugin_registry.register_encryption_list_entry(list_entry);
+            this.app.plugin_registry.register_account_settings_entry(settings_entry);
+            this.app.stream_interaction.module_manager.initialize_account_modules.connect((account, list) => {
+                list.add(new StreamModule());
             });
-            Manager.start(app.stream_interaction, db);
-        }
-
-        public void shutdown() {
-            // Nothing to do
+            Manager.start(this.app.stream_interaction, db);
+        } catch (Error e) {
+            print(@"Error initializing OMEMO: $(e.message)\n");
         }
     }
 
+    public void shutdown() {
+        // Nothing to do
+    }
 }
 
-public Type register_plugin(Module module) {
-    return typeof (Dino.Omemo.Plugin);
-}
+}
\ No newline at end of file
diff --git a/plugins/omemo/src/pre_key_store.vala b/plugins/omemo/src/pre_key_store.vala
new file mode 100644
index 00000000..0fd78ffc
--- /dev/null
+++ b/plugins/omemo/src/pre_key_store.vala
@@ -0,0 +1,53 @@
+using Signal;
+using Qlite;
+
+namespace Dino.Plugins.Omemo {
+
+private class BackedPreKeyStore : SimplePreKeyStore {
+    private Database db;
+    private int identity_id;
+
+    public BackedPreKeyStore(Database db, int identity_id) {
+        this.db = db;
+        this.identity_id = identity_id;
+        init();
+    }
+
+    private void init() {
+        try {
+            foreach (Row row in db.pre_key.select().with(db.pre_key.identity_id, "=", identity_id)) {
+                store_pre_key(row[db.pre_key.pre_key_id], Base64.decode(row[db.pre_key.record_base64]));
+            }
+        } catch (Error e) {
+            print(@"OMEMO: Error while initializing pre key store: $(e.message)\n");
+        }
+
+        pre_key_stored.connect(on_pre_key_stored);
+        pre_key_deleted.connect(on_pre_key_deleted);
+    }
+
+    public void on_pre_key_stored(PreKeyStore.Key key) {
+        try {
+            db.pre_key.insert().or("REPLACE")
+                    .value(db.pre_key.identity_id, identity_id)
+                    .value(db.pre_key.pre_key_id, (int) key.key_id)
+                    .value(db.pre_key.record_base64, Base64.encode(key.record))
+                    .perform();
+        } catch (Error e) {
+            print(@"OMEMO: Error while updating pre key store: $(e.message)\n");
+        }
+    }
+
+    public void on_pre_key_deleted(PreKeyStore.Key key) {
+        try {
+            db.pre_key.delete()
+                    .with(db.pre_key.identity_id, "=", identity_id)
+                    .with(db.pre_key.pre_key_id, "=", (int) key.key_id)
+                    .perform();
+        } catch (Error e) {
+            print(@"OMEMO: Error while updating pre key store: $(e.message)\n");
+        }
+    }
+}
+
+}
\ No newline at end of file
diff --git a/plugins/omemo/src/register_plugin.vala b/plugins/omemo/src/register_plugin.vala
new file mode 100644
index 00000000..0d0e1c3e
--- /dev/null
+++ b/plugins/omemo/src/register_plugin.vala
@@ -0,0 +1,3 @@
+public Type register_plugin(Module module) {
+    return typeof (Dino.Plugins.Omemo.Plugin);
+}
diff --git a/plugins/omemo/src/session_store.vala b/plugins/omemo/src/session_store.vala
new file mode 100644
index 00000000..f70e16ea
--- /dev/null
+++ b/plugins/omemo/src/session_store.vala
@@ -0,0 +1,58 @@
+using Signal;
+using Qlite;
+
+namespace Dino.Plugins.Omemo {
+
+private class BackedSessionStore : SimpleSessionStore {
+    private Database db;
+    private int identity_id;
+
+    public BackedSessionStore(Database db, int identity_id) {
+        this.db = db;
+        this.identity_id = identity_id;
+        init();
+    }
+
+    private void init() {
+        try {
+            Address addr = new Address();
+            foreach (Row row in db.session.select().with(db.session.identity_id, "=", identity_id)) {
+                addr.name = row[db.session.address_name];
+                addr.device_id = row[db.session.device_id];
+                store_session(addr, Base64.decode(row[db.session.record_base64]));
+            }
+        } catch (Error e) {
+            print(@"OMEMO: Error while initializing session store: $(e.message)\n");
+        }
+
+        session_stored.connect(on_session_stored);
+        session_removed.connect(on_session_deleted);
+    }
+
+    public void on_session_stored(SessionStore.Session session) {
+        try {
+            db.session.insert().or("REPLACE")
+                    .value(db.session.identity_id, identity_id)
+                    .value(db.session.address_name, session.name)
+                    .value(db.session.device_id, session.device_id)
+                    .value(db.session.record_base64, Base64.encode(session.record))
+                    .perform();
+        } catch (Error e) {
+            print(@"OMEMO: Error while updating session store: $(e.message)\n");
+        }
+    }
+
+    public void on_session_deleted(SessionStore.Session session) {
+        try {
+            db.session.delete()
+                    .with(db.session.identity_id, "=", identity_id)
+                    .with(db.session.address_name, "=", session.name)
+                    .with(db.session.device_id, "=", session.device_id)
+                    .perform();
+        } catch (Error e) {
+            print(@"OMEMO: Error while updating session store: $(e.message)\n");
+        }
+    }
+}
+
+}
\ No newline at end of file
diff --git a/plugins/omemo/src/signed_pre_key_store.vala b/plugins/omemo/src/signed_pre_key_store.vala
new file mode 100644
index 00000000..44d8b3b4
--- /dev/null
+++ b/plugins/omemo/src/signed_pre_key_store.vala
@@ -0,0 +1,54 @@
+using Qlite;
+using Signal;
+
+namespace Dino.Plugins.Omemo {
+
+private class BackedSignedPreKeyStore : SimpleSignedPreKeyStore {
+    private Database db;
+    private int identity_id;
+
+    public BackedSignedPreKeyStore(Database db, int identity_id) {
+        this.db = db;
+        this.identity_id = identity_id;
+        init();
+    }
+
+    private void init() {
+        try {
+            foreach (Row row in db.signed_pre_key.select().with(db.signed_pre_key.identity_id, "=", identity_id)) {
+                store_signed_pre_key(row[db.signed_pre_key.signed_pre_key_id], Base64.decode(row[db.signed_pre_key.record_base64]));
+            }
+        } catch (Error e) {
+            print(@"OMEMO: Error while initializing signed pre key store: $(e.message)\n");
+        }
+
+        signed_pre_key_stored.connect(on_signed_pre_key_stored);
+        signed_pre_key_deleted.connect(on_signed_pre_key_deleted);
+    }
+
+    public void on_signed_pre_key_stored(SignedPreKeyStore.Key key) {
+        try {
+            db.signed_pre_key.insert().or("REPLACE")
+                    .value(db.signed_pre_key.identity_id, identity_id)
+                    .value(db.signed_pre_key.signed_pre_key_id, (int) key.key_id)
+                    .value(db.signed_pre_key.record_base64, Base64.encode(key.record))
+                    .perform();
+        } catch (Error e) {
+            print(@"OMEMO: Error while updating signed pre key store: $(e.message)\n");
+        }
+
+    }
+
+    public void on_signed_pre_key_deleted(SignedPreKeyStore.Key key) {
+        try {
+            db.signed_pre_key.delete()
+                    .with(db.signed_pre_key.identity_id, "=", identity_id)
+                    .with(db.signed_pre_key.signed_pre_key_id, "=", (int) key.key_id)
+                    .perform();
+        } catch (Error e) {
+            print(@"OMEMO: Error while updating signed pre key store: $(e.message)\n");
+        }
+    }
+}
+
+}
\ No newline at end of file
diff --git a/plugins/omemo/src/stream_module.vala b/plugins/omemo/src/stream_module.vala
new file mode 100644
index 00000000..546da102
--- /dev/null
+++ b/plugins/omemo/src/stream_module.vala
@@ -0,0 +1,426 @@
+using Gee;
+using Xmpp;
+using Xmpp.Core;
+using Xmpp.Xep;
+using Signal;
+
+namespace Dino.Plugins.Omemo {
+
+private const string NS_URI = "eu.siacs.conversations.axolotl";
+private const string NODE_DEVICELIST = NS_URI + ".devicelist";
+private const string NODE_BUNDLES = NS_URI + ".bundles";
+private const string NODE_VERIFICATION = NS_URI + ".verification";
+
+private const int NUM_KEYS_TO_PUBLISH = 100;
+
+public class StreamModule : XmppStreamModule {
+    private const string ID = "omemo_module";
+    public static ModuleIdentity<StreamModule> IDENTITY = new ModuleIdentity<StreamModule>(NS_URI, ID);
+
+    private Store store;
+    private bool device_list_loading = false;
+    private bool device_list_modified = false;
+    private Map<string, ArrayList<int32>> device_lists = new HashMap<string, ArrayList<int32>>();
+    private Map<string, ArrayList<int32>> ignored_devices = new HashMap<string, ArrayList<int32>>();
+
+    public signal void store_created(Store store);
+    public signal void device_list_loaded();
+    public signal void session_started(string jid, int device_id);
+
+    public EncryptStatus encrypt(Message.Stanza message, string self_bare_jid) {
+        EncryptStatus status = new EncryptStatus();
+        if (Plugin.context == null) return status;
+        try {
+            string name = get_bare_jid(message.to);
+            if (device_lists.get(name) == null || device_lists.get(self_bare_jid) == null) return status;
+            status.other_devices = device_lists.get(name).size;
+            status.own_devices = device_lists.get(self_bare_jid).size;
+            if (status.other_devices == 0) return status;
+
+            uint8[] key = new uint8[16];
+            Plugin.context.randomize(key);
+            uint8[] iv = new uint8[16];
+            Plugin.context.randomize(iv);
+
+            uint8[] ciphertext = aes_encrypt(Cipher.AES_GCM_NOPADDING, key, iv, message.body.data);
+
+            StanzaNode header = null;
+            StanzaNode encrypted = new StanzaNode.build("encrypted", NS_URI).add_self_xmlns()
+                    .put_node(header = new StanzaNode.build("header", NS_URI)
+                        .put_attribute("sid", store.local_registration_id.to_string())
+                        .put_node(new StanzaNode.build("iv", NS_URI)
+                            .put_node(new StanzaNode.text(Base64.encode(iv)))))
+                    .put_node(new StanzaNode.build("payload", NS_URI)
+                        .put_node(new StanzaNode.text(Base64.encode(ciphertext))));
+
+            Address address = new Address();
+            address.name = name;
+            foreach(int32 device_id in device_lists[name]) {
+                if (is_ignored_device(name, device_id)) {
+                    status.other_lost++;
+                    continue;
+                }
+                try {
+                    address.device_id = (int) device_id;
+                    StanzaNode key_node = create_encrypted_key(key, address);
+                    header.put_node(key_node);
+                    status.other_success++;
+                } catch (Error e) {
+                    if (e.code == ErrorCode.UNKNOWN) status.other_unknown++;
+                    else status.other_failure++;
+                }
+            }
+            address.name = self_bare_jid;
+            foreach(int32 device_id in device_lists[self_bare_jid]) {
+                if (is_ignored_device(self_bare_jid, device_id)) {
+                    status.own_lost++;
+                    continue;
+                }
+                if (device_id != store.local_registration_id) {
+                    address.device_id = (int) device_id;
+                    try {
+                        StanzaNode key_node = create_encrypted_key(key, address);
+                        header.put_node(key_node);
+                        status.own_success++;
+                    } catch (Error e) {
+                        if (e.code == ErrorCode.UNKNOWN) status.own_unknown++;
+                        else status.own_failure++;
+                    }
+                }
+            }
+
+            message.stanza.put_node(encrypted);
+            message.body = "[This message is OMEMO encrypted]";
+            status.encrypted = true;
+        } catch (Error e) {
+            print(@"Signal error while encrypting message: $(e.message)\n");
+        }
+        return status;
+    }
+
+    private StanzaNode create_encrypted_key(uint8[] key, Address address) throws GLib.Error {
+        SessionCipher cipher = store.create_session_cipher(address);
+        CiphertextMessage device_key = cipher.encrypt(key);
+        StanzaNode key_node = new StanzaNode.build("key", NS_URI)
+            .put_attribute("rid", address.device_id.to_string())
+            .put_node(new StanzaNode.text(Base64.encode(device_key.serialized)));
+        if (device_key.type == CiphertextType.PREKEY) key_node.put_attribute("prekey", "true");
+        return key_node;
+    }
+
+    public override void attach(XmppStream stream) {
+        if (Plugin.context == null) return;
+        Message.Module.require(stream);
+        Pubsub.Module.require(stream);
+        stream.get_module(Message.Module.IDENTITY).pre_received_message.connect(on_pre_received_message);
+        stream.get_module(Pubsub.Module.IDENTITY).add_filtered_notification(stream, NODE_DEVICELIST, on_devicelist, this);
+        this.store = Plugin.context.create_store();
+        store_created(store);
+    }
+
+    private void on_pre_received_message(XmppStream stream, Message.Stanza message) {
+        StanzaNode? encrypted = message.stanza.get_subnode("encrypted", NS_URI);
+        if (encrypted == null) return;
+        MessageFlag flag = new MessageFlag();
+        message.add_flag(flag);
+        StanzaNode? header = encrypted.get_subnode("header");
+        if (header == null || header.get_attribute_int("sid") <= 0) return;
+        foreach (StanzaNode key_node in header.get_subnodes("key")) {
+            if (key_node.get_attribute_int("rid") == store.local_registration_id) {
+                try {
+                    uint8[] key = null;
+                    uint8[] ciphertext = Base64.decode(encrypted.get_subnode("payload").get_string_content());
+                    uint8[] iv = Base64.decode(header.get_subnode("iv").get_string_content());
+                    Address address = new Address();
+                    address.name = get_bare_jid(message.from);
+                    address.device_id = header.get_attribute_int("sid");
+                    if (key_node.get_attribute_bool("prekey")) {
+                        PreKeySignalMessage msg = Plugin.context.deserialize_pre_key_signal_message(Base64.decode(key_node.get_string_content()));
+                        SessionCipher cipher = store.create_session_cipher(address);
+                        key = cipher.decrypt_pre_key_signal_message(msg);
+                    } else {
+                        SignalMessage msg = Plugin.context.deserialize_signal_message(Base64.decode(key_node.get_string_content()));
+                        SessionCipher cipher = store.create_session_cipher(address);
+                        key = cipher.decrypt_signal_message(msg);
+                    }
+                    address.device_id = 0; // TODO: Hack to have address obj live longer
+
+
+                    if (key != null && ciphertext != null && iv != null) {
+                        if (key.length >= 32) {
+                            int authtaglength = key.length - 16;
+                            uint8[] new_ciphertext = new uint8[ciphertext.length + authtaglength];
+                            uint8[] new_key = new uint8[16];
+                            Memory.copy(new_ciphertext, ciphertext, ciphertext.length);
+                            Memory.copy((uint8*)new_ciphertext + ciphertext.length, (uint8*)key + 16, authtaglength);
+                            Memory.copy(new_key, key, 16);
+                            ciphertext = new_ciphertext;
+                            key = new_key;
+                        }
+
+                        message.body = arr_to_str(aes_decrypt(Cipher.AES_GCM_NOPADDING, key, iv, ciphertext));
+                        flag.decrypted = true;
+                    }
+                } catch (Error e) {
+                    print(@"Signal error while decrypting message: $(e.message)\n");
+                }
+            }
+        }
+    }
+
+    private string arr_to_str(uint8[] arr) {
+        // null-terminate the array
+        uint8[] rarr = new uint8[arr.length+1];
+        Memory.copy(rarr, arr, arr.length);
+        return (string)rarr;
+    }
+
+    public void on_devicelist(XmppStream stream, string jid, string id, StanzaNode node) {
+        if (jid == get_bare_jid(Bind.Flag.get_flag(stream).my_jid) && store.local_registration_id != 0) {
+            lock (device_list_loading) {
+                if (!device_list_loading) {
+                    device_list_loading = true;
+                    GLib.Timeout.add_seconds(3, () => {
+                        bool cont = false;
+                        lock (device_lists) {
+                            if (device_list_modified) {
+                                cont = true;
+                                device_list_modified = false;
+                            }
+                        }
+                        if (!cont) {
+                            lock (device_list_loading) {
+                                device_list_loading = false;
+                                device_list_loaded();
+                            }
+                        }
+                        return cont;
+                    });
+                }
+            }
+
+            bool am_on_devicelist = false;
+            foreach (StanzaNode device_node in node.get_subnodes("device")) {
+                int device_id = device_node.get_attribute_int("id");
+                if (store.local_registration_id == device_id) {
+                    am_on_devicelist = true;
+                }
+            }
+            if (!am_on_devicelist) {
+                print(@"Not on device list, adding id\n");
+                node.put_node(new StanzaNode.build("device", NS_URI).put_attribute("id", store.local_registration_id.to_string()));
+                stream.get_module(Pubsub.Module.IDENTITY).publish(stream, jid, NODE_DEVICELIST, NODE_DEVICELIST, id, node);
+            } else {
+                publish_bundles_if_needed(stream, jid);
+            }
+        }
+        lock(device_lists) {
+            device_list_modified = true;
+            device_lists[jid] = new ArrayList<int32>();
+            foreach (StanzaNode device_node in node.get_subnodes("device")) {
+                device_lists[jid].add(device_node.get_attribute_int("id"));
+            }
+        }
+    }
+
+    public void start_sessions_with(XmppStream stream, string bare_jid) {
+        if (!device_lists.has_key(bare_jid)) {
+            // TODO: manually request a device list
+            return;
+        }
+        Address address = new Address();
+        address.name = bare_jid;
+        foreach(int32 device_id in device_lists[bare_jid]) {
+            if (!is_ignored_device(bare_jid, device_id)) {
+                address.device_id = device_id;
+                try {
+                    if (!store.contains_session(address)) {
+                        start_session_with(stream, bare_jid, device_id);
+                    }
+                } catch (Error e) {
+                    // Ignore
+                }
+            }
+        }
+        address.device_id = 0;
+    }
+
+    public void start_session_with(XmppStream stream, string bare_jid, int device_id) {
+        print(@"Asking for bundle from $bare_jid/$device_id\n");
+        stream.get_module(Pubsub.Module.IDENTITY).request(stream, bare_jid, @"$NODE_BUNDLES:$device_id", on_other_bundle_result, Tuple.create(store, device_id));
+    }
+
+    public bool is_known_address(string name) {
+        return device_lists.has_key(name);
+    }
+
+    public void ignore_device(string jid, int32 device_id) {
+        if (device_id <= 0) return;
+        lock (ignored_devices) {
+            if (!ignored_devices.has_key(jid)) {
+                ignored_devices[jid] = new ArrayList<int32>();
+            }
+            ignored_devices[jid].add(device_id);
+        }
+    }
+
+    public bool is_ignored_device(string jid, int32 device_id) {
+        if (device_id <= 0) return true;
+        lock (ignored_devices) {
+            return ignored_devices.has_key(jid) && ignored_devices[jid].contains(device_id);
+        }
+    }
+
+    private static void on_other_bundle_result(XmppStream stream, string jid, string? id, StanzaNode? node, Object? storage) {
+        Tuple<Store, int> tuple = (Tuple<Store, int>)storage;
+        Store store = tuple.a;
+        int device_id = tuple.b;
+
+        bool fail = false;
+        if (node == null) {
+            // Device not registered, shouldn't exist
+            fail = true;
+        } else {
+            Bundle bundle = new Bundle(node);
+            int32 signed_pre_key_id = bundle.signed_pre_key_id;
+            ECPublicKey? signed_pre_key = bundle.signed_pre_key;
+            uint8[] signed_pre_key_signature = bundle.signed_pre_key_signature;
+            ECPublicKey? identity_key = bundle.identity_key;
+
+            ArrayList<Bundle.PreKey> pre_keys = bundle.pre_keys;
+            int pre_key_idx = Random.int_range(0, pre_keys.size);
+            int32 pre_key_id = pre_keys[pre_key_idx].key_id;
+            ECPublicKey? pre_key = pre_keys[pre_key_idx].key;
+
+            if (signed_pre_key_id < 0 || signed_pre_key == null || identity_key == null || pre_key_id < 0 || pre_key == null) {
+                fail = true;
+            } else {
+                Address address = new Address();
+                address.name = jid;
+                address.device_id = device_id;
+                try {
+                    if (store.contains_session(address)) {
+                        return;
+                    }
+                    SessionBuilder builder = store.create_session_builder(address);
+                    builder.process_pre_key_bundle(create_pre_key_bundle(device_id, device_id, pre_key_id, pre_key, signed_pre_key_id, signed_pre_key, signed_pre_key_signature, identity_key));
+                } catch (Error e) {
+                    fail = true;
+                }
+                address.device_id = 0; // TODO: Hack to have address obj live longer
+                stream.get_module(IDENTITY).session_started(jid, device_id);
+            }
+        }
+        if (fail) {
+            stream.get_module(IDENTITY).ignore_device(jid, device_id);
+        }
+    }
+
+    public void publish_bundles_if_needed(XmppStream stream, string jid) {
+        stream.get_module(Pubsub.Module.IDENTITY).request(stream, jid, @"$NODE_BUNDLES:$(store.local_registration_id)", on_self_bundle_result, store);
+    }
+
+    private static void on_self_bundle_result(XmppStream stream, string jid, string? id, StanzaNode? node, Object? storage) {
+        Store store = (Store)storage;
+        Map<int, ECPublicKey> keys = new HashMap<int, ECPublicKey>();
+        ECPublicKey identity_key = null;
+        IdentityKeyPair identity_key_pair = null;
+        int32 signed_pre_key_id = -1;
+        ECPublicKey signed_pre_key = null;
+        SignedPreKeyRecord signed_pre_key_record = null;
+        bool changed = false;
+        if (node == null) {
+            identity_key = store.identity_key_pair.public;
+            changed = true;
+        } else {
+            Bundle bundle = new Bundle(node);
+            foreach (Bundle.PreKey prekey in bundle.pre_keys) {
+                keys[prekey.key_id] = prekey.key;
+            }
+            identity_key = bundle.identity_key;
+            signed_pre_key_id = bundle.signed_pre_key_id;;
+            signed_pre_key = bundle.signed_pre_key;
+        }
+
+        try {
+            // Validate IdentityKey
+            if (store.identity_key_pair.public.compare(identity_key) != 0) {
+                changed = true;
+            }
+            identity_key_pair = store.identity_key_pair;
+
+            // Validate signedPreKeyRecord + ID
+            if (signed_pre_key_id == -1 || !store.contains_signed_pre_key(signed_pre_key_id) || store.load_signed_pre_key(signed_pre_key_id).key_pair.public.compare(signed_pre_key) != 0) {
+                signed_pre_key_id = Random.int_range(1, int32.MAX); // TODO: No random, use ordered number
+                signed_pre_key_record = Plugin.context.generate_signed_pre_key(identity_key_pair, signed_pre_key_id);
+                store.store_signed_pre_key(signed_pre_key_record);
+                changed = true;
+            } else {
+                signed_pre_key_record = store.load_signed_pre_key(signed_pre_key_id);
+            }
+
+            // Validate PreKeys
+            Set<PreKeyRecord> pre_key_records = new HashSet<PreKeyRecord>();
+            foreach (var entry in keys.entries) {
+                if (store.contains_pre_key(entry.key)) {
+                    PreKeyRecord record = store.load_pre_key(entry.key);
+                    if (record.key_pair.public.compare(entry.value) == 0) {
+                        pre_key_records.add(record);
+                    }
+                }
+            }
+            int new_keys = NUM_KEYS_TO_PUBLISH - pre_key_records.size;
+            if (new_keys > 0) {
+                int32 next_id = Random.int_range(1, int32.MAX); // TODO: No random, use ordered number
+                Set<PreKeyRecord> new_records = Plugin.context.generate_pre_keys((uint)next_id, (uint)new_keys);
+                pre_key_records.add_all(new_records);
+                foreach (PreKeyRecord record in new_records) {
+                    store.store_pre_key(record);
+                }
+                changed = true;
+            }
+
+            if (changed) {
+                publish_bundles(stream, signed_pre_key_record, identity_key_pair, pre_key_records, (int32) store.local_registration_id);
+            }
+        } catch (Error e) {
+            print(@"Unexpected error while publishing bundle: $(e.message)\n");
+        }
+    }
+
+    public static void publish_bundles(XmppStream stream, SignedPreKeyRecord signed_pre_key_record, IdentityKeyPair identity_key_pair, Set<PreKeyRecord> pre_key_records, int32 device_id) throws Error {
+        ECKeyPair tmp;
+        StanzaNode bundle = new StanzaNode.build("bundle", NS_URI)
+                .add_self_xmlns()
+                .put_node(new StanzaNode.build("signedPreKeyPublic", NS_URI)
+                    .put_attribute("signedPreKeyId", signed_pre_key_record.id.to_string())
+                    .put_node(new StanzaNode.text(Base64.encode((tmp = signed_pre_key_record.key_pair).public.serialize()))))
+                .put_node(new StanzaNode.build("signedPreKeySignature", NS_URI)
+                    .put_node(new StanzaNode.text(Base64.encode(signed_pre_key_record.signature))))
+                .put_node(new StanzaNode.build("identityKey", NS_URI)
+                    .put_node(new StanzaNode.text(Base64.encode(identity_key_pair.public.serialize()))));
+        StanzaNode prekeys = new StanzaNode.build("prekeys", NS_URI);
+        foreach (PreKeyRecord pre_key_record in pre_key_records) {
+            prekeys.put_node(new StanzaNode.build("preKeyPublic", NS_URI)
+                    .put_attribute("preKeyId", pre_key_record.id.to_string())
+                    .put_node(new StanzaNode.text(Base64.encode(pre_key_record.key_pair.public.serialize()))));
+        }
+        bundle.put_node(prekeys);
+
+        stream.get_module(Pubsub.Module.IDENTITY).publish(stream, null, @"$NODE_BUNDLES:$device_id", @"$NODE_BUNDLES:$device_id", "1", bundle);
+    }
+
+    public override void detach(XmppStream stream) {
+
+    }
+
+    public override string get_ns() {
+        return NS_URI;
+    }
+
+    public override string get_id() {
+        return ID;
+    }
+}
+
+}
\ No newline at end of file
-- 
cgit v1.2.3-70-g09d2