aboutsummaryrefslogtreecommitdiff
path: root/plugins/omemo/src/ui
diff options
context:
space:
mode:
Diffstat (limited to 'plugins/omemo/src/ui')
-rw-r--r--plugins/omemo/src/ui/account_settings_entry.vala58
-rw-r--r--plugins/omemo/src/ui/contact_details_provider.vala1
-rw-r--r--plugins/omemo/src/ui/encryption_list_entry.vala7
-rw-r--r--plugins/omemo/src/ui/encryption_preferences_entry.vala336
-rw-r--r--plugins/omemo/src/ui/util.vala46
5 files changed, 355 insertions, 93 deletions
diff --git a/plugins/omemo/src/ui/account_settings_entry.vala b/plugins/omemo/src/ui/account_settings_entry.vala
deleted file mode 100644
index 8736260b..00000000
--- a/plugins/omemo/src/ui/account_settings_entry.vala
+++ /dev/null
@@ -1,58 +0,0 @@
-using Dino.Entities;
-using Gtk;
-
-namespace Dino.Plugins.Omemo {
-
-public class AccountSettingsEntry : Plugins.AccountSettingsEntry {
- private Plugin plugin;
- private Account account;
-
- private Box box = new Box(Orientation.HORIZONTAL, 0);
- private Label fingerprint = new Label("...") { xalign=0 };
- private Button btn = new Button.from_icon_name("view-list-symbolic") { has_frame=false, valign=Align.CENTER, visible=false };
-
- public override string id { get { return "omemo_identity_key"; }}
-
- public override string name { get { return "OMEMO"; }}
-
- public AccountSettingsEntry(Plugin plugin) {
- this.plugin = plugin;
-
- Border border = new Button().get_style_context().get_padding();
- fingerprint.margin_top = border.top + 1;
- fingerprint.margin_start = border.left + 1;
- fingerprint.visible = true;
- box.append(fingerprint);
-
- btn.clicked.connect(() => {
- activated();
- ContactDetailsDialog dialog = new ContactDetailsDialog(plugin, account, account.bare_jid);
- dialog.set_transient_for((Window) box.get_root());
- dialog.present();
- });
- // TODO expand=false?
- box.append(btn);
- }
-
- public override Object? get_widget(WidgetType type) {
- if (type != WidgetType.GTK4) return null;
- return box;
- }
-
- public override void set_account(Account account) {
- this.account = account;
- btn.visible = false;
- Qlite.Row? row = plugin.db.identity.row_with(plugin.db.identity.account_id, account.id).inner;
- if (row == null) {
- fingerprint.set_markup("%s\n<span font='8'>%s</span>".printf(_("Own fingerprint"), _("Will be generated on first connection")));
- } else {
- string res = fingerprint_markup(fingerprint_from_base64(((!)row)[plugin.db.identity.identity_key_public_base64]));
- fingerprint.set_markup("%s\n<span font_family='monospace' font='8'>%s</span>".printf(_("Own fingerprint"), res));
- btn.visible = true;
- }
- }
-
- public override void deactivate() { }
-}
-
-} \ No newline at end of file
diff --git a/plugins/omemo/src/ui/contact_details_provider.vala b/plugins/omemo/src/ui/contact_details_provider.vala
index 822294cc..a97a40ad 100644
--- a/plugins/omemo/src/ui/contact_details_provider.vala
+++ b/plugins/omemo/src/ui/contact_details_provider.vala
@@ -29,6 +29,7 @@ public class ContactDetailsProvider : Plugins.ContactDetailsProvider, Object {
if (i > 0) {
Button btn = new Button.from_icon_name("view-list-symbolic") { visible = true, valign = Align.CENTER, has_frame = false };
+ btn.tooltip_text = _("OMEMO Key Management");
btn.clicked.connect(() => {
btn.activate();
ContactDetailsDialog dialog = new ContactDetailsDialog(plugin, conversation.account, conversation.counterpart);
diff --git a/plugins/omemo/src/ui/encryption_list_entry.vala b/plugins/omemo/src/ui/encryption_list_entry.vala
index b262ef81..3bb76c52 100644
--- a/plugins/omemo/src/ui/encryption_list_entry.vala
+++ b/plugins/omemo/src/ui/encryption_list_entry.vala
@@ -53,7 +53,12 @@ public class EncryptionListEntry : Plugins.EncryptionListEntry, Object {
Manager omemo_manager = plugin.app.stream_interactor.get_module(Manager.IDENTITY);
if (muc_manager.is_private_room(conversation.account, conversation.counterpart)) {
- foreach (Jid offline_member in muc_manager.get_offline_members(conversation.counterpart, conversation.account)) {
+ var offline_members = muc_manager.get_offline_members(conversation.counterpart, conversation.account);
+ if (offline_members == null) {
+ // We don't store offline members yet, and it'll be null if we're offline
+ return;
+ }
+ foreach (Jid offline_member in offline_members) {
bool ok = yield omemo_manager.ensure_get_keys_for_jid(conversation.account, offline_member);
if (!ok) {
input_status_callback(new Plugins.InputFieldStatus("A member does not support OMEMO: %s".printf(offline_member.to_string()), Plugins.InputFieldStatus.MessageType.ERROR, Plugins.InputFieldStatus.InputState.NO_SEND));
diff --git a/plugins/omemo/src/ui/encryption_preferences_entry.vala b/plugins/omemo/src/ui/encryption_preferences_entry.vala
new file mode 100644
index 00000000..7997f04d
--- /dev/null
+++ b/plugins/omemo/src/ui/encryption_preferences_entry.vala
@@ -0,0 +1,336 @@
+using Qlite;
+using Qrencode;
+using Gee;
+using Xmpp;
+using Dino.Entities;
+using Gtk;
+
+namespace Dino.Plugins.Omemo {
+
+public class OmemoPreferencesEntry : Plugins.EncryptionPreferencesEntry {
+
+ OmemoPreferencesWidget widget;
+ Plugin plugin;
+
+ public OmemoPreferencesEntry(Plugin plugin) {
+ this.plugin = plugin;
+ }
+
+ public override Object? get_widget(Account account, WidgetType type) {
+ if (type != WidgetType.GTK4) return null;
+ var widget = new OmemoPreferencesWidget(plugin);
+ widget.set_account(account);
+ return widget;
+ }
+
+ public override string id { get { return "omemo_preferences_entryption"; }}
+}
+
+[GtkTemplate (ui = "/im/dino/Dino/omemo/encryption_preferences_entry.ui")]
+public class OmemoPreferencesWidget : Adw.PreferencesGroup {
+ private Plugin plugin;
+ private Account account;
+ private Jid jid;
+ private int identity_id = 0;
+ private Signal.Store store;
+ private Set<uint32> displayed_ids = new HashSet<uint32>();
+
+ [GtkChild] private unowned Adw.ActionRow automatically_accept_new_row;
+ [GtkChild] private Switch automatically_accept_new_switch;
+ [GtkChild] private unowned Adw.ActionRow encrypt_by_default_row;
+ [GtkChild] private Switch encrypt_by_default_switch;
+ [GtkChild] private unowned Label new_keys_label;
+
+ [GtkChild] private unowned Adw.PreferencesGroup keys_preferences_group;
+ [GtkChild] private unowned ListBox new_keys_listbox;
+ [GtkChild] private unowned Picture qrcode_picture;
+ [GtkChild] private unowned Popover qrcode_popover;
+
+ private ArrayList<Widget> keys_preferences_group_children = new ArrayList<Widget>();
+
+ construct {
+ // If we set the strings in the .ui file, they don't get translated
+ encrypt_by_default_row.title = _("OMEMO by default");
+ encrypt_by_default_row.subtitle = _("Enable OMEMO encryption for new conversations");
+ automatically_accept_new_row.title = _("Encrypt to new devices");
+ automatically_accept_new_row.subtitle = _("Automatically encrypt to new devices from this contact.");
+ new_keys_label.label = _("New keys");
+ }
+
+ public OmemoPreferencesWidget(Plugin plugin) {
+ this.plugin = plugin;
+ this.account = account;
+ this.jid = jid;
+ }
+
+ public void set_account(Account account) {
+ this.account = account;
+ this.jid = account.bare_jid;
+
+ automatically_accept_new_switch.set_active(plugin.db.trust.get_blind_trust(identity_id, jid.bare_jid.to_string(), true));
+ automatically_accept_new_switch.state_set.connect(on_auto_accept_toggled);
+
+ encrypt_by_default_switch.set_active(plugin.app.settings.get_default_encryption(account) != Encryption.NONE);
+ encrypt_by_default_switch.state_set.connect(on_omemo_by_default_toggled);
+
+ identity_id = plugin.db.identity.get_id(account.id);
+ if (identity_id < 0) return;
+ Dino.Application? app = Application.get_default() as Dino.Application;
+ if (app != null) {
+ store = app.stream_interactor.module_manager.get_module(account, StreamModule.IDENTITY).store;
+ }
+
+ redraw_key_list();
+
+ // Check for unknown devices
+ fetch_unknown_bundles();
+ }
+
+ private void redraw_key_list() {
+ // Remove current widgets
+ foreach (var widget in keys_preferences_group_children) {
+ keys_preferences_group.remove(widget);
+ }
+ keys_preferences_group_children.clear();
+
+ // Dialog opened from the account settings menu
+ // Show the fingerprint for this device separately with buttons for a qrcode and to copy
+ if(jid.equals(account.bare_jid)) {
+ automatically_accept_new_row.subtitle = _("New encryption keys from your other devices will be accepted automatically.");
+ add_own_fingerprint();
+ }
+
+ //Show the normal devicelist
+ var own_id = plugin.db.identity.row_with(plugin.db.identity.account_id, account.id)[plugin.db.identity.device_id];
+ foreach (Row device in plugin.db.identity_meta.get_known_devices(identity_id, jid.to_string())) {
+ if(jid.equals(account.bare_jid) && device[plugin.db.identity_meta.device_id] == own_id) {
+ // If this is our own account, don't show this device twice (did it separately already)
+ continue;
+ }
+ add_fingerprint(device, (TrustLevel) device[plugin.db.identity_meta.trust_level]);
+ }
+
+ //Show any new devices for which the user must decide whether to accept or reject
+ foreach (Row device in plugin.db.identity_meta.get_new_devices(identity_id, jid.to_string())) {
+ add_new_fingerprint(device);
+ }
+ }
+
+ private static string escape_for_iri_path_segment(string s) {
+ // from RFC 3986, 2.2. Reserved Characters:
+ string SUB_DELIMS = "!$&'()*+,;=";
+ // from RFC 3986, 3.3. Path (pchar without unreserved and pct-encoded):
+ string ALLOWED_RESERVED_CHARS = SUB_DELIMS + ":@";
+ return GLib.Uri.escape_string(s, ALLOWED_RESERVED_CHARS, true);
+ }
+
+ private void fetch_unknown_bundles() {
+ Dino.Application app = Application.get_default() as Dino.Application;
+ XmppStream? stream = app.stream_interactor.get_stream(account);
+ if (stream == null) return;
+ StreamModule? module = stream.get_module(StreamModule.IDENTITY);
+ if (module == null) return;
+ module.bundle_fetched.connect_after((bundle_jid, device_id, bundle) => {
+ if (bundle_jid.equals(jid) && !displayed_ids.contains(device_id)) {
+ redraw_key_list();
+ }
+ });
+ foreach (Row device in plugin.db.identity_meta.get_unknown_devices(identity_id, jid.to_string())) {
+ try {
+ module.fetch_bundle(stream, new Jid(device[plugin.db.identity_meta.address_name]), device[plugin.db.identity_meta.device_id], false);
+ } catch (InvalidJidError e) {
+ warning("Ignoring device with invalid Jid: %s", e.message);
+ }
+ }
+ }
+
+ private void add_own_fingerprint() {
+ string own_b64 = plugin.db.identity.row_with(plugin.db.identity.account_id, account.id)[plugin.db.identity.identity_key_public_base64];
+ string fingerprint = fingerprint_from_base64(own_b64);
+
+ var own_action_box = new Box(Orientation.HORIZONTAL, 6);
+ var show_qrcode_button = new MenuButton() { icon_name="dino-qr-code-symbolic", valign=Align.CENTER };
+ own_action_box.append(show_qrcode_button);
+ var copy_button = new Button() { icon_name="edit-copy-symbolic", valign=Align.CENTER };
+ copy_button.clicked.connect(() => { copy_button.get_clipboard().set_text(fingerprint); });
+ own_action_box.append(copy_button);
+
+ Adw.ActionRow action_row = new Adw.ActionRow();
+
+ action_row.title = "This device";
+ action_row.subtitle = format_fingerprint(fingerprint_from_base64(own_b64));
+ action_row.add_suffix(own_action_box);
+#if Adw_1_2
+ action_row.use_markup = true;
+ action_row.subtitle = fingerprint_markup(fingerprint_from_base64(own_b64));
+#endif
+ add_key_row(action_row);
+
+ // Create and set QR code popover
+ int sid = plugin.db.identity.row_with(plugin.db.identity.account_id, account.id)[plugin.db.identity.device_id];
+ var iri_query = @"omemo-sid-$(sid)=$(fingerprint)";
+#if GLIB_2_66 && VALA_0_50
+ string iri = GLib.Uri.join(UriFlags.NONE, "xmpp", null, null, 0, jid.to_string(), iri_query, null);
+#else
+ var iri_path_seg = escape_for_iri_path_segment(jid.to_string());
+ var iri = @"xmpp:$(iri_path_seg)?$(iri_query)";
+#endif
+
+ const int QUIET_ZONE_MODULES = 4; // MUST be at least 4
+ const int MODULE_SIZE_PX = 4; // arbitrary
+ var qr_paintable = new QRcode(iri, 2)
+ .to_paintable(MODULE_SIZE_PX * qrcode_picture.scale_factor);
+ qrcode_picture.paintable = qr_paintable;
+ qrcode_picture.margin_top = qrcode_picture.margin_end =
+ qrcode_picture.margin_bottom = qrcode_picture.margin_start = QUIET_ZONE_MODULES * MODULE_SIZE_PX;
+ qrcode_popover.add_css_class("qrcode-container");
+
+ show_qrcode_button.popover = qrcode_popover;
+ }
+
+ private void add_fingerprint(Row device, TrustLevel trust) {
+ string key_base64 = device[plugin.db.identity_meta.identity_key_public_base64];
+ bool key_active = device[plugin.db.identity_meta.now_active];
+ if (store != null) {
+ try {
+ Signal.Address address = new Signal.Address(jid.to_string(), device[plugin.db.identity_meta.device_id]);
+ Signal.SessionRecord? session = null;
+ if (store.contains_session(address)) {
+ session = store.load_session(address);
+ string session_key_base64 = Base64.encode(session.state.remote_identity_key.serialize());
+ if (key_base64 != session_key_base64) {
+ critical("Session and database identity key mismatch!");
+ key_base64 = session_key_base64;
+ }
+ }
+ } catch (Error e) {
+ print("Error while reading session store: %s", e.message);
+ }
+ }
+
+ if (device[plugin.db.identity_meta.now_active]) {
+ Adw.ActionRow action_row = new Adw.ActionRow();
+ action_row.activated.connect(() => {
+ Row updated_device = plugin.db.identity_meta.get_device(device[plugin.db.identity_meta.identity_id], device[plugin.db.identity_meta.address_name], device[plugin.db.identity_meta.device_id]);
+ ManageKeyDialog manage_dialog = new ManageKeyDialog(updated_device, plugin.db);
+ manage_dialog.set_transient_for((Gtk.Window) get_root());
+ manage_dialog.present();
+ manage_dialog.response.connect((response) => {
+ update_stored_trust(response, updated_device);
+ redraw_key_list();
+ });
+ });
+ action_row.activatable = true;
+ action_row.title = "Other device";
+ action_row.subtitle = format_fingerprint(fingerprint_from_base64(key_base64));
+ string trust_str = _("Accepted");
+ switch(trust) {
+ case TrustLevel.UNTRUSTED:
+ trust_str = _("Rejected");
+ break;
+ case TrustLevel.VERIFIED:
+ trust_str = _("Verified");
+ break;
+ }
+
+ action_row.add_suffix(new Label(trust_str));
+#if Adw_1_2
+ action_row.use_markup = true;
+ action_row.subtitle = fingerprint_markup(fingerprint_from_base64(key_base64));
+#endif
+ add_key_row(action_row);
+ }
+ displayed_ids.add(device[plugin.db.identity_meta.device_id]);
+ }
+
+ private bool on_auto_accept_toggled(bool active) {
+ plugin.trust_manager.set_blind_trust(account, jid, active);
+
+ if (active) {
+ int identity_id = plugin.db.identity.get_id(account.id);
+ if (identity_id < 0) return false;
+
+ foreach (Row device in plugin.db.identity_meta.get_new_devices(identity_id, jid.to_string())) {
+ plugin.trust_manager.set_device_trust(account, jid, device[plugin.db.identity_meta.device_id], TrustLevel.TRUSTED);
+ add_fingerprint(device, TrustLevel.TRUSTED);
+ }
+ }
+ return false;
+ }
+
+ private bool on_omemo_by_default_toggled(bool active) {
+ var encryption_value = active ? Encryption.OMEMO : Encryption.NONE;
+ plugin.app.settings.set_default_encryption(account, encryption_value);
+ return false;
+ }
+
+ private void update_stored_trust(int response, Row device) {
+ switch (response) {
+ case TrustLevel.TRUSTED:
+ plugin.trust_manager.set_device_trust(account, jid, device[plugin.db.identity_meta.device_id], TrustLevel.TRUSTED);
+ break;
+ case TrustLevel.UNTRUSTED:
+ plugin.trust_manager.set_device_trust(account, jid, device[plugin.db.identity_meta.device_id], TrustLevel.UNTRUSTED);
+ break;
+ case TrustLevel.VERIFIED:
+ plugin.trust_manager.set_device_trust(account, jid, device[plugin.db.identity_meta.device_id], TrustLevel.VERIFIED);
+ plugin.trust_manager.set_blind_trust(account, jid, false);
+ automatically_accept_new_switch.set_active(false);
+ break;
+ }
+ }
+
+ private void add_new_fingerprint(Row device) {
+ Adw.ActionRow action_row = new Adw.ActionRow();
+ action_row.title = _("New device");
+ action_row.subtitle = format_fingerprint(fingerprint_from_base64(device[plugin.db.identity_meta.identity_key_public_base64]));
+
+#if Adw_1_2
+ action_row.use_markup = true;
+ action_row.subtitle = fingerprint_markup(fingerprint_from_base64(device[plugin.db.identity_meta.identity_key_public_base64]));
+#endif
+
+ Button accept_button = new Button() { visible = true, valign = Align.CENTER, hexpand = true };
+ accept_button.set_icon_name("emblem-ok-symbolic"); // using .image = sets .image-button. Together with .suggested/destructive action that breaks the button Adwaita
+ accept_button.add_css_class("suggested-action");
+ accept_button.tooltip_text = _("Accept key");
+
+ Button reject_button = new Button() { visible = true, valign = Align.CENTER, hexpand = true };
+ reject_button.set_icon_name("action-unavailable-symbolic");
+ reject_button.add_css_class("destructive-action");
+ reject_button.tooltip_text = _("Reject key");
+
+ accept_button.clicked.connect(() => {
+ plugin.trust_manager.set_device_trust(account, jid, device[plugin.db.identity_meta.device_id], TrustLevel.TRUSTED);
+ add_fingerprint(device, TrustLevel.TRUSTED);
+ remove_key_row(action_row);
+ });
+
+ reject_button.clicked.connect(() => {
+ plugin.trust_manager.set_device_trust(account, jid, device[plugin.db.identity_meta.device_id], TrustLevel.UNTRUSTED);
+ add_fingerprint(device, TrustLevel.UNTRUSTED);
+ remove_key_row(action_row);
+ });
+
+ Box control_box = new Box(Gtk.Orientation.HORIZONTAL, 0) { visible = true, hexpand = true };
+ control_box.append(accept_button);
+ control_box.append(reject_button);
+ control_box.add_css_class("linked"); // .linked: Visually link the accept / reject buttons
+
+ action_row.add_suffix(control_box);
+
+ add_key_row(action_row);
+ displayed_ids.add(device[plugin.db.identity_meta.device_id]);
+ }
+
+ private void add_key_row(Adw.PreferencesRow widget) {
+ keys_preferences_group.add(widget);
+ keys_preferences_group_children.add(widget);
+ }
+
+ private void remove_key_row(Adw.PreferencesRow widget) {
+ keys_preferences_group.remove(widget);
+ keys_preferences_group_children.remove(widget);
+ }
+}
+} \ No newline at end of file
diff --git a/plugins/omemo/src/ui/util.vala b/plugins/omemo/src/ui/util.vala
index cf61ed82..e250ff4d 100644
--- a/plugins/omemo/src/ui/util.vala
+++ b/plugins/omemo/src/ui/util.vala
@@ -17,46 +17,24 @@ public static string fingerprint_from_base64(string b64) {
}
public static string fingerprint_markup(string s) {
+ return "<span font_family='monospace' font='9'>" + format_fingerprint(s) + "</span>";
+}
+
+public static string format_fingerprint(string s) {
string markup = "";
for (int i = 0; i < s.length; i += 4) {
string four_chars = s.substring(i, 4).down();
- int raw = (int) from_hex(four_chars);
- uint8[] bytes = {(uint8) ((raw >> 8) & 0xff - 128), (uint8) (raw & 0xff - 128)};
-
- Checksum checksum = new Checksum(ChecksumType.SHA1);
- checksum.update(bytes, bytes.length);
- uint8[] digest = new uint8[20];
- size_t len = 20;
- checksum.get_digest(digest, ref len);
-
- uint8 r = digest[0];
- uint8 g = digest[1];
- uint8 b = digest[2];
-
- if (r == 0 && g == 0 && b == 0) r = g = b = 1;
-
- double brightness = 0.2126 * r + 0.7152 * g + 0.0722 * b;
-
- if (brightness < 80) {
- double factor = 80.0 / brightness;
- r = uint8.min(255, (uint8) (r * factor));
- g = uint8.min(255, (uint8) (g * factor));
- b = uint8.min(255, (uint8) (b * factor));
-
- } else if (brightness > 180) {
- double factor = 180.0 / brightness;
- r = (uint8) (r * factor);
- g = (uint8) (g * factor);
- b = (uint8) (b * factor);
- }
-
if (i % 32 == 0 && i != 0) markup += "\n";
- markup += @"<span foreground=\"$("#%02x%02x%02x".printf(r, g, b))\">$four_chars</span>";
- if (i % 8 == 4 && i % 32 != 28) markup += " ";
+ markup += four_chars;
+ if (i % 16 == 12 && i % 32 != 28) {
+ markup += " ";
+ }
+ if (i % 8 == 4 && i % 16 != 12) {
+ markup += "\u00a0"; // Non-breaking space
+ }
}
-
- return "<span font_family='monospace' font='8'>" + markup + "</span>";
+ return markup;
}
}