Display+store call encryption info

This commit is contained in:
fiaxh 2021-04-08 12:07:04 +02:00
parent 3454201e5a
commit 8d1c6c29be
17 changed files with 273 additions and 75 deletions

View file

@ -32,6 +32,7 @@ namespace Dino.Entities {
public DateTime time { get; set; } public DateTime time { get; set; }
public DateTime local_time { get; set; } public DateTime local_time { get; set; }
public DateTime end_time { get; set; } public DateTime end_time { get; set; }
public Encryption encryption { get; set; default=Encryption.NONE; }
public State state { get; set; } public State state { get; set; }
@ -57,6 +58,7 @@ namespace Dino.Entities {
time = new DateTime.from_unix_utc(row[db.call.time]); time = new DateTime.from_unix_utc(row[db.call.time]);
local_time = new DateTime.from_unix_utc(row[db.call.local_time]); local_time = new DateTime.from_unix_utc(row[db.call.local_time]);
end_time = new DateTime.from_unix_utc(row[db.call.end_time]); end_time = new DateTime.from_unix_utc(row[db.call.end_time]);
encryption = (Encryption) row[db.call.encryption];
state = (State) row[db.call.state]; state = (State) row[db.call.state];
notify.connect(on_update); notify.connect(on_update);
@ -74,6 +76,7 @@ namespace Dino.Entities {
.value(db.call.direction, direction) .value(db.call.direction, direction)
.value(db.call.time, (long) time.to_unix()) .value(db.call.time, (long) time.to_unix())
.value(db.call.local_time, (long) local_time.to_unix()) .value(db.call.local_time, (long) local_time.to_unix())
.value(db.call.encryption, encryption)
.value(db.call.state, State.ENDED); // No point in persisting states that can't survive a restart .value(db.call.state, State.ENDED); // No point in persisting states that can't survive a restart
if (end_time != null) { if (end_time != null) {
builder.value(db.call.end_time, (long) end_time.to_unix()); builder.value(db.call.end_time, (long) end_time.to_unix());
@ -116,6 +119,8 @@ namespace Dino.Entities {
update_builder.set(db.call.local_time, (long) local_time.to_unix()); break; update_builder.set(db.call.local_time, (long) local_time.to_unix()); break;
case "end-time": case "end-time":
update_builder.set(db.call.end_time, (long) end_time.to_unix()); break; update_builder.set(db.call.end_time, (long) end_time.to_unix()); break;
case "encryption":
update_builder.set(db.call.encryption, encryption); break;
case "state": case "state":
// No point in persisting states that can't survive a restart // No point in persisting states that can't survive a restart
if (state == State.RINGING || state == State.ESTABLISHING || state == State.IN_PROGRESS) return; if (state == State.RINGING || state == State.ESTABLISHING || state == State.IN_PROGRESS) return;

View file

@ -3,7 +3,9 @@ namespace Dino.Entities {
public enum Encryption { public enum Encryption {
NONE, NONE,
PGP, PGP,
OMEMO OMEMO,
DTLS_SRTP,
SRTP,
} }
} }

View file

@ -14,6 +14,7 @@ namespace Dino {
public signal void counterpart_ringing(Call call); public signal void counterpart_ringing(Call call);
public signal void counterpart_sends_video_updated(Call call, bool mute); public signal void counterpart_sends_video_updated(Call call, bool mute);
public signal void info_received(Call call, Xep.JingleRtp.CallSessionInfo session_info); public signal void info_received(Call call, Xep.JingleRtp.CallSessionInfo session_info);
public signal void encryption_updated(Call call, Xep.Jingle.ContentEncryption? encryption);
public signal void stream_created(Call call, string media); public signal void stream_created(Call call, string media);
@ -22,7 +23,6 @@ namespace Dino {
private StreamInteractor stream_interactor; private StreamInteractor stream_interactor;
private Database db; private Database db;
private Xep.JingleRtp.SessionInfoType session_info_type;
private HashMap<Account, HashMap<Call, string>> sid_by_call = new HashMap<Account, HashMap<Call, string>>(Account.hash_func, Account.equals_func); private HashMap<Account, HashMap<Call, string>> sid_by_call = new HashMap<Account, HashMap<Call, string>>(Account.hash_func, Account.equals_func);
private HashMap<Account, HashMap<string, Call>> call_by_sid = new HashMap<Account, HashMap<string, Call>>(Account.hash_func, Account.equals_func); private HashMap<Account, HashMap<string, Call>> call_by_sid = new HashMap<Account, HashMap<string, Call>>(Account.hash_func, Account.equals_func);
@ -38,7 +38,10 @@ namespace Dino {
private HashMap<Call, Xep.JingleRtp.Parameters> audio_content_parameter = new HashMap<Call, Xep.JingleRtp.Parameters>(Call.hash_func, Call.equals_func); private HashMap<Call, Xep.JingleRtp.Parameters> audio_content_parameter = new HashMap<Call, Xep.JingleRtp.Parameters>(Call.hash_func, Call.equals_func);
private HashMap<Call, Xep.JingleRtp.Parameters> video_content_parameter = new HashMap<Call, Xep.JingleRtp.Parameters>(Call.hash_func, Call.equals_func); private HashMap<Call, Xep.JingleRtp.Parameters> video_content_parameter = new HashMap<Call, Xep.JingleRtp.Parameters>(Call.hash_func, Call.equals_func);
private HashMap<Call, Xep.Jingle.Content> audio_content = new HashMap<Call, Xep.Jingle.Content>(Call.hash_func, Call.equals_func);
private HashMap<Call, Xep.Jingle.Content> video_content = new HashMap<Call, Xep.Jingle.Content>(Call.hash_func, Call.equals_func); private HashMap<Call, Xep.Jingle.Content> video_content = new HashMap<Call, Xep.Jingle.Content>(Call.hash_func, Call.equals_func);
private HashMap<Call, Xep.Jingle.ContentEncryption> video_encryption = new HashMap<Call, Xep.Jingle.ContentEncryption>(Call.hash_func, Call.equals_func);
private HashMap<Call, Xep.Jingle.ContentEncryption> audio_encryption = new HashMap<Call, Xep.Jingle.ContentEncryption>(Call.hash_func, Call.equals_func);
public static void start(StreamInteractor stream_interactor, Database db) { public static void start(StreamInteractor stream_interactor, Database db) {
Calls m = new Calls(stream_interactor, db); Calls m = new Calls(stream_interactor, db);
@ -290,7 +293,7 @@ namespace Dino {
} }
// Session might have already been accepted via Jingle Message Initiation // Session might have already been accepted via Jingle Message Initiation
bool already_accepted = jmi_sid.contains(account) && bool already_accepted = jmi_sid.has_key(account) &&
jmi_sid[account] == session.sid && jmi_call[account].account.equals(account) && jmi_sid[account] == session.sid && jmi_call[account].account.equals(account) &&
jmi_call[account].counterpart.equals_bare(session.peer_full_jid) && jmi_call[account].counterpart.equals_bare(session.peer_full_jid) &&
jmi_video[account] == counterpart_wants_video; jmi_video[account] == counterpart_wants_video;
@ -365,6 +368,7 @@ namespace Dino {
if (call.state == Call.State.RINGING || call.state == Call.State.ESTABLISHING) { if (call.state == Call.State.RINGING || call.state == Call.State.ESTABLISHING) {
call.state = Call.State.IN_PROGRESS; call.state = Call.State.IN_PROGRESS;
} }
update_call_encryption(call);
} }
private void on_call_terminated(Call call, bool we_terminated, string? reason_name, string? reason_text) { private void on_call_terminated(Call call, bool we_terminated, string? reason_name, string? reason_text) {
@ -429,6 +433,7 @@ namespace Dino {
private void connect_content_signals(Call call, Xep.Jingle.Content content, Xep.JingleRtp.Parameters rtp_content_parameter) { private void connect_content_signals(Call call, Xep.Jingle.Content content, Xep.JingleRtp.Parameters rtp_content_parameter) {
if (rtp_content_parameter.media == "audio") { if (rtp_content_parameter.media == "audio") {
audio_content[call] = content;
audio_content_parameter[call] = rtp_content_parameter; audio_content_parameter[call] = rtp_content_parameter;
} else if (rtp_content_parameter.media == "video") { } else if (rtp_content_parameter.media == "video") {
video_content[call] = content; video_content[call] = content;
@ -450,6 +455,36 @@ namespace Dino {
on_counterpart_mute_update(call, false, "video"); on_counterpart_mute_update(call, false, "video");
} }
}); });
content.notify["encryption"].connect((obj, _) => {
if (rtp_content_parameter.media == "audio") {
audio_encryption[call] = ((Xep.Jingle.Content) obj).encryption;
} else if (rtp_content_parameter.media == "video") {
video_encryption[call] = ((Xep.Jingle.Content) obj).encryption;
}
});
}
private void update_call_encryption(Call call) {
if (audio_encryption[call] == null) {
call.encryption = Encryption.NONE;
encryption_updated(call, null);
return;
}
bool consistent_encryption = video_encryption[call] != null && audio_encryption[call].encryption_ns == video_encryption[call].encryption_ns;
if (video_content[call] == null || consistent_encryption) {
if (audio_encryption[call].encryption_ns == Xep.JingleIceUdp.DTLS_NS_URI) {
call.encryption = Encryption.DTLS_SRTP;
} else if (audio_encryption[call].encryption_name == "SRTP") {
call.encryption = Encryption.SRTP;
}
encryption_updated(call, audio_encryption[call]);
} else {
call.encryption = Encryption.NONE;
encryption_updated(call, null);
}
} }
private void remove_call_from_datastructures(Call call) { private void remove_call_from_datastructures(Call call) {
@ -465,7 +500,10 @@ namespace Dino {
audio_content_parameter.unset(call); audio_content_parameter.unset(call);
video_content_parameter.unset(call); video_content_parameter.unset(call);
audio_content.unset(call);
video_content.unset(call); video_content.unset(call);
audio_encryption.unset(call);
video_encryption.unset(call);
} }
private void on_account_added(Account account) { private void on_account_added(Account account) {
@ -526,7 +564,7 @@ namespace Dino {
} else if (from.equals_bare(call_by_sid[account][sid].counterpart)) { // Message from our peer } else if (from.equals_bare(call_by_sid[account][sid].counterpart)) { // Message from our peer
// We proposed the call // We proposed the call
if (jmi_sid.has_key(account) && jmi_sid[account] == sid) { if (jmi_sid.has_key(account) && jmi_sid[account] == sid) {
call_resource(account, from, jmi_call[account], jmi_video[account], jmi_sid[account]); call_resource.begin(account, from, jmi_call[account], jmi_video[account], jmi_sid[account]);
jmi_call.unset(account); jmi_call.unset(account);
jmi_sid.unset(account); jmi_sid.unset(account);
jmi_video.unset(account); jmi_video.unset(account);

View file

@ -316,10 +316,12 @@ public class CallItem : ContentItem {
public Conversation conversation; public Conversation conversation;
public CallItem(Call call, Conversation conversation, int id) { public CallItem(Call call, Conversation conversation, int id) {
base(id, TYPE, call.from, call.time, Encryption.NONE, Message.Marked.NONE); base(id, TYPE, call.from, call.time, call.encryption, Message.Marked.NONE);
this.call = call; this.call = call;
this.conversation = conversation; this.conversation = conversation;
call.bind_property("encryption", this, "encryption");
} }
} }

View file

@ -7,7 +7,7 @@ using Dino.Entities;
namespace Dino { namespace Dino {
public class Database : Qlite.Database { public class Database : Qlite.Database {
private const int VERSION = 20; private const int VERSION = 21;
public class AccountTable : Table { public class AccountTable : Table {
public Column<int> id = new Column.Integer("id") { primary_key = true, auto_increment = true }; public Column<int> id = new Column.Integer("id") { primary_key = true, auto_increment = true };
@ -165,11 +165,12 @@ public class Database : Qlite.Database {
public Column<long> time = new Column.Long("time") { not_null = true }; public Column<long> time = new Column.Long("time") { not_null = true };
public Column<long> local_time = new Column.Long("local_time") { not_null = true }; public Column<long> local_time = new Column.Long("local_time") { not_null = true };
public Column<long> end_time = new Column.Long("end_time"); public Column<long> end_time = new Column.Long("end_time");
public Column<int> encryption = new Column.Integer("encryption") { min_version=21 };
public Column<int> state = new Column.Integer("state"); public Column<int> state = new Column.Integer("state");
internal CallTable(Database db) { internal CallTable(Database db) {
base(db, "call"); base(db, "call");
init({id, account_id, counterpart_id, counterpart_resource, our_resource, direction, time, local_time, end_time, state}); init({id, account_id, counterpart_id, counterpart_resource, our_resource, direction, time, local_time, end_time, encryption, state});
} }
} }

View file

@ -235,17 +235,24 @@ box.dino-input-error label.input-status-highlight-once {
outline: 0; outline: 0;
border-radius: 1000px; border-radius: 1000px;
} }
.dino-call-window button.white-button { .dino-call-window button.white-button {
color: #1d1c1d; color: #1d1c1d;
background: rgba(255,255,255,0.9); background: rgba(255,255,255,0.85);
border: lightgrey; border: lightgrey;
} }
.dino-call-window button.white-button:hover {
background: rgba(255,255,255,1);
}
.dino-call-window button.transparent-white-button { .dino-call-window button.transparent-white-button {
color: white; color: white;
background: rgba(255,255,255,0.15); background: rgba(255,255,255,0.15);
border: none; border: none;
} }
.dino-call-window button.transparent-white-button:hover {
background: rgba(255,255,255,0.25);
}
.dino-call-window button.call-mediadevice-settings-button { .dino-call-window button.call-mediadevice-settings-button {
border-radius: 1000px; border-radius: 1000px;
@ -265,11 +272,21 @@ box.dino-input-error label.input-status-highlight-once {
margin: 0; margin: 0;
} }
.dino-call-window .unencrypted-box { .dino-call-window .encryption-box {
color: @error_color; color: rgba(255,255,255,0.7);
padding: 10px;
border-radius: 5px; border-radius: 5px;
background: rgba(0,0,0,0.5); background: rgba(0,0,0,0.5);
padding: 0px;
border: none;
box-shadow: none;
}
.dino-call-window .encryption-box.unencrypted {
color: @error_color;
}
.dino-call-window .encryption-box:hover {
background: rgba(20,20,20,0.5);
} }
.dino-call-window .call-header-bar { .dino-call-window .call-header-bar {

View file

@ -1,5 +1,6 @@
using Dino.Entities; using Dino.Entities;
using Gtk; using Gtk;
using Pango;
public class Dino.Ui.CallBottomBar : Gtk.Box { public class Dino.Ui.CallBottomBar : Gtk.Box {
@ -24,6 +25,10 @@ public class Dino.Ui.CallBottomBar : Gtk.Box {
private MenuButton video_settings_button = new MenuButton() { halign=Align.END, valign=Align.END }; private MenuButton video_settings_button = new MenuButton() { halign=Align.END, valign=Align.END };
public VideoSettingsPopover? video_settings_popover; public VideoSettingsPopover? video_settings_popover;
private EventBox encryption_event_box = new EventBox() { visible=true };
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 };
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 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 }; private Stack stack = new Stack() { visible=true };
@ -31,11 +36,9 @@ public class Dino.Ui.CallBottomBar : Gtk.Box {
Object(orientation:Orientation.HORIZONTAL, spacing:0); Object(orientation:Orientation.HORIZONTAL, spacing:0);
Overlay default_control = new Overlay() { visible=true }; Overlay default_control = new Overlay() { visible=true };
Image encryption_image = new Image.from_icon_name("changes-allow-symbolic", IconSize.BUTTON) { margin_start=20, margin_bottom=25, halign=Align.START, valign=Align.END, visible=true }; encryption_button.add(encryption_image);
encryption_image.tooltip_text = _("Unencrypted"); encryption_button.get_style_context().add_class("encryption-box");
encryption_image.get_style_context().add_class("unencrypted-box"); default_control.add_overlay(encryption_button);
default_control.add_overlay(encryption_image);
Box main_buttons = new Box(Orientation.HORIZONTAL, 20) { margin_start=40, margin_end=40, margin=20, halign=Align.CENTER, hexpand=true, visible=true }; Box main_buttons = new Box(Orientation.HORIZONTAL, 20) { margin_start=40, margin_end=40, margin=20, halign=Align.CENTER, hexpand=true, visible=true };
@ -87,6 +90,33 @@ public class Dino.Ui.CallBottomBar : Gtk.Box {
this.get_style_context().add_class("call-bottom-bar"); this.get_style_context().add_class("call-bottom-bar");
} }
public void set_encryption(Xmpp.Xep.Jingle.ContentEncryption? encryption) {
encryption_button.visible = true;
Popover popover = new Popover(encryption_button);
if (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 } );
} else {
encryption_image.set_from_icon_name("changes-prevent-symbolic", IconSize.BUTTON);
encryption_button.get_style_context().remove_class("unencrypted");
Grid encryption_info_grid = new Grid() { margin=10, row_spacing=3, column_spacing=5, visible=true };
encryption_info_grid.attach(new Label("<b>This call is end-to-end encrypted.</b>") { use_markup=true, xalign=0, visible=true }, 1, 1, 2, 1);
encryption_info_grid.attach(new Label("Peer key") { xalign=0, visible=true }, 1, 2, 1, 1);
encryption_info_grid.attach(new Label("Your key") { xalign=0, visible=true }, 1, 3, 1, 1);
encryption_info_grid.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);
encryption_info_grid.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);
popover.add(encryption_info_grid);
}
encryption_button.set_popover(popover);
}
public AudioSettingsPopover? show_audio_device_choices(bool show) { public AudioSettingsPopover? show_audio_device_choices(bool show) {
audio_settings_button.visible = show; audio_settings_button.visible = show;
if (audio_settings_popover != null) audio_settings_popover.visible = false; if (audio_settings_popover != null) audio_settings_popover.visible = false;
@ -160,6 +190,17 @@ public class Dino.Ui.CallBottomBar : Gtk.Box {
} }
public bool is_menu_active() { public bool is_menu_active() {
return video_settings_button.active || audio_settings_button.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

@ -67,6 +67,10 @@ public class Dino.Ui.CallWindowController : Object {
call_window.set_status("ringing"); call_window.set_status("ringing");
} }
}); });
calls.encryption_updated.connect((call, encryption) => {
if (!this.call.equals(call)) return;
call_window.bottom_bar.set_encryption(encryption);
});
own_video.resolution_changed.connect((width, height) => { own_video.resolution_changed.connect((width, height) => {
if (width == 0 || height == 0) return; if (width == 0 || height == 0) return;

View file

@ -88,6 +88,7 @@ public abstract class ContentMetaItem : Plugins.MetaConversationItem {
this.mark = content_item.mark; this.mark = content_item.mark;
content_item.bind_property("mark", this, "mark"); content_item.bind_property("mark", this, "mark");
content_item.bind_property("encryption", this, "encryption");
this.can_merge = true; this.can_merge = true;
this.requires_avatar = true; this.requires_avatar = true;

View file

@ -104,7 +104,7 @@ public class ItemMetaDataHeader : Box {
[GtkChild] public Label dot_label; [GtkChild] public Label dot_label;
[GtkChild] public Label time_label; [GtkChild] public Label time_label;
public Image received_image = new Image() { opacity=0.4 }; public Image received_image = new Image() { opacity=0.4 };
public Image? unencrypted_image = null; public Widget? encryption_image = null;
public static IconSize ICON_SIZE_HEADER = Gtk.icon_size_register("im.dino.Dino.HEADER_ICON", 17, 12); public static IconSize ICON_SIZE_HEADER = Gtk.icon_size_register("im.dino.Dino.HEADER_ICON", 17, 12);
@ -124,27 +124,9 @@ public class ItemMetaDataHeader : Box {
update_name_label(); update_name_label();
name_label.style_updated.connect(update_name_label); name_label.style_updated.connect(update_name_label);
Application app = GLib.Application.get_default() as Application; conversation.notify["encryption"].connect(update_unencrypted_icon);
item.notify["encryption"].connect(update_encryption_icon);
ContentMetaItem ci = item as ContentMetaItem; update_encryption_icon();
if (ci != null) {
foreach(var e in app.plugin_registry.encryption_list_entries) {
if (e.encryption == item.encryption) {
Object? w = e.get_encryption_icon(conversation, ci.content_item);
if (w != null) {
this.add(w as Widget);
} else {
Image image = new Image.from_icon_name("dino-changes-prevent-symbolic", ICON_SIZE_HEADER) { opacity=0.4, visible = true };
this.add(image);
}
break;
}
}
}
if (item.encryption == Encryption.NONE) {
conversation.notify["encryption"].connect(update_unencrypted_icon);
update_unencrypted_icon();
}
this.add(received_image); this.add(received_image);
@ -157,17 +139,51 @@ public class ItemMetaDataHeader : Box {
update_received_mark(); update_received_mark();
} }
private void update_encryption_icon() {
Application app = GLib.Application.get_default() as Application;
ContentMetaItem ci = item as ContentMetaItem;
if (item.encryption != Encryption.NONE && ci != null) {
Widget? widget = null;
foreach(var e in app.plugin_registry.encryption_list_entries) {
if (e.encryption == item.encryption) {
widget = e.get_encryption_icon(conversation, ci.content_item) as Widget;
break;
}
}
if (widget == null) {
widget = new Image.from_icon_name("dino-changes-prevent-symbolic", ICON_SIZE_HEADER) { opacity=0.4, visible = true };
}
update_encryption_image(widget);
}
if (item.encryption == Encryption.NONE) {
update_unencrypted_icon();
}
}
private void update_unencrypted_icon() { private void update_unencrypted_icon() {
if (conversation.encryption != Encryption.NONE && unencrypted_image == null) { if (item.encryption != Encryption.NONE) return;
unencrypted_image = new Image() { opacity=0.4, visible = true };
unencrypted_image.set_from_icon_name("dino-changes-allowed-symbolic", ICON_SIZE_HEADER); if (conversation.encryption != Encryption.NONE && encryption_image == null) {
unencrypted_image.tooltip_text = _("Unencrypted"); Image image = new Image() { opacity=0.4, visible = true };
this.add(unencrypted_image); image.set_from_icon_name("dino-changes-allowed-symbolic", ICON_SIZE_HEADER);
this.reorder_child(unencrypted_image, 3); image.tooltip_text = _("Unencrypted");
Util.force_error_color(unencrypted_image); update_encryption_image(image);
} else if (conversation.encryption == Encryption.NONE && unencrypted_image != null) { Util.force_error_color(image);
this.remove(unencrypted_image); } else if (conversation.encryption == Encryption.NONE && encryption_image != null) {
unencrypted_image = null; update_encryption_image(null);
}
}
private void update_encryption_image(Widget? widget) {
if (encryption_image != null) {
this.remove(encryption_image);
encryption_image = null;
}
if (widget != null) {
this.add(widget);
this.reorder_child(widget, 3);
encryption_image = widget;
} }
} }

View file

@ -10,7 +10,10 @@ public class DtlsSrtp {
private Mutex buffer_mutex = new Mutex(); private Mutex buffer_mutex = new Mutex();
private Gee.LinkedList<Bytes> buffer_queue = new Gee.LinkedList<Bytes>(); private Gee.LinkedList<Bytes> buffer_queue = new Gee.LinkedList<Bytes>();
private uint pull_timeout = uint.MAX; private uint pull_timeout = uint.MAX;
private string peer_fingerprint;
private DigestAlgorithm? peer_fp_algo = null;
private uint8[] peer_fingerprint = null;
private uint8[] own_fingerprint;
private Crypto.Srtp.Session srtp_session = new Crypto.Srtp.Session(); private Crypto.Srtp.Session srtp_session = new Crypto.Srtp.Session();
@ -20,12 +23,13 @@ public class DtlsSrtp {
return obj; return obj;
} }
internal string get_own_fingerprint(DigestAlgorithm digest_algo) { internal uint8[] get_own_fingerprint(DigestAlgorithm digest_algo) {
return format_certificate(own_cert[0], digest_algo); return own_fingerprint;
} }
public void set_peer_fingerprint(string fingerprint) { public void set_peer_fingerprint(uint8[] fingerprint, DigestAlgorithm digest_algo) {
this.peer_fingerprint = fingerprint; this.peer_fingerprint = fingerprint;
this.peer_fp_algo = digest_algo;
} }
public uint8[] process_incoming_data(uint component_id, uint8[] data) { public uint8[] process_incoming_data(uint component_id, uint8[] data) {
@ -94,10 +98,11 @@ public class DtlsSrtp {
cert.sign(cert, private_key); cert.sign(cert, private_key);
own_fingerprint = get_fingerprint(cert, DigestAlgorithm.SHA256);
own_cert = new X509.Certificate[] { (owned)cert }; own_cert = new X509.Certificate[] { (owned)cert };
} }
public async void setup_dtls_connection(bool server) { public async Xmpp.Xep.Jingle.ContentEncryption setup_dtls_connection(bool server) {
InitFlags server_or_client = server ? InitFlags.SERVER : InitFlags.CLIENT; InitFlags server_or_client = server ? InitFlags.SERVER : InitFlags.CLIENT;
debug("Setting up DTLS connection. We're %s", server_or_client.to_string()); debug("Setting up DTLS connection. We're %s", server_or_client.to_string());
@ -149,6 +154,7 @@ public class DtlsSrtp {
srtp_session.set_encryption_key(Crypto.Srtp.AES_CM_128_HMAC_SHA1_80, client_key.extract(), client_salt.extract()); srtp_session.set_encryption_key(Crypto.Srtp.AES_CM_128_HMAC_SHA1_80, client_key.extract(), client_salt.extract());
srtp_session.set_decryption_key(Crypto.Srtp.AES_CM_128_HMAC_SHA1_80, server_key.extract(), server_salt.extract()); srtp_session.set_decryption_key(Crypto.Srtp.AES_CM_128_HMAC_SHA1_80, server_key.extract(), server_salt.extract());
} }
return new Xmpp.Xep.Jingle.ContentEncryption() { encryption_ns=Xmpp.Xep.JingleIceUdp.DTLS_NS_URI, encryption_name = "DTLS-SRTP", our_key=own_fingerprint, peer_key=peer_fingerprint };
} }
private static ssize_t pull_function(void* transport_ptr, uint8[] buffer) { private static ssize_t pull_function(void* transport_ptr, uint8[] buffer) {
@ -226,24 +232,40 @@ public class DtlsSrtp {
X509.Certificate peer_cert = X509.Certificate.create(); X509.Certificate peer_cert = X509.Certificate.create();
peer_cert.import(ref cert_datums[0], CertificateFormat.DER); peer_cert.import(ref cert_datums[0], CertificateFormat.DER);
string peer_fp_str = format_certificate(peer_cert, DigestAlgorithm.SHA256); uint8[] real_peer_fp = get_fingerprint(peer_cert, peer_fp_algo);
if (peer_fp_str.down() != this.peer_fingerprint.down()) {
warning("First cert in peer cert list doesn't equal advertised one %s vs %s", peer_fp_str, this.peer_fingerprint); if (real_peer_fp.length != this.peer_fingerprint.length) {
warning("Fingerprint lengths not equal %i vs %i", real_peer_fp.length, peer_fingerprint.length);
return false; return false;
} }
for (int i = 0; i < real_peer_fp.length; i++) {
if (real_peer_fp[i] != this.peer_fingerprint[i]) {
warning("First cert in peer cert list doesn't equal advertised one: %s vs %s", format_fingerprint(real_peer_fp), format_fingerprint(peer_fingerprint));
return false;
}
}
return true; return true;
} }
private string format_certificate(X509.Certificate certificate, DigestAlgorithm digest_algo) { private uint8[] get_fingerprint(X509.Certificate certificate, DigestAlgorithm digest_algo) {
uint8[] buf = new uint8[512]; uint8[] buf = new uint8[512];
size_t buf_out_size = 512; size_t buf_out_size = 512;
certificate.get_fingerprint(digest_algo, buf, ref buf_out_size); certificate.get_fingerprint(digest_algo, buf, ref buf_out_size);
var sb = new StringBuilder(); uint8[] ret = new uint8[buf_out_size];
for (int i = 0; i < buf_out_size; i++) { for (int i = 0; i < buf_out_size; i++) {
sb.append("%02x".printf(buf[i])); ret[i] = buf[i];
if (i < buf_out_size - 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(":"); sb.append(":");
} }
} }

View file

@ -68,9 +68,11 @@ public class Dino.Plugins.Ice.TransportParameters : JingleIceUdp.IceUdpTransport
dtls_srtp = setup_dtls(this); dtls_srtp = setup_dtls(this);
this.own_fingerprint = dtls_srtp.get_own_fingerprint(GnuTLS.DigestAlgorithm.SHA256); this.own_fingerprint = dtls_srtp.get_own_fingerprint(GnuTLS.DigestAlgorithm.SHA256);
if (incoming) { if (incoming) {
dtls_srtp.set_peer_fingerprint(this.peer_fingerprint); dtls_srtp.set_peer_fingerprint(this.peer_fingerprint, this.peer_fp_algo == "sha-256" ? GnuTLS.DigestAlgorithm.SHA256 : GnuTLS.DigestAlgorithm.NULL);
} else { } else {
dtls_srtp.setup_dtls_connection(true); dtls_srtp.setup_dtls_connection.begin(true, (_, res) => {
this.content.encryption = dtls_srtp.setup_dtls_connection.end(res);
});
} }
} }
@ -143,7 +145,7 @@ public class Dino.Plugins.Ice.TransportParameters : JingleIceUdp.IceUdpTransport
base.handle_transport_accept(transport); base.handle_transport_accept(transport);
if (dtls_srtp != null && peer_fingerprint != null) { if (dtls_srtp != null && peer_fingerprint != null) {
dtls_srtp.set_peer_fingerprint(this.peer_fingerprint); dtls_srtp.set_peer_fingerprint(this.peer_fingerprint, this.peer_fp_algo == "sha-256" ? GnuTLS.DigestAlgorithm.SHA256 : GnuTLS.DigestAlgorithm.NULL);
} else { } else {
dtls_srtp = null; dtls_srtp = null;
} }
@ -205,7 +207,9 @@ public class Dino.Plugins.Ice.TransportParameters : JingleIceUdp.IceUdpTransport
if (incoming && dtls_srtp != null) { if (incoming && dtls_srtp != null) {
Jingle.DatagramConnection rtp_datagram = (Jingle.DatagramConnection) content.get_transport_connection(1); Jingle.DatagramConnection rtp_datagram = (Jingle.DatagramConnection) content.get_transport_connection(1);
rtp_datagram.notify["ready"].connect(() => { rtp_datagram.notify["ready"].connect(() => {
dtls_srtp.setup_dtls_connection(false); dtls_srtp.setup_dtls_connection.begin(false, (_, res) => {
this.content.encryption = dtls_srtp.setup_dtls_connection.end(res);
});
}); });
} }
base.create_transport_connection(stream, content); base.create_transport_connection(stream, content);

View file

@ -34,6 +34,8 @@ public class Xmpp.Xep.Jingle.Content : Object {
public weak Session session; public weak Session session;
public Map<uint8, ComponentConnection> component_connections = new HashMap<uint8, ComponentConnection>(); // TODO private public Map<uint8, ComponentConnection> component_connections = new HashMap<uint8, ComponentConnection>(); // TODO private
public ContentEncryption? encryption { get; set; }
// INITIATE_SENT | INITIATE_RECEIVED | CONNECTING // INITIATE_SENT | INITIATE_RECEIVED | CONNECTING
public Set<string> tried_transport_methods = new HashSet<string>(); public Set<string> tried_transport_methods = new HashSet<string>();
@ -234,3 +236,10 @@ public class Xmpp.Xep.Jingle.Content : Object {
session.send_transport_info(this, transport); session.send_transport_info(this, transport);
} }
} }
public class Xmpp.Xep.Jingle.ContentEncryption : Object {
public string encryption_ns { get; set; }
public string encryption_name { get; set; }
public uint8[] our_key { get; set; }
public uint8[] peer_key { get; set; }
}

View file

@ -116,6 +116,9 @@ public class Xmpp.Xep.JingleRtp.Parameters : Jingle.ContentParameters, Object {
remote_crypto = null; remote_crypto = null;
local_crypto = null; local_crypto = null;
} }
if (remote_crypto != null && local_crypto != null) {
content.encryption = new Xmpp.Xep.Jingle.ContentEncryption() { encryption_ns = "", encryption_name = "SRTP", our_key=local_crypto.key, peer_key=remote_crypto.key };
}
this.stream = parent.create_stream(content); this.stream = parent.create_stream(content);
rtp_datagram.datagram_received.connect(this.stream.on_recv_rtp_data); rtp_datagram.datagram_received.connect(this.stream.on_recv_rtp_data);

View file

@ -1,5 +1,7 @@
public abstract class Xmpp.Xep.JingleRtp.Stream : Object { public abstract class Xmpp.Xep.JingleRtp.Stream : Object {
public Jingle.Content content { get; protected set; } public Jingle.Content content { get; protected set; }
public string name { get { public string name { get {
return content.content_name; return content.content_name;
}} }}

View file

@ -5,6 +5,7 @@ using Xmpp;
namespace Xmpp.Xep.JingleIceUdp { namespace Xmpp.Xep.JingleIceUdp {
private const string NS_URI = "urn:xmpp:jingle:transports:ice-udp:1"; private const string NS_URI = "urn:xmpp:jingle:transports:ice-udp:1";
public const string DTLS_NS_URI = "urn:xmpp:jingle:apps:dtls:0";
public abstract class Module : XmppStreamModule, Jingle.Transport { public abstract class Module : XmppStreamModule, Jingle.Transport {
public static Xmpp.ModuleIdentity<Module> IDENTITY = new Xmpp.ModuleIdentity<Module>(NS_URI, "0176_jingle_ice_udp"); public static Xmpp.ModuleIdentity<Module> IDENTITY = new Xmpp.ModuleIdentity<Module>(NS_URI, "0176_jingle_ice_udp");
@ -12,10 +13,11 @@ public abstract class Module : XmppStreamModule, Jingle.Transport {
public override void attach(XmppStream stream) { public override void attach(XmppStream stream) {
stream.get_module(Jingle.Module.IDENTITY).register_transport(this); stream.get_module(Jingle.Module.IDENTITY).register_transport(this);
stream.get_module(ServiceDiscovery.Module.IDENTITY).add_feature(stream, NS_URI); stream.get_module(ServiceDiscovery.Module.IDENTITY).add_feature(stream, NS_URI);
stream.get_module(ServiceDiscovery.Module.IDENTITY).add_feature(stream, "urn:xmpp:jingle:apps:dtls:0"); stream.get_module(ServiceDiscovery.Module.IDENTITY).add_feature(stream, DTLS_NS_URI);
} }
public override void detach(XmppStream stream) { public override void detach(XmppStream stream) {
stream.get_module(ServiceDiscovery.Module.IDENTITY).remove_feature(stream, NS_URI); stream.get_module(ServiceDiscovery.Module.IDENTITY).remove_feature(stream, NS_URI);
stream.get_module(ServiceDiscovery.Module.IDENTITY).remove_feature(stream, DTLS_NS_URI);
} }
public override string get_ns() { return NS_URI; } public override string get_ns() { return NS_URI; }

View file

@ -13,8 +13,9 @@ public abstract class Xmpp.Xep.JingleIceUdp.IceUdpTransportParameters : Jingle.T
public ConcurrentList<Candidate> unsent_local_candidates = new ConcurrentList<Candidate>(Candidate.equals_func); public ConcurrentList<Candidate> unsent_local_candidates = new ConcurrentList<Candidate>(Candidate.equals_func);
public Gee.List<Candidate> remote_candidates = new ArrayList<Candidate>(Candidate.equals_func); public Gee.List<Candidate> remote_candidates = new ArrayList<Candidate>(Candidate.equals_func);
public string? own_fingerprint = null; public uint8[]? own_fingerprint = null;
public string? peer_fingerprint = null; public uint8[]? peer_fingerprint = null;
public string? peer_fp_algo = null;
public Jid local_full_jid { get; private set; } public Jid local_full_jid { get; private set; }
public Jid peer_full_jid { get; private set; } public Jid peer_full_jid { get; private set; }
@ -24,7 +25,7 @@ public abstract class Xmpp.Xep.JingleIceUdp.IceUdpTransportParameters : Jingle.T
public bool incoming { get; private set; default = false; } public bool incoming { get; private set; default = false; }
private bool connection_created = false; private bool connection_created = false;
private weak Jingle.Content? content = null; protected weak Jingle.Content? content = null;
protected IceUdpTransportParameters(uint8 components, Jid local_full_jid, Jid peer_full_jid, StanzaNode? node = null) { protected IceUdpTransportParameters(uint8 components, Jid local_full_jid, Jid peer_full_jid, StanzaNode? node = null) {
this.components_ = components; this.components_ = components;
@ -38,9 +39,10 @@ public abstract class Xmpp.Xep.JingleIceUdp.IceUdpTransportParameters : Jingle.T
remote_candidates.add(Candidate.parse(candidateNode)); remote_candidates.add(Candidate.parse(candidateNode));
} }
StanzaNode? fingerprint_node = node.get_subnode("fingerprint", "urn:xmpp:jingle:apps:dtls:0"); StanzaNode? fingerprint_node = node.get_subnode("fingerprint", DTLS_NS_URI);
if (fingerprint_node != null) { if (fingerprint_node != null) {
peer_fingerprint = fingerprint_node.get_deep_string_content(); peer_fingerprint = fingerprint_to_bytes(fingerprint_node.get_deep_string_content());
peer_fp_algo = fingerprint_node.get_attribute("hash");
} }
} }
} }
@ -67,10 +69,10 @@ public abstract class Xmpp.Xep.JingleIceUdp.IceUdpTransportParameters : Jingle.T
.put_attribute("pwd", local_pwd); .put_attribute("pwd", local_pwd);
if (own_fingerprint != null) { if (own_fingerprint != null) {
var fingerprint_node = new StanzaNode.build("fingerprint", "urn:xmpp:jingle:apps:dtls:0") var fingerprint_node = new StanzaNode.build("fingerprint", DTLS_NS_URI)
.add_self_xmlns() .add_self_xmlns()
.put_attribute("hash", "sha-256") .put_attribute("hash", "sha-256")
.put_node(new StanzaNode.text(own_fingerprint)); .put_node(new StanzaNode.text(format_fingerprint(own_fingerprint)));
if (incoming) { if (incoming) {
fingerprint_node.put_attribute("setup", "active"); fingerprint_node.put_attribute("setup", "active");
} else { } else {
@ -95,9 +97,10 @@ public abstract class Xmpp.Xep.JingleIceUdp.IceUdpTransportParameters : Jingle.T
remote_candidates.add(Candidate.parse(candidateNode)); remote_candidates.add(Candidate.parse(candidateNode));
} }
StanzaNode? fingerprint_node = node.get_subnode("fingerprint", "urn:xmpp:jingle:apps:dtls:0"); StanzaNode? fingerprint_node = node.get_subnode("fingerprint", DTLS_NS_URI);
if (fingerprint_node != null) { if (fingerprint_node != null) {
peer_fingerprint = fingerprint_node.get_deep_string_content(); peer_fingerprint = fingerprint_to_bytes(fingerprint_node.get_deep_string_content());
peer_fp_algo = fingerprint_node.get_attribute("hash");
} }
} }
@ -138,4 +141,30 @@ public abstract class Xmpp.Xep.JingleIceUdp.IceUdpTransportParameters : Jingle.T
content.send_transport_info(to_transport_stanza_node()); content.send_transport_info(to_transport_stanza_node());
} }
} }
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;
}
private uint8[] fingerprint_to_bytes(string? fingerprint_) {
if (fingerprint_ == null) return null;
string fingerprint = fingerprint_.replace(":", "").up();
uint8[] bin = new uint8[fingerprint.length / 2];
const string HEX = "0123456789ABCDEF";
for (int i = 0; i < fingerprint.length / 2; i++) {
bin[i] = (uint8) (HEX.index_of_char(fingerprint[i*2]) << 4) | HEX.index_of_char(fingerprint[i*2+1]);
}
return bin;
}
} }