Calls: Indicate whether OMEMO key is verified

This commit is contained in:
fiaxh 2021-05-03 13:17:17 +02:00
parent 8044b546d0
commit 90f9ecf62b
12 changed files with 181 additions and 74 deletions

View file

@ -29,6 +29,16 @@ public interface EncryptionListEntry : Object {
public abstract Object? get_encryption_icon(Entities.Conversation conversation, ContentItem content_item);
}
public interface CallEncryptionEntry : Object {
public abstract CallEncryptionWidget? get_widget(Account account, Xmpp.Xep.Jingle.ContentEncryption encryption);
}
public interface CallEncryptionWidget : Object {
public abstract string? get_title();
public abstract bool show_keys();
public abstract string? get_icon_name();
}
public abstract class AccountSettingsEntry : Object {
public abstract string id { get; }
public virtual Priority priority { get { return Priority.DEFAULT; } }

View file

@ -4,6 +4,7 @@ namespace Dino.Plugins {
public class Registry {
internal ArrayList<EncryptionListEntry> encryption_list_entries = new ArrayList<EncryptionListEntry>();
internal HashMap<string, CallEncryptionEntry> call_encryption_entries = new HashMap<string, CallEncryptionEntry>();
internal ArrayList<AccountSettingsEntry> account_settings_entries = new ArrayList<AccountSettingsEntry>();
internal ArrayList<ContactDetailsProvider> contact_details_entries = new ArrayList<ContactDetailsProvider>();
internal Map<string, TextCommand> text_commands = new HashMap<string, TextCommand>();
@ -25,6 +26,13 @@ public class Registry {
}
}
public bool register_call_entryption_entry(string ns, CallEncryptionEntry entry) {
lock (call_encryption_entries) {
call_encryption_entries[ns] = entry;
}
return true;
}
public bool register_account_settings_entry(AccountSettingsEntry entry) {
lock(account_settings_entries) {
foreach(var e in account_settings_entries) {

View file

@ -75,7 +75,7 @@ namespace Dino {
call.account = conversation.account;
call.counterpart = conversation.counterpart;
call.ourpart = conversation.account.full_jid;
call.time = call.local_time = new DateTime.now_utc();
call.time = call.local_time = call.end_time = new DateTime.now_utc();
call.state = Call.State.RINGING;
stream_interactor.get_module(CallStore.IDENTITY).add_call(call, conversation);
@ -380,7 +380,7 @@ namespace Dino {
call.counterpart = from;
}
call.account = account;
call.time = call.local_time = new DateTime.now_utc();
call.time = call.local_time = call.end_time = new DateTime.now_utc();
call.state = Call.State.RINGING;
Conversation conversation = stream_interactor.get_module(ConversationManager.IDENTITY).create_conversation(call.counterpart.bare_jid, account, Conversation.Type.CHAT);

View file

@ -139,6 +139,7 @@ SOURCES
src/ui/call_window/audio_settings_popover.vala
src/ui/call_window/call_bottom_bar.vala
src/ui/call_window/call_encryption_button.vala
src/ui/call_window/call_window.vala
src/ui/call_window/call_window_controller.vala
src/ui/call_window/video_settings_popover.vala

View file

@ -25,8 +25,7 @@ public class Dino.Ui.CallBottomBar : Gtk.Box {
private MenuButton video_settings_button = new MenuButton() { halign=Align.END, valign=Align.END };
public VideoSettingsPopover? video_settings_popover;
private MenuButton encryption_button = new MenuButton() { relief=ReliefStyle.NONE, height_request=30, width_request=30, margin_start=20, margin_bottom=25, halign=Align.START, valign=Align.END };
private Image encryption_image = new Image.from_icon_name("changes-allow-symbolic", IconSize.BUTTON) { visible=true };
public CallEntryptionButton encryption_button = new CallEntryptionButton() { relief=ReliefStyle.NONE, height_request=30, width_request=30, margin_start=20, margin_bottom=25, halign=Align.START, valign=Align.END };
private Label label = new Label("") { margin=20, halign=Align.CENTER, valign=Align.CENTER, wrap=true, wrap_mode=Pango.WrapMode.WORD_CHAR, hexpand=true, visible=true };
private Stack stack = new Stack() { visible=true };
@ -35,8 +34,6 @@ public class Dino.Ui.CallBottomBar : Gtk.Box {
Object(orientation:Orientation.HORIZONTAL, spacing:0);
Overlay default_control = new Overlay() { visible=true };
encryption_button.add(encryption_image);
encryption_button.get_style_context().add_class("encryption-box");
default_control.add_overlay(encryption_button);
Box main_buttons = new Box(Orientation.HORIZONTAL, 20) { margin_start=40, margin_end=40, margin=20, halign=Align.CENTER, hexpand=true, visible=true };
@ -89,54 +86,6 @@ public class Dino.Ui.CallBottomBar : Gtk.Box {
this.get_style_context().add_class("call-bottom-bar");
}
public void set_encryption(Xmpp.Xep.Jingle.ContentEncryption? audio_encryption, Xmpp.Xep.Jingle.ContentEncryption? video_encryption, bool same) {
encryption_button.visible = true;
Popover popover = new Popover(encryption_button);
if (audio_encryption == null) {
encryption_image.set_from_icon_name("changes-allow-symbolic", IconSize.BUTTON);
encryption_button.get_style_context().add_class("unencrypted");
popover.add(new Label("This call isn't encrypted.") { margin=10, visible=true } );
return;
}
encryption_image.set_from_icon_name("changes-prevent-symbolic", IconSize.BUTTON);
encryption_button.get_style_context().remove_class("unencrypted");
Box box = new Box(Orientation.VERTICAL, 5) { margin=10, visible=true };
if (audio_encryption.encryption_name == "OMEMO") {
box.add(new Label("<b>This call is encrypted with OMEMO.</b>") { use_markup=true, xalign=0, visible=true } );
} else {
box.add(new Label("<b>This call is end-to-end encrypted.</b>") { use_markup=true, xalign=0, visible=true });
}
if (same) {
box.add(create_media_encryption_grid(audio_encryption));
} else {
box.add(new Label("<b>Audio</b>") { use_markup=true, xalign=0, visible=true });
box.add(create_media_encryption_grid(audio_encryption));
box.add(new Label("<b>Video</b>") { use_markup=true, xalign=0, visible=true });
box.add(create_media_encryption_grid(video_encryption));
}
popover.add(box);
encryption_button.set_popover(popover);
}
private Grid create_media_encryption_grid(Xmpp.Xep.Jingle.ContentEncryption? encryption) {
Grid ret = new Grid() { row_spacing=3, column_spacing=5, visible=true };
if (encryption.peer_key.length > 0) {
ret.attach(new Label("Peer call key") { xalign=0, visible=true }, 1, 2, 1, 1);
ret.attach(new Label("<span font_family='monospace'>" + format_fingerprint(encryption.peer_key) + "</span>") { use_markup=true, max_width_chars=25, ellipsize=EllipsizeMode.MIDDLE, xalign=0, hexpand=true, visible=true }, 2, 2, 1, 1);
}
if (encryption.our_key.length > 0) {
ret.attach(new Label("Your call key") { xalign=0, visible=true }, 1, 3, 1, 1);
ret.attach(new Label("<span font_family='monospace'>" + format_fingerprint(encryption.our_key) + "</span>") { use_markup=true, max_width_chars=25, ellipsize=EllipsizeMode.MIDDLE, xalign=0, hexpand=true, visible=true }, 2, 3, 1, 1);
}
return ret;
}
public AudioSettingsPopover? show_audio_device_choices(bool show) {
audio_settings_button.visible = show;
if (audio_settings_popover != null) audio_settings_popover.visible = false;
@ -212,15 +161,4 @@ public class Dino.Ui.CallBottomBar : Gtk.Box {
public bool is_menu_active() {
return video_settings_button.active || audio_settings_button.active || encryption_button.active;
}
private string format_fingerprint(uint8[] fingerprint) {
var sb = new StringBuilder();
for (int i = 0; i < fingerprint.length; i++) {
sb.append("%02x".printf(fingerprint[i]));
if (i < fingerprint.length - 1) {
sb.append(":");
}
}
return sb.str;
}
}

View file

@ -0,0 +1,77 @@
using Dino.Entities;
using Gtk;
using Pango;
public class Dino.Ui.CallEntryptionButton : MenuButton {
private Image encryption_image = new Image.from_icon_name("changes-allow-symbolic", IconSize.BUTTON) { visible=true };
construct {
add(encryption_image);
get_style_context().add_class("encryption-box");
this.set_popover(popover);
}
public void set_icon(bool encrypted, string? icon_name) {
this.visible = true;
if (encrypted) {
encryption_image.set_from_icon_name(icon_name ?? "changes-prevent-symbolic", IconSize.BUTTON);
get_style_context().remove_class("unencrypted");
} else {
encryption_image.set_from_icon_name(icon_name ?? "changes-allow-symbolic", IconSize.BUTTON);
get_style_context().add_class("unencrypted");
}
}
public void set_info(string? title, bool show_keys, Xmpp.Xep.Jingle.ContentEncryption? audio_encryption, Xmpp.Xep.Jingle.ContentEncryption? video_encryption) {
Popover popover = new Popover(this);
this.set_popover(popover);
if (audio_encryption == null) {
popover.add(new Label("This call is unencrypted.") { margin=10, visible=true } );
return;
}
if (title != null && !show_keys) {
popover.add(new Label(title) { use_markup=true, margin=10, visible=true } );
return;
}
Box box = new Box(Orientation.VERTICAL, 10) { margin=10, visible=true };
box.add(new Label("<b>%s</b>".printf(title ?? "This call is end-to-end encrypted.")) { use_markup=true, xalign=0, visible=true });
if (video_encryption == null) {
box.add(create_media_encryption_grid(audio_encryption));
} else {
box.add(new Label("<b>Audio</b>") { use_markup=true, xalign=0, visible=true });
box.add(create_media_encryption_grid(audio_encryption));
box.add(new Label("<b>Video</b>") { use_markup=true, xalign=0, visible=true });
box.add(create_media_encryption_grid(video_encryption));
}
popover.add(box);
}
private Grid create_media_encryption_grid(Xmpp.Xep.Jingle.ContentEncryption? encryption) {
Grid ret = new Grid() { row_spacing=3, column_spacing=5, visible=true };
if (encryption.peer_key.length > 0) {
ret.attach(new Label("Peer call key") { xalign=0, visible=true }, 1, 2, 1, 1);
ret.attach(new Label("<span font_family='monospace'>" + format_fingerprint(encryption.peer_key) + "</span>") { use_markup=true, max_width_chars=25, ellipsize=EllipsizeMode.MIDDLE, xalign=0, hexpand=true, visible=true }, 2, 2, 1, 1);
}
if (encryption.our_key.length > 0) {
ret.attach(new Label("Your call key") { xalign=0, visible=true }, 1, 3, 1, 1);
ret.attach(new Label("<span font_family='monospace'>" + format_fingerprint(encryption.our_key) + "</span>") { use_markup=true, max_width_chars=25, ellipsize=EllipsizeMode.MIDDLE, xalign=0, hexpand=true, visible=true }, 2, 3, 1, 1);
}
return ret;
}
private string format_fingerprint(uint8[] fingerprint) {
var sb = new StringBuilder();
for (int i = 0; i < fingerprint.length; i++) {
sb.append("%02x".printf(fingerprint[i]));
if (i < fingerprint.length - 1) {
sb.append(":");
}
}
return sb.str;
}
}

View file

@ -78,7 +78,22 @@ public class Dino.Ui.CallWindowController : Object {
});
calls.encryption_updated.connect((call, audio_encryption, video_encryption, same) => {
if (!this.call.equals(call)) return;
call_window.bottom_bar.set_encryption(audio_encryption, video_encryption, same);
string? title = null;
string? icon_name = null;
bool show_keys = true;
Plugins.Registry registry = Dino.Application.get_default().plugin_registry;
Plugins.CallEncryptionEntry? encryption_entry = audio_encryption != null ? registry.call_encryption_entries[audio_encryption.encryption_ns] : null;
if (encryption_entry != null) {
Plugins.CallEncryptionWidget? encryption_widgets = encryption_entry.get_widget(call.account, audio_encryption);
if (encryption_widgets != null) {
title = encryption_widgets.get_title();
icon_name = encryption_widgets.get_icon_name();
show_keys = encryption_widgets.show_keys();
}
}
call_window.bottom_bar.encryption_button.set_info(title, show_keys, audio_encryption, same ? null :video_encryption);
call_window.bottom_bar.encryption_button.set_icon(audio_encryption != null, icon_name);
});
own_video.resolution_changed.connect((width, height) => {

View file

@ -55,6 +55,7 @@ SOURCES
src/ui/account_settings_entry.vala
src/ui/account_settings_widget.vala
src/ui/bad_messages_populator.vala
src/ui/call_encryption_entry.vala
src/ui/contact_details_provider.vala
src/ui/contact_details_dialog.vala
src/ui/device_notification_populator.vala

View file

@ -66,7 +66,7 @@ namespace Dino.Plugins.Omemo.DtlsSrtpVerificationDraft {
stream.get_flag(Xep.Jingle.Flag.IDENTITY).get_session.begin(jingle_sid, (_, res) => {
Xep.Jingle.Session? session = stream.get_flag(Xep.Jingle.Flag.IDENTITY).get_session.end(res);
if (session == null || !session.contents_map.has_key(content_name)) return;
var encryption = new OmemoContentEncryption() { encryption_ns=NS_URI, encryption_name="OMEMO", our_key=new uint8[0], peer_key=new uint8[0], peer_device_id=device_id_by_jingle_sid[jingle_sid] };
var encryption = new OmemoContentEncryption() { encryption_ns=NS_URI, encryption_name="OMEMO", our_key=new uint8[0], peer_key=new uint8[0], sid=device_id_by_jingle_sid[jingle_sid], jid=iq.from.bare_jid };
session.contents_map[content_name].encryptions[NS_URI] = encryption;
if (iq.stanza.get_deep_attribute(Xep.Jingle.NS_URI + ":jingle", "action") == "session-accept") {
@ -143,7 +143,7 @@ namespace Dino.Plugins.Omemo.DtlsSrtpVerificationDraft {
private void on_content_add_received(XmppStream stream, Xep.Jingle.Content content) {
if (!content_names_by_jingle_sid.has_key(content.session.sid) || content_names_by_jingle_sid[content.session.sid].contains(content.content_name)) {
var encryption = new OmemoContentEncryption() { encryption_ns=NS_URI, encryption_name="OMEMO", our_key=new uint8[0], peer_key=new uint8[0], peer_device_id=device_id_by_jingle_sid[content.session.sid] };
var encryption = new OmemoContentEncryption() { encryption_ns=NS_URI, encryption_name="OMEMO", our_key=new uint8[0], peer_key=new uint8[0], sid=device_id_by_jingle_sid[content.session.sid], jid=content.peer_full_jid.bare_jid };
content.encryptions[encryption.encryption_ns] = encryption;
}
}
@ -188,7 +188,8 @@ namespace Dino.Plugins.Omemo.DtlsSrtpVerificationDraft {
}
public class OmemoContentEncryption : Xep.Jingle.ContentEncryption {
public int peer_device_id { get; set; }
public Jid jid { get; set; }
public int sid { get; set; }
}
}

View file

@ -59,9 +59,10 @@ namespace Dino.Plugins.Omemo {
message.real_jid = possible_jid;
}
trust_manager.message_device_id_map[message] = data.sid;
message.body = cleartext;
message.encryption = Encryption.OMEMO;
trust_manager.message_device_id_map[message] = data.sid;
return true;
} catch (Error e) {
debug("Decrypting message from %s/%d failed: %s", possible_jid.to_string(), data.sid, e.message);

View file

@ -35,7 +35,6 @@ public class Plugin : RootInterface, Object {
public DeviceNotificationPopulator device_notification_populator;
public OwnNotifications own_notifications;
public TrustManager trust_manager;
public DecryptMessageListener decrypt_message_listener;
public HashMap<Account, OmemoDecryptor> decryptors = new HashMap<Account, OmemoDecryptor>(Account.hash_func, Account.equals_func);
public HashMap<Account, OmemoEncryptor> encryptors = new HashMap<Account, OmemoEncryptor>(Account.hash_func, Account.equals_func);
@ -54,6 +53,7 @@ public class Plugin : RootInterface, Object {
this.app.plugin_registry.register_contact_details_entry(contact_details_provider);
this.app.plugin_registry.register_notification_populator(device_notification_populator);
this.app.plugin_registry.register_conversation_addition_populator(new BadMessagesPopulator(this.app.stream_interactor, this));
this.app.plugin_registry.register_call_entryption_entry(DtlsSrtpVerificationDraft.NS_URI, new CallEncryptionEntry(db));
this.app.stream_interactor.module_manager.initialize_account_modules.connect((account, list) => {
Signal.Store signal_store = Plugin.get_context().create_store();
@ -67,9 +67,7 @@ public class Plugin : RootInterface, Object {
this.own_notifications = new OwnNotifications(this, this.app.stream_interactor, account);
});
decrypt_message_listener = new DecryptMessageListener(decryptors);
app.stream_interactor.get_module(MessageProcessor.IDENTITY).received_pipeline.connect(decrypt_message_listener);
app.stream_interactor.get_module(MessageProcessor.IDENTITY).received_pipeline.connect(new DecryptMessageListener(decryptors));
app.stream_interactor.get_module(FileManager.IDENTITY).add_file_decryptor(new OmemoFileDecryptor());
app.stream_interactor.get_module(FileManager.IDENTITY).add_file_encryptor(new OmemoFileEncryptor());
JingleFileHelperRegistry.instance.add_encryption_helper(Encryption.OMEMO, new JetOmemo.EncryptionHelper(app.stream_interactor));

View file

@ -0,0 +1,57 @@
using Dino.Entities;
using Gtk;
using Qlite;
using Xmpp;
namespace Dino.Plugins.Omemo {
public class CallEncryptionEntry : Plugins.CallEncryptionEntry, Object {
private Database db;
public CallEncryptionEntry(Database db) {
this.db = db;
}
public Plugins.CallEncryptionWidget? get_widget(Account account, Xmpp.Xep.Jingle.ContentEncryption encryption) {
DtlsSrtpVerificationDraft.OmemoContentEncryption? omemo_encryption = encryption as DtlsSrtpVerificationDraft.OmemoContentEncryption;
if (omemo_encryption == null) return null;
int identity_id = db.identity.get_id(account.id);
Row? device = db.identity_meta.get_device(identity_id, omemo_encryption.jid.to_string(), omemo_encryption.sid);
if (device == null) return null;
TrustLevel trust = (TrustLevel) device[db.identity_meta.trust_level];
return new CallEncryptionWidget(trust);
}
}
public class CallEncryptionWidget : Plugins.CallEncryptionWidget, Object {
string? title = null;
string? icon = null;
bool should_show_keys = false;
public CallEncryptionWidget(TrustLevel trust) {
if (trust == TrustLevel.VERIFIED) {
title = "This call is <b>encrypted and verified</b> with OMEMO.";
icon = "dino-security-high-symbolic";
should_show_keys = false;
} else {
title = "This call is encrypted with OMEMO.";
should_show_keys = true;
}
}
public string? get_title() {
return title;
}
public string? get_icon_name() {
return icon;
}
public bool show_keys() {
return should_show_keys;
}
}
}