diff --git a/.gitignore b/.gitignore index 1cd39c55..47bfd683 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ Makefile *.iml .idea .sqlite3 +gschemas.compiled diff --git a/libdino/CMakeLists.txt b/libdino/CMakeLists.txt index 10972b9a..08bf88d6 100644 --- a/libdino/CMakeLists.txt +++ b/libdino/CMakeLists.txt @@ -4,7 +4,6 @@ include(${VALA_USE_FILE}) set(LIBDINO_PACKAGES gee-0.8 - gio-2.0 glib-2.0 gtk+-3.0 gmodule-2.0 diff --git a/libdino/src/plugin/loader.vala b/libdino/src/plugin/loader.vala index 43ce0801..acb26ff4 100644 --- a/libdino/src/plugin/loader.vala +++ b/libdino/src/plugin/loader.vala @@ -1,12 +1,5 @@ namespace Dino.Plugins { -public errordomain Error { - NOT_SUPPORTED, - UNEXPECTED_TYPE, - NO_REGISTRATION_FUNCTION, - FAILED -} - private class Info : Object { public Module module; public Type gtype; @@ -26,24 +19,24 @@ public class Loader : Object { public RootInterface load(string name, Dino.Application app) throws Error { if (Module.supported () == false) { - throw new Error.NOT_SUPPORTED ("Plugins are not supported"); + throw new Error (-1, 0, "Plugins are not supported"); } Module module = Module.open ("plugins/" + name, ModuleFlags.BIND_LAZY); if (module == null) { - throw new Error.FAILED (Module.error ()); + throw new Error (-1, 1, Module.error ()); } void* function; module.symbol ("register_plugin", out function); if (function == null) { - throw new Error.NO_REGISTRATION_FUNCTION ("register_plugin () not found"); + throw new Error (-1, 2, "register_plugin () not found"); } RegisterPluginFunction register_plugin = (RegisterPluginFunction) function; Type type = register_plugin (module); if (type.is_a (typeof (RootInterface)) == false) { - throw new Error.UNEXPECTED_TYPE ("Unexpected type"); + throw new Error (-1, 3, "Unexpected type"); } Info info = new Plugins.Info (type, (owned) module); diff --git a/main/src/main.vala b/main/src/main.vala index 0fe72878..dfaa661e 100644 --- a/main/src/main.vala +++ b/main/src/main.vala @@ -10,7 +10,7 @@ void main(string[] args) { foreach(string plugin in new string[]{"omemo", "openpgp"}) { try { loader.load(plugin, app); - } catch (Plugins.Error e) { + } catch (Error e) { print(@"Error loading plugin $plugin: $(e.message)\n"); } } 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\nWill be generated on first connect"); + } 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$res"); + } + } catch (Qlite.DatabaseError e) { + fingerprint.set_markup(@"Own fingerprint\nDatabase error"); + } + } + + 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 pre_keys { owned get { + ArrayList list = new ArrayList(); + 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.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); - - 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(); - try { + store.identity_key_store.local_registration_id = Random.int_range(1, int32.MAX); + + 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(); + 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/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\nWill be generated on first connect"); - } 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$res"); - } - } catch (Qlite.DatabaseError e) { - fingerprint.set_markup(@"Own fingerprint\nDatabase error"); - } - } - - 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/module.vala b/plugins/omemo/src/stream_module.vala similarity index 69% rename from plugins/omemo/src/module.vala rename to plugins/omemo/src/stream_module.vala index 728251f0..546da102 100644 --- a/plugins/omemo/src/module.vala +++ b/plugins/omemo/src/stream_module.vala @@ -4,7 +4,7 @@ using Xmpp.Core; using Xmpp.Xep; using Signal; -namespace Dino.Omemo { +namespace Dino.Plugins.Omemo { private const string NS_URI = "eu.siacs.conversations.axolotl"; private const string NODE_DEVICELIST = NS_URI + ".devicelist"; @@ -13,36 +13,23 @@ 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 IDENTITY = new ModuleIdentity(NS_URI, ID); +public class StreamModule : XmppStreamModule { + private const string ID = "omemo_module"; + public static ModuleIdentity IDENTITY = new ModuleIdentity(NS_URI, ID); private Store store; - internal static Context context; private bool device_list_loading = false; private bool device_list_modified = false; private Map> device_lists = new HashMap>(); private Map> ignored_devices = new HashMap>(); - public signal void store_created(Context context, Store store); + public signal void store_created(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; + 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; @@ -51,9 +38,9 @@ public class Module : XmppStreamModule { if (status.other_devices == 0) return status; uint8[] key = new uint8[16]; - context.randomize(key); + Plugin.context.randomize(key); uint8[] iv = new uint8[16]; - context.randomize(iv); + Plugin.context.randomize(iv); uint8[] ciphertext = aes_encrypt(Cipher.AES_GCM_NOPADDING, key, iv, message.body.data); @@ -106,7 +93,7 @@ public class Module : XmppStreamModule { message.body = "[This message is OMEMO encrypted]"; status.encrypted = true; } catch (Error e) { - print(@"Axolotl error while encrypting message: $(e.message)\n"); + print(@"Signal error while encrypting message: $(e.message)\n"); } return status; } @@ -122,13 +109,13 @@ public class Module : XmppStreamModule { } public override void attach(XmppStream stream) { - if (context == null) return; + 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 = context.create_store(); - store_created(context, store); + this.store = Plugin.context.create_store(); + store_created(store); } private void on_pre_received_message(XmppStream stream, Message.Stanza message) { @@ -148,11 +135,11 @@ public class Module : XmppStreamModule { 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())); + 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 = context.deserialize_signal_message(Base64.decode(key_node.get_string_content())); + 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); } @@ -175,7 +162,7 @@ public class Module : XmppStreamModule { flag.decrypted = true; } } catch (Error e) { - print(@"Axolotl error while decrypting message: $(e.message)\n"); + print(@"Signal error while decrypting message: $(e.message)\n"); } } } @@ -246,8 +233,12 @@ public class Module : XmppStreamModule { 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); + try { + if (!store.contains_session(address)) { + start_session_with(stream, bare_jid, device_id); + } + } catch (Error e) { + // Ignore } } } @@ -259,6 +250,10 @@ public class Module : XmppStreamModule { 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) { @@ -313,11 +308,11 @@ public class Module : XmppStreamModule { fail = true; } address.device_id = 0; // TODO: Hack to have address obj live longer - get_module(stream).session_started(jid, device_id); + stream.get_module(IDENTITY).session_started(jid, device_id); } } if (fail) { - get_module(stream).ignore_device(jid, device_id); + stream.get_module(IDENTITY).ignore_device(jid, device_id); } } @@ -347,49 +342,53 @@ public class Module : XmppStreamModule { 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; + 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 = 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 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 pre_key_records = new HashSet(); - 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); + // Validate PreKeys + Set pre_key_records = new HashSet(); + 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 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); + 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 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; } - changed = true; - } - if (changed) { - publish_bundles(stream, signed_pre_key_record, identity_key_pair, pre_key_records, (int32) store.local_registration_id); + 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 pre_key_records, int32 device_id) { + public static void publish_bundles(XmppStream stream, SignedPreKeyRecord signed_pre_key_record, IdentityKeyPair identity_key_pair, Set pre_key_records, int32 device_id) throws Error { ECKeyPair tmp; StanzaNode bundle = new StanzaNode.build("bundle", NS_URI) .add_self_xmlns() @@ -415,10 +414,6 @@ public class Module : XmppStreamModule { } - public static Module? get_module(XmppStream stream) { - return (Module?) stream.get_module(IDENTITY); - } - public override string get_ns() { return NS_URI; } @@ -428,120 +423,4 @@ public class Module : XmppStreamModule { } } -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 pre_keys { owned get { - if (node == null || node.get_subnode("prekeys") == null) return null; - ArrayList list = new ArrayList(); - node.get_deep_subnodes("prekeys", "preKeyPublic") - .filter((node) => node.get_attribute("preKeyId") != null) - .map(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/xmpp-vala/CMakeLists.txt b/xmpp-vala/CMakeLists.txt index 62c653e2..d1c66726 100644 --- a/xmpp-vala/CMakeLists.txt +++ b/xmpp-vala/CMakeLists.txt @@ -1,6 +1,5 @@ find_package(Vala REQUIRED) find_package(PkgConfig REQUIRED) -find_package(GPGME REQUIRED) find_package(LIBUUID REQUIRED) include(GlibCompileResourcesSupport) include(${VALA_USE_FILE}) diff --git a/xmpp-vala/src/core/stanza_node.vala b/xmpp-vala/src/core/stanza_node.vala index f615a240..aff1770d 100644 --- a/xmpp-vala/src/core/stanza_node.vala +++ b/xmpp-vala/src/core/stanza_node.vala @@ -99,7 +99,7 @@ public class StanzaNode : StanzaEntry { return res.down() == "true" || res == "1"; } - public StanzaAttribute get_attribute_raw(string name, string? ns_uri = null) { + public StanzaAttribute? get_attribute_raw(string name, string? ns_uri = null) { string _name = name; string? _ns_uri = ns_uri; if (_ns_uri == null) { @@ -225,12 +225,12 @@ public class StanzaNode : StanzaEntry { public ArrayList get_deep_subnodes_(va_list l) { StanzaNode? node = this; string? subnode_name = l.arg(); - if (subnode_name == null) return null; + if (subnode_name == null) return new ArrayList(); while(true) { string? s = l.arg(); if (s == null) break; node = node.get_subnode(subnode_name); - if (node == null) return null; + if (node == null) return new ArrayList(); subnode_name = s; } return node.get_subnodes(subnode_name); diff --git a/xmpp-vala/src/core/xmpp_stream.vala b/xmpp-vala/src/core/xmpp_stream.vala index 38b4abb4..57eafe45 100644 --- a/xmpp-vala/src/core/xmpp_stream.vala +++ b/xmpp-vala/src/core/xmpp_stream.vala @@ -93,7 +93,7 @@ public class XmppStream { } } - public IOStream? get_stream() { + internal IOStream? get_stream() { return stream; }