From dfd79401044834b164c50f5948986719eabf8127 Mon Sep 17 00:00:00 2001 From: Marvin W Date: Sun, 21 Mar 2021 12:41:32 +0100 Subject: Add support for Jingle RTP sessions (XEP-0167) to xmpp-vala Co-authored-by: fiaxh --- .../xep/0167_jingle_rtp/content_parameters.vala | 142 +++++++++++++++++ .../module/xep/0167_jingle_rtp/content_type.vala | 23 +++ .../xep/0167_jingle_rtp/jingle_rtp_module.vala | 175 +++++++++++++++++++++ .../module/xep/0167_jingle_rtp/payload_type.vala | 52 ++++++ .../xep/0167_jingle_rtp/session_info_type.vala | 67 ++++++++ .../src/module/xep/0167_jingle_rtp/stream.vala | 46 ++++++ 6 files changed, 505 insertions(+) create mode 100644 xmpp-vala/src/module/xep/0167_jingle_rtp/content_parameters.vala create mode 100644 xmpp-vala/src/module/xep/0167_jingle_rtp/content_type.vala create mode 100644 xmpp-vala/src/module/xep/0167_jingle_rtp/jingle_rtp_module.vala create mode 100644 xmpp-vala/src/module/xep/0167_jingle_rtp/payload_type.vala create mode 100644 xmpp-vala/src/module/xep/0167_jingle_rtp/session_info_type.vala create mode 100644 xmpp-vala/src/module/xep/0167_jingle_rtp/stream.vala (limited to 'xmpp-vala/src/module/xep/0167_jingle_rtp') diff --git a/xmpp-vala/src/module/xep/0167_jingle_rtp/content_parameters.vala b/xmpp-vala/src/module/xep/0167_jingle_rtp/content_parameters.vala new file mode 100644 index 00000000..8a3668b2 --- /dev/null +++ b/xmpp-vala/src/module/xep/0167_jingle_rtp/content_parameters.vala @@ -0,0 +1,142 @@ +using Gee; +using Xmpp; +using Xmpp.Xep; + +public class Xmpp.Xep.JingleRtp.Parameters : Jingle.ContentParameters, Object { + + public signal void stream_created(Stream stream); + public signal void connection_ready(); + + public string media { get; private set; } + public string? ssrc { get; private set; } + public bool rtcp_mux { get; private set; } + + public string? bandwidth { get; private set; } + public string? bandwidth_type { get; private set; } + + public bool encryption_required { get; private set; default = false; } + public PayloadType? agreed_payload_type { get; private set; } + public Gee.List payload_types = new ArrayList(PayloadType.equals_func); + public Gee.List cryptos = new ArrayList(); + + public weak Stream? stream { get; private set; } + + private Module parent; + + public Parameters(Module parent, + string media, Gee.List payload_types, + string? ssrc = null, bool rtcp_mux = false, + string? bandwidth = null, string? bandwidth_type = null, + bool encryption_required = false, Gee.List cryptos = new ArrayList() + ) { + this.parent = parent; + this.media = media; + this.ssrc = ssrc; + this.rtcp_mux = rtcp_mux; + this.bandwidth = bandwidth; + this.bandwidth_type = bandwidth_type; + this.encryption_required = encryption_required; + this.payload_types = payload_types; + this.cryptos = cryptos; + } + + public Parameters.from_node(Module parent, StanzaNode node) throws Jingle.IqError { + this.parent = parent; + this.media = node.get_attribute("media"); + this.ssrc = node.get_attribute("ssrc"); + this.rtcp_mux = node.get_subnode("rtcp-mux") != null; + StanzaNode? encryption = node.get_subnode("encryption"); + if (encryption != null) { + this.encryption_required = encryption.get_attribute_bool("required", this.encryption_required); + foreach (StanzaNode crypto in encryption.get_subnodes("crypto")) { + this.cryptos.add(Crypto.parse(crypto)); + } + } + foreach (StanzaNode payloadType in node.get_subnodes("payload-type")) { + this.payload_types.add(PayloadType.parse(payloadType)); + } + } + + public async void handle_proposed_content(XmppStream stream, Jingle.Session session, Jingle.Content content) { + agreed_payload_type = yield parent.pick_payload_type(media, payload_types); + if (agreed_payload_type == null) { + debug("no usable payload type"); + content.reject(); + return; + } + } + + public void accept(XmppStream stream, Jingle.Session session, Jingle.Content content) { + debug("[%p] Jingle RTP on_accept", stream); + + Jingle.DatagramConnection rtp_datagram = (Jingle.DatagramConnection) content.get_transport_connection(1); + Jingle.DatagramConnection rtcp_datagram = (Jingle.DatagramConnection) content.get_transport_connection(2); + + ulong rtcp_ready_handler_id = 0; + rtcp_ready_handler_id = rtcp_datagram.notify["ready"].connect(() => { + this.stream.on_rtcp_ready(); + + rtcp_datagram.disconnect(rtcp_ready_handler_id); + rtcp_ready_handler_id = 0; + }); + + ulong rtp_ready_handler_id = 0; + rtp_ready_handler_id = rtp_datagram.notify["ready"].connect(() => { + this.stream.on_rtp_ready(); + connection_ready(); + + rtp_datagram.disconnect(rtp_ready_handler_id); + rtp_ready_handler_id = 0; + }); + + session.notify["state"].connect((obj, _) => { + Jingle.Session session2 = (Jingle.Session) obj; + if (session2.state == Jingle.Session.State.ENDED) { + if (rtcp_ready_handler_id != 0) rtcp_datagram.disconnect(rtcp_ready_handler_id); + if (rtp_ready_handler_id != 0) rtp_datagram.disconnect(rtp_ready_handler_id); + } + }); + + this.stream = parent.create_stream(content); + rtp_datagram.datagram_received.connect(this.stream.on_recv_rtp_data); + rtcp_datagram.datagram_received.connect(this.stream.on_recv_rtcp_data); + this.stream.on_send_rtp_data.connect(rtp_datagram.send_datagram); + this.stream.on_send_rtcp_data.connect(rtcp_datagram.send_datagram); + this.stream_created(this.stream); + this.stream.create(); + } + + public void handle_accept(XmppStream stream, Jingle.Session session, Jingle.Content content, StanzaNode description_node) { + Gee.List payload_type_nodes = description_node.get_subnodes("payload-type"); + if (payload_type_nodes.size == 0) { + warning("Counterpart didn't include any payload types"); + return; + } + PayloadType preferred_payload_type = PayloadType.parse(payload_type_nodes[0]); + if (!payload_types.contains(preferred_payload_type)) { + warning("Counterpart's preferred content type doesn't match any of our sent ones"); + } + agreed_payload_type = preferred_payload_type; + + accept(stream, session, content); + } + + public void terminate(bool we_terminated, string? reason_name, string? reason_text) { + if (stream != null) parent.close_stream(stream); + } + + public StanzaNode get_description_node() { + StanzaNode ret = new StanzaNode.build("description", NS_URI) + .add_self_xmlns() + .put_attribute("media", media); + + if (agreed_payload_type != null) { + ret.put_node(agreed_payload_type.to_xml()); + } else { + foreach (PayloadType payload_type in payload_types) { + ret.put_node(payload_type.to_xml()); + } + } + return ret; + } +} \ No newline at end of file diff --git a/xmpp-vala/src/module/xep/0167_jingle_rtp/content_type.vala b/xmpp-vala/src/module/xep/0167_jingle_rtp/content_type.vala new file mode 100644 index 00000000..5a8ed1b6 --- /dev/null +++ b/xmpp-vala/src/module/xep/0167_jingle_rtp/content_type.vala @@ -0,0 +1,23 @@ +using Gee; +using Xmpp; +using Xmpp.Xep; + +public class Xmpp.Xep.JingleRtp.ContentType : Jingle.ContentType, Object { + public string ns_uri { get { return NS_URI; } } + public Jingle.TransportType required_transport_type { get { return Jingle.TransportType.DATAGRAM; } } + public uint8 required_components { get { return 2; /* RTP + RTCP */ } } + + private Module module; + + public ContentType(Module module) { + this.module = module; + } + + public Jingle.ContentParameters parse_content_parameters(StanzaNode description) throws Jingle.IqError { + return new Parameters.from_node(module, description); + } + + public Jingle.ContentParameters create_content_parameters(Object object) throws Jingle.IqError { + assert_not_reached(); + } +} \ No newline at end of file diff --git a/xmpp-vala/src/module/xep/0167_jingle_rtp/jingle_rtp_module.vala b/xmpp-vala/src/module/xep/0167_jingle_rtp/jingle_rtp_module.vala new file mode 100644 index 00000000..35e03168 --- /dev/null +++ b/xmpp-vala/src/module/xep/0167_jingle_rtp/jingle_rtp_module.vala @@ -0,0 +1,175 @@ +using Gee; +using Xmpp; +using Xmpp.Xep; + +namespace Xmpp.Xep.JingleRtp { + +public const string NS_URI = "urn:xmpp:jingle:apps:rtp:1"; +public const string NS_URI_AUDIO = "urn:xmpp:jingle:apps:rtp:audio"; +public const string NS_URI_VIDEO = "urn:xmpp:jingle:apps:rtp:video"; + +public abstract class Module : XmppStreamModule { + public static Xmpp.ModuleIdentity IDENTITY = new Xmpp.ModuleIdentity(NS_URI, "0167_jingle_rtp"); + + private ContentType content_type; + public SessionInfoType session_info_type = new SessionInfoType(); + + protected Module() { + content_type = new ContentType(this); + } + + public abstract async Gee.List get_supported_payloads(string media); + public abstract async PayloadType? pick_payload_type(string media, Gee.List payloads); + public abstract Stream create_stream(Jingle.Content content); + public abstract void close_stream(Stream stream); + + public async Jingle.Session start_call(XmppStream stream, Jid receiver_full_jid, bool video) throws Jingle.Error { + + Jingle.Module jingle_module = stream.get_module(Jingle.Module.IDENTITY); + + Jid? my_jid = stream.get_flag(Bind.Flag.IDENTITY).my_jid; + if (my_jid == null) { + throw new Jingle.Error.GENERAL("Couldn't determine own JID"); + } + + ArrayList contents = new ArrayList(); + + // Create audio content + Parameters audio_content_parameters = new Parameters(this, "audio", yield get_supported_payloads("audio")); + Jingle.Transport? audio_transport = yield jingle_module.select_transport(stream, content_type.required_transport_type, content_type.required_components, receiver_full_jid, Set.empty()); + if (audio_transport == null) { + throw new Jingle.Error.NO_SHARED_PROTOCOLS("No suitable audio transports"); + } + Jingle.TransportParameters audio_transport_params = audio_transport.create_transport_parameters(stream, content_type.required_components, my_jid, receiver_full_jid); + Jingle.Content audio_content = new Jingle.Content.initiate_sent("voice", Jingle.Senders.BOTH, + content_type, audio_content_parameters, + audio_transport, audio_transport_params, + null, null, + my_jid, receiver_full_jid); + contents.add(audio_content); + + Jingle.Content? video_content = null; + if (video) { + // Create video content + Parameters video_content_parameters = new Parameters(this, "video", yield get_supported_payloads("video")); + Jingle.Transport? video_transport = yield stream.get_module(Jingle.Module.IDENTITY).select_transport(stream, content_type.required_transport_type, content_type.required_components, receiver_full_jid, Set.empty()); + if (video_transport == null) { + throw new Jingle.Error.NO_SHARED_PROTOCOLS("No suitable video transports"); + } + Jingle.TransportParameters video_transport_params = video_transport.create_transport_parameters(stream, content_type.required_components, my_jid, receiver_full_jid); + video_content = new Jingle.Content.initiate_sent("webcam", Jingle.Senders.BOTH, + content_type, video_content_parameters, + video_transport, video_transport_params, + null, null, + my_jid, receiver_full_jid); + contents.add(video_content); + } + + // Create session + try { + Jingle.Session session = yield jingle_module.create_session(stream, contents, receiver_full_jid); + return session; + } catch (Jingle.Error e) { + throw new Jingle.Error.GENERAL(@"Couldn't create Jingle session: $(e.message)"); + } + } + + public async Jingle.Content add_outgoing_video_content(XmppStream stream, Jingle.Session session) { + Jid my_jid = session.local_full_jid; + Jid receiver_full_jid = session.peer_full_jid; + + Jingle.Content? content = null; + foreach (Jingle.Content c in session.contents.values) { + Parameters? parameters = c.content_params as Parameters; + if (parameters == null) continue; + + if (parameters.media == "video") { + content = c; + break; + } + } + + if (content == null) { + // Content for video does not yet exist -> create it + Parameters video_content_parameters = new Parameters(this, "video", yield get_supported_payloads("video")); + Jingle.Transport? video_transport = yield stream.get_module(Jingle.Module.IDENTITY).select_transport(stream, content_type.required_transport_type, content_type.required_components, receiver_full_jid, Set.empty()); + if (video_transport == null) { + throw new Jingle.Error.NO_SHARED_PROTOCOLS("No suitable video transports"); + } + Jingle.TransportParameters video_transport_params = video_transport.create_transport_parameters(stream, content_type.required_components, my_jid, receiver_full_jid); + content = new Jingle.Content.initiate_sent("webcam", + session.we_initiated ? Jingle.Senders.INITIATOR : Jingle.Senders.RESPONDER, + content_type, video_content_parameters, + video_transport, video_transport_params, + null, null, + my_jid, receiver_full_jid); + + session.add_content.begin(content); + } else { + // Content for video already exists -> modify senders + bool we_initiated = session.we_initiated; + Jingle.Senders want_sender = we_initiated ? Jingle.Senders.INITIATOR : Jingle.Senders.RESPONDER; + if (content.senders == Jingle.Senders.BOTH || content.senders == want_sender) { + warning("want to add video but senders is already both/target"); + } else if (content.senders == Jingle.Senders.NONE) { + content.modify(want_sender); + } else { + content.modify(Jingle.Senders.BOTH); + } + } + + return content; + } + + public override void attach(XmppStream stream) { + stream.get_module(ServiceDiscovery.Module.IDENTITY).add_feature(stream, NS_URI); + stream.get_module(ServiceDiscovery.Module.IDENTITY).add_feature(stream, NS_URI_AUDIO); + stream.get_module(ServiceDiscovery.Module.IDENTITY).add_feature(stream, NS_URI_VIDEO); + stream.get_module(Jingle.Module.IDENTITY).register_content_type(content_type); + stream.get_module(Jingle.Module.IDENTITY).register_session_info_type(session_info_type); + } + + 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_AUDIO); + stream.get_module(ServiceDiscovery.Module.IDENTITY).remove_feature(stream, NS_URI_VIDEO); + } + + public async bool is_available(XmppStream stream, Jid full_jid) { + bool? has_feature = yield stream.get_module(ServiceDiscovery.Module.IDENTITY).has_entity_feature(stream, full_jid, NS_URI); + if (has_feature == null || !(!)has_feature) { + return false; + } + return yield stream.get_module(Jingle.Module.IDENTITY).is_available(stream, content_type.required_transport_type, content_type.required_components, full_jid); + } + + public override string get_ns() { return NS_URI; } + public override string get_id() { return IDENTITY.id; } +} + +public class Crypto { + public string cryptoSuite { get; private set; } + public string keyParams { get; private set; } + public string? sessionParams { get; private set; } + public string? tag { get; private set; } + + public static Crypto parse(StanzaNode node) { + Crypto crypto = new Crypto(); + crypto.cryptoSuite = node.get_attribute("crypto-suite"); + crypto.keyParams = node.get_attribute("key-params"); + crypto.sessionParams = node.get_attribute("session-params"); + crypto.tag = node.get_attribute("tag"); + return crypto; + } + + public StanzaNode to_xml() { + StanzaNode node = new StanzaNode.build("crypto", NS_URI) + .put_attribute("crypto-suite", cryptoSuite) + .put_attribute("key-params", keyParams); + if (sessionParams != null) node.put_attribute("session-params", sessionParams); + if (tag != null) node.put_attribute("tag", tag); + return node; + } +} + +} diff --git a/xmpp-vala/src/module/xep/0167_jingle_rtp/payload_type.vala b/xmpp-vala/src/module/xep/0167_jingle_rtp/payload_type.vala new file mode 100644 index 00000000..452f1d65 --- /dev/null +++ b/xmpp-vala/src/module/xep/0167_jingle_rtp/payload_type.vala @@ -0,0 +1,52 @@ +using Gee; +using Xmpp; +using Xmpp.Xep; + +public class Xmpp.Xep.JingleRtp.PayloadType { + public uint8 id { get; set; } + public string? name { get; set; } + public uint8 channels { get; set; default = 1; } + public uint32 clockrate { get; set; } + public uint32 maxptime { get; set; } + public uint32 ptime { get; set; } + public Map parameters = new HashMap(); + + public static PayloadType parse(StanzaNode node) { + PayloadType payloadType = new PayloadType(); + payloadType.channels = (uint8) node.get_attribute_uint("channels", payloadType.channels); + payloadType.clockrate = node.get_attribute_uint("clockrate"); + payloadType.id = (uint8) node.get_attribute_uint("id"); + payloadType.maxptime = node.get_attribute_uint("maxptime"); + payloadType.name = node.get_attribute("name"); + payloadType.ptime = node.get_attribute_uint("ptime"); + foreach (StanzaNode parameter in node.get_subnodes("parameter")) { + payloadType.parameters[parameter.get_attribute("name")] = parameter.get_attribute("value"); + } + return payloadType; + } + + public StanzaNode to_xml() { + StanzaNode node = new StanzaNode.build("payload-type", NS_URI) + .put_attribute("id", id.to_string()); + if (channels != 1) node.put_attribute("channels", channels.to_string()); + if (clockrate != 0) node.put_attribute("clockrate", clockrate.to_string()); + if (maxptime != 0) node.put_attribute("maxptime", maxptime.to_string()); + if (name != null) node.put_attribute("name", name); + if (ptime != 0) node.put_attribute("ptime", ptime.to_string()); + foreach (string parameter in parameters.keys) { + node.put_node(new StanzaNode.build("parameter", NS_URI) + .put_attribute("name", parameter) + .put_attribute("value", parameters[parameter])); + } + return node; + } + + public static bool equals_func(PayloadType a, PayloadType b) { + return a.id == b.id && + a.name == b.name && + a.channels == b.channels && + a.clockrate == b.clockrate && + a.maxptime == b.maxptime && + a.ptime == b.ptime; + } +} \ No newline at end of file diff --git a/xmpp-vala/src/module/xep/0167_jingle_rtp/session_info_type.vala b/xmpp-vala/src/module/xep/0167_jingle_rtp/session_info_type.vala new file mode 100644 index 00000000..d36255f0 --- /dev/null +++ b/xmpp-vala/src/module/xep/0167_jingle_rtp/session_info_type.vala @@ -0,0 +1,67 @@ +using Gee; +using Xmpp; +using Xmpp.Xep; + +namespace Xmpp.Xep.JingleRtp { + + public enum CallSessionInfo { + ACTIVE, + HOLD, + UNHOLD, + MUTE, + UNMUTE, + RINGING + } + + public class SessionInfoType : Jingle.SessionInfoNs, Object { + public const string NS_URI = "urn:xmpp:jingle:apps:rtp:info:1"; + public string ns_uri { get { return NS_URI; } } + + public signal void info_received(Jingle.Session session, CallSessionInfo info); + public signal void mute_update_received(Jingle.Session session, bool mute, string name); + + public void handle_content_session_info(XmppStream stream, Jingle.Session session, StanzaNode info, Iq.Stanza iq) throws Jingle.IqError { + switch (info.name) { + case "active": + info_received(session, CallSessionInfo.ACTIVE); + break; + case "hold": + info_received(session, CallSessionInfo.HOLD); + break; + case "unhold": + info_received(session, CallSessionInfo.UNHOLD); + break; + case "mute": + string? name = info.get_attribute("name"); + mute_update_received(session, true, name); + info_received(session, CallSessionInfo.MUTE); + break; + case "unmute": + string? name = info.get_attribute("name"); + mute_update_received(session, false, name); + info_received(session, CallSessionInfo.UNMUTE); + break; + case "ringing": + info_received(session, CallSessionInfo.RINGING); + break; + } + } + + public void send_mute(Jingle.Session session, bool mute, string media) { + string node_name = mute ? "mute" : "unmute"; + + foreach (Jingle.Content content in session.contents.values) { + Parameters? parameters = content.content_params as Parameters; + if (parameters != null && parameters.media == media) { + StanzaNode session_info_content = new StanzaNode.build(node_name, NS_URI).add_self_xmlns().put_attribute("name", content.content_name); + session.send_session_info(session_info_content); + } + } + } + + public void send_ringing(Jingle.Session session) { + StanzaNode session_info_content = new StanzaNode.build("ringing", NS_URI).add_self_xmlns(); + session.send_session_info(session_info_content); + } + } +} diff --git a/xmpp-vala/src/module/xep/0167_jingle_rtp/stream.vala b/xmpp-vala/src/module/xep/0167_jingle_rtp/stream.vala new file mode 100644 index 00000000..62d85dec --- /dev/null +++ b/xmpp-vala/src/module/xep/0167_jingle_rtp/stream.vala @@ -0,0 +1,46 @@ +public abstract class Xmpp.Xep.JingleRtp.Stream : Object { + public Jingle.Content content { get; protected set; } + public string name { get { + return content.content_name; + }} + public string? media { get { + var content_params = content.content_params; + if (content_params is Parameters) { + return ((Parameters)content_params).media; + } + return null; + }} + public JingleRtp.PayloadType? payload_type { get { + var content_params = content.content_params; + if (content_params is Parameters) { + return ((Parameters)content_params).agreed_payload_type; + } + return null; + }} + public bool sending { get { + return content.session.senders_include_us(content.senders); + }} + public bool receiving { get { + return content.session.senders_include_counterpart(content.senders); + }} + + protected Stream(Jingle.Content content) { + this.content = content; + } + + public signal void on_send_rtp_data(Bytes bytes); + public signal void on_send_rtcp_data(Bytes bytes); + + public abstract void on_recv_rtp_data(Bytes bytes); + public abstract void on_recv_rtcp_data(Bytes bytes); + + public abstract void on_rtp_ready(); + public abstract void on_rtcp_ready(); + + public abstract void create(); + public abstract void destroy(); + + public string to_string() { + return @"$name/$media stream in $(content.session.sid)"; + } +} \ No newline at end of file -- cgit v1.2.3-54-g00ecf From b393d4160182873ea2acd9fbc6421f7e1a3adb9e Mon Sep 17 00:00:00 2001 From: Marvin W Date: Tue, 23 Mar 2021 15:05:50 +0100 Subject: Add support for SRTP --- plugins/crypto-vala/CMakeLists.txt | 1 + plugins/crypto-vala/src/random.vala | 5 + plugins/rtp/CMakeLists.txt | 8 +- plugins/rtp/src/module.vala | 80 +- plugins/rtp/src/srtp.c | 836 +++++++++++++++++++++ plugins/rtp/src/srtp.h | 82 ++ plugins/rtp/src/srtp.vapi | 103 +++ plugins/rtp/src/stream.vala | 60 +- .../xep/0167_jingle_rtp/content_parameters.vala | 46 +- .../xep/0167_jingle_rtp/jingle_rtp_module.vala | 132 +++- .../src/module/xep/0167_jingle_rtp/stream.vala | 14 + 11 files changed, 1286 insertions(+), 81 deletions(-) create mode 100644 plugins/crypto-vala/src/random.vala create mode 100644 plugins/rtp/src/srtp.c create mode 100644 plugins/rtp/src/srtp.h create mode 100644 plugins/rtp/src/srtp.vapi (limited to 'xmpp-vala/src/module/xep/0167_jingle_rtp') diff --git a/plugins/crypto-vala/CMakeLists.txt b/plugins/crypto-vala/CMakeLists.txt index 2c9f790a..036e45f6 100644 --- a/plugins/crypto-vala/CMakeLists.txt +++ b/plugins/crypto-vala/CMakeLists.txt @@ -10,6 +10,7 @@ SOURCES "src/cipher.vala" "src/cipher_converter.vala" "src/error.vala" + "src/random.vala" CUSTOM_VAPIS "${CMAKE_CURRENT_SOURCE_DIR}/vapi/gcrypt.vapi" PACKAGES diff --git a/plugins/crypto-vala/src/random.vala b/plugins/crypto-vala/src/random.vala new file mode 100644 index 00000000..3f5d3ba9 --- /dev/null +++ b/plugins/crypto-vala/src/random.vala @@ -0,0 +1,5 @@ +namespace Crypto { +public static void randomize(uint8[] buffer) { + GCrypt.Random.randomize(buffer); +} +} \ No newline at end of file diff --git a/plugins/rtp/CMakeLists.txt b/plugins/rtp/CMakeLists.txt index 2b66f6ff..ef2f7698 100644 --- a/plugins/rtp/CMakeLists.txt +++ b/plugins/rtp/CMakeLists.txt @@ -18,18 +18,20 @@ SOURCES src/video_widget.vala src/register_plugin.vala CUSTOM_VAPIS + ${CMAKE_BINARY_DIR}/exports/crypto-vala.vapi ${CMAKE_BINARY_DIR}/exports/xmpp-vala.vapi ${CMAKE_BINARY_DIR}/exports/dino.vapi ${CMAKE_BINARY_DIR}/exports/qlite.vapi + ${CMAKE_CURRENT_SOURCE_DIR}/src/srtp.vapi PACKAGES ${RTP_PACKAGES} OPTIONS --vapidir=${CMAKE_CURRENT_SOURCE_DIR}/vapi ) -add_definitions(${VALA_CFLAGS} -DG_LOG_DOMAIN="rtp") -add_library(rtp SHARED ${RTP_VALA_C}) -target_link_libraries(rtp libdino ${RTP_PACKAGES}) +add_definitions(${VALA_CFLAGS} -DG_LOG_DOMAIN="rtp" -I${CMAKE_CURRENT_SOURCE_DIR}/src) +add_library(rtp SHARED ${RTP_VALA_C} src/srtp.c) +target_link_libraries(rtp libdino crypto-vala ${RTP_PACKAGES}) set_target_properties(rtp PROPERTIES PREFIX "") set_target_properties(rtp PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/plugins/) diff --git a/plugins/rtp/src/module.vala b/plugins/rtp/src/module.vala index 577e9f53..ecf7b658 100644 --- a/plugins/rtp/src/module.vala +++ b/plugins/rtp/src/module.vala @@ -200,65 +200,23 @@ public class Dino.Plugins.Rtp.Module : JingleRtp.Module { plugin.close_stream(rtp_stream); } -// public uint32 get_session_id(string id) { -// return (uint32) id.split("-")[0].to_int(); -// } -// -// public string create_feed(string media, bool incoming) { -// init(); -// string id = random_uuid(); -// if (media == "audio") { -// id = "0-" + id; -// } else { -// id = "1-" + id; -// } -// MediaDevice? device = plugin.get_preferred_device(media, incoming); -// Feed feed; -// if (incoming) { -// if (media == "audio") { -// feed = new IncomingAudioFeed(id, this, device); -// } else if (media == "video") { -// feed = new IncomingVideoFeed(id, this, device); -// } else { -// critical("Incoming feed of media '%s' not supported", media); -// return id; -// } -// } else { -// if (media == "audio") { -// string? matching_incoming_feed_id = null; -// foreach (Feed match in plugin.feeds.values) { -// if (match is IncomingAudioFeed) { -// matching_incoming_feed_id = match.id; -// } -// } -// feed = new OutgoingAudioFeed(id, this, device); -// } else if (media == "video") { -// feed = new OutgoingVideoFeed(id, this, device); -// } else { -// critical("Outgoing feed of media '%s' not supported", media); -// return id; -// } -// } -// plugin.add_feed(id, feed); -// return id; -// } -// -// public void connect_feed(string id, JingleRtp.PayloadType payload, Jingle.DatagramConnection connection) { -// if (!plugin.feeds.has_key(id)) { -// critical("Tried to connect feed with id %s, but no such feed found", id); -// return; -// } -// Feed feed = plugin.feeds[id]; -// feed.connect(payload, connection); -// } -// -// public void destroy_feed(string id) { -// if (!plugin.feeds.has_key(id)) { -// critical("Tried to destroy feed with id %s, but no such feed found", id); -// return; -// } -// Feed feed = plugin.feeds[id]; -// feed.destroy(); -// plugin.feeds.remove(id); -// } + public override JingleRtp.Crypto? generate_local_crypto() { + uint8[] keyAndSalt = new uint8[30]; + Crypto.randomize(keyAndSalt); + return JingleRtp.Crypto.create(JingleRtp.Crypto.AES_CM_128_HMAC_SHA1_80, keyAndSalt); + } + + public override JingleRtp.Crypto? pick_remote_crypto(Gee.List cryptos) { + foreach (JingleRtp.Crypto crypto in cryptos) { + if (crypto.is_valid) return crypto; + } + return null; + } + + public override JingleRtp.Crypto? pick_local_crypto(JingleRtp.Crypto? remote) { + if (remote == null || !remote.is_valid) return null; + uint8[] keyAndSalt = new uint8[30]; + Crypto.randomize(keyAndSalt); + return remote.rekey(keyAndSalt); + } } \ No newline at end of file diff --git a/plugins/rtp/src/srtp.c b/plugins/rtp/src/srtp.c new file mode 100644 index 00000000..708244d9 --- /dev/null +++ b/plugins/rtp/src/srtp.c @@ -0,0 +1,836 @@ +/* + * Secure RTP with libgcrypt + * Copyright (C) 2007 Rémi Denis-Courmont + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +/* TODO: + * Useless stuff (because nothing depends on it): + * - non-nul key derivation rate + * - MKI payload + */ + +#ifdef HAVE_CONFIG_H +# include +#endif + +#include +#include + +#include "srtp.h" + +#include +#include +#include +#include + +#include + +#ifdef _WIN32 +# include +#else +# include +#endif + +#define debug( ... ) (void)0 + +typedef struct srtp_proto_t +{ + gcry_cipher_hd_t cipher; + gcry_md_hd_t mac; + uint64_t window; + uint32_t salt[4]; +} srtp_proto_t; + +struct srtp_session_t +{ + srtp_proto_t rtp; + srtp_proto_t rtcp; + unsigned flags; + unsigned kdr; + uint32_t rtcp_index; + uint32_t rtp_roc; + uint16_t rtp_seq; + uint16_t rtp_rcc; + uint8_t tag_len; +}; + +enum +{ + SRTP_CRYPT, + SRTP_AUTH, + SRTP_SALT, + SRTCP_CRYPT, + SRTCP_AUTH, + SRTCP_SALT +}; + + +static inline unsigned rcc_mode (const srtp_session_t *s) +{ + return (s->flags >> 4) & 3; +} + + +static void proto_destroy (srtp_proto_t *p) +{ + gcry_md_close (p->mac); + gcry_cipher_close (p->cipher); +} + + +/** + * Releases all resources associated with a Secure RTP session. + */ +void srtp_destroy (srtp_session_t *s) +{ + assert (s != NULL); + + proto_destroy (&s->rtcp); + proto_destroy (&s->rtp); + free (s); +} + + +static int proto_create (srtp_proto_t *p, int gcipher, int gmd) +{ + if (gcry_cipher_open (&p->cipher, gcipher, GCRY_CIPHER_MODE_CTR, 0) == 0) + { + if (gcry_md_open (&p->mac, gmd, GCRY_MD_FLAG_HMAC) == 0) + return 0; + gcry_cipher_close (p->cipher); + } + return -1; +} + + +/** + * Allocates a Secure RTP one-way session. + * The same session cannot be used both ways because this would confuse + * internal cryptographic counters; it is however of course feasible to open + * multiple simultaneous sessions with the same master key. + * + * @param encr encryption algorithm number + * @param auth authentication algortihm number + * @param tag_len authentication tag byte length (NOT including RCC) + * @param flags OR'ed optional flags. + * + * @return NULL in case of error + */ +srtp_session_t * +srtp_create (int encr, int auth, unsigned tag_len, int prf, unsigned flags) +{ + if ((flags & ~SRTP_FLAGS_MASK)) + return NULL; + + int cipher, md; + switch (encr) + { + case SRTP_ENCR_NULL: + cipher = GCRY_CIPHER_NONE; + break; + + case SRTP_ENCR_AES_CM: + cipher = GCRY_CIPHER_AES; + break; + + default: + return NULL; + } + + switch (auth) + { + case SRTP_AUTH_NULL: + md = GCRY_MD_NONE; + break; + + case SRTP_AUTH_HMAC_SHA1: + md = GCRY_MD_SHA1; + break; + + default: + return NULL; + } + + if (tag_len > gcry_md_get_algo_dlen (md)) + return NULL; + + if (prf != SRTP_PRF_AES_CM) + return NULL; + + srtp_session_t *s = malloc (sizeof (*s)); + if (s == NULL) + return NULL; + + memset (s, 0, sizeof (*s)); + s->flags = flags; + s->tag_len = tag_len; + s->rtp_rcc = 1; /* Default RCC rate */ + if (rcc_mode (s)) + { + if (tag_len < 4) + goto error; + } + + if (proto_create (&s->rtp, cipher, md) == 0) + { + if (proto_create (&s->rtcp, cipher, md) == 0) + return s; + proto_destroy (&s->rtp); + } + + error: + free (s); + return NULL; +} + + +/** + * Counter Mode encryption/decryption (ctr length = 16 bytes) + * with non-padded (truncated) text + */ +static int +do_ctr_crypt (gcry_cipher_hd_t hd, const void *ctr, uint8_t *data, size_t len) +{ + const size_t ctrlen = 16; + div_t d = div (len, ctrlen); + + if (gcry_cipher_setctr (hd, ctr, ctrlen) + || gcry_cipher_encrypt (hd, data, d.quot * ctrlen, NULL, 0)) + return -1; + + if (d.rem) + { + /* Truncated last block */ + uint8_t dummy[ctrlen]; + data += d.quot * ctrlen; + memcpy (dummy, data, d.rem); + memset (dummy + d.rem, 0, ctrlen - d.rem); + + if (gcry_cipher_encrypt (hd, dummy, ctrlen, data, ctrlen)) + return -1; + memcpy (data, dummy, d.rem); + } + + return 0; +} + + +/** + * AES-CM key derivation (saltlen = 14 bytes) + */ +static int +do_derive (gcry_cipher_hd_t prf, const void *salt, + const uint8_t *r, size_t rlen, uint8_t label, + void *out, size_t outlen) +{ + uint8_t iv[16]; + + memcpy (iv, salt, 14); + iv[14] = iv[15] = 0; + + assert (rlen < 14); + iv[13 - rlen] ^= label; + for (size_t i = 0; i < rlen; i++) + iv[sizeof (iv) - rlen + i] ^= r[i]; + + memset (out, 0, outlen); + return do_ctr_crypt (prf, iv, out, outlen); +} + + +/** + * Sets (or resets) the master key and master salt for a SRTP session. + * This must be done at least once before using srtp_send(), srtp_recv(), + * srtcp_send() or srtcp_recv(). Also, rekeying is required every + * 2^48 RTP packets or 2^31 RTCP packets (whichever comes first), + * otherwise the protocol security might be broken. + * + * @return 0 on success, in case of error: + * EINVAL invalid or unsupported key/salt sizes combination + */ +int +srtp_setkey (srtp_session_t *s, const void *key, size_t keylen, + const void *salt, size_t saltlen) +{ + /* SRTP/SRTCP cipher/salt/MAC keys derivation */ + gcry_cipher_hd_t prf; + uint8_t r[6], keybuf[20]; + + if (saltlen != 14) + return EINVAL; + + if (gcry_cipher_open (&prf, GCRY_CIPHER_AES, GCRY_CIPHER_MODE_CTR, 0) + || gcry_cipher_setkey (prf, key, keylen)) + return EINVAL; + + /* SRTP key derivation */ +#if 0 + if (s->kdr != 0) + { + uint64_t index = (((uint64_t)s->rtp_roc) << 16) | s->rtp_seq; + index /= s->kdr; + + for (int i = sizeof (r) - 1; i >= 0; i--) + { + r[i] = index & 0xff; + index = index >> 8; + } + } + else +#endif + memset (r, 0, sizeof (r)); + if (do_derive (prf, salt, r, 6, SRTP_CRYPT, keybuf, 16) + || gcry_cipher_setkey (s->rtp.cipher, keybuf, 16) + || do_derive (prf, salt, r, 6, SRTP_AUTH, keybuf, 20) + || gcry_md_setkey (s->rtp.mac, keybuf, 20) + || do_derive (prf, salt, r, 6, SRTP_SALT, s->rtp.salt, 14)) + return -1; + + /* SRTCP key derivation */ + memcpy (r, &(uint32_t){ htonl (s->rtcp_index) }, 4); + if (do_derive (prf, salt, r, 4, SRTCP_CRYPT, keybuf, 16) + || gcry_cipher_setkey (s->rtcp.cipher, keybuf, 16) + || do_derive (prf, salt, r, 4, SRTCP_AUTH, keybuf, 20) + || gcry_md_setkey (s->rtcp.mac, keybuf, 20) + || do_derive (prf, salt, r, 4, SRTCP_SALT, s->rtcp.salt, 14)) + return -1; + + (void)gcry_cipher_close (prf); + return 0; +} + +static int hexdigit (char c) +{ + if ((c >= '0') && (c <= '9')) + return c - '0'; + if ((c >= 'A') && (c <= 'F')) + return c - 'A' + 0xA; + if ((c >= 'a') && (c <= 'f')) + return c - 'a' + 0xa; + return -1; +} + +static ssize_t hexstring (const char *in, uint8_t *out, size_t outlen) +{ + size_t inlen = strlen (in); + + if ((inlen > (2 * outlen)) || (inlen & 1)) + return -1; + + for (size_t i = 0; i < inlen; i += 2) + { + int a = hexdigit (in[i]), b = hexdigit (in[i + 1]); + if ((a == -1) || (b == -1)) + return -1; + out[i / 2] = (a << 4) | b; + } + return inlen / 2; +} + +/** + * Sets (or resets) the master key and master salt for a SRTP session + * from hexadecimal strings. See also srtp_setkey(). + * + * @return 0 on success, in case of error: + * EINVAL invalid or unsupported key/salt sizes combination + */ +int +srtp_setkeystring (srtp_session_t *s, const char *key, const char *salt) +{ + uint8_t bkey[16]; /* TODO/NOTE: hard-coded for AES */ + uint8_t bsalt[14]; /* TODO/NOTE: hard-coded for the PRF-AES-CM */ + ssize_t bkeylen = hexstring (key, bkey, sizeof (bkey)); + ssize_t bsaltlen = hexstring (salt, bsalt, sizeof (bsalt)); + + if ((bkeylen == -1) || (bsaltlen == -1)) + return EINVAL; + return srtp_setkey (s, bkey, bkeylen, bsalt, bsaltlen) ? EINVAL : 0; +} + +/** + * Sets Roll-over-Counter Carry (RCC) rate for the SRTP session. If not + * specified (through this function), the default rate of ONE is assumed + * (i.e. every RTP packets will carry the RoC). RCC rate is ignored if none + * of the RCC mode has been selected. + * + * The RCC mode is selected through one of these flags for srtp_create(): + * SRTP_RCC_MODE1: integrity protection only for RoC carrying packets + * SRTP_RCC_MODE2: integrity protection for all packets + * SRTP_RCC_MODE3: no integrity protection + * + * RCC mode 3 is insecure. Compared to plain RTP, it provides confidentiality + * (through encryption) but is much more prone to DoS. It can only be used if + * anti-spoofing protection is provided by lower network layers (e.g. IPsec, + * or trusted routers and proper source address filtering). + * + * If RCC rate is 1, RCC mode 1 and 2 are functionally identical. + * + * @param rate RoC Carry rate (MUST NOT be zero) + */ +void srtp_setrcc_rate (srtp_session_t *s, uint16_t rate) +{ + assert (rate != 0); + s->rtp_rcc = rate; +} + + +/** AES-CM for RTP (salt = 14 bytes + 2 nul bytes) */ +static int +rtp_crypt (gcry_cipher_hd_t hd, uint32_t ssrc, uint32_t roc, uint16_t seq, + const uint32_t *salt, uint8_t *data, size_t len) +{ + /* Determines cryptographic counter (IV) */ + uint32_t counter[4]; + counter[0] = salt[0]; + counter[1] = salt[1] ^ ssrc; + counter[2] = salt[2] ^ htonl (roc); + counter[3] = salt[3] ^ htonl (seq << 16); + + /* Encryption */ + return do_ctr_crypt (hd, counter, data, len); +} + + +/** Determines SRTP Roll-Over-Counter (in host-byte order) */ +static uint32_t +srtp_compute_roc (const srtp_session_t *s, uint16_t seq) +{ + uint32_t roc = s->rtp_roc; + + if (((seq - s->rtp_seq) & 0xffff) < 0x8000) + { + /* Sequence is ahead, good */ + if (seq < s->rtp_seq) + roc++; /* Sequence number wrap */ + } + else + { + /* Sequence is late, bad */ + if (seq > s->rtp_seq) + roc--; /* Wrap back */ + } + return roc; +} + + +/** Returns RTP sequence (in host-byte order) */ +static inline uint16_t rtp_seq (const uint8_t *buf) +{ + return (buf[2] << 8) | buf[3]; +} + + +/** Message Authentication and Integrity for RTP */ +static const uint8_t * +rtp_digest (gcry_md_hd_t md, const uint8_t *data, size_t len, + uint32_t roc) +{ + gcry_md_reset (md); + gcry_md_write (md, data, len); + gcry_md_write (md, &(uint32_t){ htonl (roc) }, 4); + return gcry_md_read (md, 0); +} + + +/** + * Encrypts/decrypts a RTP packet and updates SRTP context + * (CTR block cypher mode of operation has identical encryption and + * decryption function). + * + * @param buf RTP packet to be en-/decrypted + * @param len RTP packet length + * + * @return 0 on success, in case of error: + * EINVAL malformatted RTP packet + * EACCES replayed packet or out-of-window or sync lost + */ +static int srtp_crypt (srtp_session_t *s, uint8_t *buf, size_t len) +{ + assert (s != NULL); + assert (len >= 12u); + + if ((buf[0] >> 6) != 2) + return EINVAL; + + /* Computes encryption offset */ + uint16_t offset = 12; + offset += (buf[0] & 0xf) * 4; // skips CSRC + + if (buf[0] & 0x10) + { + uint16_t extlen; + + offset += 4; + if (len < offset) + return EINVAL; + + memcpy (&extlen, buf + offset - 2, 2); + offset += htons (extlen); // skips RTP extension header + } + + if (len < offset) + return EINVAL; + + /* Determines RTP 48-bits counter and SSRC */ + uint16_t seq = rtp_seq (buf); + uint32_t roc = srtp_compute_roc (s, seq), ssrc; + memcpy (&ssrc, buf + 8, 4); + + /* Updates ROC and sequence (it's safe now) */ + int16_t diff = seq - s->rtp_seq; + if (diff > 0) + { + /* Sequence in the future, good */ + s->rtp.window = s->rtp.window << diff; + s->rtp.window |= UINT64_C(1); + s->rtp_seq = seq, s->rtp_roc = roc; + } + else + { + /* Sequence in the past/present, bad */ + diff = -diff; + if ((diff >= 64) || ((s->rtp.window >> diff) & 1)) + return EACCES; /* Replay attack */ + s->rtp.window |= UINT64_C(1) << diff; + } + + /* Encrypt/Decrypt */ + if (s->flags & SRTP_UNENCRYPTED) + return 0; + + if (rtp_crypt (s->rtp.cipher, ssrc, roc, seq, s->rtp.salt, + buf + offset, len - offset)) + return EINVAL; + + return 0; +} + + +/** + * Turns a RTP packet into a SRTP packet: encrypt it, then computes + * the authentication tag and appends it. + * Note that you can encrypt packet in disorder. + * + * @param buf RTP packet to be encrypted/digested + * @param lenp pointer to the RTP packet length on entry, + * set to the SRTP length on exit (undefined on non-ENOSPC error) + * @param bufsize size (bytes) of the packet buffer + * + * @return 0 on success, in case of error: + * EINVAL malformatted RTP packet or internal error + * ENOSPC bufsize is too small to add authentication tag + * ( will hold the required byte size) + * EACCES packet would trigger a replay error on receiver + */ +int +srtp_send (srtp_session_t *s, uint8_t *buf, size_t *lenp, size_t bufsize) +{ + size_t len = *lenp; + size_t tag_len; + size_t roc_len = 0; + + /* Compute required buffer size */ + if (len < 12u) + return EINVAL; + + if (!(s->flags & SRTP_UNAUTHENTICATED)) + { + tag_len = s->tag_len; + + if (rcc_mode (s)) + { + assert (tag_len >= 4); + assert (s->rtp_rcc != 0); + if ((rtp_seq (buf) % s->rtp_rcc) == 0) + { + roc_len = 4; + if (rcc_mode (s) == 3) + tag_len = 0; /* RCC mode 3 -> no auth*/ + else + tag_len -= 4; /* RCC mode 1 or 2 -> auth*/ + } + else + { + if (rcc_mode (s) & 1) + tag_len = 0; /* RCC mode 1 or 3 -> no auth */ + } + } + + *lenp = len + roc_len + tag_len; + } + else + tag_len = 0; + + if (bufsize < *lenp) + return ENOSPC; + + /* Encrypt payload */ + int val = srtp_crypt (s, buf, len); + if (val) + return val; + + /* Authenticate payload */ + if (!(s->flags & SRTP_UNAUTHENTICATED)) + { + uint32_t roc = srtp_compute_roc (s, rtp_seq (buf)); + const uint8_t *tag = rtp_digest (s->rtp.mac, buf, len, roc); + + if (roc_len) + { + memcpy (buf + len, &(uint32_t){ htonl (s->rtp_roc) }, 4); + len += 4; + } + memcpy (buf + len, tag, tag_len); +#if 0 + printf ("Sent : 0x"); + for (unsigned i = 0; i < tag_len; i++) + printf ("%02x", tag[i]); + puts (""); +#endif + } + + return 0; +} + + +/** + * Turns a SRTP packet into a RTP packet: authenticates the packet, + * then decrypts it. + * + * @param buf RTP packet to be digested/decrypted + * @param lenp pointer to the SRTP packet length on entry, + * set to the RTP length on exit (undefined in case of error) + * + * @return 0 on success, in case of error: + * EINVAL malformatted SRTP packet + * EACCES authentication failed (spoofed packet or out-of-sync) + */ +int +srtp_recv (srtp_session_t *s, uint8_t *buf, size_t *lenp) +{ + size_t len = *lenp; + if (len < 12u) + return EINVAL; + + if (!(s->flags & SRTP_UNAUTHENTICATED)) + { + size_t tag_len = s->tag_len, roc_len = 0; + if (rcc_mode (s)) + { + if ((rtp_seq (buf) % s->rtp_rcc) == 0) + { + roc_len = 4; + if (rcc_mode (s) == 3) + tag_len = 0; + else + tag_len -= 4; + } + else + { + if (rcc_mode (s) & 1) + tag_len = 0; // RCC mode 1 or 3: no auth + } + } + + if (len < (12u + roc_len + tag_len)) + return EINVAL; + len -= roc_len + tag_len; + + uint32_t roc = srtp_compute_roc (s, rtp_seq (buf)), rcc; + if (roc_len) + { + assert (roc_len == 4); + memcpy (&rcc, buf + len, 4); + rcc = ntohl (rcc); + } + else + rcc = roc; + + const uint8_t *tag = rtp_digest (s->rtp.mac, buf, len, rcc); +#if 0 + printf ("Computed: 0x"); + for (unsigned i = 0; i < tag_len; i++) + printf ("%02x", tag[i]); + printf ("\nReceived: 0x"); + for (unsigned i = 0; i < tag_len; i++) + printf ("%02x", buf[len + roc_len + i]); + puts (""); +#endif + if (memcmp (buf + len + roc_len, tag, tag_len)) + return EACCES; + + if (roc_len) + { + /* Authenticated packet carried a Roll-Over-Counter */ + s->rtp_roc += rcc - roc; + assert (srtp_compute_roc (s, rtp_seq (buf)) == rcc); + } + *lenp = len; + } + + return srtp_crypt (s, buf, len); +} + + +/** AES-CM for RTCP (salt = 14 bytes + 2 nul bytes) */ +static int +rtcp_crypt (gcry_cipher_hd_t hd, uint32_t ssrc, uint32_t index, + const uint32_t *salt, uint8_t *data, size_t len) +{ + return rtp_crypt (hd, ssrc, index >> 16, index & 0xffff, salt, data, len); +} + + +/** Message Authentication and Integrity for RTCP */ +static const uint8_t * +rtcp_digest (gcry_md_hd_t md, const void *data, size_t len) +{ + gcry_md_reset (md); + gcry_md_write (md, data, len); + return gcry_md_read (md, 0); +} + + +/** + * Encrypts/decrypts a RTCP packet and updates SRTCP context + * (CTR block cypher mode of operation has identical encryption and + * decryption function). + * + * @param buf RTCP packet to be en-/decrypted + * @param len RTCP packet length + * + * @return 0 on success, in case of error: + * EINVAL malformatted RTCP packet + */ +static int srtcp_crypt (srtp_session_t *s, uint8_t *buf, size_t len) +{ + assert (s != NULL); + + /* 8-bytes unencrypted header, and 4-bytes unencrypted footer */ + if ((len < 12) || ((buf[0] >> 6) != 2)) + return EINVAL; + + uint32_t index; + memcpy (&index, buf + len, 4); + index = ntohl (index); + if (((index >> 31) != 0) != ((s->flags & SRTCP_UNENCRYPTED) == 0)) + return EINVAL; // E-bit mismatch + + index &= ~(1 << 31); // clear E-bit for counter + + /* Updates SRTCP index (safe here) */ + int32_t diff = index - s->rtcp_index; + if (diff > 0) + { + /* Packet in the future, good */ + s->rtcp.window = s->rtcp.window << diff; + s->rtcp.window |= UINT64_C(1); + s->rtcp_index = index; + } + else + { + /* Packet in the past/present, bad */ + diff = -diff; + if ((diff >= 64) || ((s->rtcp.window >> diff) & 1)) + return EACCES; // replay attack! + s->rtp.window |= UINT64_C(1) << diff; + } + + /* Crypts SRTCP */ + if (s->flags & SRTCP_UNENCRYPTED) + return 0; + + uint32_t ssrc; + memcpy (&ssrc, buf + 4, 4); + + if (rtcp_crypt (s->rtcp.cipher, ssrc, index, s->rtp.salt, + buf + 8, len - 8)) + return EINVAL; + return 0; +} + + +/** + * Turns a RTCP packet into a SRTCP packet: encrypt it, then computes + * the authentication tag and appends it. + * + * @param buf RTCP packet to be encrypted/digested + * @param lenp pointer to the RTCP packet length on entry, + * set to the SRTCP length on exit (undefined in case of error) + * @param bufsize size (bytes) of the packet buffer + * + * @return 0 on success, in case of error: + * EINVAL malformatted RTCP packet or internal error + * ENOSPC bufsize is too small (to add index and authentication tag) + */ +int +srtcp_send (srtp_session_t *s, uint8_t *buf, size_t *lenp, size_t bufsize) +{ + size_t len = *lenp; + if (bufsize < (len + 4 + s->tag_len)) + return ENOSPC; + + uint32_t index = ++s->rtcp_index; + if (index >> 31) + s->rtcp_index = index = 0; /* 31-bit wrap */ + + if ((s->flags & SRTCP_UNENCRYPTED) == 0) + index |= 0x80000000; /* Set Encrypted bit */ + memcpy (buf + len, &(uint32_t){ htonl (index) }, 4); + + int val = srtcp_crypt (s, buf, len); + if (val) + return val; + + len += 4; /* Digests SRTCP index too */ + + const uint8_t *tag = rtcp_digest (s->rtcp.mac, buf, len); + memcpy (buf + len, tag, s->tag_len); + *lenp = len + s->tag_len; + return 0; +} + + +/** + * Turns a SRTCP packet into a RTCP packet: authenticates the packet, + * then decrypts it. + * + * @param buf RTCP packet to be digested/decrypted + * @param lenp pointer to the SRTCP packet length on entry, + * set to the RTCP length on exit (undefined in case of error) + * + * @return 0 on success, in case of error: + * EINVAL malformatted SRTCP packet + * EACCES authentication failed (spoofed packet or out-of-sync) + */ +int +srtcp_recv (srtp_session_t *s, uint8_t *buf, size_t *lenp) +{ + size_t len = *lenp; + + if (len < (4u + s->tag_len)) + return EINVAL; + len -= s->tag_len; + + const uint8_t *tag = rtcp_digest (s->rtcp.mac, buf, len); + if (memcmp (buf + len, tag, s->tag_len)) + return EACCES; + + len -= 4; /* Remove SRTCP index before decryption */ + *lenp = len; + return srtcp_crypt (s, buf, len); +} \ No newline at end of file diff --git a/plugins/rtp/src/srtp.h b/plugins/rtp/src/srtp.h new file mode 100644 index 00000000..abca6988 --- /dev/null +++ b/plugins/rtp/src/srtp.h @@ -0,0 +1,82 @@ +/* + * Secure RTP with libgcrypt + * Copyright (C) 2007 Rémi Denis-Courmont + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 + * of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + ****************************************************************************/ + +#ifndef LIBVLC_SRTP_H +# define LIBVLC_SRTP_H 1 +#include + +typedef struct srtp_session_t srtp_session_t; + +enum +{ + SRTP_UNENCRYPTED=0x1, //< do not encrypt SRTP packets + SRTCP_UNENCRYPTED=0x2, //< do not encrypt SRTCP packets + SRTP_UNAUTHENTICATED=0x4, //< authenticate only SRTCP packets + + SRTP_RCC_MODE1=0x10, //< use Roll-over-Counter Carry mode 1 + SRTP_RCC_MODE2=0x20, //< use Roll-over-Counter Carry mode 2 + SRTP_RCC_MODE3=0x30, //< use Roll-over-Counter Carry mode 3 (insecure) + + SRTP_FLAGS_MASK=0x37 //< mask for valid flags +}; + +/** SRTP encryption algorithms (ciphers); same values as MIKEY */ +enum +{ + SRTP_ENCR_NULL=0, //< no encryption + SRTP_ENCR_AES_CM=1, //< AES counter mode + SRTP_ENCR_AES_F8=2, //< AES F8 mode (not implemented) +}; + +/** SRTP authenticaton algorithms; same values as MIKEY */ +enum +{ + SRTP_AUTH_NULL=0, //< no authentication code + SRTP_AUTH_HMAC_SHA1=1, //< HMAC-SHA1 +}; + +/** SRTP pseudo random function; same values as MIKEY */ +enum +{ + SRTP_PRF_AES_CM=0, //< AES counter mode +}; + +# ifdef __cplusplus +extern "C" { +# endif + +srtp_session_t *srtp_create (int encr, int auth, unsigned tag_len, int prf, + unsigned flags); +void srtp_destroy (srtp_session_t *s); + +int srtp_setkey (srtp_session_t *s, const void *key, size_t keylen, + const void *salt, size_t saltlen); +int srtp_setkeystring (srtp_session_t *s, const char *key, const char *salt); + +void srtp_setrcc_rate (srtp_session_t *s, uint16_t rate); + +int srtp_send (srtp_session_t *s, uint8_t *buf, size_t *lenp, size_t maxsize); +int srtp_recv (srtp_session_t *s, uint8_t *buf, size_t *lenp); +int srtcp_send (srtp_session_t *s, uint8_t *buf, size_t *lenp, size_t maxsiz); +int srtcp_recv (srtp_session_t *s, uint8_t *buf, size_t *lenp); + +# ifdef __cplusplus +} +# endif +#endif \ No newline at end of file diff --git a/plugins/rtp/src/srtp.vapi b/plugins/rtp/src/srtp.vapi new file mode 100644 index 00000000..c5ce7fec --- /dev/null +++ b/plugins/rtp/src/srtp.vapi @@ -0,0 +1,103 @@ +[Compact] +[CCode (cname = "srtp_session_t", free_function = "srtp_destroy", cheader_filename="srtp.h")] +public class Dino.Plugins.Rtp.SrtpSession { + [CCode (cname = "srtp_create")] + public SrtpSession(SrtpEncryption encr, SrtpAuthentication auth, uint tag_len, SrtpPrf prf, SrtpFlags flags); + [CCode (cname = "srtp_setkey")] + public int setkey(uint8[] key, uint8[] salt); + [CCode (cname = "srtp_setkeystring")] + public int setkeystring(string key, string salt); + [CCode (cname = "srtp_setrcc_rate")] + public void setrcc_rate(uint16 rate); + + [CCode (cname = "srtp_send")] + private int rtp_send([CCode (array_length = false)] uint8[] buf, ref size_t len, size_t maxsize); + [CCode (cname = "srtcp_send")] + private int rtcp_send([CCode (array_length = false)] uint8[] buf, ref size_t len, size_t maxsize); + [CCode (cname = "srtp_recv")] + private int rtp_recv([CCode (array_length = false)] uint8[] buf, ref size_t len); + [CCode (cname = "srtcp_recv")] + private int rtcp_recv([CCode (array_length = false)] uint8[] buf, ref size_t len); + + public uint8[] encrypt_rtp(uint8[] input, uint tag_len = 10) throws GLib.Error { + uint8[] buf = new uint8[input.length+tag_len]; + GLib.Memory.copy(buf, input, input.length); + size_t buf_use = input.length; + int res = rtp_send(buf, ref buf_use, buf.length); + if (res != 0) { + throw new GLib.Error(-1, res, "RTP encrypt failed"); + } + uint8[] ret = new uint8[buf_use]; + GLib.Memory.copy(ret, buf, buf_use); + return ret; + } + + public uint8[] encrypt_rtcp(uint8[] input, uint tag_len = 10) throws GLib.Error { + uint8[] buf = new uint8[input.length+tag_len+4]; + GLib.Memory.copy(buf, input, input.length); + size_t buf_use = input.length; + int res = rtcp_send(buf, ref buf_use, buf.length); + if (res != 0) { + throw new GLib.Error(-1, res, "RTCP encrypt failed"); + } + uint8[] ret = new uint8[buf_use]; + GLib.Memory.copy(ret, buf, buf_use); + return ret; + } + + public uint8[] decrypt_rtp(uint8[] input) throws GLib.Error { + uint8[] buf = new uint8[input.length]; + GLib.Memory.copy(buf, input, input.length); + size_t buf_use = input.length; + int res = rtp_recv(buf, ref buf_use); + if (res != 0) { + throw new GLib.Error(-1, res, "RTP decrypt failed"); + } + uint8[] ret = new uint8[buf_use]; + GLib.Memory.copy(ret, buf, buf_use); + return ret; + } + + public uint8[] decrypt_rtcp(uint8[] input) throws GLib.Error { + uint8[] buf = new uint8[input.length]; + GLib.Memory.copy(buf, input, input.length); + size_t buf_use = input.length; + int res = rtcp_recv(buf, ref buf_use); + if (res != 0) { + throw new GLib.Error(-1, res, "RTCP decrypt failed"); + } + uint8[] ret = new uint8[buf_use]; + GLib.Memory.copy(ret, buf, buf_use); + return ret; + } +} + +[Flags] +[CCode (cname = "unsigned", cprefix = "", cheader_filename="srtp.h", has_type_id = false)] +public enum Dino.Plugins.Rtp.SrtpFlags { + SRTP_UNENCRYPTED, + SRTCP_UNENCRYPTED, + SRTP_UNAUTHENTICATED, + + SRTP_RCC_MODE1, + SRTP_RCC_MODE2, + SRTP_RCC_MODE3 +} + +[CCode (cname = "int", cprefix = "SRTP_ENCR_", cheader_filename="srtp.h", has_type_id = false)] +public enum Dino.Plugins.Rtp.SrtpEncryption { + NULL, + AES_CM, + AES_F8 +} + +[CCode (cname = "int", cprefix = "SRTP_AUTH_", cheader_filename="srtp.h", has_type_id = false)] +public enum Dino.Plugins.Rtp.SrtpAuthentication { + NULL, + HMAC_SHA1 +} + +[CCode (cname = "int", cprefix = "SRTP_PRF_", cheader_filename="srtp.h", has_type_id = false)] +public enum Dino.Plugins.Rtp.SrtpPrf { + AES_CM +} \ No newline at end of file diff --git a/plugins/rtp/src/stream.vala b/plugins/rtp/src/stream.vala index aea2fe85..362e2d16 100644 --- a/plugins/rtp/src/stream.vala +++ b/plugins/rtp/src/stream.vala @@ -53,6 +53,9 @@ public class Dino.Plugins.Rtp.Stream : Xmpp.Xep.JingleRtp.Stream { private Gst.Pad send_rtp_sink_pad; private Gst.Pad send_rtp_src_pad; + private SrtpSession? local_crypto_session; + private SrtpSession? remote_crypto_session; + public Stream(Plugin plugin, Xmpp.Xep.Jingle.Content content) { base(content); this.plugin = plugin; @@ -144,6 +147,20 @@ public class Dino.Plugins.Rtp.Stream : Xmpp.Xep.JingleRtp.Stream { plugin.unpause(); } + private void prepare_local_crypto() { + if (local_crypto != null && local_crypto_session == null) { + local_crypto_session = new SrtpSession( + local_crypto.crypto_suite == Xep.JingleRtp.Crypto.F8_128_HMAC_SHA1_80 ? SrtpEncryption.AES_F8 : SrtpEncryption.AES_CM, + SrtpAuthentication.HMAC_SHA1, + local_crypto.crypto_suite == Xep.JingleRtp.Crypto.AES_CM_128_HMAC_SHA1_32 ? 4 : 10, + SrtpPrf.AES_CM, + 0 + ); + local_crypto_session.setkey(local_crypto.key, local_crypto.salt); + debug("Setting up encryption with key params %s", local_crypto.key_params); + } + } + private Gst.FlowReturn on_new_sample(Gst.App.Sink sink) { if (sink == null) { debug("Sink is null"); @@ -153,9 +170,16 @@ public class Dino.Plugins.Rtp.Stream : Xmpp.Xep.JingleRtp.Stream { Gst.Buffer buffer = sample.get_buffer(); uint8[] data; buffer.extract_dup(0, buffer.get_size(), out data); + prepare_local_crypto(); if (sink == send_rtp) { + if (local_crypto_session != null) { + data = local_crypto_session.encrypt_rtp(data, local_crypto.crypto_suite == Xep.JingleRtp.Crypto.AES_CM_128_HMAC_SHA1_32 ? 4 : 10); + } on_send_rtp_data(new Bytes.take(data)); } else if (sink == send_rtcp) { + if (local_crypto_session != null) { + data = local_crypto_session.encrypt_rtcp(data, local_crypto.crypto_suite == Xep.JingleRtp.Crypto.AES_CM_128_HMAC_SHA1_32 ? 4 : 10); + } on_send_rtcp_data(new Bytes.take(data)); } else { warning("unknown sample"); @@ -258,15 +282,47 @@ public class Dino.Plugins.Rtp.Stream : Xmpp.Xep.JingleRtp.Stream { recv_rtp_src_pad = null; } + private void prepare_remote_crypto() { + if (remote_crypto != null && remote_crypto_session == null) { + remote_crypto_session = new SrtpSession( + remote_crypto.crypto_suite == Xep.JingleRtp.Crypto.F8_128_HMAC_SHA1_80 ? SrtpEncryption.AES_F8 : SrtpEncryption.AES_CM, + SrtpAuthentication.HMAC_SHA1, + remote_crypto.crypto_suite == Xep.JingleRtp.Crypto.AES_CM_128_HMAC_SHA1_32 ? 4 : 10, + SrtpPrf.AES_CM, + 0 + ); + remote_crypto_session.setkey(remote_crypto.key, remote_crypto.salt); + debug("Setting up decryption with key params %s", remote_crypto.key_params); + } + } + public override void on_recv_rtp_data(Bytes bytes) { + prepare_remote_crypto(); + uint8[] data = bytes.get_data(); + if (remote_crypto_session != null) { + try { + data = remote_crypto_session.decrypt_rtp(data); + } catch (Error e) { + warning("%s (%d)", e.message, e.code); + } + } if (push_recv_data) { - recv_rtp.push_buffer(new Gst.Buffer.wrapped_bytes(bytes)); + recv_rtp.push_buffer(new Gst.Buffer.wrapped((owned) data)); } } public override void on_recv_rtcp_data(Bytes bytes) { + prepare_remote_crypto(); + uint8[] data = bytes.get_data(); + if (remote_crypto_session != null) { + try { + data = remote_crypto_session.decrypt_rtcp(data); + } catch (Error e) { + warning("%s (%d)", e.message, e.code); + } + } if (push_recv_data) { - recv_rtcp.push_buffer(new Gst.Buffer.wrapped_bytes(bytes)); + recv_rtcp.push_buffer(new Gst.Buffer.wrapped((owned) data)); } } diff --git a/xmpp-vala/src/module/xep/0167_jingle_rtp/content_parameters.vala b/xmpp-vala/src/module/xep/0167_jingle_rtp/content_parameters.vala index 8a3668b2..cca03543 100644 --- a/xmpp-vala/src/module/xep/0167_jingle_rtp/content_parameters.vala +++ b/xmpp-vala/src/module/xep/0167_jingle_rtp/content_parameters.vala @@ -17,7 +17,9 @@ public class Xmpp.Xep.JingleRtp.Parameters : Jingle.ContentParameters, Object { public bool encryption_required { get; private set; default = false; } public PayloadType? agreed_payload_type { get; private set; } public Gee.List payload_types = new ArrayList(PayloadType.equals_func); - public Gee.List cryptos = new ArrayList(); + public Gee.List remote_cryptos = new ArrayList(); + public Crypto? local_crypto = null; + public Crypto? remote_crypto = null; public weak Stream? stream { get; private set; } @@ -27,7 +29,7 @@ public class Xmpp.Xep.JingleRtp.Parameters : Jingle.ContentParameters, Object { string media, Gee.List payload_types, string? ssrc = null, bool rtcp_mux = false, string? bandwidth = null, string? bandwidth_type = null, - bool encryption_required = false, Gee.List cryptos = new ArrayList() + bool encryption_required = false, Crypto? local_crypto = null ) { this.parent = parent; this.media = media; @@ -37,7 +39,7 @@ public class Xmpp.Xep.JingleRtp.Parameters : Jingle.ContentParameters, Object { this.bandwidth_type = bandwidth_type; this.encryption_required = encryption_required; this.payload_types = payload_types; - this.cryptos = cryptos; + this.local_crypto = local_crypto; } public Parameters.from_node(Module parent, StanzaNode node) throws Jingle.IqError { @@ -49,7 +51,7 @@ public class Xmpp.Xep.JingleRtp.Parameters : Jingle.ContentParameters, Object { if (encryption != null) { this.encryption_required = encryption.get_attribute_bool("required", this.encryption_required); foreach (StanzaNode crypto in encryption.get_subnodes("crypto")) { - this.cryptos.add(Crypto.parse(crypto)); + this.remote_cryptos.add(Crypto.parse(crypto)); } } foreach (StanzaNode payloadType in node.get_subnodes("payload-type")) { @@ -64,6 +66,15 @@ public class Xmpp.Xep.JingleRtp.Parameters : Jingle.ContentParameters, Object { content.reject(); return; } + remote_crypto = parent.pick_remote_crypto(remote_cryptos); + if (local_crypto == null && remote_crypto != null) { + local_crypto = parent.pick_local_crypto(remote_crypto); + } + if ((local_crypto == null || remote_crypto == null) && encryption_required) { + debug("no usable encryption, but encryption required"); + content.reject(); + return; + } } public void accept(XmppStream stream, Jingle.Session session, Jingle.Content content) { @@ -97,6 +108,15 @@ public class Xmpp.Xep.JingleRtp.Parameters : Jingle.ContentParameters, Object { } }); + if (remote_crypto == null || local_crypto == null) { + if (encryption_required) { + warning("Encryption required but not provided in both directions"); + return; + } + remote_crypto = null; + local_crypto = null; + } + this.stream = parent.create_stream(content); rtp_datagram.datagram_received.connect(this.stream.on_recv_rtp_data); rtcp_datagram.datagram_received.connect(this.stream.on_recv_rtcp_data); @@ -118,6 +138,20 @@ public class Xmpp.Xep.JingleRtp.Parameters : Jingle.ContentParameters, Object { } agreed_payload_type = preferred_payload_type; + Gee.List crypto_nodes = description_node.get_deep_subnodes("encryption", "crypto"); + if (crypto_nodes.size == 0) { + warning("Counterpart didn't include any cryptos"); + if (encryption_required) { + return; + } + } else { + Crypto preferred_crypto = Crypto.parse(crypto_nodes[0]); + if (local_crypto.crypto_suite != preferred_crypto.crypto_suite) { + warning("Counterpart's crypto suite doesn't match any of our sent ones"); + } + remote_crypto = preferred_crypto; + } + accept(stream, session, content); } @@ -137,6 +171,10 @@ public class Xmpp.Xep.JingleRtp.Parameters : Jingle.ContentParameters, Object { ret.put_node(payload_type.to_xml()); } } + if (local_crypto != null) { + ret.put_node(new StanzaNode.build("encryption", NS_URI) + .put_node(local_crypto.to_xml())); + } return ret; } } \ No newline at end of file diff --git a/xmpp-vala/src/module/xep/0167_jingle_rtp/jingle_rtp_module.vala b/xmpp-vala/src/module/xep/0167_jingle_rtp/jingle_rtp_module.vala index 35e03168..23aee6c9 100644 --- a/xmpp-vala/src/module/xep/0167_jingle_rtp/jingle_rtp_module.vala +++ b/xmpp-vala/src/module/xep/0167_jingle_rtp/jingle_rtp_module.vala @@ -20,6 +20,9 @@ public abstract class Module : XmppStreamModule { public abstract async Gee.List get_supported_payloads(string media); public abstract async PayloadType? pick_payload_type(string media, Gee.List payloads); + public abstract Crypto? generate_local_crypto(); + public abstract Crypto? pick_remote_crypto(Gee.List cryptos); + public abstract Crypto? pick_local_crypto(Crypto? remote); public abstract Stream create_stream(Jingle.Content content); public abstract void close_stream(Stream stream); @@ -36,6 +39,7 @@ public abstract class Module : XmppStreamModule { // Create audio content Parameters audio_content_parameters = new Parameters(this, "audio", yield get_supported_payloads("audio")); + audio_content_parameters.local_crypto = generate_local_crypto(); Jingle.Transport? audio_transport = yield jingle_module.select_transport(stream, content_type.required_transport_type, content_type.required_components, receiver_full_jid, Set.empty()); if (audio_transport == null) { throw new Jingle.Error.NO_SHARED_PROTOCOLS("No suitable audio transports"); @@ -52,6 +56,7 @@ public abstract class Module : XmppStreamModule { if (video) { // Create video content Parameters video_content_parameters = new Parameters(this, "video", yield get_supported_payloads("video")); + video_content_parameters.local_crypto = generate_local_crypto(); Jingle.Transport? video_transport = yield stream.get_module(Jingle.Module.IDENTITY).select_transport(stream, content_type.required_transport_type, content_type.required_components, receiver_full_jid, Set.empty()); if (video_transport == null) { throw new Jingle.Error.NO_SHARED_PROTOCOLS("No suitable video transports"); @@ -92,6 +97,7 @@ public abstract class Module : XmppStreamModule { if (content == null) { // Content for video does not yet exist -> create it Parameters video_content_parameters = new Parameters(this, "video", yield get_supported_payloads("video")); + video_content_parameters.local_crypto = generate_local_crypto(); Jingle.Transport? video_transport = yield stream.get_module(Jingle.Module.IDENTITY).select_transport(stream, content_type.required_transport_type, content_type.required_components, receiver_full_jid, Set.empty()); if (video_transport == null) { throw new Jingle.Error.NO_SHARED_PROTOCOLS("No suitable video transports"); @@ -148,26 +154,130 @@ public abstract class Module : XmppStreamModule { } public class Crypto { - public string cryptoSuite { get; private set; } - public string keyParams { get; private set; } - public string? sessionParams { get; private set; } - public string? tag { get; private set; } + public const string AES_CM_128_HMAC_SHA1_80 = "AES_CM_128_HMAC_SHA1_80"; + public const string AES_CM_128_HMAC_SHA1_32 = "AES_CM_128_HMAC_SHA1_32"; + public const string F8_128_HMAC_SHA1_80 = "F8_128_HMAC_SHA1_80"; + + public string crypto_suite { get; private set; } + public string key_params { get; private set; } + public string? session_params { get; private set; } + public string tag { get; private set; } + + public uint8[] key_and_salt { owned get { + if (!key_params.has_prefix("inline:")) return null; + int endIndex = key_params.index_of("|"); + if (endIndex < 0) endIndex = key_params.length; + string sub = key_params.substring(7, endIndex - 7); + return Base64.decode(sub); + }} + + public string? lifetime { owned get { + if (!key_params.has_prefix("inline:")) return null; + int firstIndex = key_params.index_of("|"); + if (firstIndex < 0) return null; + int endIndex = key_params.index_of("|", firstIndex + 1); + if (endIndex < 0) { + if (key_params.index_of(":", firstIndex) > 0) return null; // Is MKI + endIndex = key_params.length; + } + return key_params.substring(firstIndex + 1, endIndex); + }} + + public int mki { get { + if (!key_params.has_prefix("inline:")) return -1; + int firstIndex = key_params.index_of("|"); + if (firstIndex < 0) return -1; + int splitIndex = key_params.index_of(":", firstIndex); + if (splitIndex < 0) return -1; + int secondIndex = key_params.index_of("|", firstIndex + 1); + if (secondIndex < 0) { + return int.parse(key_params.substring(firstIndex + 1, splitIndex)); + } else if (splitIndex > secondIndex) { + return int.parse(key_params.substring(secondIndex + 1, splitIndex)); + } + return -1; + }} + + public int mki_length { get { + if (!key_params.has_prefix("inline:")) return -1; + int firstIndex = key_params.index_of("|"); + if (firstIndex < 0) return -1; + int splitIndex = key_params.index_of(":", firstIndex); + if (splitIndex < 0) return -1; + int secondIndex = key_params.index_of("|", firstIndex + 1); + if (secondIndex < 0 || splitIndex > secondIndex) { + return int.parse(key_params.substring(splitIndex + 1, key_params.length)); + } + return -1; + }} + + public bool is_valid { get { + switch(crypto_suite) { + case AES_CM_128_HMAC_SHA1_80: + case AES_CM_128_HMAC_SHA1_32: + case F8_128_HMAC_SHA1_80: + return key_and_salt.length == 30; + } + return false; + }} + + public uint8[] key { owned get { + uint8[] key_and_salt = key_and_salt; + switch(crypto_suite) { + case AES_CM_128_HMAC_SHA1_80: + case AES_CM_128_HMAC_SHA1_32: + case F8_128_HMAC_SHA1_80: + if (key_and_salt.length >= 16) return key_and_salt[0:16]; + break; + } + return null; + }} + + public uint8[] salt { owned get { + uint8[] keyAndSalt = key_and_salt; + switch(crypto_suite) { + case AES_CM_128_HMAC_SHA1_80: + case AES_CM_128_HMAC_SHA1_32: + case F8_128_HMAC_SHA1_80: + if (keyAndSalt.length >= 30) return keyAndSalt[16:30]; + break; + } + return null; + }} + + public static Crypto create(string crypto_suite, uint8[] key_and_salt, string? session_params = null, string tag = "1") { + Crypto crypto = new Crypto(); + crypto.crypto_suite = crypto_suite; + crypto.key_params = "inline:" + Base64.encode(key_and_salt); + crypto.session_params = session_params; + crypto.tag = tag; + return crypto; + } + + public Crypto rekey(uint8[] key_and_salt) { + Crypto crypto = new Crypto(); + crypto.crypto_suite = crypto_suite; + crypto.key_params = "inline:" + Base64.encode(key_and_salt); + crypto.session_params = session_params; + crypto.tag = tag; + return crypto; + } public static Crypto parse(StanzaNode node) { Crypto crypto = new Crypto(); - crypto.cryptoSuite = node.get_attribute("crypto-suite"); - crypto.keyParams = node.get_attribute("key-params"); - crypto.sessionParams = node.get_attribute("session-params"); + crypto.crypto_suite = node.get_attribute("crypto-suite"); + crypto.key_params = node.get_attribute("key-params"); + crypto.session_params = node.get_attribute("session-params"); crypto.tag = node.get_attribute("tag"); return crypto; } public StanzaNode to_xml() { StanzaNode node = new StanzaNode.build("crypto", NS_URI) - .put_attribute("crypto-suite", cryptoSuite) - .put_attribute("key-params", keyParams); - if (sessionParams != null) node.put_attribute("session-params", sessionParams); - if (tag != null) node.put_attribute("tag", tag); + .put_attribute("crypto-suite", crypto_suite) + .put_attribute("key-params", key_params) + .put_attribute("tag", tag); + if (session_params != null) node.put_attribute("session-params", session_params); return node; } } diff --git a/xmpp-vala/src/module/xep/0167_jingle_rtp/stream.vala b/xmpp-vala/src/module/xep/0167_jingle_rtp/stream.vala index 62d85dec..2fc29291 100644 --- a/xmpp-vala/src/module/xep/0167_jingle_rtp/stream.vala +++ b/xmpp-vala/src/module/xep/0167_jingle_rtp/stream.vala @@ -17,6 +17,20 @@ public abstract class Xmpp.Xep.JingleRtp.Stream : Object { } return null; }} + public JingleRtp.Crypto? local_crypto { get { + var content_params = content.content_params; + if (content_params is Parameters) { + return ((Parameters)content_params).local_crypto; + } + return null; + }} + public JingleRtp.Crypto? remote_crypto { get { + var content_params = content.content_params; + if (content_params is Parameters) { + return ((Parameters)content_params).remote_crypto; + } + return null; + }} public bool sending { get { return content.session.senders_include_us(content.senders); }} -- cgit v1.2.3-54-g00ecf From ec35f95e13f4f2f756c81a35ded0980245acc5f4 Mon Sep 17 00:00:00 2001 From: fiaxh Date: Wed, 24 Mar 2021 14:12:42 +0100 Subject: Add initial support for DTLS-SRTP --- cmake/FindGnuTLS.cmake | 13 + libdino/src/service/calls.vala | 22 +- plugins/ice/CMakeLists.txt | 7 +- plugins/ice/src/dtls_srtp.vala | 247 ++++++++++++ plugins/ice/src/transport_parameters.vala | 48 ++- plugins/ice/vapi/gnutls.vapi | 419 +++++++++++++++++++++ plugins/rtp/CMakeLists.txt | 1 + .../src/module/xep/0166_jingle/reason_element.vala | 1 + xmpp-vala/src/module/xep/0166_jingle/session.vala | 38 +- .../xep/0167_jingle_rtp/content_parameters.vala | 5 +- .../xep/0167_jingle_rtp/jingle_rtp_module.vala | 2 +- .../xep/0167_jingle_rtp/session_info_type.vala | 2 +- .../0176_jingle_ice_udp/jingle_ice_udp_module.vala | 1 + .../0176_jingle_ice_udp/transport_parameters.vala | 27 ++ 14 files changed, 791 insertions(+), 42 deletions(-) create mode 100644 cmake/FindGnuTLS.cmake create mode 100644 plugins/ice/src/dtls_srtp.vala create mode 100644 plugins/ice/vapi/gnutls.vapi (limited to 'xmpp-vala/src/module/xep/0167_jingle_rtp') diff --git a/cmake/FindGnuTLS.cmake b/cmake/FindGnuTLS.cmake new file mode 100644 index 00000000..6b27abd7 --- /dev/null +++ b/cmake/FindGnuTLS.cmake @@ -0,0 +1,13 @@ +include(PkgConfigWithFallback) +find_pkg_config_with_fallback(GnuTLS + PKG_CONFIG_NAME gnutls + LIB_NAMES gnutls + INCLUDE_NAMES gnutls/gnutls.h + INCLUDE_DIR_SUFFIXES gnutls gnutls/include + DEPENDS GLib +) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(GnuTLS + REQUIRED_VARS GnuTLS_LIBRARY + VERSION_VAR GnuTLS_VERSION) \ No newline at end of file diff --git a/libdino/src/service/calls.vala b/libdino/src/service/calls.vala index 5224bdd1..54c353b0 100644 --- a/libdino/src/service/calls.vala +++ b/libdino/src/service/calls.vala @@ -125,7 +125,7 @@ namespace Dino { call.state = Call.State.ESTABLISHING; if (sessions.has_key(call)) { - foreach (Xep.Jingle.Content content in sessions[call].contents.values) { + foreach (Xep.Jingle.Content content in sessions[call].contents) { content.accept(); } } else { @@ -146,7 +146,7 @@ namespace Dino { call.state = Call.State.DECLINED; if (sessions.has_key(call)) { - foreach (Xep.Jingle.Content content in sessions[call].contents.values) { + foreach (Xep.Jingle.Content content in sessions[call].contents) { content.reject(); } remove_call_from_datastructures(call); @@ -223,16 +223,6 @@ namespace Dino { foreach (Jid full_jid in full_jids) { bool supports_rtc = yield stream.get_module(Xep.JingleRtp.Module.IDENTITY).is_available(stream, full_jid); if (!supports_rtc) continue; - - // dtls support indicates webRTC support. Clients tend to not do normal ice udp in that case. Except Dino. - bool supports_dtls = yield stream_interactor.get_module(EntityInfo.IDENTITY).has_feature(conversation.account, full_jid, "urn:xmpp:jingle:apps:dtls:0"); - if (supports_dtls) { - Xep.ServiceDiscovery.Identity? identity = yield stream_interactor.get_module(EntityInfo.IDENTITY).get_identity(conversation.account, full_jid); - bool is_dino = identity != null && identity.name == "Dino"; - - if (!is_dino) continue; - } - ret.add(full_jid); } return ret; @@ -253,7 +243,7 @@ namespace Dino { private void on_incoming_call(Account account, Xep.Jingle.Session session) { bool counterpart_wants_video = false; - foreach (Xep.Jingle.Content content in session.contents.values) { + foreach (Xep.Jingle.Content content in session.contents) { Xep.JingleRtp.Parameters? rtp_content_parameter = content.content_params as Xep.JingleRtp.Parameters; if (rtp_content_parameter == null) continue; if (rtp_content_parameter.media == "video" && session.senders_include_us(content.senders)) { @@ -391,7 +381,7 @@ namespace Dino { on_incoming_content_add(stream, call, session, content) ); - foreach (Xep.Jingle.Content content in session.contents.values) { + foreach (Xep.Jingle.Content content in session.contents) { Xep.JingleRtp.Parameters? rtp_content_parameter = content.content_params as Xep.JingleRtp.Parameters; if (rtp_content_parameter == null) continue; @@ -446,7 +436,7 @@ namespace Dino { Xep.Jingle.Module jingle_module = stream_interactor.module_manager.get_module(account, Xep.Jingle.Module.IDENTITY); jingle_module.session_initiate_received.connect((stream, session) => { - foreach (Xep.Jingle.Content content in session.contents.values) { + foreach (Xep.Jingle.Content content in session.contents) { Xep.JingleRtp.Parameters? rtp_content_parameter = content.content_params as Xep.JingleRtp.Parameters; if (rtp_content_parameter != null) { on_incoming_call(account, session); @@ -460,7 +450,7 @@ namespace Dino { if (!call_by_sid[account].has_key(session.sid)) return; Call call = call_by_sid[account][session.sid]; - foreach (Xep.Jingle.Content content in session.contents.values) { + foreach (Xep.Jingle.Content content in session.contents) { if (name == null || content.content_name == name) { Xep.JingleRtp.Parameters? rtp_content_parameter = content.content_params as Xep.JingleRtp.Parameters; if (rtp_content_parameter != null) { diff --git a/plugins/ice/CMakeLists.txt b/plugins/ice/CMakeLists.txt index 90fe5b7d..38025aa0 100644 --- a/plugins/ice/CMakeLists.txt +++ b/plugins/ice/CMakeLists.txt @@ -2,6 +2,7 @@ find_packages(ICE_PACKAGES REQUIRED Gee GLib GModule + GnuTLS GObject GTK3 Nice @@ -9,8 +10,9 @@ find_packages(ICE_PACKAGES REQUIRED vala_precompile(ICE_VALA_C SOURCES - src/plugin.vala + src/dtls_srtp.vala src/module.vala + src/plugin.vala src/transport_parameters.vala src/util.vala src/register_plugin.vala @@ -18,6 +20,7 @@ CUSTOM_VAPIS ${CMAKE_BINARY_DIR}/exports/xmpp-vala.vapi ${CMAKE_BINARY_DIR}/exports/dino.vapi ${CMAKE_BINARY_DIR}/exports/qlite.vapi + ${CMAKE_BINARY_DIR}/exports/crypto.vapi PACKAGES ${ICE_PACKAGES} OPTIONS @@ -26,7 +29,7 @@ OPTIONS add_definitions(${VALA_CFLAGS} -DG_LOG_DOMAIN="ice") add_library(ice SHARED ${ICE_VALA_C}) -target_link_libraries(ice libdino ${ICE_PACKAGES}) +target_link_libraries(ice libdino crypto-vala ${ICE_PACKAGES}) set_target_properties(ice PROPERTIES PREFIX "") set_target_properties(ice PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/plugins/) diff --git a/plugins/ice/src/dtls_srtp.vala b/plugins/ice/src/dtls_srtp.vala new file mode 100644 index 00000000..a21c242b --- /dev/null +++ b/plugins/ice/src/dtls_srtp.vala @@ -0,0 +1,247 @@ +using GnuTLS; + +public class DtlsSrtp { + + public signal void send_data(uint8[] data); + + private X509.Certificate[] own_cert; + private X509.PrivateKey private_key; + private Cond buffer_cond = new Cond(); + private Mutex buffer_mutex = new Mutex(); + private Gee.LinkedList buffer_queue = new Gee.LinkedList(); + private uint pull_timeout = uint.MAX; + private string peer_fingerprint; + + private Crypto.Srtp.Session encrypt_session; + private Crypto.Srtp.Session decrypt_session; + + public static DtlsSrtp setup() throws GLib.Error { + var obj = new DtlsSrtp(); + obj.generate_credentials(); + return obj; + } + + internal string get_own_fingerprint(DigestAlgorithm digest_algo) { + return format_certificate(own_cert[0], digest_algo); + } + + public void set_peer_fingerprint(string fingerprint) { + this.peer_fingerprint = fingerprint; + } + + public uint8[] process_incoming_data(uint component_id, uint8[] data) { + if (decrypt_session != null) { + if (component_id == 1) return decrypt_session.decrypt_rtp(data); + if (component_id == 2) return decrypt_session.decrypt_rtcp(data); + } else if (component_id == 1) { + on_data_rec(data); + } + return null; + } + + public uint8[] process_outgoing_data(uint component_id, uint8[] data) { + if (encrypt_session != null) { + if (component_id == 1) return encrypt_session.encrypt_rtp(data); + if (component_id == 2) return encrypt_session.encrypt_rtcp(data); + } + return null; + } + + public void on_data_rec(owned uint8[] data) { + buffer_mutex.lock(); + buffer_queue.add(new Bytes.take(data)); + buffer_cond.signal(); + buffer_mutex.unlock(); + } + + private void generate_credentials() throws GLib.Error { + int err = 0; + + private_key = X509.PrivateKey.create(); + err = private_key.generate(PKAlgorithm.RSA, 2048); + throw_if_error(err); + + var start_time = new DateTime.now_local().add_days(1); + var end_time = start_time.add_days(2); + + X509.Certificate cert = X509.Certificate.create(); + cert.set_key(private_key); + cert.set_version(1); + cert.set_activation_time ((time_t) start_time.to_unix ()); + cert.set_expiration_time ((time_t) end_time.to_unix ()); + + uint32 serial = 1; + cert.set_serial(&serial, sizeof(uint32)); + + cert.sign(cert, private_key); + + own_cert = new X509.Certificate[] { (owned)cert }; + } + + public async void setup_dtls_connection(bool server) { + InitFlags server_or_client = server ? InitFlags.SERVER : InitFlags.CLIENT; + debug("Setting up DTLS connection. We're %s", server_or_client.to_string()); + + CertificateCredentials cert_cred = CertificateCredentials.create(); + int err = cert_cred.set_x509_key(own_cert, private_key); + throw_if_error(err); + + Session? session = Session.create(server_or_client | InitFlags.DATAGRAM); + session.enable_heartbeat(1); + session.set_srtp_profile_direct("SRTP_AES128_CM_HMAC_SHA1_80"); + session.set_credentials(GnuTLS.CredentialsType.CERTIFICATE, cert_cred); + session.server_set_request(CertificateRequest.REQUEST); + session.set_priority_from_string("NORMAL:!VERS-TLS-ALL:+VERS-DTLS-ALL:+CTYPE-CLI-X509"); + + session.set_transport_pointer(this); + session.set_pull_function(pull_function); + session.set_pull_timeout_function(pull_timeout_function); + session.set_push_function(push_function); + session.set_verify_function(verify_function); + + Thread thread = new Thread (null, () => { + DateTime maximum_time = new DateTime.now_utc().add_seconds(20); + do { + err = session.handshake(); + + DateTime current_time = new DateTime.now_utc(); + if (maximum_time.compare(current_time) < 0) { + warning("DTLS handshake timeouted"); + return -1; + } + } while (err < 0 && !((ErrorCode)err).is_fatal()); + Idle.add(setup_dtls_connection.callback); + return err; + }); + yield; + err = thread.join(); + + uint8[] km = new uint8[150]; + Datum? client_key, client_salt, server_key, server_salt; + session.get_srtp_keys(km, km.length, out client_key, out client_salt, out server_key, out server_salt); + if (client_key == null || client_salt == null || server_key == null || server_salt == null) { + warning("SRTP client/server key/salt null"); + } + + Crypto.Srtp.Session encrypt_session = new Crypto.Srtp.Session(Crypto.Srtp.Encryption.AES_CM, Crypto.Srtp.Authentication.HMAC_SHA1, 10, Crypto.Srtp.Prf.AES_CM, 0); + Crypto.Srtp.Session decrypt_session = new Crypto.Srtp.Session(Crypto.Srtp.Encryption.AES_CM, Crypto.Srtp.Authentication.HMAC_SHA1, 10, Crypto.Srtp.Prf.AES_CM, 0); + + if (server) { + encrypt_session.setkey(server_key.extract(), server_salt.extract()); + decrypt_session.setkey(client_key.extract(), client_salt.extract()); + } else { + encrypt_session.setkey(client_key.extract(), client_salt.extract()); + decrypt_session.setkey(server_key.extract(), server_salt.extract()); + } + + this.encrypt_session = (owned)encrypt_session; + this.decrypt_session = (owned)decrypt_session; + } + + private static ssize_t pull_function(void* transport_ptr, uint8[] buffer) { + DtlsSrtp self = transport_ptr as DtlsSrtp; + + self.buffer_mutex.lock(); + while (self.buffer_queue.size == 0) { + self.buffer_cond.wait(self.buffer_mutex); + } + owned Bytes data = self.buffer_queue.remove_at(0); + self.buffer_mutex.unlock(); + + uint8[] data_uint8 = Bytes.unref_to_data(data); + Memory.copy(buffer, data_uint8, data_uint8.length); + + // The callback should return 0 on connection termination, a positive number indicating the number of bytes received, and -1 on error. + return (ssize_t)data.length; + } + + private static int pull_timeout_function(void* transport_ptr, uint ms) { + DtlsSrtp self = transport_ptr as DtlsSrtp; + + DateTime current_time = new DateTime.now_utc(); + current_time.add_seconds(ms/1000); + int64 end_time = current_time.to_unix(); + + self.buffer_mutex.lock(); + while (self.buffer_queue.size == 0) { + self.buffer_cond.wait_until(self.buffer_mutex, end_time); + + DateTime new_current_time = new DateTime.now_utc(); + if (new_current_time.compare(current_time) > 0) { + break; + } + } + self.buffer_mutex.unlock(); + + // The callback should return 0 on timeout, a positive number if data can be received, and -1 on error. + return 1; + } + + private static ssize_t push_function(void* transport_ptr, uint8[] buffer) { + DtlsSrtp self = transport_ptr as DtlsSrtp; + self.send_data(buffer); + + // The callback should return a positive number indicating the bytes sent, and -1 on error. + return (ssize_t)buffer.length; + } + + private static int verify_function(Session session) { + DtlsSrtp self = session.get_transport_pointer() as DtlsSrtp; + try { + bool valid = self.verify_peer_cert(session); + if (!valid) { + warning("DTLS certificate invalid. Aborting handshake."); + return 1; + } + } catch (Error e) { + warning("Error during DTLS certificate validation: %s. Aborting handshake.", e.message); + return 1; + } + + // The callback function should return 0 for the handshake to continue or non-zero to terminate. + return 0; + } + + private bool verify_peer_cert(Session session) throws GLib.Error { + unowned Datum[] cert_datums = session.get_peer_certificates(); + if (cert_datums.length == 0) { + warning("No peer certs"); + return false; + } + if (cert_datums.length > 1) warning("More than one peer cert"); + + X509.Certificate peer_cert = X509.Certificate.create(); + peer_cert.import(ref cert_datums[0], CertificateFormat.DER); + + string peer_fp_str = format_certificate(peer_cert, DigestAlgorithm.SHA256); + 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); + return false; + } + + return true; + } + + private string format_certificate(X509.Certificate certificate, DigestAlgorithm digest_algo) { + uint8[] buf = new uint8[512]; + size_t buf_out_size = 512; + certificate.get_fingerprint(digest_algo, buf, ref buf_out_size); + + var sb = new StringBuilder(); + for (int i = 0; i < buf_out_size; i++) { + sb.append("%02x".printf(buf[i])); + if (i < buf_out_size - 1) { + sb.append(":"); + } + } + return sb.str; + } + + private uint8[] uint8_pt_to_a(uint8* data, uint size) { + uint8[size] ret = new uint8[size]; + for (int i = 0; i < size; i++) { + ret[i] = data[i]; + } + return ret; + } +} \ No newline at end of file diff --git a/plugins/ice/src/transport_parameters.vala b/plugins/ice/src/transport_parameters.vala index a8172678..5b6431c2 100644 --- a/plugins/ice/src/transport_parameters.vala +++ b/plugins/ice/src/transport_parameters.vala @@ -9,9 +9,11 @@ public class Dino.Plugins.Ice.TransportParameters : JingleIceUdp.IceUdpTransport private bool we_want_connection; private bool remote_credentials_set; private Map connections = new HashMap(); + private DtlsSrtp? dtls_srtp; private class DatagramConnection : Jingle.DatagramConnection { private Nice.Agent agent; + private DtlsSrtp? dtls_srtp; private uint stream_id; private string? error; private ulong sent; @@ -20,8 +22,9 @@ public class Dino.Plugins.Ice.TransportParameters : JingleIceUdp.IceUdpTransport private ulong recv_reported; private ulong datagram_received_id; - public DatagramConnection(Nice.Agent agent, uint stream_id, uint8 component_id) { + public DatagramConnection(Nice.Agent agent, DtlsSrtp? dtls_srtp, uint stream_id, uint8 component_id) { this.agent = agent; + this.dtls_srtp = dtls_srtp; this.stream_id = stream_id; this.component_id = component_id; this.datagram_received_id = this.datagram_received.connect((datagram) => { @@ -41,7 +44,12 @@ public class Dino.Plugins.Ice.TransportParameters : JingleIceUdp.IceUdpTransport public override void send_datagram(Bytes datagram) { if (this.agent != null && is_component_ready(agent, stream_id, component_id)) { - agent.send(stream_id, component_id, datagram.get_data()); + uint8[] encrypted_data = null; + if (dtls_srtp != null) { + encrypted_data = dtls_srtp.process_outgoing_data(component_id, datagram.get_data()); + if (encrypted_data == null) return; + } + agent.send(stream_id, component_id, encrypted_data ?? datagram.get_data()); sent += datagram.length; if (sent > sent_reported + 100000) { debug("Sent %lu bytes via stream %u component %u", sent, stream_id, component_id); @@ -55,6 +63,20 @@ public class Dino.Plugins.Ice.TransportParameters : JingleIceUdp.IceUdpTransport base(components, local_full_jid, peer_full_jid, node); this.we_want_connection = (node == null); this.agent = agent; + + if (this.peer_fingerprint != null || !incoming) { + dtls_srtp = DtlsSrtp.setup(); + dtls_srtp.send_data.connect((data) => { + agent.send(stream_id, 1, data); + }); + this.own_fingerprint = dtls_srtp.get_own_fingerprint(GnuTLS.DigestAlgorithm.SHA256); + if (incoming) { + dtls_srtp.set_peer_fingerprint(this.peer_fingerprint); + } else { + dtls_srtp.setup_dtls_connection(true); + } + } + agent.candidate_gathering_done.connect(on_candidate_gathering_done); agent.initial_binding_request_received.connect(on_initial_binding_request_received); agent.component_state_changed.connect(on_component_state_changed); @@ -112,6 +134,12 @@ public class Dino.Plugins.Ice.TransportParameters : JingleIceUdp.IceUdpTransport public override void handle_transport_accept(StanzaNode transport) throws Jingle.IqError { debug("on_transport_accept from %s", peer_full_jid.to_string()); base.handle_transport_accept(transport); + + if (dtls_srtp != null && peer_fingerprint != null) { + dtls_srtp.set_peer_fingerprint(this.peer_fingerprint); + } else { + dtls_srtp = null; + } } public override void handle_transport_info(StanzaNode transport) throws Jingle.IqError { @@ -163,9 +191,16 @@ public class Dino.Plugins.Ice.TransportParameters : JingleIceUdp.IceUdpTransport int new_candidates = agent.set_remote_candidates(stream_id, i, candidates); debug("Initiated component %u with %i remote candidates", i, new_candidates); - connections[i] = new DatagramConnection(agent, stream_id, i); + connections[i] = new DatagramConnection(agent, dtls_srtp, stream_id, i); content.set_transport_connection(connections[i], i); } + + if (incoming && dtls_srtp != null) { + Jingle.DatagramConnection rtp_datagram = (Jingle.DatagramConnection) content.get_transport_connection(1); + rtp_datagram.notify["ready"].connect(() => { + dtls_srtp.setup_dtls_connection(false); + }); + } base.create_transport_connection(stream, content); } @@ -194,12 +229,17 @@ public class Dino.Plugins.Ice.TransportParameters : JingleIceUdp.IceUdpTransport private void on_recv(Nice.Agent agent, uint stream_id, uint component_id, uint8[] data) { if (stream_id != this.stream_id) return; + uint8[] decrypt_data = null; + if (dtls_srtp != null) { + decrypt_data = dtls_srtp.process_incoming_data(component_id, data); + if (decrypt_data == null) return; + } may_consider_ready(stream_id, component_id); if (connections.has_key((uint8) component_id)) { if (!connections[(uint8) component_id].ready) { debug("on_recv stream %u component %u when state %s", stream_id, component_id, agent.get_component_state(stream_id, component_id).to_string()); } - connections[(uint8) component_id].datagram_received(new Bytes(data)); + connections[(uint8) component_id].datagram_received(new Bytes(decrypt_data ?? data)); } else { debug("on_recv stream %u component %u length %u", stream_id, component_id, data.length); } diff --git a/plugins/ice/vapi/gnutls.vapi b/plugins/ice/vapi/gnutls.vapi new file mode 100644 index 00000000..a8f75e14 --- /dev/null +++ b/plugins/ice/vapi/gnutls.vapi @@ -0,0 +1,419 @@ +[CCode (cprefix = "gnutls_", lower_case_cprefix = "gnutls_", cheader_filename = "gnutls/gnutls.h")] +namespace GnuTLS { + + public int global_init(); + + [CCode (cname = "gnutls_pull_func", has_target = false)] + public delegate ssize_t PullFunc(void* transport_ptr, [CCode (ctype = "void*", array_length_type="size_t")] uint8[] array); + + [CCode (cname = "gnutls_pull_timeout_func", has_target = false)] + public delegate int PullTimeoutFunc(void* transport_ptr, uint ms); + + [CCode (cname = "gnutls_push_func", has_target = false)] + public delegate ssize_t PushFunc(void* transport_ptr, [CCode (ctype = "void*", array_length_type="size_t")] uint8[] array); + + [CCode (cname = "gnutls_certificate_verify_function", has_target = false)] + public delegate int VerifyFunc(Session session); + + [Compact] + [CCode (cname = "struct gnutls_session_int", free_function = "gnutls_deinit")] + public class Session { + + public static Session? create(int con_end) throws GLib.Error { + Session result; + var ret = init(out result, con_end); + throw_if_error(ret); + return result; + } + + [CCode (cname = "gnutls_init")] + private static int init(out Session session, int con_end); + + [CCode (cname = "gnutls_transport_set_push_function")] + public void set_push_function(PushFunc func); + + [CCode (cname = "gnutls_transport_set_pull_function")] + public void set_pull_function(PullFunc func); + + [CCode (cname = "gnutls_transport_set_pull_timeout_function")] + public void set_pull_timeout_function(PullTimeoutFunc func); + + [CCode (cname = "gnutls_transport_set_ptr")] + public void set_transport_pointer(void* ptr); + + [CCode (cname = "gnutls_transport_get_ptr")] + public void* get_transport_pointer(); + + [CCode (cname = "gnutls_heartbeat_enable")] + public int enable_heartbeat(uint type); + + [CCode (cname = "gnutls_certificate_server_set_request")] + public void server_set_request(CertificateRequest req); + + [CCode (cname = "gnutls_credentials_set")] + public int set_credentials_(CredentialsType type, void* cred); + [CCode (cname = "gnutls_credentials_set_")] + public void set_credentials(CredentialsType type, void* cred) throws GLib.Error { + int err = set_credentials_(type, cred); + throw_if_error(err); + } + + [CCode (cname = "gnutls_priority_set_direct")] + public int set_priority_from_string_(string priority, out unowned string err_pos = null); + [CCode (cname = "gnutls_priority_set_direct_")] + public void set_priority_from_string(string priority, out unowned string err_pos = null) throws GLib.Error { + int err = set_priority_from_string_(priority, out err_pos); + throw_if_error(err); + } + + [CCode (cname = "gnutls_srtp_set_profile_direct")] + public int set_srtp_profile_direct_(string profiles, out unowned string err_pos = null); + [CCode (cname = "gnutls_srtp_set_profile_direct_")] + public void set_srtp_profile_direct(string profiles, out unowned string err_pos = null) throws GLib.Error { + int err = set_srtp_profile_direct_(profiles, out err_pos); + throw_if_error(err); + } + + [CCode (cname = "gnutls_transport_set_int")] + public void transport_set_int(int fd); + + [CCode (cname = "gnutls_handshake")] + public int handshake(); + + [CCode (cname = "gnutls_srtp_get_keys")] + public int get_srtp_keys_(void *key_material, uint32 key_material_size, out Datum client_key, out Datum client_salt, out Datum server_key, out Datum server_salt); + [CCode (cname = "gnutls_srtp_get_keys_")] + public void get_srtp_keys(void *key_material, uint32 key_material_size, out Datum client_key, out Datum client_salt, out Datum server_key, out Datum server_salt) throws GLib.Error { + get_srtp_keys_(key_material, key_material_size, out client_key, out client_salt, out server_key, out server_salt); + } + + [CCode (cname = "gnutls_certificate_get_peers", array_length_type = "unsigned int")] + public unowned Datum[]? get_peer_certificates(); + + [CCode (cname = "gnutls_session_set_verify_function")] + public void set_verify_function(VerifyFunc func); + } + + [Compact] + [CCode (cname = "struct gnutls_certificate_credentials_st", free_function = "gnutls_certificate_free_credentials", cprefix = "gnutls_certificate_")] + public class CertificateCredentials { + + [CCode (cname = "gnutls_certificate_allocate_credentials")] + private static int allocate(out CertificateCredentials credentials); + + public static CertificateCredentials create() throws GLib.Error { + CertificateCredentials result; + var ret = allocate (out result); + throw_if_error(ret); + return result; + } + + public void get_x509_crt(uint index, [CCode (array_length_type = "unsigned int")] out unowned X509.Certificate[] x509_ca_list); + + public int set_x509_key(X509.Certificate[] cert_list, X509.PrivateKey key); + } + + [CCode (cheader_filename = "gnutls/x509.h", cprefix = "GNUTLS_")] + namespace X509 { + + [Compact] + [CCode (cname = "struct gnutls_x509_crt_int", cprefix = "gnutls_x509_crt_", free_function = "gnutls_x509_crt_deinit")] + public class Certificate { + + [CCode (cname = "gnutls_x509_crt_init")] + private static int init (out Certificate cert); + public static Certificate create() throws GLib.Error { + Certificate result; + var ret = init (out result); + throw_if_error(ret); + return result; + } + + [CCode (cname = "gnutls_x509_crt_import")] + public int import_(ref Datum data, CertificateFormat format); + [CCode (cname = "gnutls_x509_crt_import_")] + public void import(ref Datum data, CertificateFormat format) throws GLib.Error { + int err = import_(ref data, format); + throw_if_error(err); + } + + [CCode (cname = "gnutls_x509_crt_set_version")] + public int set_version_(uint version); + [CCode (cname = "gnutls_x509_crt_set_version_")] + public void set_version(uint version) throws GLib.Error { + int err = set_version_(version); + throw_if_error(err); + } + + [CCode (cname = "gnutls_x509_crt_set_key")] + public int set_key_(PrivateKey key); + [CCode (cname = "gnutls_x509_crt_set_key_")] + public void set_key(PrivateKey key) throws GLib.Error { + int err = set_key_(key); + throw_if_error(err); + } + + [CCode (cname = "gnutls_x509_crt_set_activation_time")] + public int set_activation_time_(time_t act_time); + [CCode (cname = "gnutls_x509_crt_set_activation_time_")] + public void set_activation_time(time_t act_time) throws GLib.Error { + int err = set_activation_time_(act_time); + throw_if_error(err); + } + + [CCode (cname = "gnutls_x509_crt_set_expiration_time")] + public int set_expiration_time_(time_t exp_time); + [CCode (cname = "gnutls_x509_crt_set_expiration_time_")] + public void set_expiration_time(time_t exp_time) throws GLib.Error { + int err = set_expiration_time_(exp_time); + throw_if_error(err); + } + + [CCode (cname = "gnutls_x509_crt_set_serial")] + public int set_serial_(void* serial, size_t serial_size); + [CCode (cname = "gnutls_x509_crt_set_serial_")] + public void set_serial(void* serial, size_t serial_size) throws GLib.Error { + int err = set_serial_(serial, serial_size); + throw_if_error(err); + } + + [CCode (cname = "gnutls_x509_crt_sign")] + public int sign_(Certificate issuer, PrivateKey issuer_key); + [CCode (cname = "gnutls_x509_crt_sign_")] + public void sign(Certificate issuer, PrivateKey issuer_key) throws GLib.Error { + int err = sign_(issuer, issuer_key); + throw_if_error(err); + } + + [CCode (cname = "gnutls_x509_crt_get_fingerprint")] + public int get_fingerprint_(DigestAlgorithm algo, void* buf, ref size_t buf_size); + [CCode (cname = "gnutls_x509_crt_get_fingerprint_")] + public void get_fingerprint(DigestAlgorithm algo, void* buf, ref size_t buf_size) throws GLib.Error { + int err = get_fingerprint_(algo, buf, ref buf_size); + throw_if_error(err); + } + } + + [Compact] + [CCode (cname = "struct gnutls_x509_privkey_int", cprefix = "gnutls_x509_privkey_", free_function = "gnutls_x509_privkey_deinit")] + public class PrivateKey { + private static int init (out PrivateKey key); + public static PrivateKey create () throws GLib.Error { + PrivateKey result; + var ret = init (out result); + throw_if_error(ret); + return result; + } + + public int generate(PKAlgorithm algo, uint bits, uint flags = 0); + } + + } + + [CCode (cname = "gnutls_certificate_request_t", cprefix = "GNUTLS_CERT_", has_type_id = false)] + public enum CertificateRequest { + IGNORE, + REQUEST, + REQUIRE + } + + [CCode (cname = "gnutls_pk_algorithm_t", cprefix = "GNUTLS_PK_", has_type_id = false)] + public enum PKAlgorithm { + UNKNOWN, + RSA, + DSA; + } + + [CCode (cname = "gnutls_digest_algorithm_t", cprefix = "GNUTLS_DIG_", has_type_id = false)] + public enum DigestAlgorithm { + NULL, + MD5, + SHA1, + RMD160, + MD2, + SHA224, + SHA256, + SHA384, + SHA512; + } + + [Flags] + [CCode (cname = "gnutls_init_flags_t", cprefix = "GNUTLS_", has_type_id = false)] + public enum InitFlags { + SERVER, + CLIENT, + DATAGRAM + } + + [CCode (cname = "gnutls_credentials_type_t", cprefix = "GNUTLS_CRD_", has_type_id = false)] + public enum CredentialsType { + CERTIFICATE, + ANON, + SRP, + PSK, + IA + } + + [CCode (cname = "gnutls_x509_crt_fmt_t", cprefix = "GNUTLS_X509_FMT_", has_type_id = false)] + public enum CertificateFormat { + DER, + PEM + } + + [Flags] + [CCode (cname = "gnutls_certificate_status_t", cprefix = "GNUTLS_CERT_", has_type_id = false)] + public enum CertificateStatus { + INVALID, // will be set if the certificate was not verified. + REVOKED, // in X.509 this will be set only if CRLs are checked + SIGNER_NOT_FOUND, + SIGNER_NOT_CA, + INSECURE_ALGORITHM + } + + [SimpleType] + [CCode (cname = "gnutls_datum_t", has_type_id = false)] + public struct Datum { + public uint8* data; + public uint size; + + public uint8[] extract() { + uint8[size] ret = new uint8[size]; + for (int i = 0; i < size; i++) { + ret[i] = data[i]; + } + return ret; + } + } + + // Gnutls error codes. The mapping to a TLS alert is also shown in comments. + [CCode (cname = "int", cprefix = "GNUTLS_E_", lower_case_cprefix = "gnutls_error_", has_type_id = false)] + public enum ErrorCode { + SUCCESS, + UNKNOWN_COMPRESSION_ALGORITHM, + UNKNOWN_CIPHER_TYPE, + LARGE_PACKET, + UNSUPPORTED_VERSION_PACKET, // GNUTLS_A_PROTOCOL_VERSION + UNEXPECTED_PACKET_LENGTH, // GNUTLS_A_RECORD_OVERFLOW + INVALID_SESSION, + FATAL_ALERT_RECEIVED, + UNEXPECTED_PACKET, // GNUTLS_A_UNEXPECTED_MESSAGE + WARNING_ALERT_RECEIVED, + ERROR_IN_FINISHED_PACKET, + UNEXPECTED_HANDSHAKE_PACKET, + UNKNOWN_CIPHER_SUITE, // GNUTLS_A_HANDSHAKE_FAILURE + UNWANTED_ALGORITHM, + MPI_SCAN_FAILED, + DECRYPTION_FAILED, // GNUTLS_A_DECRYPTION_FAILED, GNUTLS_A_BAD_RECORD_MAC + MEMORY_ERROR, + DECOMPRESSION_FAILED, // GNUTLS_A_DECOMPRESSION_FAILURE + COMPRESSION_FAILED, + AGAIN, + EXPIRED, + DB_ERROR, + SRP_PWD_ERROR, + INSUFFICIENT_CREDENTIALS, + HASH_FAILED, + BASE64_DECODING_ERROR, + MPI_PRINT_FAILED, + REHANDSHAKE, // GNUTLS_A_NO_RENEGOTIATION + GOT_APPLICATION_DATA, + RECORD_LIMIT_REACHED, + ENCRYPTION_FAILED, + PK_ENCRYPTION_FAILED, + PK_DECRYPTION_FAILED, + PK_SIGN_FAILED, + X509_UNSUPPORTED_CRITICAL_EXTENSION, + KEY_USAGE_VIOLATION, + NO_CERTIFICATE_FOUND, // GNUTLS_A_BAD_CERTIFICATE + INVALID_REQUEST, + SHORT_MEMORY_BUFFER, + INTERRUPTED, + PUSH_ERROR, + PULL_ERROR, + RECEIVED_ILLEGAL_PARAMETER, // GNUTLS_A_ILLEGAL_PARAMETER + REQUESTED_DATA_NOT_AVAILABLE, + PKCS1_WRONG_PAD, + RECEIVED_ILLEGAL_EXTENSION, + INTERNAL_ERROR, + DH_PRIME_UNACCEPTABLE, + FILE_ERROR, + TOO_MANY_EMPTY_PACKETS, + UNKNOWN_PK_ALGORITHM, + // returned if libextra functionality was requested but + // gnutls_global_init_extra() was not called. + + INIT_LIBEXTRA, + LIBRARY_VERSION_MISMATCH, + // returned if you need to generate temporary RSA + // parameters. These are needed for export cipher suites. + + NO_TEMPORARY_RSA_PARAMS, + LZO_INIT_FAILED, + NO_COMPRESSION_ALGORITHMS, + NO_CIPHER_SUITES, + OPENPGP_GETKEY_FAILED, + PK_SIG_VERIFY_FAILED, + ILLEGAL_SRP_USERNAME, + SRP_PWD_PARSING_ERROR, + NO_TEMPORARY_DH_PARAMS, + // For certificate and key stuff + + ASN1_ELEMENT_NOT_FOUND, + ASN1_IDENTIFIER_NOT_FOUND, + ASN1_DER_ERROR, + ASN1_VALUE_NOT_FOUND, + ASN1_GENERIC_ERROR, + ASN1_VALUE_NOT_VALID, + ASN1_TAG_ERROR, + ASN1_TAG_IMPLICIT, + ASN1_TYPE_ANY_ERROR, + ASN1_SYNTAX_ERROR, + ASN1_DER_OVERFLOW, + OPENPGP_UID_REVOKED, + CERTIFICATE_ERROR, + CERTIFICATE_KEY_MISMATCH, + UNSUPPORTED_CERTIFICATE_TYPE, // GNUTLS_A_UNSUPPORTED_CERTIFICATE + X509_UNKNOWN_SAN, + OPENPGP_FINGERPRINT_UNSUPPORTED, + X509_UNSUPPORTED_ATTRIBUTE, + UNKNOWN_HASH_ALGORITHM, + UNKNOWN_PKCS_CONTENT_TYPE, + UNKNOWN_PKCS_BAG_TYPE, + INVALID_PASSWORD, + MAC_VERIFY_FAILED, // for PKCS #12 MAC + CONSTRAINT_ERROR, + WARNING_IA_IPHF_RECEIVED, + WARNING_IA_FPHF_RECEIVED, + IA_VERIFY_FAILED, + UNKNOWN_ALGORITHM, + BASE64_ENCODING_ERROR, + INCOMPATIBLE_CRYPTO_LIBRARY, + INCOMPATIBLE_LIBTASN1_LIBRARY, + OPENPGP_KEYRING_ERROR, + X509_UNSUPPORTED_OID, + RANDOM_FAILED, + BASE64_UNEXPECTED_HEADER_ERROR, + OPENPGP_SUBKEY_ERROR, + CRYPTO_ALREADY_REGISTERED, + HANDSHAKE_TOO_LARGE, + UNIMPLEMENTED_FEATURE, + APPLICATION_ERROR_MAX, // -65000 + APPLICATION_ERROR_MIN; // -65500 + + [CCode (cname = "gnutls_error_is_fatal")] + public bool is_fatal(); + + [CCode (cname = "gnutls_perror")] + public void print(); + + [CCode (cname = "gnutls_strerror")] + public unowned string to_string(); + } + + public void throw_if_error(int err_int) throws GLib.Error { + ErrorCode error = (ErrorCode)err_int; + if (error != ErrorCode.SUCCESS) { + throw new GLib.Error(-1, error, "%s%s", error.to_string(), error.is_fatal() ? " fatal" : ""); + } + } +} \ No newline at end of file diff --git a/plugins/rtp/CMakeLists.txt b/plugins/rtp/CMakeLists.txt index 5311fac3..8ce2a7c6 100644 --- a/plugins/rtp/CMakeLists.txt +++ b/plugins/rtp/CMakeLists.txt @@ -2,6 +2,7 @@ find_packages(RTP_PACKAGES REQUIRED Gee GLib GModule + GnuTLS GObject GTK3 Gst diff --git a/xmpp-vala/src/module/xep/0166_jingle/reason_element.vala b/xmpp-vala/src/module/xep/0166_jingle/reason_element.vala index 1cbdf936..4d47d4cd 100644 --- a/xmpp-vala/src/module/xep/0166_jingle/reason_element.vala +++ b/xmpp-vala/src/module/xep/0166_jingle/reason_element.vala @@ -24,6 +24,7 @@ namespace Xmpp.Xep.Jingle.ReasonElement { BUSY, CANCEL, DECLINE, + GONE, SUCCESS }; } \ No newline at end of file diff --git a/xmpp-vala/src/module/xep/0166_jingle/session.vala b/xmpp-vala/src/module/xep/0166_jingle/session.vala index e9ad9169..2d359f01 100644 --- a/xmpp-vala/src/module/xep/0166_jingle/session.vala +++ b/xmpp-vala/src/module/xep/0166_jingle/session.vala @@ -24,9 +24,10 @@ public class Xmpp.Xep.Jingle.Session : Object { public Jid peer_full_jid { get; private set; } public bool we_initiated { get; private set; } - public HashMap contents = new HashMap(); + public HashMap contents_map = new HashMap(); + public Gee.List contents = new ArrayList(); // Keep the order contents - public SecurityParameters? security { get { return contents.values.to_array()[0].security_params; } } + public SecurityParameters? security { get { return contents.to_array()[0].security_params; } } public Session.initiate_sent(XmppStream stream, string sid, Jid local_full_jid, Jid peer_full_jid) { this.stream = stream; @@ -94,7 +95,7 @@ public class Xmpp.Xep.Jingle.Session : Object { } else if (action.has_prefix("transport-")) { ContentNode content_node = get_single_content_node(jingle); - if (!contents.has_key(content_node.name)) { + if (!contents_map.has_key(content_node.name)) { throw new IqError.BAD_REQUEST("unknown content"); } @@ -102,7 +103,7 @@ public class Xmpp.Xep.Jingle.Session : Object { throw new IqError.BAD_REQUEST("missing transport node"); } - Content content = contents[content_node.name]; + Content content = contents_map[content_node.name]; if (content_node.creator != content.content_creator) { throw new IqError.BAD_REQUEST("unknown content; creator"); @@ -128,11 +129,11 @@ public class Xmpp.Xep.Jingle.Session : Object { } else if (action == "description-info") { ContentNode content_node = get_single_content_node(jingle); - if (!contents.has_key(content_node.name)) { + if (!contents_map.has_key(content_node.name)) { throw new IqError.BAD_REQUEST("unknown content"); } - Content content = contents[content_node.name]; + Content content = contents_map[content_node.name]; if (content_node.creator != content.content_creator) { throw new IqError.BAD_REQUEST("unknown content; creator"); @@ -149,7 +150,8 @@ public class Xmpp.Xep.Jingle.Session : Object { } internal void insert_content(Content content) { - this.contents[content.content_name] = content; + this.contents_map[content.content_name] = content; + this.contents.add(content); content.set_session(this); } @@ -209,7 +211,8 @@ public class Xmpp.Xep.Jingle.Session : Object { public async void add_content(Content content) { content.session = this; - this.contents[content.content_name] = content; + this.contents_map[content.content_name] = content; + contents.add(content); StanzaNode content_add_node = new StanzaNode.build("jingle", NS_URI) .add_self_xmlns() @@ -228,9 +231,9 @@ public class Xmpp.Xep.Jingle.Session : Object { private void handle_content_accept(ContentNode content_node) throws IqError { if (content_node.description == null || content_node.transport == null) throw new IqError.BAD_REQUEST("missing description or transport node"); - if (!contents.has_key(content_node.name)) throw new IqError.BAD_REQUEST("unknown content"); + if (!contents_map.has_key(content_node.name)) throw new IqError.BAD_REQUEST("unknown content"); - Content content = contents[content_node.name]; + Content content = contents_map[content_node.name]; if (content_node.creator != content.content_creator) warning("Counterpart accepts content with an unexpected `creator`"); if (content_node.senders != content.senders) warning("Counterpart accepts content with an unexpected `senders`"); @@ -242,7 +245,7 @@ public class Xmpp.Xep.Jingle.Session : Object { private void handle_content_modify(XmppStream stream, StanzaNode jingle_node, Iq.Stanza iq) throws IqError { ContentNode content_node = get_single_content_node(jingle_node); - Content? content = contents[content_node.name]; + Content? content = contents_map[content_node.name]; if (content == null) throw new IqError.BAD_REQUEST("no such content"); if (content_node.creator != content.content_creator) throw new IqError.BAD_REQUEST("mismatching creator"); @@ -301,7 +304,7 @@ public class Xmpp.Xep.Jingle.Session : Object { } } - foreach (Content content in contents.values) { + foreach (Content content in contents) { content.terminate(false, reason_name, reason_text); } @@ -336,7 +339,7 @@ public class Xmpp.Xep.Jingle.Session : Object { .add_self_xmlns() .put_attribute("action", "session-accept") .put_attribute("sid", sid); - foreach (Content content in contents.values) { + foreach (Content content in contents) { StanzaNode content_node = new StanzaNode.build("content", NS_URI) .put_attribute("creator", "initiator") .put_attribute("name", content.content_name) @@ -345,12 +348,13 @@ public class Xmpp.Xep.Jingle.Session : Object { .put_node(content.transport_params.to_transport_stanza_node()); jingle.put_node(content_node); } + Iq.Stanza iq = new Iq.Stanza.set(jingle) { to=peer_full_jid }; stream.get_module(Iq.Module.IDENTITY).send_iq(stream, iq); - foreach (Content content in contents.values) { - content.on_accept(stream); + foreach (Content content2 in contents) { + content2.on_accept(stream); } state = State.ACTIVE; @@ -359,7 +363,7 @@ public class Xmpp.Xep.Jingle.Session : Object { internal void accept_content(Content content) { if (state == State.INITIATE_RECEIVED) { bool all_accepted = true; - foreach (Content c in contents.values) { + foreach (Content c in contents) { if (c.state != Content.State.WANTS_TO_BE_ACCEPTED) { all_accepted = false; } @@ -413,7 +417,7 @@ public class Xmpp.Xep.Jingle.Session : Object { } else { reason_str = "local session-terminate"; } - foreach (Content content in contents.values) { + foreach (Content content in contents) { content.terminate(true, reason_name, reason_text); } } diff --git a/xmpp-vala/src/module/xep/0167_jingle_rtp/content_parameters.vala b/xmpp-vala/src/module/xep/0167_jingle_rtp/content_parameters.vala index cca03543..32ea1df6 100644 --- a/xmpp-vala/src/module/xep/0167_jingle_rtp/content_parameters.vala +++ b/xmpp-vala/src/module/xep/0167_jingle_rtp/content_parameters.vala @@ -34,7 +34,7 @@ public class Xmpp.Xep.JingleRtp.Parameters : Jingle.ContentParameters, Object { this.parent = parent; this.media = media; this.ssrc = ssrc; - this.rtcp_mux = rtcp_mux; + this.rtcp_mux = true; this.bandwidth = bandwidth; this.bandwidth_type = bandwidth_type; this.encryption_required = encryption_required; @@ -175,6 +175,9 @@ public class Xmpp.Xep.JingleRtp.Parameters : Jingle.ContentParameters, Object { ret.put_node(new StanzaNode.build("encryption", NS_URI) .put_node(local_crypto.to_xml())); } + if (rtcp_mux) { + ret.put_node(new StanzaNode.build("rtcp-mux", NS_URI)); + } return ret; } } \ No newline at end of file diff --git a/xmpp-vala/src/module/xep/0167_jingle_rtp/jingle_rtp_module.vala b/xmpp-vala/src/module/xep/0167_jingle_rtp/jingle_rtp_module.vala index 23aee6c9..3a9ea09f 100644 --- a/xmpp-vala/src/module/xep/0167_jingle_rtp/jingle_rtp_module.vala +++ b/xmpp-vala/src/module/xep/0167_jingle_rtp/jingle_rtp_module.vala @@ -84,7 +84,7 @@ public abstract class Module : XmppStreamModule { Jid receiver_full_jid = session.peer_full_jid; Jingle.Content? content = null; - foreach (Jingle.Content c in session.contents.values) { + foreach (Jingle.Content c in session.contents) { Parameters? parameters = c.content_params as Parameters; if (parameters == null) continue; diff --git a/xmpp-vala/src/module/xep/0167_jingle_rtp/session_info_type.vala b/xmpp-vala/src/module/xep/0167_jingle_rtp/session_info_type.vala index d36255f0..32cd9016 100644 --- a/xmpp-vala/src/module/xep/0167_jingle_rtp/session_info_type.vala +++ b/xmpp-vala/src/module/xep/0167_jingle_rtp/session_info_type.vala @@ -50,7 +50,7 @@ namespace Xmpp.Xep.JingleRtp { public void send_mute(Jingle.Session session, bool mute, string media) { string node_name = mute ? "mute" : "unmute"; - foreach (Jingle.Content content in session.contents.values) { + foreach (Jingle.Content content in session.contents) { Parameters? parameters = content.content_params as Parameters; if (parameters != null && parameters.media == media) { StanzaNode session_info_content = new StanzaNode.build(node_name, NS_URI).add_self_xmlns().put_attribute("name", content.content_name); diff --git a/xmpp-vala/src/module/xep/0176_jingle_ice_udp/jingle_ice_udp_module.vala b/xmpp-vala/src/module/xep/0176_jingle_ice_udp/jingle_ice_udp_module.vala index 9ed494ff..4b7c7a36 100644 --- a/xmpp-vala/src/module/xep/0176_jingle_ice_udp/jingle_ice_udp_module.vala +++ b/xmpp-vala/src/module/xep/0176_jingle_ice_udp/jingle_ice_udp_module.vala @@ -12,6 +12,7 @@ public abstract class Module : XmppStreamModule, Jingle.Transport { public override void attach(XmppStream stream) { 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, "urn:xmpp:jingle:apps:dtls:0"); } public override void detach(XmppStream stream) { stream.get_module(ServiceDiscovery.Module.IDENTITY).remove_feature(stream, NS_URI); diff --git a/xmpp-vala/src/module/xep/0176_jingle_ice_udp/transport_parameters.vala b/xmpp-vala/src/module/xep/0176_jingle_ice_udp/transport_parameters.vala index 8b8aa07d..3c69d0af 100644 --- a/xmpp-vala/src/module/xep/0176_jingle_ice_udp/transport_parameters.vala +++ b/xmpp-vala/src/module/xep/0176_jingle_ice_udp/transport_parameters.vala @@ -13,6 +13,9 @@ public abstract class Xmpp.Xep.JingleIceUdp.IceUdpTransportParameters : Jingle.T public ConcurrentList unsent_local_candidates = new ConcurrentList(Candidate.equals_func); public Gee.List remote_candidates = new ArrayList(Candidate.equals_func); + public string? own_fingerprint = null; + public string? peer_fingerprint = null; + public Jid local_full_jid { get; private set; } public Jid peer_full_jid { get; private set; } private uint8 components_; @@ -34,6 +37,11 @@ public abstract class Xmpp.Xep.JingleIceUdp.IceUdpTransportParameters : Jingle.T foreach (StanzaNode candidateNode in node.get_subnodes("candidate")) { remote_candidates.add(Candidate.parse(candidateNode)); } + + StanzaNode? fingerprint_node = node.get_subnode("fingerprint", "urn:xmpp:jingle:apps:dtls:0"); + if (fingerprint_node != null) { + peer_fingerprint = fingerprint_node.get_deep_string_content(); + } } } @@ -57,6 +65,20 @@ public abstract class Xmpp.Xep.JingleIceUdp.IceUdpTransportParameters : Jingle.T .add_self_xmlns() .put_attribute("ufrag", local_ufrag) .put_attribute("pwd", local_pwd); + + if (own_fingerprint != null) { + var fingerprint_node = new StanzaNode.build("fingerprint", "urn:xmpp:jingle:apps:dtls:0") + .add_self_xmlns() + .put_attribute("hash", "sha-256") + .put_node(new StanzaNode.text(own_fingerprint)); + if (incoming) { + fingerprint_node.put_attribute("setup", "active"); + } else { + fingerprint_node.put_attribute("setup", "actpass"); + } + node.put_node(fingerprint_node); + } + foreach (Candidate candidate in unsent_local_candidates) { node.put_node(candidate.to_xml()); } @@ -72,6 +94,11 @@ public abstract class Xmpp.Xep.JingleIceUdp.IceUdpTransportParameters : Jingle.T foreach (StanzaNode candidateNode in node.get_subnodes("candidate")) { remote_candidates.add(Candidate.parse(candidateNode)); } + + StanzaNode? fingerprint_node = node.get_subnode("fingerprint", "urn:xmpp:jingle:apps:dtls:0"); + if (fingerprint_node != null) { + peer_fingerprint = fingerprint_node.get_deep_string_content(); + } } public virtual void handle_transport_info(StanzaNode node) throws Jingle.IqError { -- cgit v1.2.3-54-g00ecf From c7d1ee4dc5a08715ed68ed69e918f5ec9cbd4b40 Mon Sep 17 00:00:00 2001 From: Marvin W Date: Mon, 29 Mar 2021 13:19:52 +0200 Subject: Make RTCP-MUX a stream property --- xmpp-vala/src/module/xep/0167_jingle_rtp/content_parameters.vala | 1 + xmpp-vala/src/module/xep/0167_jingle_rtp/stream.vala | 7 +++++++ 2 files changed, 8 insertions(+) (limited to 'xmpp-vala/src/module/xep/0167_jingle_rtp') diff --git a/xmpp-vala/src/module/xep/0167_jingle_rtp/content_parameters.vala b/xmpp-vala/src/module/xep/0167_jingle_rtp/content_parameters.vala index 32ea1df6..ff3d31f4 100644 --- a/xmpp-vala/src/module/xep/0167_jingle_rtp/content_parameters.vala +++ b/xmpp-vala/src/module/xep/0167_jingle_rtp/content_parameters.vala @@ -127,6 +127,7 @@ public class Xmpp.Xep.JingleRtp.Parameters : Jingle.ContentParameters, Object { } public void handle_accept(XmppStream stream, Jingle.Session session, Jingle.Content content, StanzaNode description_node) { + rtcp_mux = description_node.get_subnode("rtcp-mux") != null; Gee.List payload_type_nodes = description_node.get_subnodes("payload-type"); if (payload_type_nodes.size == 0) { warning("Counterpart didn't include any payload types"); diff --git a/xmpp-vala/src/module/xep/0167_jingle_rtp/stream.vala b/xmpp-vala/src/module/xep/0167_jingle_rtp/stream.vala index 2fc29291..730ce9f8 100644 --- a/xmpp-vala/src/module/xep/0167_jingle_rtp/stream.vala +++ b/xmpp-vala/src/module/xep/0167_jingle_rtp/stream.vala @@ -37,6 +37,13 @@ public abstract class Xmpp.Xep.JingleRtp.Stream : Object { public bool receiving { get { return content.session.senders_include_counterpart(content.senders); }} + public bool rtcp_mux { get { + var content_params = content.content_params; + if (content_params is Parameters) { + return ((Parameters)content_params).rtcp_mux; + } + return false; + }} protected Stream(Jingle.Content content) { this.content = content; -- cgit v1.2.3-54-g00ecf From 3454201e5a3da058ccbef0bbaf467599912a8c38 Mon Sep 17 00:00:00 2001 From: fiaxh Date: Thu, 1 Apr 2021 12:03:04 +0200 Subject: Use outgoing JMI if contact has supporting device --- libdino/src/entity/call.vala | 2 + libdino/src/service/calls.vala | 110 +++++++++++++++------ main/src/ui/conversation_titlebar/call_entry.vala | 4 +- plugins/ice/src/dtls_srtp.vala | 8 -- .../src/module/xep/0166_jingle/jingle_module.vala | 4 +- .../xep/0167_jingle_rtp/jingle_rtp_module.vala | 4 +- .../module/xep/0353_jingle_message_initiation.vala | 14 ++- 7 files changed, 99 insertions(+), 47 deletions(-) (limited to 'xmpp-vala/src/module/xep/0167_jingle_rtp') diff --git a/libdino/src/entity/call.vala b/libdino/src/entity/call.vala index b836e2cd..7891dae7 100644 --- a/libdino/src/entity/call.vala +++ b/libdino/src/entity/call.vala @@ -77,6 +77,8 @@ namespace Dino.Entities { .value(db.call.state, State.ENDED); // No point in persisting states that can't survive a restart if (end_time != null) { builder.value(db.call.end_time, (long) end_time.to_unix()); + } else { + builder.value(db.call.end_time, (long) local_time.to_unix()); } id = (int) builder.perform(); diff --git a/libdino/src/service/calls.vala b/libdino/src/service/calls.vala index 54c353b0..93636c03 100644 --- a/libdino/src/service/calls.vala +++ b/libdino/src/service/calls.vala @@ -21,15 +21,16 @@ namespace Dino { public string id { get { return IDENTITY.id; } } private StreamInteractor stream_interactor; + private Database db; private Xep.JingleRtp.SessionInfoType session_info_type; private HashMap> sid_by_call = new HashMap>(Account.hash_func, Account.equals_func); private HashMap> call_by_sid = new HashMap>(Account.hash_func, Account.equals_func); public HashMap sessions = new HashMap(Call.hash_func, Call.equals_func); - public Call? mi_accepted_call = null; - public string? mi_accepted_sid = null; - public bool mi_accepted_video = false; + public HashMap jmi_call = new HashMap(Account.hash_func, Account.equals_func); + public HashMap jmi_sid = new HashMap(Account.hash_func, Account.equals_func); + public HashMap jmi_video = new HashMap(Account.hash_func, Account.equals_func); private HashMap counterpart_sends_video = new HashMap(Call.hash_func, Call.equals_func); private HashMap we_should_send_video = new HashMap(Call.hash_func, Call.equals_func); @@ -46,6 +47,7 @@ namespace Dino { private Calls(StreamInteractor stream_interactor, Database db) { this.stream_interactor = stream_interactor; + this.db = db; stream_interactor.account_added.connect(on_account_added); } @@ -75,22 +77,32 @@ namespace Dino { stream_interactor.get_module(CallStore.IDENTITY).add_call(call, conversation); - XmppStream? stream = stream_interactor.get_stream(conversation.account); - if (stream == null) return null; + we_should_send_video[call] = video; + we_should_send_audio[call] = true; - Gee.List call_resources = yield get_call_resources(conversation); - if (call_resources.size > 0) { - Jid full_jid = call_resources[0]; - Xep.Jingle.Session session = yield stream.get_module(Xep.JingleRtp.Module.IDENTITY).start_call(stream, full_jid, video); - sessions[call] = session; - call_by_sid[call.account][session.sid] = call; - sid_by_call[call.account][call] = session.sid; + if (yield has_jmi_resources(conversation)) { + XmppStream? stream = stream_interactor.get_stream(conversation.account); + jmi_call[conversation.account] = call; + jmi_video[conversation.account] = video; + jmi_sid[conversation.account] = Xmpp.random_uuid(); - connect_session_signals(call, session); - } + call_by_sid[call.account][jmi_sid[conversation.account]] = call; - we_should_send_video[call] = video; - we_should_send_audio[call] = true; + var descriptions = new ArrayList(); + descriptions.add(new StanzaNode.build("description", "urn:xmpp:jingle:apps:rtp:1").add_self_xmlns().put_attribute("media", "audio")); + if (video) { + descriptions.add(new StanzaNode.build("description", "urn:xmpp:jingle:apps:rtp:1").add_self_xmlns().put_attribute("media", "video")); + } + + stream.get_module(Xmpp.Xep.JingleMessageInitiation.Module.IDENTITY).send_session_propose_to_peer(stream, conversation.counterpart, jmi_sid[call.account], descriptions); + } else { + Gee.List call_resources = yield get_call_resources(conversation); + if (call_resources.size == 0) { + warning("No call resources"); + return null; + } + yield call_resource(conversation.account, call_resources[0], call, video); + } conversation.last_active = call.time; call_outgoing(call, conversation); @@ -98,6 +110,17 @@ namespace Dino { return call; } + private async void call_resource(Account account, Jid full_jid, Call call, bool video, string? sid = null) { + XmppStream? stream = stream_interactor.get_stream(account); + if (stream == null) return; + + Xep.Jingle.Session session = yield stream.get_module(Xep.JingleRtp.Module.IDENTITY).start_call(stream, full_jid, video, sid); + sessions[call] = session; + sid_by_call[call.account][call] = session.sid; + + connect_session_signals(call, session); + } + public void end_call(Conversation conversation, Call call) { XmppStream? stream = stream_interactor.get_stream(call.account); if (stream == null) return; @@ -130,15 +153,17 @@ namespace Dino { } } else { // Only a JMI so far - XmppStream stream = stream_interactor.get_stream(call.account); + Account account = call.account; + string sid = sid_by_call[call.account][call]; + XmppStream stream = stream_interactor.get_stream(account); if (stream == null) return; - mi_accepted_call = call; - mi_accepted_sid = sid_by_call[call.account][call]; - mi_accepted_video = we_should_send_video[call]; + jmi_call[account] = call; + jmi_sid[account] = sid; + jmi_video[account] = we_should_send_video[call]; - stream.get_module(Xep.JingleMessageInitiation.Module.IDENTITY).send_session_accept_to_self(stream, mi_accepted_sid); - stream.get_module(Xep.JingleMessageInitiation.Module.IDENTITY).send_session_proceed_to_peer(stream, call.counterpart, mi_accepted_sid); + stream.get_module(Xep.JingleMessageInitiation.Module.IDENTITY).send_session_accept_to_self(stream, sid); + stream.get_module(Xep.JingleMessageInitiation.Module.IDENTITY).send_session_proceed_to_peer(stream, call.counterpart, sid); } } @@ -211,7 +236,11 @@ namespace Dino { // If video_feed == null && !mute we're trying to mute a non-existant feed. It will be muted as soon as it is created. } - public async Gee.List get_call_resources(Conversation conversation) { + public async bool can_do_calls(Conversation conversation) { + return (yield get_call_resources(conversation)).size > 0 || yield has_jmi_resources(conversation); + } + + private async Gee.List get_call_resources(Conversation conversation) { ArrayList ret = new ArrayList(); XmppStream? stream = stream_interactor.get_stream(conversation.account); @@ -228,6 +257,15 @@ namespace Dino { return ret; } + private async bool has_jmi_resources(Conversation conversation) { + int64 jmi_resources = db.entity.select() + .with(db.entity.jid_id, "=", db.get_jid_id(conversation.counterpart)) + .join_with(db.entity_feature, db.entity.caps_hash, db.entity_feature.entity) + .with(db.entity_feature.feature, "=", Xep.JingleMessageInitiation.NS_URI) + .count(); + return jmi_resources > 0; + } + public bool should_we_send_video(Call call) { return we_should_send_video[call]; } @@ -252,13 +290,14 @@ namespace Dino { } // Session might have already been accepted via Jingle Message Initiation - bool already_accepted = mi_accepted_sid == session.sid && mi_accepted_call.account.equals(account) && - mi_accepted_call.counterpart.equals_bare(session.peer_full_jid) && - mi_accepted_video == counterpart_wants_video; + bool already_accepted = jmi_sid.contains(account) && + jmi_sid[account] == session.sid && jmi_call[account].account.equals(account) && + jmi_call[account].counterpart.equals_bare(session.peer_full_jid) && + jmi_video[account] == counterpart_wants_video; Call? call = null; if (already_accepted) { - call = mi_accepted_call; + call = jmi_call[account]; } else { call = create_received_call(account, session.peer_full_jid, account.full_jid, counterpart_wants_video); } @@ -334,16 +373,15 @@ namespace Dino { } if (call.state == Call.State.IN_PROGRESS) { call.state = Call.State.ENDED; - call_terminated(call, reason_name, reason_text); } else if (call.state == Call.State.RINGING || call.state == Call.State.ESTABLISHING) { if (reason_name == Xep.Jingle.ReasonElement.DECLINE) { call.state = Call.State.DECLINED; } else { call.state = Call.State.FAILED; } - call_terminated(call, reason_name, reason_text); } + call_terminated(call, reason_name, reason_text); remove_call_from_datastructures(call); } @@ -478,11 +516,21 @@ namespace Dino { mi_module.session_accepted.connect((from, sid) => { if (!call_by_sid[account].has_key(sid)) return; - // Ignore session-accepted from ourselves - if (!from.equals(account.full_jid)) { + if (from.equals_bare(account.bare_jid)) { // Carboned message from our account + // Ignore carbon from ourselves + if (from.equals(account.full_jid)) return; + Call call = call_by_sid[account][sid]; call.state = Call.State.OTHER_DEVICE_ACCEPTED; remove_call_from_datastructures(call); + } else if (from.equals_bare(call_by_sid[account][sid].counterpart)) { // Message from our peer + // We proposed the call + if (jmi_sid.has_key(account) && jmi_sid[account] == sid) { + call_resource(account, from, jmi_call[account], jmi_video[account], jmi_sid[account]); + jmi_call.unset(account); + jmi_sid.unset(account); + jmi_video.unset(account); + } } }); mi_module.session_rejected.connect((from, to, sid) => { diff --git a/main/src/ui/conversation_titlebar/call_entry.vala b/main/src/ui/conversation_titlebar/call_entry.vala index 1ac4dd83..5e28ecbe 100644 --- a/main/src/ui/conversation_titlebar/call_entry.vala +++ b/main/src/ui/conversation_titlebar/call_entry.vala @@ -116,9 +116,9 @@ namespace Dino.Ui { private async void update_visibility() { if (conversation.type_ == Conversation.Type.CHAT) { Conversation conv_bak = conversation; - Gee.List? resources = yield stream_interactor.get_module(Calls.IDENTITY).get_call_resources(conversation); + bool can_do_calls = yield stream_interactor.get_module(Calls.IDENTITY).can_do_calls(conversation); if (conv_bak != conversation) return; - visible = resources != null && resources.size > 0; + visible = can_do_calls; } else { visible = false; } diff --git a/plugins/ice/src/dtls_srtp.vala b/plugins/ice/src/dtls_srtp.vala index b742ccab..f294e66b 100644 --- a/plugins/ice/src/dtls_srtp.vala +++ b/plugins/ice/src/dtls_srtp.vala @@ -249,12 +249,4 @@ public class DtlsSrtp { } return sb.str; } - - private uint8[] uint8_pt_to_a(uint8* data, uint size) { - uint8[size] ret = new uint8[size]; - for (int i = 0; i < size; i++) { - ret[i] = data[i]; - } - return ret; - } } \ No newline at end of file diff --git a/xmpp-vala/src/module/xep/0166_jingle/jingle_module.vala b/xmpp-vala/src/module/xep/0166_jingle/jingle_module.vala index 1e8a36d1..7314ca6c 100644 --- a/xmpp-vala/src/module/xep/0166_jingle/jingle_module.vala +++ b/xmpp-vala/src/module/xep/0166_jingle/jingle_module.vala @@ -102,7 +102,7 @@ namespace Xmpp.Xep.Jingle { return (yield is_jingle_available(stream, full_jid)) && (yield select_transport(stream, type, components, full_jid, Set.empty())) != null; } - public async Session create_session(XmppStream stream, Gee.List contents, Jid receiver_full_jid, string sid = random_uuid()) throws Error { + public async Session create_session(XmppStream stream, Gee.List contents, Jid receiver_full_jid, string? sid = null) throws Error { if (!yield is_jingle_available(stream, receiver_full_jid)) { throw new Error.NO_SHARED_PROTOCOLS("No Jingle support"); } @@ -111,7 +111,7 @@ namespace Xmpp.Xep.Jingle { throw new Error.GENERAL("Couldn't determine own JID"); } - Session session = new Session.initiate_sent(stream, sid, my_jid, receiver_full_jid); + Session session = new Session.initiate_sent(stream, sid ?? random_uuid(), my_jid, receiver_full_jid); session.terminated.connect((session, stream, _1, _2, _3) => { stream.get_flag(Flag.IDENTITY).remove_session(session.sid); }); foreach (Content content in contents) { diff --git a/xmpp-vala/src/module/xep/0167_jingle_rtp/jingle_rtp_module.vala b/xmpp-vala/src/module/xep/0167_jingle_rtp/jingle_rtp_module.vala index 3a9ea09f..3adad114 100644 --- a/xmpp-vala/src/module/xep/0167_jingle_rtp/jingle_rtp_module.vala +++ b/xmpp-vala/src/module/xep/0167_jingle_rtp/jingle_rtp_module.vala @@ -26,7 +26,7 @@ public abstract class Module : XmppStreamModule { public abstract Stream create_stream(Jingle.Content content); public abstract void close_stream(Stream stream); - public async Jingle.Session start_call(XmppStream stream, Jid receiver_full_jid, bool video) throws Jingle.Error { + public async Jingle.Session start_call(XmppStream stream, Jid receiver_full_jid, bool video, string? sid = null) throws Jingle.Error { Jingle.Module jingle_module = stream.get_module(Jingle.Module.IDENTITY); @@ -72,7 +72,7 @@ public abstract class Module : XmppStreamModule { // Create session try { - Jingle.Session session = yield jingle_module.create_session(stream, contents, receiver_full_jid); + Jingle.Session session = yield jingle_module.create_session(stream, contents, receiver_full_jid, sid); return session; } catch (Jingle.Error e) { throw new Jingle.Error.GENERAL(@"Couldn't create Jingle session: $(e.message)"); diff --git a/xmpp-vala/src/module/xep/0353_jingle_message_initiation.vala b/xmpp-vala/src/module/xep/0353_jingle_message_initiation.vala index acb2ba2e..dbb6fd81 100644 --- a/xmpp-vala/src/module/xep/0353_jingle_message_initiation.vala +++ b/xmpp-vala/src/module/xep/0353_jingle_message_initiation.vala @@ -1,7 +1,7 @@ using Gee; namespace Xmpp.Xep.JingleMessageInitiation { - private const string NS_URI = "urn:xmpp:jingle-message:0"; + public const string NS_URI = "urn:xmpp:jingle-message:0"; public class Module : XmppStreamModule { public static ModuleIdentity IDENTITY = new ModuleIdentity(NS_URI, "0353_jingle_message_initiation"); @@ -11,6 +11,17 @@ namespace Xmpp.Xep.JingleMessageInitiation { public signal void session_accepted(Jid from, string sid); public signal void session_rejected(Jid from, Jid to, string sid); + public void send_session_propose_to_peer(XmppStream stream, Jid to, string sid, Gee.List descriptions) { + StanzaNode propose_node = new StanzaNode.build("propose", NS_URI).add_self_xmlns().put_attribute("id", sid, NS_URI); + foreach (StanzaNode desc_node in descriptions) { + propose_node.put_node(desc_node); + } + + MessageStanza accepted_message = new MessageStanza() { to=to }; + accepted_message.stanza.put_node(propose_node); + stream.get_module(MessageModule.IDENTITY).send_message.begin(stream, accepted_message); + } + public void send_session_accept_to_self(XmppStream stream, string sid) { MessageStanza accepted_message = new MessageStanza() { to=Bind.Flag.get_my_jid(stream).bare_jid }; accepted_message.stanza.put_node( @@ -58,7 +69,6 @@ namespace Xmpp.Xep.JingleMessageInitiation { switch (mi_node.name) { case "accept": case "proceed": - if (!message.from.equals_bare(Bind.Flag.get_my_jid(stream))) return; session_accepted(message.from, mi_node.get_attribute("id")); break; case "propose": -- cgit v1.2.3-54-g00ecf From 8d1c6c29be7018c74ec3f8ea05f5849eac5b4069 Mon Sep 17 00:00:00 2001 From: fiaxh Date: Thu, 8 Apr 2021 12:07:04 +0200 Subject: Display+store call encryption info --- libdino/src/entity/call.vala | 5 ++ libdino/src/entity/encryption.vala | 4 +- libdino/src/service/calls.vala | 44 ++++++++++++- libdino/src/service/content_item_store.vala | 4 +- libdino/src/service/database.vala | 5 +- main/data/theme.css | 25 ++++++-- main/src/ui/call_window/call_bottom_bar.vala | 53 ++++++++++++++-- .../src/ui/call_window/call_window_controller.vala | 4 ++ .../content_populator.vala | 1 + .../conversation_item_skeleton.vala | 74 +++++++++++++--------- plugins/ice/src/dtls_srtp.vala | 46 ++++++++++---- plugins/ice/src/transport_parameters.vala | 12 ++-- xmpp-vala/src/module/xep/0166_jingle/content.vala | 9 +++ .../xep/0167_jingle_rtp/content_parameters.vala | 3 + .../src/module/xep/0167_jingle_rtp/stream.vala | 2 + .../0176_jingle_ice_udp/jingle_ice_udp_module.vala | 4 +- .../0176_jingle_ice_udp/transport_parameters.vala | 47 +++++++++++--- 17 files changed, 270 insertions(+), 72 deletions(-) (limited to 'xmpp-vala/src/module/xep/0167_jingle_rtp') diff --git a/libdino/src/entity/call.vala b/libdino/src/entity/call.vala index 7891dae7..577b3ab8 100644 --- a/libdino/src/entity/call.vala +++ b/libdino/src/entity/call.vala @@ -32,6 +32,7 @@ namespace Dino.Entities { public DateTime time { get; set; } public DateTime local_time { get; set; } public DateTime end_time { get; set; } + public Encryption encryption { get; set; default=Encryption.NONE; } public State state { get; set; } @@ -57,6 +58,7 @@ namespace Dino.Entities { time = new DateTime.from_unix_utc(row[db.call.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]); + encryption = (Encryption) row[db.call.encryption]; state = (State) row[db.call.state]; notify.connect(on_update); @@ -74,6 +76,7 @@ namespace Dino.Entities { .value(db.call.direction, direction) .value(db.call.time, (long) 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 if (end_time != null) { 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; case "end-time": 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": // No point in persisting states that can't survive a restart if (state == State.RINGING || state == State.ESTABLISHING || state == State.IN_PROGRESS) return; diff --git a/libdino/src/entity/encryption.vala b/libdino/src/entity/encryption.vala index b50556f9..25d55eb1 100644 --- a/libdino/src/entity/encryption.vala +++ b/libdino/src/entity/encryption.vala @@ -3,7 +3,9 @@ namespace Dino.Entities { public enum Encryption { NONE, PGP, - OMEMO + OMEMO, + DTLS_SRTP, + SRTP, } } \ No newline at end of file diff --git a/libdino/src/service/calls.vala b/libdino/src/service/calls.vala index 93636c03..b457c764 100644 --- a/libdino/src/service/calls.vala +++ b/libdino/src/service/calls.vala @@ -14,6 +14,7 @@ namespace Dino { public signal void counterpart_ringing(Call call); 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 encryption_updated(Call call, Xep.Jingle.ContentEncryption? encryption); public signal void stream_created(Call call, string media); @@ -22,7 +23,6 @@ namespace Dino { private StreamInteractor stream_interactor; private Database db; - private Xep.JingleRtp.SessionInfoType session_info_type; private HashMap> sid_by_call = new HashMap>(Account.hash_func, Account.equals_func); private HashMap> call_by_sid = new HashMap>(Account.hash_func, Account.equals_func); @@ -38,7 +38,10 @@ namespace Dino { private HashMap audio_content_parameter = new HashMap(Call.hash_func, Call.equals_func); private HashMap video_content_parameter = new HashMap(Call.hash_func, Call.equals_func); + private HashMap audio_content = new HashMap(Call.hash_func, Call.equals_func); private HashMap video_content = new HashMap(Call.hash_func, Call.equals_func); + private HashMap video_encryption = new HashMap(Call.hash_func, Call.equals_func); + private HashMap audio_encryption = new HashMap(Call.hash_func, Call.equals_func); public static void start(StreamInteractor stream_interactor, Database db) { Calls m = new Calls(stream_interactor, db); @@ -290,7 +293,7 @@ namespace Dino { } // 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_call[account].counterpart.equals_bare(session.peer_full_jid) && jmi_video[account] == counterpart_wants_video; @@ -365,6 +368,7 @@ namespace Dino { if (call.state == Call.State.RINGING || call.state == Call.State.ESTABLISHING) { 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) { @@ -429,6 +433,7 @@ namespace Dino { private void connect_content_signals(Call call, Xep.Jingle.Content content, Xep.JingleRtp.Parameters rtp_content_parameter) { if (rtp_content_parameter.media == "audio") { + audio_content[call] = content; audio_content_parameter[call] = rtp_content_parameter; } else if (rtp_content_parameter.media == "video") { video_content[call] = content; @@ -450,6 +455,36 @@ namespace Dino { 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) { @@ -465,7 +500,10 @@ namespace Dino { audio_content_parameter.unset(call); video_content_parameter.unset(call); + audio_content.unset(call); video_content.unset(call); + audio_encryption.unset(call); + video_encryption.unset(call); } 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 // We proposed the call 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_sid.unset(account); jmi_video.unset(account); diff --git a/libdino/src/service/content_item_store.vala b/libdino/src/service/content_item_store.vala index cde8dd10..6ab0529c 100644 --- a/libdino/src/service/content_item_store.vala +++ b/libdino/src/service/content_item_store.vala @@ -316,10 +316,12 @@ public class CallItem : ContentItem { public Conversation conversation; 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.conversation = conversation; + + call.bind_property("encryption", this, "encryption"); } } diff --git a/libdino/src/service/database.vala b/libdino/src/service/database.vala index 98e18d16..9703260a 100644 --- a/libdino/src/service/database.vala +++ b/libdino/src/service/database.vala @@ -7,7 +7,7 @@ using Dino.Entities; namespace Dino { public class Database : Qlite.Database { - private const int VERSION = 20; + private const int VERSION = 21; public class AccountTable : Table { public Column id = new Column.Integer("id") { primary_key = true, auto_increment = true }; @@ -165,11 +165,12 @@ public class Database : Qlite.Database { public Column time = new Column.Long("time") { not_null = true }; public Column local_time = new Column.Long("local_time") { not_null = true }; public Column end_time = new Column.Long("end_time"); + public Column encryption = new Column.Integer("encryption") { min_version=21 }; public Column state = new Column.Integer("state"); internal CallTable(Database db) { 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}); } } diff --git a/main/data/theme.css b/main/data/theme.css index 423cbf68..454bd2c1 100644 --- a/main/data/theme.css +++ b/main/data/theme.css @@ -235,17 +235,24 @@ box.dino-input-error label.input-status-highlight-once { outline: 0; border-radius: 1000px; } + .dino-call-window button.white-button { color: #1d1c1d; - background: rgba(255,255,255,0.9); + background: rgba(255,255,255,0.85); border: lightgrey; } +.dino-call-window button.white-button:hover { + background: rgba(255,255,255,1); +} .dino-call-window button.transparent-white-button { color: white; background: rgba(255,255,255,0.15); 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 { border-radius: 1000px; @@ -265,11 +272,21 @@ box.dino-input-error label.input-status-highlight-once { margin: 0; } -.dino-call-window .unencrypted-box { - color: @error_color; - padding: 10px; +.dino-call-window .encryption-box { + color: rgba(255,255,255,0.7); border-radius: 5px; 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 { diff --git a/main/src/ui/call_window/call_bottom_bar.vala b/main/src/ui/call_window/call_bottom_bar.vala index bc800485..c6375ea2 100644 --- a/main/src/ui/call_window/call_bottom_bar.vala +++ b/main/src/ui/call_window/call_bottom_bar.vala @@ -1,5 +1,6 @@ using Dino.Entities; using Gtk; +using Pango; 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 }; 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 Stack stack = new Stack() { visible=true }; @@ -31,11 +36,9 @@ public class Dino.Ui.CallBottomBar : Gtk.Box { Object(orientation:Orientation.HORIZONTAL, spacing:0); 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_image.tooltip_text = _("Unencrypted"); - encryption_image.get_style_context().add_class("unencrypted-box"); - - default_control.add_overlay(encryption_image); + 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 }; @@ -87,6 +90,33 @@ public class Dino.Ui.CallBottomBar : Gtk.Box { 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("This call is end-to-end encrypted.") { 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("" + format_fingerprint(encryption.peer_key) + "") { 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("" + format_fingerprint(encryption.our_key) + "") { 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) { audio_settings_button.visible = show; 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() { - 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; } } \ No newline at end of file diff --git a/main/src/ui/call_window/call_window_controller.vala b/main/src/ui/call_window/call_window_controller.vala index 09c8f88c..f66a37e1 100644 --- a/main/src/ui/call_window/call_window_controller.vala +++ b/main/src/ui/call_window/call_window_controller.vala @@ -67,6 +67,10 @@ public class Dino.Ui.CallWindowController : Object { 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) => { if (width == 0 || height == 0) return; diff --git a/main/src/ui/conversation_content_view/content_populator.vala b/main/src/ui/conversation_content_view/content_populator.vala index d7ce9ce5..ef859bde 100644 --- a/main/src/ui/conversation_content_view/content_populator.vala +++ b/main/src/ui/conversation_content_view/content_populator.vala @@ -88,6 +88,7 @@ public abstract class ContentMetaItem : Plugins.MetaConversationItem { this.mark = content_item.mark; content_item.bind_property("mark", this, "mark"); + content_item.bind_property("encryption", this, "encryption"); this.can_merge = true; this.requires_avatar = true; diff --git a/main/src/ui/conversation_content_view/conversation_item_skeleton.vala b/main/src/ui/conversation_content_view/conversation_item_skeleton.vala index c0099bf4..bcb6864e 100644 --- a/main/src/ui/conversation_content_view/conversation_item_skeleton.vala +++ b/main/src/ui/conversation_content_view/conversation_item_skeleton.vala @@ -104,7 +104,7 @@ public class ItemMetaDataHeader : Box { [GtkChild] public Label dot_label; [GtkChild] public Label time_label; 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); @@ -124,50 +124,66 @@ public class ItemMetaDataHeader : Box { update_name_label(); name_label.style_updated.connect(update_name_label); + conversation.notify["encryption"].connect(update_unencrypted_icon); + item.notify["encryption"].connect(update_encryption_icon); + update_encryption_icon(); + + this.add(received_image); + + if (item.time != null) { + update_time(); + } + + item.bind_property("mark", this, "item-mark"); + this.notify["item-mark"].connect_after(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 (ci != null) { + 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) { - 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); - } + 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) { - conversation.notify["encryption"].connect(update_unencrypted_icon); update_unencrypted_icon(); } + } - this.add(received_image); - - if (item.time != null) { - update_time(); + private void update_unencrypted_icon() { + if (item.encryption != Encryption.NONE) return; + + if (conversation.encryption != Encryption.NONE && encryption_image == null) { + Image image = new Image() { opacity=0.4, visible = true }; + image.set_from_icon_name("dino-changes-allowed-symbolic", ICON_SIZE_HEADER); + image.tooltip_text = _("Unencrypted"); + update_encryption_image(image); + Util.force_error_color(image); + } else if (conversation.encryption == Encryption.NONE && encryption_image != null) { + update_encryption_image(null); } - - item.bind_property("mark", this, "item-mark"); - this.notify["item-mark"].connect_after(update_received_mark); - update_received_mark(); } - private void update_unencrypted_icon() { - if (conversation.encryption != Encryption.NONE && unencrypted_image == null) { - unencrypted_image = new Image() { opacity=0.4, visible = true }; - unencrypted_image.set_from_icon_name("dino-changes-allowed-symbolic", ICON_SIZE_HEADER); - unencrypted_image.tooltip_text = _("Unencrypted"); - this.add(unencrypted_image); - this.reorder_child(unencrypted_image, 3); - Util.force_error_color(unencrypted_image); - } else if (conversation.encryption == Encryption.NONE && unencrypted_image != null) { - this.remove(unencrypted_image); - unencrypted_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; } } diff --git a/plugins/ice/src/dtls_srtp.vala b/plugins/ice/src/dtls_srtp.vala index f294e66b..e2470cf6 100644 --- a/plugins/ice/src/dtls_srtp.vala +++ b/plugins/ice/src/dtls_srtp.vala @@ -10,7 +10,10 @@ public class DtlsSrtp { private Mutex buffer_mutex = new Mutex(); private Gee.LinkedList buffer_queue = new Gee.LinkedList(); 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(); @@ -20,12 +23,13 @@ public class DtlsSrtp { return obj; } - internal string get_own_fingerprint(DigestAlgorithm digest_algo) { - return format_certificate(own_cert[0], digest_algo); + internal uint8[] get_own_fingerprint(DigestAlgorithm 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_fp_algo = digest_algo; } public uint8[] process_incoming_data(uint component_id, uint8[] data) { @@ -94,10 +98,11 @@ public class DtlsSrtp { cert.sign(cert, private_key); + own_fingerprint = get_fingerprint(cert, DigestAlgorithm.SHA256); 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; 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_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) { @@ -226,24 +232,40 @@ public class DtlsSrtp { X509.Certificate peer_cert = X509.Certificate.create(); peer_cert.import(ref cert_datums[0], CertificateFormat.DER); - string peer_fp_str = format_certificate(peer_cert, DigestAlgorithm.SHA256); - 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); + uint8[] real_peer_fp = get_fingerprint(peer_cert, peer_fp_algo); + + 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; } + 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; } - 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]; size_t buf_out_size = 512; 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++) { - sb.append("%02x".printf(buf[i])); - if (i < buf_out_size - 1) { + ret[i] = buf[i]; + } + 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(":"); } } diff --git a/plugins/ice/src/transport_parameters.vala b/plugins/ice/src/transport_parameters.vala index 2db1ab1b..f95be261 100644 --- a/plugins/ice/src/transport_parameters.vala +++ b/plugins/ice/src/transport_parameters.vala @@ -68,9 +68,11 @@ public class Dino.Plugins.Ice.TransportParameters : JingleIceUdp.IceUdpTransport dtls_srtp = setup_dtls(this); this.own_fingerprint = dtls_srtp.get_own_fingerprint(GnuTLS.DigestAlgorithm.SHA256); 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 { - 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); 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 { dtls_srtp = null; } @@ -205,7 +207,9 @@ public class Dino.Plugins.Ice.TransportParameters : JingleIceUdp.IceUdpTransport if (incoming && dtls_srtp != null) { Jingle.DatagramConnection rtp_datagram = (Jingle.DatagramConnection) content.get_transport_connection(1); 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); diff --git a/xmpp-vala/src/module/xep/0166_jingle/content.vala b/xmpp-vala/src/module/xep/0166_jingle/content.vala index 67c13dd8..bce03a7b 100644 --- a/xmpp-vala/src/module/xep/0166_jingle/content.vala +++ b/xmpp-vala/src/module/xep/0166_jingle/content.vala @@ -34,6 +34,8 @@ public class Xmpp.Xep.Jingle.Content : Object { public weak Session session; public Map component_connections = new HashMap(); // TODO private + public ContentEncryption? encryption { get; set; } + // INITIATE_SENT | INITIATE_RECEIVED | CONNECTING public Set tried_transport_methods = new HashSet(); @@ -233,4 +235,11 @@ public class Xmpp.Xep.Jingle.Content : Object { public void send_transport_info(StanzaNode 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; } } \ No newline at end of file diff --git a/xmpp-vala/src/module/xep/0167_jingle_rtp/content_parameters.vala b/xmpp-vala/src/module/xep/0167_jingle_rtp/content_parameters.vala index ff3d31f4..ac65f88c 100644 --- a/xmpp-vala/src/module/xep/0167_jingle_rtp/content_parameters.vala +++ b/xmpp-vala/src/module/xep/0167_jingle_rtp/content_parameters.vala @@ -116,6 +116,9 @@ public class Xmpp.Xep.JingleRtp.Parameters : Jingle.ContentParameters, Object { remote_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); rtp_datagram.datagram_received.connect(this.stream.on_recv_rtp_data); diff --git a/xmpp-vala/src/module/xep/0167_jingle_rtp/stream.vala b/xmpp-vala/src/module/xep/0167_jingle_rtp/stream.vala index 730ce9f8..adae11f5 100644 --- a/xmpp-vala/src/module/xep/0167_jingle_rtp/stream.vala +++ b/xmpp-vala/src/module/xep/0167_jingle_rtp/stream.vala @@ -1,5 +1,7 @@ public abstract class Xmpp.Xep.JingleRtp.Stream : Object { + public Jingle.Content content { get; protected set; } + public string name { get { return content.content_name; }} diff --git a/xmpp-vala/src/module/xep/0176_jingle_ice_udp/jingle_ice_udp_module.vala b/xmpp-vala/src/module/xep/0176_jingle_ice_udp/jingle_ice_udp_module.vala index 4b7c7a36..5211e3a9 100644 --- a/xmpp-vala/src/module/xep/0176_jingle_ice_udp/jingle_ice_udp_module.vala +++ b/xmpp-vala/src/module/xep/0176_jingle_ice_udp/jingle_ice_udp_module.vala @@ -5,6 +5,7 @@ using Xmpp; namespace Xmpp.Xep.JingleIceUdp { 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 static Xmpp.ModuleIdentity IDENTITY = new Xmpp.ModuleIdentity(NS_URI, "0176_jingle_ice_udp"); @@ -12,10 +13,11 @@ public abstract class Module : XmppStreamModule, Jingle.Transport { public override void attach(XmppStream stream) { 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, "urn:xmpp:jingle:apps:dtls:0"); + stream.get_module(ServiceDiscovery.Module.IDENTITY).add_feature(stream, DTLS_NS_URI); } 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, DTLS_NS_URI); } public override string get_ns() { return NS_URI; } diff --git a/xmpp-vala/src/module/xep/0176_jingle_ice_udp/transport_parameters.vala b/xmpp-vala/src/module/xep/0176_jingle_ice_udp/transport_parameters.vala index 3c69d0af..f194f06d 100644 --- a/xmpp-vala/src/module/xep/0176_jingle_ice_udp/transport_parameters.vala +++ b/xmpp-vala/src/module/xep/0176_jingle_ice_udp/transport_parameters.vala @@ -13,8 +13,9 @@ public abstract class Xmpp.Xep.JingleIceUdp.IceUdpTransportParameters : Jingle.T public ConcurrentList unsent_local_candidates = new ConcurrentList(Candidate.equals_func); public Gee.List remote_candidates = new ArrayList(Candidate.equals_func); - public string? own_fingerprint = null; - public string? peer_fingerprint = null; + public uint8[]? own_fingerprint = null; + public uint8[]? peer_fingerprint = null; + public string? peer_fp_algo = null; public Jid local_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; } 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) { this.components_ = components; @@ -38,9 +39,10 @@ public abstract class Xmpp.Xep.JingleIceUdp.IceUdpTransportParameters : Jingle.T 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) { - 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); 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() .put_attribute("hash", "sha-256") - .put_node(new StanzaNode.text(own_fingerprint)); + .put_node(new StanzaNode.text(format_fingerprint(own_fingerprint))); if (incoming) { fingerprint_node.put_attribute("setup", "active"); } else { @@ -95,9 +97,10 @@ public abstract class Xmpp.Xep.JingleIceUdp.IceUdpTransportParameters : Jingle.T 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) { - 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()); } } + + + + 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; + } } \ No newline at end of file -- cgit v1.2.3-54-g00ecf From fbc10c2023a4c2b874f87940f0a71bc0d8d7b57d Mon Sep 17 00:00:00 2001 From: Marvin W Date: Fri, 9 Apr 2021 22:23:53 +0200 Subject: DTLS-SRTP: Wait for setup finish and handle setup=passive --- plugins/crypto-vala/src/srtp.vala | 4 +- plugins/ice/src/dtls_srtp.vala | 162 ++++++++++++++------- plugins/ice/src/transport_parameters.vala | 65 +++++---- .../xep/0167_jingle_rtp/content_parameters.vala | 3 + .../0176_jingle_ice_udp/transport_parameters.vala | 16 +- 5 files changed, 159 insertions(+), 91 deletions(-) (limited to 'xmpp-vala/src/module/xep/0167_jingle_rtp') diff --git a/plugins/crypto-vala/src/srtp.vala b/plugins/crypto-vala/src/srtp.vala index 77b5acde..493afdb0 100644 --- a/plugins/crypto-vala/src/srtp.vala +++ b/plugins/crypto-vala/src/srtp.vala @@ -6,8 +6,8 @@ public class Crypto.Srtp { public const string F8_128_HMAC_SHA1_80 = "F8_128_HMAC_SHA1_80"; public class Session { - public bool has_encrypt { get; private set; } - public bool has_decrypt { get; private set; } + public bool has_encrypt { get; private set; default = false; } + public bool has_decrypt { get; private set; default = false; } private Context encrypt_context; private Context decrypt_context; diff --git a/plugins/ice/src/dtls_srtp.vala b/plugins/ice/src/dtls_srtp.vala index e2470cf6..8a9b5dfa 100644 --- a/plugins/ice/src/dtls_srtp.vala +++ b/plugins/ice/src/dtls_srtp.vala @@ -1,9 +1,26 @@ using GnuTLS; -public class DtlsSrtp { +namespace Dino.Plugins.Ice.DtlsSrtp { + +public static Handler setup() throws GLib.Error { + var obj = new Handler(); + obj.generate_credentials(); + return obj; +} + +public class Handler { public signal void send_data(uint8[] data); + public bool ready { get { + return srtp_session.has_encrypt && srtp_session.has_decrypt; + }} + + public Mode mode { get; set; default = Mode.CLIENT; } + public uint8[] own_fingerprint { get; private set; } + public uint8[] peer_fingerprint { get; set; } + public string peer_fp_algo { get; set; } + private X509.Certificate[] own_cert; private X509.PrivateKey private_key; private Cond buffer_cond = new Cond(); @@ -11,27 +28,12 @@ public class DtlsSrtp { private Gee.LinkedList buffer_queue = new Gee.LinkedList(); private uint pull_timeout = uint.MAX; - private DigestAlgorithm? peer_fp_algo = null; - private uint8[] peer_fingerprint = null; - private uint8[] own_fingerprint; + private bool running = false; + private bool stop = false; + private bool restart = false; private Crypto.Srtp.Session srtp_session = new Crypto.Srtp.Session(); - public static DtlsSrtp setup() throws GLib.Error { - var obj = new DtlsSrtp(); - obj.generate_credentials(); - return obj; - } - - internal uint8[] get_own_fingerprint(DigestAlgorithm digest_algo) { - return own_fingerprint; - } - - public void set_peer_fingerprint(uint8[] fingerprint, DigestAlgorithm digest_algo) { - this.peer_fingerprint = fingerprint; - this.peer_fp_algo = digest_algo; - } - public uint8[] process_incoming_data(uint component_id, uint8[] data) { if (srtp_session.has_decrypt) { try { @@ -77,7 +79,7 @@ public class DtlsSrtp { buffer_mutex.unlock(); } - private void generate_credentials() throws GLib.Error { + internal void generate_credentials() throws GLib.Error { int err = 0; private_key = X509.PrivateKey.create(); @@ -102,8 +104,29 @@ public class DtlsSrtp { own_cert = new X509.Certificate[] { (owned)cert }; } - public async Xmpp.Xep.Jingle.ContentEncryption setup_dtls_connection(bool server) { - InitFlags server_or_client = server ? InitFlags.SERVER : InitFlags.CLIENT; + public void stop_dtls_connection() { + buffer_mutex.lock(); + stop = true; + buffer_cond.signal(); + buffer_mutex.unlock(); + } + + public async Xmpp.Xep.Jingle.ContentEncryption? setup_dtls_connection() { + buffer_mutex.lock(); + if (stop) { + restart = true; + buffer_mutex.unlock(); + return null; + } + if (running || ready) { + buffer_mutex.unlock(); + return null; + } + running = true; + restart = false; + buffer_mutex.unlock(); + + InitFlags server_or_client = mode == Mode.SERVER ? InitFlags.SERVER : InitFlags.CLIENT; debug("Setting up DTLS connection. We're %s", server_or_client.to_string()); CertificateCredentials cert_cred = CertificateCredentials.create(); @@ -131,7 +154,7 @@ public class DtlsSrtp { DateTime current_time = new DateTime.now_utc(); if (maximum_time.compare(current_time) < 0) { warning("DTLS handshake timeouted"); - return -1; + return ErrorCode.APPLICATION_ERROR_MIN + 1; } } while (err < 0 && !((ErrorCode)err).is_fatal()); Idle.add(setup_dtls_connection.callback); @@ -139,6 +162,17 @@ public class DtlsSrtp { }); yield; err = thread.join(); + buffer_mutex.lock(); + if (stop) { + stop = false; + running = false; + bool restart = restart; + buffer_mutex.unlock(); + if (restart) return yield setup_dtls_connection(); + return null; + } + buffer_mutex.unlock(); + throw_if_error(err); uint8[] km = new uint8[150]; Datum? client_key, client_salt, server_key, server_salt; @@ -147,7 +181,8 @@ public class DtlsSrtp { warning("SRTP client/server key/salt null"); } - if (server) { + debug("Finished DTLS connection. We're %s", server_or_client.to_string()); + if (mode == Mode.SERVER) { srtp_session.set_encryption_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, client_key.extract(), client_salt.extract()); } else { @@ -158,24 +193,28 @@ public class DtlsSrtp { } private static ssize_t pull_function(void* transport_ptr, uint8[] buffer) { - DtlsSrtp self = transport_ptr as DtlsSrtp; + Handler self = transport_ptr as Handler; self.buffer_mutex.lock(); while (self.buffer_queue.size == 0) { self.buffer_cond.wait(self.buffer_mutex); + if (self.stop) { + self.buffer_mutex.unlock(); + return -1; + } } - owned Bytes data = self.buffer_queue.remove_at(0); + Bytes data = self.buffer_queue.remove_at(0); self.buffer_mutex.unlock(); - uint8[] data_uint8 = Bytes.unref_to_data(data); + uint8[] data_uint8 = Bytes.unref_to_data((owned) data); Memory.copy(buffer, data_uint8, data_uint8.length); // The callback should return 0 on connection termination, a positive number indicating the number of bytes received, and -1 on error. - return (ssize_t)data.length; + return (ssize_t)data_uint8.length; } private static int pull_timeout_function(void* transport_ptr, uint ms) { - DtlsSrtp self = transport_ptr as DtlsSrtp; + Handler self = transport_ptr as Handler; DateTime current_time = new DateTime.now_utc(); current_time.add_seconds(ms/1000); @@ -184,6 +223,10 @@ public class DtlsSrtp { self.buffer_mutex.lock(); while (self.buffer_queue.size == 0) { self.buffer_cond.wait_until(self.buffer_mutex, end_time); + if (self.stop) { + self.buffer_mutex.unlock(); + return -1; + } DateTime new_current_time = new DateTime.now_utc(); if (new_current_time.compare(current_time) > 0) { @@ -197,7 +240,7 @@ public class DtlsSrtp { } private static ssize_t push_function(void* transport_ptr, uint8[] buffer) { - DtlsSrtp self = transport_ptr as DtlsSrtp; + Handler self = transport_ptr as Handler; self.send_data(buffer); // The callback should return a positive number indicating the bytes sent, and -1 on error. @@ -205,7 +248,7 @@ public class DtlsSrtp { } private static int verify_function(Session session) { - DtlsSrtp self = session.get_transport_pointer() as DtlsSrtp; + Handler self = session.get_transport_pointer() as Handler; try { bool valid = self.verify_peer_cert(session); if (!valid) { @@ -232,7 +275,17 @@ public class DtlsSrtp { X509.Certificate peer_cert = X509.Certificate.create(); peer_cert.import(ref cert_datums[0], CertificateFormat.DER); - uint8[] real_peer_fp = get_fingerprint(peer_cert, peer_fp_algo); + DigestAlgorithm algo; + switch (peer_fp_algo) { + case "sha-256": + algo = DigestAlgorithm.SHA256; + break; + default: + warning("Unkown peer fingerprint algorithm: %s", peer_fp_algo); + return false; + } + + uint8[] real_peer_fp = get_fingerprint(peer_cert, algo); if (real_peer_fp.length != this.peer_fingerprint.length) { warning("Fingerprint lengths not equal %i vs %i", real_peer_fp.length, peer_fingerprint.length); @@ -248,27 +301,34 @@ public class DtlsSrtp { return true; } +} - private uint8[] get_fingerprint(X509.Certificate certificate, DigestAlgorithm digest_algo) { - uint8[] buf = new uint8[512]; - size_t buf_out_size = 512; - certificate.get_fingerprint(digest_algo, buf, ref buf_out_size); +private uint8[] get_fingerprint(X509.Certificate certificate, DigestAlgorithm digest_algo) { + uint8[] buf = new uint8[512]; + size_t buf_out_size = 512; + certificate.get_fingerprint(digest_algo, buf, ref buf_out_size); - uint8[] ret = new uint8[buf_out_size]; - for (int i = 0; i < buf_out_size; i++) { - ret[i] = buf[i]; - } - return ret; + uint8[] ret = new uint8[buf_out_size]; + for (int i = 0; i < buf_out_size; i++) { + ret[i] = buf[i]; } - - 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 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; } -} \ No newline at end of file + return sb.str; +} + + +public enum Mode { + CLIENT, SERVER +} + +} diff --git a/plugins/ice/src/transport_parameters.vala b/plugins/ice/src/transport_parameters.vala index f95be261..e4862edc 100644 --- a/plugins/ice/src/transport_parameters.vala +++ b/plugins/ice/src/transport_parameters.vala @@ -9,11 +9,11 @@ public class Dino.Plugins.Ice.TransportParameters : JingleIceUdp.IceUdpTransport private bool we_want_connection; private bool remote_credentials_set; private Map connections = new HashMap(); - private DtlsSrtp? dtls_srtp; + private DtlsSrtp.Handler? dtls_srtp_handler; private class DatagramConnection : Jingle.DatagramConnection { private Nice.Agent agent; - private DtlsSrtp? dtls_srtp; + private DtlsSrtp.Handler? dtls_srtp_handler; private uint stream_id; private string? error; private ulong sent; @@ -22,9 +22,9 @@ public class Dino.Plugins.Ice.TransportParameters : JingleIceUdp.IceUdpTransport private ulong recv_reported; private ulong datagram_received_id; - public DatagramConnection(Nice.Agent agent, DtlsSrtp? dtls_srtp, uint stream_id, uint8 component_id) { + public DatagramConnection(Nice.Agent agent, DtlsSrtp.Handler? dtls_srtp_handler, uint stream_id, uint8 component_id) { this.agent = agent; - this.dtls_srtp = dtls_srtp; + this.dtls_srtp_handler = dtls_srtp_handler; this.stream_id = stream_id; this.component_id = component_id; this.datagram_received_id = this.datagram_received.connect((datagram) => { @@ -45,8 +45,8 @@ public class Dino.Plugins.Ice.TransportParameters : JingleIceUdp.IceUdpTransport public override void send_datagram(Bytes datagram) { if (this.agent != null && is_component_ready(agent, stream_id, component_id)) { uint8[] encrypted_data = null; - if (dtls_srtp != null) { - encrypted_data = dtls_srtp.process_outgoing_data(component_id, datagram.get_data()); + if (dtls_srtp_handler != null) { + encrypted_data = dtls_srtp_handler.process_outgoing_data(component_id, datagram.get_data()); if (encrypted_data == null) return; } agent.send(stream_id, component_id, encrypted_data ?? datagram.get_data()); @@ -65,13 +65,18 @@ public class Dino.Plugins.Ice.TransportParameters : JingleIceUdp.IceUdpTransport this.agent = agent; if (this.peer_fingerprint != null || !incoming) { - dtls_srtp = setup_dtls(this); - this.own_fingerprint = dtls_srtp.get_own_fingerprint(GnuTLS.DigestAlgorithm.SHA256); + dtls_srtp_handler = setup_dtls(this); + own_fingerprint = dtls_srtp_handler.own_fingerprint; if (incoming) { - dtls_srtp.set_peer_fingerprint(this.peer_fingerprint, this.peer_fp_algo == "sha-256" ? GnuTLS.DigestAlgorithm.SHA256 : GnuTLS.DigestAlgorithm.NULL); + own_setup = "active"; + dtls_srtp_handler.mode = DtlsSrtp.Mode.CLIENT; + dtls_srtp_handler.peer_fingerprint = peer_fingerprint; + dtls_srtp_handler.peer_fp_algo = peer_fp_algo; } else { - dtls_srtp.setup_dtls_connection.begin(true, (_, res) => { - this.content.encryption = dtls_srtp.setup_dtls_connection.end(res); + own_setup = "actpass"; + dtls_srtp_handler.mode = DtlsSrtp.Mode.SERVER; + dtls_srtp_handler.setup_dtls_connection.begin((_, res) => { + this.content.encryption = dtls_srtp_handler.setup_dtls_connection.end(res) ?? this.content.encryption; }); } } @@ -104,9 +109,9 @@ public class Dino.Plugins.Ice.TransportParameters : JingleIceUdp.IceUdpTransport agent.gather_candidates(stream_id); } - private static DtlsSrtp setup_dtls(TransportParameters tp) { + private static DtlsSrtp.Handler setup_dtls(TransportParameters tp) { var weak_self = new WeakRef(tp); - DtlsSrtp dtls_srtp = DtlsSrtp.setup(); + DtlsSrtp.Handler dtls_srtp = DtlsSrtp.setup(); dtls_srtp.send_data.connect((data) => { TransportParameters self = (TransportParameters) weak_self.get(); if (self != null) self.agent.send(self.stream_id, 1, data); @@ -144,10 +149,15 @@ public class Dino.Plugins.Ice.TransportParameters : JingleIceUdp.IceUdpTransport debug("on_transport_accept from %s", peer_full_jid.to_string()); base.handle_transport_accept(transport); - if (dtls_srtp != null && peer_fingerprint != null) { - dtls_srtp.set_peer_fingerprint(this.peer_fingerprint, this.peer_fp_algo == "sha-256" ? GnuTLS.DigestAlgorithm.SHA256 : GnuTLS.DigestAlgorithm.NULL); + if (dtls_srtp_handler != null && peer_fingerprint != null) { + dtls_srtp_handler.peer_fingerprint = peer_fingerprint; + dtls_srtp_handler.peer_fp_algo = peer_fp_algo; + if (peer_setup == "passive") { + dtls_srtp_handler.mode = DtlsSrtp.Mode.CLIENT; + dtls_srtp_handler.stop_dtls_connection(); + } } else { - dtls_srtp = null; + dtls_srtp_handler = null; } } @@ -200,18 +210,10 @@ public class Dino.Plugins.Ice.TransportParameters : JingleIceUdp.IceUdpTransport int new_candidates = agent.set_remote_candidates(stream_id, i, candidates); debug("Initiated component %u with %i remote candidates", i, new_candidates); - connections[i] = new DatagramConnection(agent, dtls_srtp, stream_id, i); + connections[i] = new DatagramConnection(agent, dtls_srtp_handler, stream_id, i); content.set_transport_connection(connections[i], i); } - if (incoming && dtls_srtp != null) { - Jingle.DatagramConnection rtp_datagram = (Jingle.DatagramConnection) content.get_transport_connection(1); - rtp_datagram.notify["ready"].connect(() => { - dtls_srtp.setup_dtls_connection.begin(false, (_, res) => { - this.content.encryption = dtls_srtp.setup_dtls_connection.end(res); - }); - }); - } base.create_transport_connection(stream, content); } @@ -219,11 +221,16 @@ public class Dino.Plugins.Ice.TransportParameters : JingleIceUdp.IceUdpTransport if (stream_id != this.stream_id) return; debug("stream %u component %u state changed to %s", stream_id, component_id, agent.get_component_state(stream_id, component_id).to_string()); may_consider_ready(stream_id, component_id); + if (incoming && dtls_srtp_handler != null && !dtls_srtp_handler.ready && is_component_ready(agent, stream_id, component_id) && dtls_srtp_handler.mode == DtlsSrtp.Mode.CLIENT) { + dtls_srtp_handler.setup_dtls_connection.begin((_, res) => { + this.content.encryption = dtls_srtp_handler.setup_dtls_connection.end(res) ?? this.content.encryption; + }); + } } private void may_consider_ready(uint stream_id, uint component_id) { if (stream_id != this.stream_id) return; - if (connections.has_key((uint8) component_id) && is_component_ready(agent, stream_id, component_id) && connections.has_key((uint8) component_id) && !connections[(uint8)component_id].ready) { + if (connections.has_key((uint8) component_id) && !connections[(uint8)component_id].ready && is_component_ready(agent, stream_id, component_id) && (dtls_srtp_handler == null || dtls_srtp_handler.ready)) { connections[(uint8)component_id].ready = true; } } @@ -241,8 +248,8 @@ public class Dino.Plugins.Ice.TransportParameters : JingleIceUdp.IceUdpTransport private void on_recv(Nice.Agent agent, uint stream_id, uint component_id, uint8[] data) { if (stream_id != this.stream_id) return; uint8[] decrypt_data = null; - if (dtls_srtp != null) { - decrypt_data = dtls_srtp.process_incoming_data(component_id, data); + if (dtls_srtp_handler != null) { + decrypt_data = dtls_srtp_handler.process_incoming_data(component_id, data); if (decrypt_data == null) return; } may_consider_ready(stream_id, component_id); @@ -317,4 +324,4 @@ public class Dino.Plugins.Ice.TransportParameters : JingleIceUdp.IceUdpTransport return candidate; } -} \ No newline at end of file +} diff --git a/xmpp-vala/src/module/xep/0167_jingle_rtp/content_parameters.vala b/xmpp-vala/src/module/xep/0167_jingle_rtp/content_parameters.vala index ac65f88c..c37c19cc 100644 --- a/xmpp-vala/src/module/xep/0167_jingle_rtp/content_parameters.vala +++ b/xmpp-vala/src/module/xep/0167_jingle_rtp/content_parameters.vala @@ -94,6 +94,9 @@ public class Xmpp.Xep.JingleRtp.Parameters : Jingle.ContentParameters, Object { ulong rtp_ready_handler_id = 0; rtp_ready_handler_id = rtp_datagram.notify["ready"].connect(() => { this.stream.on_rtp_ready(); + if (rtcp_mux) { + this.stream.on_rtcp_ready(); + } connection_ready(); rtp_datagram.disconnect(rtp_ready_handler_id); diff --git a/xmpp-vala/src/module/xep/0176_jingle_ice_udp/transport_parameters.vala b/xmpp-vala/src/module/xep/0176_jingle_ice_udp/transport_parameters.vala index f194f06d..4976f560 100644 --- a/xmpp-vala/src/module/xep/0176_jingle_ice_udp/transport_parameters.vala +++ b/xmpp-vala/src/module/xep/0176_jingle_ice_udp/transport_parameters.vala @@ -14,8 +14,10 @@ public abstract class Xmpp.Xep.JingleIceUdp.IceUdpTransportParameters : Jingle.T public Gee.List remote_candidates = new ArrayList(Candidate.equals_func); public uint8[]? own_fingerprint = null; + public string? own_setup = null; public uint8[]? peer_fingerprint = null; public string? peer_fp_algo = null; + public string? peer_setup = null; public Jid local_full_jid { get; private set; } public Jid peer_full_jid { get; private set; } @@ -41,8 +43,9 @@ public abstract class Xmpp.Xep.JingleIceUdp.IceUdpTransportParameters : Jingle.T StanzaNode? fingerprint_node = node.get_subnode("fingerprint", DTLS_NS_URI); if (fingerprint_node != null) { - peer_fingerprint = fingerprint_to_bytes(fingerprint_node.get_deep_string_content()); + peer_fingerprint = fingerprint_to_bytes(fingerprint_node.get_string_content()); peer_fp_algo = fingerprint_node.get_attribute("hash"); + peer_setup = fingerprint_node.get_attribute("setup"); } } } @@ -73,11 +76,7 @@ public abstract class Xmpp.Xep.JingleIceUdp.IceUdpTransportParameters : Jingle.T .add_self_xmlns() .put_attribute("hash", "sha-256") .put_node(new StanzaNode.text(format_fingerprint(own_fingerprint))); - if (incoming) { - fingerprint_node.put_attribute("setup", "active"); - } else { - fingerprint_node.put_attribute("setup", "actpass"); - } + fingerprint_node.put_attribute("setup", own_setup); node.put_node(fingerprint_node); } @@ -101,6 +100,7 @@ public abstract class Xmpp.Xep.JingleIceUdp.IceUdpTransportParameters : Jingle.T if (fingerprint_node != null) { peer_fingerprint = fingerprint_to_bytes(fingerprint_node.get_deep_string_content()); peer_fp_algo = fingerprint_node.get_attribute("hash"); + peer_setup = fingerprint_node.get_attribute("setup"); } } @@ -142,8 +142,6 @@ public abstract class Xmpp.Xep.JingleIceUdp.IceUdpTransportParameters : Jingle.T } } - - private string format_fingerprint(uint8[] fingerprint) { var sb = new StringBuilder(); for (int i = 0; i < fingerprint.length; i++) { @@ -167,4 +165,4 @@ public abstract class Xmpp.Xep.JingleIceUdp.IceUdpTransportParameters : Jingle.T } return bin; } -} \ No newline at end of file +} -- cgit v1.2.3-54-g00ecf From 5e11986838057a5cdbdf9d271316513da1bd4764 Mon Sep 17 00:00:00 2001 From: fiaxh Date: Fri, 9 Apr 2021 19:04:24 +0200 Subject: Fix dtls pull_timeout_function, fix cyclic references --- plugins/ice/src/dtls_srtp.vala | 10 ++++------ plugins/ice/src/transport_parameters.vala | 8 ++++++++ xmpp-vala/src/module/xep/0166_jingle/content.vala | 1 + xmpp-vala/src/module/xep/0166_jingle/session.vala | 4 +--- .../src/module/xep/0167_jingle_rtp/content_parameters.vala | 14 +++++++++----- 5 files changed, 23 insertions(+), 14 deletions(-) (limited to 'xmpp-vala/src/module/xep/0167_jingle_rtp') diff --git a/plugins/ice/src/dtls_srtp.vala b/plugins/ice/src/dtls_srtp.vala index 8a9b5dfa..e8fc01c7 100644 --- a/plugins/ice/src/dtls_srtp.vala +++ b/plugins/ice/src/dtls_srtp.vala @@ -216,9 +216,7 @@ public class Handler { private static int pull_timeout_function(void* transport_ptr, uint ms) { Handler self = transport_ptr as Handler; - DateTime current_time = new DateTime.now_utc(); - current_time.add_seconds(ms/1000); - int64 end_time = current_time.to_unix(); + int64 end_time = get_monotonic_time() + ms * 1000; self.buffer_mutex.lock(); while (self.buffer_queue.size == 0) { @@ -228,9 +226,9 @@ public class Handler { return -1; } - DateTime new_current_time = new DateTime.now_utc(); - if (new_current_time.compare(current_time) > 0) { - break; + if (get_monotonic_time() > end_time) { + self.buffer_mutex.unlock(); + return 0; } } self.buffer_mutex.unlock(); diff --git a/plugins/ice/src/transport_parameters.vala b/plugins/ice/src/transport_parameters.vala index e4862edc..f854a367 100644 --- a/plugins/ice/src/transport_parameters.vala +++ b/plugins/ice/src/transport_parameters.vala @@ -40,6 +40,7 @@ public class Dino.Plugins.Ice.TransportParameters : JingleIceUdp.IceUdpTransport yield base.terminate(we_terminated, reason_string, reason_text); this.disconnect(datagram_received_id); agent = null; + dtls_srtp_handler = null; } public override void send_datagram(Bytes datagram) { @@ -324,4 +325,11 @@ public class Dino.Plugins.Ice.TransportParameters : JingleIceUdp.IceUdpTransport return candidate; } + + public override void dispose() { + base.dispose(); + agent = null; + dtls_srtp_handler = null; + connections.clear(); + } } diff --git a/xmpp-vala/src/module/xep/0166_jingle/content.vala b/xmpp-vala/src/module/xep/0166_jingle/content.vala index bce03a7b..67510c36 100644 --- a/xmpp-vala/src/module/xep/0166_jingle/content.vala +++ b/xmpp-vala/src/module/xep/0166_jingle/content.vala @@ -107,6 +107,7 @@ public class Xmpp.Xep.Jingle.Content : Object { public void terminate(bool we_terminated, string? reason_name, string? reason_text) { content_params.terminate(we_terminated, reason_name, reason_text); + transport_params.dispose(); foreach (ComponentConnection connection in component_connections.values) { connection.terminate(we_terminated, reason_name, reason_text); diff --git a/xmpp-vala/src/module/xep/0166_jingle/session.vala b/xmpp-vala/src/module/xep/0166_jingle/session.vala index 2d359f01..5fe89415 100644 --- a/xmpp-vala/src/module/xep/0166_jingle/session.vala +++ b/xmpp-vala/src/module/xep/0166_jingle/session.vala @@ -210,9 +210,7 @@ public class Xmpp.Xep.Jingle.Session : Object { } public async void add_content(Content content) { - content.session = this; - this.contents_map[content.content_name] = content; - contents.add(content); + insert_content(content); StanzaNode content_add_node = new StanzaNode.build("jingle", NS_URI) .add_self_xmlns() diff --git a/xmpp-vala/src/module/xep/0167_jingle_rtp/content_parameters.vala b/xmpp-vala/src/module/xep/0167_jingle_rtp/content_parameters.vala index c37c19cc..d6f1acd2 100644 --- a/xmpp-vala/src/module/xep/0167_jingle_rtp/content_parameters.vala +++ b/xmpp-vala/src/module/xep/0167_jingle_rtp/content_parameters.vala @@ -84,30 +84,34 @@ public class Xmpp.Xep.JingleRtp.Parameters : Jingle.ContentParameters, Object { Jingle.DatagramConnection rtcp_datagram = (Jingle.DatagramConnection) content.get_transport_connection(2); ulong rtcp_ready_handler_id = 0; - rtcp_ready_handler_id = rtcp_datagram.notify["ready"].connect(() => { + rtcp_ready_handler_id = rtcp_datagram.notify["ready"].connect((rtcp_datagram, _) => { this.stream.on_rtcp_ready(); - rtcp_datagram.disconnect(rtcp_ready_handler_id); + ((Jingle.DatagramConnection)rtcp_datagram).disconnect(rtcp_ready_handler_id); rtcp_ready_handler_id = 0; }); ulong rtp_ready_handler_id = 0; - rtp_ready_handler_id = rtp_datagram.notify["ready"].connect(() => { + rtp_ready_handler_id = rtp_datagram.notify["ready"].connect((rtp_datagram, _) => { this.stream.on_rtp_ready(); if (rtcp_mux) { this.stream.on_rtcp_ready(); } connection_ready(); - rtp_datagram.disconnect(rtp_ready_handler_id); + ((Jingle.DatagramConnection)rtp_datagram).disconnect(rtp_ready_handler_id); rtp_ready_handler_id = 0; }); - session.notify["state"].connect((obj, _) => { + ulong session_state_handler_id = 0; + session_state_handler_id = session.notify["state"].connect((obj, _) => { Jingle.Session session2 = (Jingle.Session) obj; if (session2.state == Jingle.Session.State.ENDED) { if (rtcp_ready_handler_id != 0) rtcp_datagram.disconnect(rtcp_ready_handler_id); if (rtp_ready_handler_id != 0) rtp_datagram.disconnect(rtp_ready_handler_id); + if (session_state_handler_id != 0) { + session2.disconnect(session_state_handler_id); + } } }); -- cgit v1.2.3-54-g00ecf From 3880628de4785db4c0a03a79a0c486507fe9b1a8 Mon Sep 17 00:00:00 2001 From: Marvin W Date: Thu, 29 Apr 2021 15:46:06 +0200 Subject: Video optimizations --- cmake/FindGstRtp.cmake | 14 + plugins/rtp/CMakeLists.txt | 4 +- plugins/rtp/src/codec_util.vala | 115 +++- plugins/rtp/src/device.vala | 9 +- plugins/rtp/src/module.vala | 133 +++-- plugins/rtp/src/plugin.vala | 14 + plugins/rtp/src/stream.vala | 220 +++++++- plugins/rtp/src/video_widget.vala | 10 +- plugins/rtp/vapi/gstreamer-rtp-1.0.vapi | 625 +++++++++++++++++++++ .../xep/0167_jingle_rtp/content_parameters.vala | 40 +- .../xep/0167_jingle_rtp/jingle_rtp_module.vala | 5 + .../module/xep/0167_jingle_rtp/payload_type.vala | 49 +- .../src/module/xep/0167_jingle_rtp/stream.vala | 7 + 13 files changed, 1126 insertions(+), 119 deletions(-) create mode 100644 cmake/FindGstRtp.cmake create mode 100644 plugins/rtp/vapi/gstreamer-rtp-1.0.vapi (limited to 'xmpp-vala/src/module/xep/0167_jingle_rtp') diff --git a/cmake/FindGstRtp.cmake b/cmake/FindGstRtp.cmake new file mode 100644 index 00000000..0756a985 --- /dev/null +++ b/cmake/FindGstRtp.cmake @@ -0,0 +1,14 @@ +include(PkgConfigWithFallback) +find_pkg_config_with_fallback(GstRtp + PKG_CONFIG_NAME gstreamer-rtp-1.0 + LIB_NAMES gstrtp + LIB_DIR_HINTS gstreamer-1.0 + INCLUDE_NAMES gst/rtp/rtp.h + INCLUDE_DIR_SUFFIXES gstreamer-1.0 gstreamer-1.0/include gstreamer-rtp-1.0 gstreamer-rtp-1.0/include + DEPENDS Gst +) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(GstRtp + REQUIRED_VARS GstRtp_LIBRARY + VERSION_VAR GstRtp_VERSION) diff --git a/plugins/rtp/CMakeLists.txt b/plugins/rtp/CMakeLists.txt index 76d6e66d..92ec1b97 100644 --- a/plugins/rtp/CMakeLists.txt +++ b/plugins/rtp/CMakeLists.txt @@ -1,3 +1,4 @@ +find_package(GstRtp REQUIRED) find_packages(RTP_PACKAGES REQUIRED Gee GLib @@ -27,6 +28,7 @@ CUSTOM_VAPIS ${CMAKE_BINARY_DIR}/exports/xmpp-vala.vapi ${CMAKE_BINARY_DIR}/exports/dino.vapi ${CMAKE_BINARY_DIR}/exports/qlite.vapi + ${CMAKE_CURRENT_SOURCE_DIR}/vapi/gstreamer-rtp-1.0.vapi PACKAGES ${RTP_PACKAGES} DEFINITIONS @@ -35,7 +37,7 @@ DEFINITIONS add_definitions(${VALA_CFLAGS} -DG_LOG_DOMAIN="rtp" -I${CMAKE_CURRENT_SOURCE_DIR}/src) add_library(rtp SHARED ${RTP_VALA_C}) -target_link_libraries(rtp libdino crypto-vala ${RTP_PACKAGES}) +target_link_libraries(rtp libdino crypto-vala ${RTP_PACKAGES} gstreamer-rtp-1.0) set_target_properties(rtp PROPERTIES PREFIX "") set_target_properties(rtp PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/plugins/) diff --git a/plugins/rtp/src/codec_util.vala b/plugins/rtp/src/codec_util.vala index 6bd465c1..7537c11d 100644 --- a/plugins/rtp/src/codec_util.vala +++ b/plugins/rtp/src/codec_util.vala @@ -6,7 +6,7 @@ public class Dino.Plugins.Rtp.CodecUtil { private Set supported_elements = new HashSet(); private Set unsupported_elements = new HashSet(); - public static Gst.Caps get_caps(string media, JingleRtp.PayloadType payload_type) { + public static Gst.Caps get_caps(string media, JingleRtp.PayloadType payload_type, bool incoming) { Gst.Caps caps = new Gst.Caps.simple("application/x-rtp", "media", typeof(string), media, "payload", typeof(int), payload_type.id); @@ -19,6 +19,15 @@ public class Dino.Plugins.Rtp.CodecUtil { if (payload_type.name != null) { s.set("encoding-name", typeof(string), payload_type.name.up()); } + if (incoming) { + foreach (JingleRtp.RtcpFeedback rtcp_fb in payload_type.rtcp_fbs) { + if (rtcp_fb.subtype == null) { + s.set(@"rtcp-fb-$(rtcp_fb.type_)", typeof(bool), true); + } else { + s.set(@"rtcp-fb-$(rtcp_fb.type_)-$(rtcp_fb.subtype)", typeof(bool), true); + } + } + } return caps; } @@ -122,32 +131,82 @@ public class Dino.Plugins.Rtp.CodecUtil { return new string[0]; } - public static string? get_encode_prefix(string media, string codec, string encode) { + public static string? get_encode_prefix(string media, string codec, string encode, JingleRtp.PayloadType? payload_type) { if (encode == "msdkh264enc") return "video/x-raw,format=NV12 ! "; if (encode == "vaapih264enc") return "video/x-raw,format=NV12 ! "; return null; } - public static string? get_encode_suffix(string media, string codec, string encode) { + public static string? get_encode_args(string media, string codec, string encode, JingleRtp.PayloadType? payload_type) { // H264 - const string h264_suffix = " ! video/x-h264,profile=constrained-baseline ! h264parse"; - if (encode == "msdkh264enc") return @" bitrate=256 rate-control=vbr target-usage=7$h264_suffix"; - if (encode == "vaapih264enc") return @" bitrate=256 quality-level=7 tune=low-power$h264_suffix"; - if (encode == "x264enc") return @" byte-stream=1 bitrate=256 profile=baseline speed-preset=ultrafast tune=zerolatency$h264_suffix"; - if (media == "video" && codec == "h264") return h264_suffix; + if (encode == "msdkh264enc") return @" rate-control=vbr"; + if (encode == "vaapih264enc") return @" tune=low-power"; + if (encode == "x264enc") return @" byte-stream=1 profile=baseline speed-preset=ultrafast tune=zerolatency"; // VP8 - if (encode == "msdkvp8enc") return " bitrate=256 rate-control=vbr target-usage=7"; - if (encode == "vaapivp8enc") return " bitrate=256 rate-control=vbr quality-level=7"; - if (encode == "vp8enc") return " target-bitrate=256000 deadline=1 error-resilient=1"; + if (encode == "msdkvp8enc") return " rate-control=vbr"; + if (encode == "vaapivp8enc") return " rate-control=vbr"; + if (encode == "vp8enc") return " deadline=1 error-resilient=1"; // OPUS - if (encode == "opusenc") return " audio-type=voice"; + if (encode == "opusenc") { + if (payload_type != null && payload_type.parameters.has("useinbandfec", "1")) return " audio-type=voice inband-fec=true"; + return " audio-type=voice"; + } return null; } - public static string? get_decode_prefix(string media, string codec, string decode) { + public static string? get_encode_suffix(string media, string codec, string encode, JingleRtp.PayloadType? payload_type) { + // H264 + if (media == "video" && codec == "h264") return " ! video/x-h264,profile=constrained-baseline ! h264parse"; + return null; + } + + public uint update_bitrate(string media, JingleRtp.PayloadType payload_type, Gst.Element encode_element, uint bitrate) { + Gst.Bin? encode_bin = encode_element as Gst.Bin; + if (encode_bin == null) return 0; + string? codec = get_codec_from_payload(media, payload_type); + string? encode_name = get_encode_element_name(media, codec); + if (encode_name == null) return 0; + Gst.Element encode = encode_bin.get_by_name(@"$(encode_bin.name)_encode"); + + bitrate = uint.min(2048000, bitrate); + + switch (encode_name) { + case "msdkh264enc": + case "vaapih264enc": + case "x264enc": + case "msdkvp8enc": + case "vaapivp8enc": + bitrate = uint.min(2048000, bitrate); + encode.set("bitrate", bitrate); + return bitrate; + case "vp8enc": + bitrate = uint.min(2147483, bitrate); + encode.set("target-bitrate", bitrate * 1000); + return bitrate; + } + + return 0; + } + + public static string? get_decode_prefix(string media, string codec, string decode, JingleRtp.PayloadType? payload_type) { + return null; + } + + public static string? get_decode_args(string media, string codec, string decode, JingleRtp.PayloadType? payload_type) { + if (decode == "opusdec" && payload_type != null && payload_type.parameters.has("useinbandfec", "1")) return " use-inband-fec=true"; + if (decode == "vaapivp9dec" || decode == "vaapivp8dec" || decode == "vaapih264dec") return " max-errors=100"; + return null; + } + + public static string? get_decode_suffix(string media, string codec, string encode, JingleRtp.PayloadType? payload_type) { + return null; + } + + public static string? get_depay_args(string media, string codec, string encode, JingleRtp.PayloadType? payload_type) { + if (codec == "vp8") return " wait-for-keyframe=true"; return null; } @@ -195,21 +254,24 @@ public class Dino.Plugins.Rtp.CodecUtil { unsupported_elements.add(element_name); } - public string? get_decode_bin_description(string media, string? codec, string? element_name = null, string? name = null) { + public string? get_decode_bin_description(string media, string? codec, JingleRtp.PayloadType? payload_type, string? element_name = null, string? name = null) { if (codec == null) return null; string base_name = name ?? @"encode-$codec-$(Random.next_int())"; string depay = get_depay_element_name(media, codec); string decode = element_name ?? get_decode_element_name(media, codec); if (depay == null || decode == null) return null; - string decode_prefix = get_decode_prefix(media, codec, decode) ?? ""; - string resample = media == "audio" ? @" ! audioresample name=$base_name-resample" : ""; - return @"$depay name=$base_name-rtp-depay ! $decode_prefix$decode name=$base_name-decode ! $(media)convert name=$base_name-convert$resample"; + string decode_prefix = get_decode_prefix(media, codec, decode, payload_type) ?? ""; + string decode_args = get_decode_args(media, codec, decode, payload_type) ?? ""; + string decode_suffix = get_decode_suffix(media, codec, decode, payload_type) ?? ""; + string depay_args = get_depay_args(media, codec, decode, payload_type) ?? ""; + string resample = media == "audio" ? @" ! audioresample name=$(base_name)_resample" : ""; + return @"$depay$depay_args name=$(base_name)_rtp_depay ! $decode_prefix$decode$decode_args name=$(base_name)_$(codec)_decode$decode_suffix ! $(media)convert name=$(base_name)_convert$resample"; } public Gst.Element? get_decode_bin(string media, JingleRtp.PayloadType payload_type, string? name = null) { string? codec = get_codec_from_payload(media, payload_type); string base_name = name ?? @"encode-$codec-$(Random.next_int())"; - string? desc = get_decode_bin_description(media, codec, null, base_name); + string? desc = get_decode_bin_description(media, codec, payload_type, null, base_name); if (desc == null) return null; debug("Pipeline to decode %s %s: %s", media, codec, desc); Gst.Element bin = Gst.parse_bin_from_description(desc, true); @@ -217,22 +279,23 @@ public class Dino.Plugins.Rtp.CodecUtil { return bin; } - public string? get_encode_bin_description(string media, string? codec, string? element_name = null, uint pt = 96, string? name = null) { + public string? get_encode_bin_description(string media, string? codec, JingleRtp.PayloadType? payload_type, string? element_name = null, string? name = null) { if (codec == null) return null; - string base_name = name ?? @"encode-$codec-$(Random.next_int())"; + string base_name = name ?? @"encode_$(codec)_$(Random.next_int())"; string pay = get_pay_element_name(media, codec); string encode = element_name ?? get_encode_element_name(media, codec); if (pay == null || encode == null) return null; - string encode_prefix = get_encode_prefix(media, codec, encode) ?? ""; - string encode_suffix = get_encode_suffix(media, codec, encode) ?? ""; - string resample = media == "audio" ? @" ! audioresample name=$base_name-resample" : ""; - return @"$(media)convert name=$base_name-convert$resample ! $encode_prefix$encode$encode_suffix ! $pay pt=$pt name=$base_name-rtp-pay"; + string encode_prefix = get_encode_prefix(media, codec, encode, payload_type) ?? ""; + string encode_args = get_encode_args(media, codec, encode, payload_type) ?? ""; + string encode_suffix = get_encode_suffix(media, codec, encode, payload_type) ?? ""; + string resample = media == "audio" ? @" ! audioresample name=$(base_name)_resample" : ""; + return @"$(media)convert name=$(base_name)_convert$resample ! $encode_prefix$encode$encode_args name=$(base_name)_encode$encode_suffix ! $pay pt=$(payload_type != null ? payload_type.id : 96) name=$(base_name)_rtp_pay"; } public Gst.Element? get_encode_bin(string media, JingleRtp.PayloadType payload_type, string? name = null) { string? codec = get_codec_from_payload(media, payload_type); - string base_name = name ?? @"encode-$codec-$(Random.next_int())"; - string? desc = get_encode_bin_description(media, codec, null, payload_type.id, base_name); + string base_name = name ?? @"encode_$(codec)_$(Random.next_int())"; + string? desc = get_encode_bin_description(media, codec, payload_type, null, base_name); if (desc == null) return null; debug("Pipeline to encode %s %s: %s", media, codec, desc); Gst.Element bin = Gst.parse_bin_from_description(desc, true); diff --git a/plugins/rtp/src/device.vala b/plugins/rtp/src/device.vala index 3c9a38d2..785f853a 100644 --- a/plugins/rtp/src/device.vala +++ b/plugins/rtp/src/device.vala @@ -126,19 +126,20 @@ public class Dino.Plugins.Rtp.Device : MediaDevice, Object { element = device.create_element(id); pipe.add(element); if (is_source) { - filter = Gst.ElementFactory.make("capsfilter", @"$id-caps-filter"); + element.@set("do-timestamp", true); + filter = Gst.ElementFactory.make("capsfilter", @"caps_filter_$id"); filter.@set("caps", get_best_caps()); pipe.add(filter); element.link(filter); if (media == "audio" && plugin.echoprobe != null) { - dsp = Gst.ElementFactory.make("webrtcdsp", @"$id-dsp"); + dsp = Gst.ElementFactory.make("webrtcdsp", @"dsp_$id"); if (dsp != null) { dsp.@set("probe", plugin.echoprobe.name); pipe.add(dsp); filter.link(dsp); } } - tee = Gst.ElementFactory.make("tee", @"$id-tee"); + tee = Gst.ElementFactory.make("tee", @"tee_$id"); tee.@set("allow-not-linked", true); pipe.add(tee); (dsp ?? filter).link(tee); @@ -148,7 +149,7 @@ public class Dino.Plugins.Rtp.Device : MediaDevice, Object { element.@set("sync", false); } if (is_sink && media == "audio") { - filter = Gst.ElementFactory.make("capsfilter", @"$id-caps-filter"); + filter = Gst.ElementFactory.make("capsfilter", @"caps_filter_$id"); filter.@set("caps", get_best_caps()); pipe.add(filter); if (plugin.echoprobe != null) { diff --git a/plugins/rtp/src/module.vala b/plugins/rtp/src/module.vala index 231a9dde..52cc1880 100644 --- a/plugins/rtp/src/module.vala +++ b/plugins/rtp/src/module.vala @@ -63,7 +63,7 @@ public class Dino.Plugins.Rtp.Module : JingleRtp.Module { return supported; } - private async bool supports(string media, JingleRtp.PayloadType payload_type) { + private async bool is_payload_supported(string media, JingleRtp.PayloadType payload_type) { string codec = CodecUtil.get_codec_from_payload(media, payload_type); if (codec == null) return false; if (unsupported_codecs.contains(codec)) return false; @@ -77,7 +77,7 @@ public class Dino.Plugins.Rtp.Module : JingleRtp.Module { return false; } - string encode_bin = codec_util.get_encode_bin_description(media, codec, encode_element); + string encode_bin = codec_util.get_encode_bin_description(media, codec, null, encode_element); while (!(yield pipeline_works(media, encode_bin))) { debug("%s not suited for encoding %s", encode_element, codec); codec_util.mark_element_unsupported(encode_element); @@ -87,11 +87,11 @@ public class Dino.Plugins.Rtp.Module : JingleRtp.Module { unsupported_codecs.add(codec); return false; } - encode_bin = codec_util.get_encode_bin_description(media, codec, encode_element); + encode_bin = codec_util.get_encode_bin_description(media, codec, null, encode_element); } debug("using %s to encode %s", encode_element, codec); - string decode_bin = codec_util.get_decode_bin_description(media, codec, decode_element); + string decode_bin = codec_util.get_decode_bin_description(media, codec, null, decode_element); while (!(yield pipeline_works(media, @"$encode_bin ! $decode_bin"))) { debug("%s not suited for decoding %s", decode_element, codec); codec_util.mark_element_unsupported(decode_element); @@ -101,7 +101,7 @@ public class Dino.Plugins.Rtp.Module : JingleRtp.Module { unsupported_codecs.add(codec); return false; } - decode_bin = codec_util.get_decode_bin_description(media, codec, decode_element); + decode_bin = codec_util.get_decode_bin_description(media, codec, null, decode_element); } debug("using %s to decode %s", decode_element, codec); @@ -109,8 +109,21 @@ public class Dino.Plugins.Rtp.Module : JingleRtp.Module { return true; } + public override bool is_header_extension_supported(string media, JingleRtp.HeaderExtension ext) { + if (media == "video" && ext.uri == "urn:3gpp:video-orientation") return true; + return false; + } + + public override Gee.List get_suggested_header_extensions(string media) { + Gee.List exts = new ArrayList(); + if (media == "video") { + exts.add(new JingleRtp.HeaderExtension(1, "urn:3gpp:video-orientation")); + } + return exts; + } + public async void add_if_supported(Gee.List list, string media, JingleRtp.PayloadType payload_type) { - if (yield supports(media, payload_type)) { + if (yield is_payload_supported(media, payload_type)) { list.add(payload_type); } } @@ -118,58 +131,34 @@ public class Dino.Plugins.Rtp.Module : JingleRtp.Module { public override async Gee.List get_supported_payloads(string media) { Gee.List list = new ArrayList(JingleRtp.PayloadType.equals_func); if (media == "audio") { - yield add_if_supported(list, media, new JingleRtp.PayloadType() { - channels = 2, - clockrate = 48000, - name = "opus", - id = 99 - }); - yield add_if_supported(list, media, new JingleRtp.PayloadType() { - channels = 1, - clockrate = 32000, - name = "speex", - id = 100 - }); - yield add_if_supported(list, media, new JingleRtp.PayloadType() { - channels = 1, - clockrate = 16000, - name = "speex", - id = 101 - }); - yield add_if_supported(list, media, new JingleRtp.PayloadType() { - channels = 1, - clockrate = 8000, - name = "speex", - id = 102 - }); - yield add_if_supported(list, media, new JingleRtp.PayloadType() { - channels = 1, - clockrate = 8000, - name = "PCMU", - id = 0 - }); - yield add_if_supported(list, media, new JingleRtp.PayloadType() { - channels = 1, - clockrate = 8000, - name = "PCMA", - id = 8 - }); + var opus = new JingleRtp.PayloadType() { channels = 2, clockrate = 48000, name = "opus", id = 99 }; + opus.parameters["useinbandfec"] = "1"; + var speex32 = new JingleRtp.PayloadType() { channels = 1, clockrate = 32000, name = "speex", id = 100 }; + var speex16 = new JingleRtp.PayloadType() { channels = 1, clockrate = 16000, name = "speex", id = 101 }; + var speex8 = new JingleRtp.PayloadType() { channels = 1, clockrate = 8000, name = "speex", id = 102 }; + var pcmu = new JingleRtp.PayloadType() { channels = 1, clockrate = 8000, name = "PCMU", id = 0 }; + var pcma = new JingleRtp.PayloadType() { channels = 1, clockrate = 8000, name = "PCMA", id = 8 }; + yield add_if_supported(list, media, opus); + yield add_if_supported(list, media, speex32); + yield add_if_supported(list, media, speex16); + yield add_if_supported(list, media, speex8); + yield add_if_supported(list, media, pcmu); + yield add_if_supported(list, media, pcma); } else if (media == "video") { - yield add_if_supported(list, media, new JingleRtp.PayloadType() { - clockrate = 90000, - name = "H264", - id = 96 - }); - yield add_if_supported(list, media, new JingleRtp.PayloadType() { - clockrate = 90000, - name = "VP9", - id = 97 - }); - yield add_if_supported(list, media, new JingleRtp.PayloadType() { - clockrate = 90000, - name = "VP8", - id = 98 - }); + var h264 = new JingleRtp.PayloadType() { clockrate = 90000, name = "H264", id = 96 }; + var vp9 = new JingleRtp.PayloadType() { clockrate = 90000, name = "VP9", id = 97 }; + var vp8 = new JingleRtp.PayloadType() { clockrate = 90000, name = "VP8", id = 98 }; + var rtcp_fbs = new ArrayList(); + rtcp_fbs.add(new JingleRtp.RtcpFeedback("goog-remb")); + rtcp_fbs.add(new JingleRtp.RtcpFeedback("ccm", "fir")); + rtcp_fbs.add(new JingleRtp.RtcpFeedback("nack")); + rtcp_fbs.add(new JingleRtp.RtcpFeedback("nack", "pli")); + h264.rtcp_fbs.add_all(rtcp_fbs); + vp9.rtcp_fbs.add_all(rtcp_fbs); + vp8.rtcp_fbs.add_all(rtcp_fbs); + yield add_if_supported(list, media, h264); + yield add_if_supported(list, media, vp9); + yield add_if_supported(list, media, vp8); } else { warning("Unsupported media type: %s", media); } @@ -179,11 +168,15 @@ public class Dino.Plugins.Rtp.Module : JingleRtp.Module { public override async JingleRtp.PayloadType? pick_payload_type(string media, Gee.List payloads) { if (media == "audio") { foreach (JingleRtp.PayloadType type in payloads) { - if (yield supports(media, type)) return type; + if (yield is_payload_supported(media, type)) return adjust_payload_type(media, type.clone()); } } else if (media == "video") { + // We prefer H.264 (best support for hardware acceleration and good overall codec quality) + JingleRtp.PayloadType? h264 = payloads.first_match((it) => it.name.up() == "H264"); + if (h264 != null && yield is_payload_supported(media, h264)) return adjust_payload_type(media, h264.clone()); + // Take first of the list that we do support otherwise foreach (JingleRtp.PayloadType type in payloads) { - if (yield supports(media, type)) return type; + if (yield is_payload_supported(media, type)) return adjust_payload_type(media, type.clone()); } } else { warning("Unsupported media type: %s", media); @@ -191,6 +184,28 @@ public class Dino.Plugins.Rtp.Module : JingleRtp.Module { return null; } + public JingleRtp.PayloadType adjust_payload_type(string media, JingleRtp.PayloadType type) { + var iter = type.rtcp_fbs.iterator(); + while (iter.next()) { + var fb = iter.@get(); + switch (fb.type_) { + case "goog-remb": + if (fb.subtype != null) iter.remove(); + break; + case "ccm": + if (fb.subtype != "fir") iter.remove(); + break; + case "nack": + if (fb.subtype != null && fb.subtype != "pli") iter.remove(); + break; + default: + iter.remove(); + break; + } + } + return type; + } + public override JingleRtp.Stream create_stream(Jingle.Content content) { return plugin.open_stream(content); } diff --git a/plugins/rtp/src/plugin.vala b/plugins/rtp/src/plugin.vala index 40ad1e0f..f0ad7db2 100644 --- a/plugins/rtp/src/plugin.vala +++ b/plugins/rtp/src/plugin.vala @@ -65,6 +65,9 @@ public class Dino.Plugins.Rtp.Plugin : RootInterface, VideoCallPlugin, Object { } rtpbin.pad_added.connect(on_rtp_pad_added); rtpbin.@set("latency", 100); + rtpbin.@set("do-lost", true); + rtpbin.@set("do-sync-event", true); + rtpbin.@set("drop-on-latency", true); rtpbin.connect("signal::request-pt-map", request_pt_map, this); pipe.add(rtpbin); @@ -160,6 +163,17 @@ public class Dino.Plugins.Rtp.Plugin : RootInterface, VideoCallPlugin, Object { case Gst.MessageType.QOS: // Ignore break; + case Gst.MessageType.LATENCY: + if (message.src != null && message.src.name != null && message.src is Gst.Element) { + Gst.Query latency_query = new Gst.Query.latency(); + if (((Gst.Element)message.src).query(latency_query)) { + bool live; + Gst.ClockTime min_latency, max_latency; + latency_query.parse_latency(out live, out min_latency, out max_latency); + debug("Latency message from %s: live=%s, min_latency=%s, max_latency=%s", message.src.name, live.to_string(), min_latency.to_string(), max_latency.to_string()); + } + } + break; default: debug("Pipe bus message: %s", message.type.to_string()); break; diff --git a/plugins/rtp/src/stream.vala b/plugins/rtp/src/stream.vala index 3a63f3fa..23634aa3 100644 --- a/plugins/rtp/src/stream.vala +++ b/plugins/rtp/src/stream.vala @@ -19,9 +19,12 @@ public class Dino.Plugins.Rtp.Stream : Xmpp.Xep.JingleRtp.Stream { private Gst.App.Src recv_rtp; private Gst.App.Src recv_rtcp; private Gst.Element encode; + private Gst.RTP.BasePayload encode_pay; private Gst.Element decode; + private Gst.RTP.BaseDepayload decode_depay; private Gst.Element input; private Gst.Element output; + private Gst.Element session; private Device _input_device; public Device input_device { get { return _input_device; } set { @@ -85,15 +88,15 @@ public class Dino.Plugins.Rtp.Stream : Xmpp.Xep.JingleRtp.Stream { } // Create app elements - send_rtp = Gst.ElementFactory.make("appsink", @"rtp-sink-$rtpid") as Gst.App.Sink; + send_rtp = Gst.ElementFactory.make("appsink", @"rtp_sink_$rtpid") as Gst.App.Sink; send_rtp.async = false; - send_rtp.caps = CodecUtil.get_caps(media, payload_type); + send_rtp.caps = CodecUtil.get_caps(media, payload_type, false); send_rtp.emit_signals = true; send_rtp.sync = false; send_rtp.new_sample.connect(on_new_sample); pipe.add(send_rtp); - send_rtcp = Gst.ElementFactory.make("appsink", @"rtcp-sink-$rtpid") as Gst.App.Sink; + send_rtcp = Gst.ElementFactory.make("appsink", @"rtcp_sink_$rtpid") as Gst.App.Sink; send_rtcp.async = false; send_rtcp.caps = new Gst.Caps.empty_simple("application/x-rtcp"); send_rtcp.emit_signals = true; @@ -101,14 +104,14 @@ public class Dino.Plugins.Rtp.Stream : Xmpp.Xep.JingleRtp.Stream { send_rtcp.new_sample.connect(on_new_sample); pipe.add(send_rtcp); - recv_rtp = Gst.ElementFactory.make("appsrc", @"rtp-src-$rtpid") as Gst.App.Src; - recv_rtp.caps = CodecUtil.get_caps(media, payload_type); + recv_rtp = Gst.ElementFactory.make("appsrc", @"rtp_src_$rtpid") as Gst.App.Src; + recv_rtp.caps = CodecUtil.get_caps(media, payload_type, true); recv_rtp.do_timestamp = true; recv_rtp.format = Gst.Format.TIME; recv_rtp.is_live = true; pipe.add(recv_rtp); - recv_rtcp = Gst.ElementFactory.make("appsrc", @"rtcp-src-$rtpid") as Gst.App.Src; + recv_rtcp = Gst.ElementFactory.make("appsrc", @"rtcp_src_$rtpid") as Gst.App.Src; recv_rtcp.caps = new Gst.Caps.empty_simple("application/x-rtcp"); recv_rtcp.do_timestamp = true; recv_rtcp.format = Gst.Format.TIME; @@ -122,7 +125,8 @@ public class Dino.Plugins.Rtp.Stream : Xmpp.Xep.JingleRtp.Stream { recv_rtcp.get_static_pad("src").link(recv_rtcp_sink_pad); // Connect input - encode = codec_util.get_encode_bin(media, payload_type, @"encode-$rtpid"); + encode = codec_util.get_encode_bin(media, payload_type, @"encode_$rtpid"); + encode_pay = (Gst.RTP.BasePayload)((Gst.Bin)encode).get_by_name(@"encode_$(rtpid)_rtp_pay"); pipe.add(encode); send_rtp_sink_pad = rtpbin.get_request_pad(@"send_rtp_sink_$rtpid"); encode.get_static_pad("src").link(send_rtp_sink_pad); @@ -131,7 +135,8 @@ public class Dino.Plugins.Rtp.Stream : Xmpp.Xep.JingleRtp.Stream { } // Connect output - decode = codec_util.get_decode_bin(media, payload_type, @"decode-$rtpid"); + decode = codec_util.get_decode_bin(media, payload_type, @"decode_$rtpid"); + decode_depay = (Gst.RTP.BaseDepayload)((Gst.Bin)encode).get_by_name(@"decode_$(rtpid)_rtp_depay"); pipe.add(decode); if (output != null) { decode.link(output); @@ -144,6 +149,110 @@ public class Dino.Plugins.Rtp.Stream : Xmpp.Xep.JingleRtp.Stream { created = true; push_recv_data = true; plugin.unpause(); + + GLib.Signal.emit_by_name(rtpbin, "get-session", rtpid, out session); + if (session != null && payload_type.rtcp_fbs.any_match((it) => it.type_ == "goog-remb")) { + Object internal_session; + session.@get("internal-session", out internal_session); + if (internal_session != null) { + internal_session.connect("signal::on-feedback-rtcp", on_feedback_rtcp, this); + } + Timeout.add(1000, () => remb_adjust()); + } + if (media == "video") { + codec_util.update_bitrate(media, payload_type, encode, 256); + } + } + + private uint remb = 256; + private int last_packets_lost = -1; + private uint64 last_packets_received; + private uint64 last_octets_received; + private bool remb_adjust() { + unowned Gst.Structure? stats; + if (session == null) { + debug("Session for %u finished, turning off remb adjustment", rtpid); + return Source.REMOVE; + } + session.get("stats", out stats); + if (stats == null) { + warning("No stats for session %u", rtpid); + return Source.REMOVE; + } + unowned ValueArray? source_stats; + stats.get("source-stats", typeof(ValueArray), out source_stats); + if (source_stats == null) { + warning("No source-stats for session %u", rtpid); + return Source.REMOVE; + } + foreach (Value value in source_stats.values) { + unowned Gst.Structure source_stat = (Gst.Structure) value.get_boxed(); + uint ssrc; + if (!source_stat.get_uint("ssrc", out ssrc)) continue; + if (ssrc.to_string() == participant_ssrc) { + int packets_lost; + uint64 packets_received, octets_received; + source_stat.get_int("packets-lost", out packets_lost); + source_stat.get_uint64("packets-received", out packets_received); + source_stat.get_uint64("octets-received", out octets_received); + int new_lost = packets_lost - last_packets_lost; + uint64 new_received = packets_received - last_packets_received; + uint64 new_octets = octets_received - last_octets_received; + if (new_received == 0) continue; + last_packets_lost = packets_lost; + last_packets_received = packets_received; + last_octets_received = octets_received; + double loss_rate = (double)new_lost / (double)(new_lost + new_received); + if (new_lost <= 0 || loss_rate < 0.02) { + remb = (uint)(1.08 * (double)remb); + } else if (loss_rate > 0.1) { + remb = (uint)((1.0 - 0.5 * loss_rate) * (double)remb); + } + remb = uint.max(remb, (uint)((new_octets * 8) / 1000)); + remb = uint.max(16, remb); // Never go below 16 + uint8[] data = new uint8[] { + 143, 206, 0, 5, + 0, 0, 0, 0, + 0, 0, 0, 0, + 'R', 'E', 'M', 'B', + 1, 0, 0, 0, + 0, 0, 0, 0 + }; + data[4] = (uint8)((encode_pay.ssrc >> 24) & 0xff); + data[5] = (uint8)((encode_pay.ssrc >> 16) & 0xff); + data[6] = (uint8)((encode_pay.ssrc >> 8) & 0xff); + data[7] = (uint8)(encode_pay.ssrc & 0xff); + uint8 br_exp = 0; + uint32 br_mant = remb * 1000; + uint8 bits = (uint8)Math.log2(br_mant); + if (bits > 16) { + br_exp = (uint8)bits - 16; + br_mant = br_mant >> br_exp; + } + data[17] = (uint8)((br_exp << 2) | ((br_mant >> 16) & 0x3)); + data[18] = (uint8)((br_mant >> 8) & 0xff); + data[19] = (uint8)(br_mant & 0xff); + data[20] = (uint8)((ssrc >> 24) & 0xff); + data[21] = (uint8)((ssrc >> 16) & 0xff); + data[22] = (uint8)((ssrc >> 8) & 0xff); + data[23] = (uint8)(ssrc & 0xff); + encrypt_and_send_rtcp(data); + } + } + return Source.CONTINUE; + } + + private static void on_feedback_rtcp(Gst.Element session, uint type, uint fbtype, uint sender_ssrc, uint media_ssrc, Gst.Buffer? fci, Stream self) { + if (type == 206 && fbtype == 15 && fci != null && sender_ssrc.to_string() == self.participant_ssrc) { + // https://tools.ietf.org/html/draft-alvestrand-rmcat-remb-03 + uint8[] data; + fci.extract_dup(0, fci.get_size(), out data); + if (data[0] != 'R' || data[1] != 'E' || data[2] != 'M' || data[3] != 'B') return; + uint8 br_exp = data[5] >> 2; + uint32 br_mant = (((uint32)data[5] & 0x3) << 16) + ((uint32)data[6] << 8) + (uint32)data[7]; + uint bitrate = (br_mant << br_exp) / 1000; + self.codec_util.update_bitrate(self.media, self.payload_type, self.encode, bitrate * 8); + } } private void prepare_local_crypto() { @@ -167,22 +276,26 @@ public class Dino.Plugins.Rtp.Stream : Xmpp.Xep.JingleRtp.Stream { if (crypto_session.has_encrypt) { data = crypto_session.encrypt_rtp(data); } - on_send_rtp_data(new Bytes.take(data)); + on_send_rtp_data(new Bytes.take((owned) data)); } else if (sink == send_rtcp) { - if (crypto_session.has_encrypt) { - data = crypto_session.encrypt_rtcp(data); - } - if (rtcp_mux) { - on_send_rtp_data(new Bytes.take(data)); - } else { - on_send_rtcp_data(new Bytes.take(data)); - } + encrypt_and_send_rtcp((owned) data); } else { warning("unknown sample"); } return Gst.FlowReturn.OK; } + private void encrypt_and_send_rtcp(owned uint8[] data) { + if (crypto_session.has_encrypt) { + data = crypto_session.encrypt_rtcp(data); + } + if (rtcp_mux) { + on_send_rtp_data(new Bytes.take((owned) data)); + } else { + on_send_rtcp_data(new Bytes.take((owned) data)); + } + } + private static Gst.PadProbeReturn drop_probe() { return Gst.PadProbeReturn.DROP; } @@ -211,6 +324,7 @@ public class Dino.Plugins.Rtp.Stream : Xmpp.Xep.JingleRtp.Stream { encode.get_static_pad("src").unlink(send_rtp_sink_pad); pipe.remove(encode); encode = null; + encode_pay = null; // Disconnect RTP sending if (send_rtp_src_pad != null) { @@ -243,6 +357,7 @@ public class Dino.Plugins.Rtp.Stream : Xmpp.Xep.JingleRtp.Stream { decode.set_state(Gst.State.NULL); pipe.remove(decode); decode = null; + decode_depay = null; output = null; // Disconnect output device @@ -276,6 +391,8 @@ public class Dino.Plugins.Rtp.Stream : Xmpp.Xep.JingleRtp.Stream { send_rtcp_src_pad = null; send_rtp_src_pad = null; recv_rtp_src_pad = null; + + session = null; } private void prepare_remote_crypto() { @@ -285,6 +402,9 @@ public class Dino.Plugins.Rtp.Stream : Xmpp.Xep.JingleRtp.Stream { } } + private uint16 previous_video_orientation_degree = uint16.MAX; + public signal void video_orientation_changed(uint16 degree); + public override void on_recv_rtp_data(Bytes bytes) { if (rtcp_mux && bytes.length >= 2 && bytes.get(1) >= 192 && bytes.get(1) < 224) { on_recv_rtcp_data(bytes); @@ -301,6 +421,33 @@ public class Dino.Plugins.Rtp.Stream : Xmpp.Xep.JingleRtp.Stream { } if (push_recv_data) { Gst.Buffer buffer = new Gst.Buffer.wrapped((owned) data); + Gst.RTP.Buffer rtp_buffer; + if (Gst.RTP.Buffer.map(buffer, Gst.MapFlags.READ, out rtp_buffer)) { + if (rtp_buffer.get_extension()) { + Xmpp.Xep.JingleRtp.HeaderExtension? ext = header_extensions.first_match((it) => it.uri == "urn:3gpp:video-orientation"); + if (ext != null) { + unowned uint8[] extension_data; + if (rtp_buffer.get_extension_onebyte_header(ext.id, 0, out extension_data) && extension_data.length == 1) { + bool camera = (extension_data[0] & 0x8) > 0; + bool flip = (extension_data[0] & 0x4) > 0; + uint8 rotation = extension_data[0] & 0x3; + uint16 rotation_degree = uint16.MAX; + switch(rotation) { + case 0: rotation_degree = 0; break; + case 1: rotation_degree = 90; break; + case 2: rotation_degree = 180; break; + case 3: rotation_degree = 270; break; + } + if (rotation_degree != previous_video_orientation_degree) { + video_orientation_changed(rotation_degree); + previous_video_orientation_degree = rotation_degree; + } + } + } + } + rtp_buffer.unmap(); + } + // FIXME: VAPI file in Vala < 0.49.1 has a bug that results in broken ownership of buffer in push_buffer() // We workaround by using the plain signal. The signal unfortunately will cause an unnecessary copy of // the underlying buffer, so and some point we should move over to the new version (once we require @@ -449,6 +596,8 @@ public class Dino.Plugins.Rtp.Stream : Xmpp.Xep.JingleRtp.Stream { public class Dino.Plugins.Rtp.VideoStream : Stream { private Gee.List outputs = new ArrayList(); private Gst.Element output_tee; + private Gst.Element rotate; + private ulong video_orientation_changed_handler; public VideoStream(Plugin plugin, Xmpp.Xep.Jingle.Content content) { base(plugin, content); @@ -456,11 +605,15 @@ public class Dino.Plugins.Rtp.VideoStream : Stream { } public override void create() { + video_orientation_changed_handler = video_orientation_changed.connect(on_video_orientation_changed); plugin.pause(); - output_tee = Gst.ElementFactory.make("tee", null); + rotate = Gst.ElementFactory.make("videoflip", @"video_rotate_$rtpid"); + pipe.add(rotate); + output_tee = Gst.ElementFactory.make("tee", @"video_tee_$rtpid"); output_tee.@set("allow-not-linked", true); pipe.add(output_tee); - add_output(output_tee); + rotate.link(output_tee); + add_output(rotate); base.create(); foreach (Gst.Element output in outputs) { output_tee.link(output); @@ -468,19 +621,44 @@ public class Dino.Plugins.Rtp.VideoStream : Stream { plugin.unpause(); } + private void on_video_orientation_changed(uint16 degree) { + if (rotate != null) { + switch (degree) { + case 0: + rotate.@set("method", 0); + break; + case 90: + rotate.@set("method", 1); + break; + case 180: + rotate.@set("method", 2); + break; + case 270: + rotate.@set("method", 3); + break; + } + } + } + public override void destroy() { foreach (Gst.Element output in outputs) { output_tee.unlink(output); } base.destroy(); + rotate.set_locked_state(true); + rotate.set_state(Gst.State.NULL); + rotate.unlink(output_tee); + pipe.remove(rotate); + rotate = null; output_tee.set_locked_state(true); output_tee.set_state(Gst.State.NULL); pipe.remove(output_tee); output_tee = null; + disconnect(video_orientation_changed_handler); } public override void add_output(Gst.Element element) { - if (element == output_tee) { + if (element == output_tee || element == rotate) { base.add_output(element); return; } @@ -491,7 +669,7 @@ public class Dino.Plugins.Rtp.VideoStream : Stream { } public override void remove_output(Gst.Element element) { - if (element == output_tee) { + if (element == output_tee || element == rotate) { base.remove_output(element); return; } diff --git a/plugins/rtp/src/video_widget.vala b/plugins/rtp/src/video_widget.vala index fa5ba138..351069a7 100644 --- a/plugins/rtp/src/video_widget.vala +++ b/plugins/rtp/src/video_widget.vala @@ -19,7 +19,7 @@ public class Dino.Plugins.Rtp.VideoWidget : Gtk.Bin, Dino.Plugins.VideoCallWidge this.plugin = plugin; id = last_id++; - element = Gst.ElementFactory.make("gtksink", @"video-widget-$id"); + element = Gst.ElementFactory.make("gtksink", @"video_widget_$id"); if (element != null) { Gtk.Widget widget; element.@get("widget", out widget); @@ -51,8 +51,8 @@ public class Dino.Plugins.Rtp.VideoWidget : Gtk.Bin, Dino.Plugins.VideoCallWidge if (connected_stream == null) return; plugin.pause(); pipe.add(element); - convert = Gst.parse_bin_from_description(@"videoconvert name=video-widget-$id-convert", true); - convert.name = @"video-widget-$id-prepare"; + convert = Gst.parse_bin_from_description(@"videoconvert name=video_widget_$(id)_convert", true); + convert.name = @"video_widget_$(id)_prepare"; pipe.add(convert); convert.link(element); connected_stream.add_output(convert); @@ -68,8 +68,8 @@ public class Dino.Plugins.Rtp.VideoWidget : Gtk.Bin, Dino.Plugins.VideoCallWidge if (connected_device == null) return; plugin.pause(); pipe.add(element); - convert = Gst.parse_bin_from_description(@"videoflip method=horizontal-flip name=video-widget-$id-flip ! videoconvert name=video-widget-$id-convert", true); - convert.name = @"video-widget-$id-prepare"; + convert = Gst.parse_bin_from_description(@"videoflip method=horizontal-flip name=video_widget_$(id)_flip ! videoconvert name=video_widget_$(id)_convert", true); + convert.name = @"video_widget_$(id)_prepare"; pipe.add(convert); convert.link(element); connected_device.link_source().link(convert); diff --git a/plugins/rtp/vapi/gstreamer-rtp-1.0.vapi b/plugins/rtp/vapi/gstreamer-rtp-1.0.vapi new file mode 100644 index 00000000..30490896 --- /dev/null +++ b/plugins/rtp/vapi/gstreamer-rtp-1.0.vapi @@ -0,0 +1,625 @@ +// Fixme: This is fetched from development code of Vala upstream which fixed a few bugs. +/* gstreamer-rtp-1.0.vapi generated by vapigen, do not modify. */ + +[CCode (cprefix = "Gst", gir_namespace = "GstRtp", gir_version = "1.0", lower_case_cprefix = "gst_")] +namespace Gst { + namespace RTCP { + [CCode (cheader_filename = "gst/rtp/rtp.h", has_type_id = false)] + [GIR (name = "RTCPBuffer")] + public struct Buffer { + public weak Gst.Buffer buffer; + public bool add_packet (Gst.RTCP.Type type, Gst.RTCP.Packet packet); + public bool get_first_packet (Gst.RTCP.Packet packet); + public uint get_packet_count (); + public static bool map (Gst.Buffer buffer, Gst.MapFlags flags, out Gst.RTCP.Buffer rtcp); + public static Gst.Buffer @new (uint mtu); + public static Gst.Buffer new_copy_data ([CCode (array_length_cname = "len", array_length_pos = 1.1, array_length_type = "guint")] uint8[] data); + public static Gst.Buffer new_take_data ([CCode (array_length_cname = "len", array_length_pos = 1.1, array_length_type = "guint")] owned uint8[] data); + public bool unmap (); + public static bool validate (Gst.Buffer buffer); + public static bool validate_data ([CCode (array_length_cname = "len", array_length_pos = 1.1, array_length_type = "guint")] uint8[] data); + [Version (since = "1.6")] + public static bool validate_data_reduced ([CCode (array_length_cname = "len", array_length_pos = 1.1, array_length_type = "guint")] uint8[] data); + [Version (since = "1.6")] + public static bool validate_reduced (Gst.Buffer buffer); + } + [CCode (cheader_filename = "gst/rtp/rtp.h", has_type_id = false)] + [GIR (name = "RTCPPacket")] + public struct Packet { + public weak Gst.RTCP.Buffer? rtcp; + public uint offset; + [Version (since = "1.10")] + public bool add_profile_specific_ext ([CCode (array_length_cname = "len", array_length_pos = 1.1, array_length_type = "guint")] uint8[] data); + public bool add_rb (uint32 ssrc, uint8 fractionlost, int32 packetslost, uint32 exthighestseq, uint32 jitter, uint32 lsr, uint32 dlsr); + [Version (since = "1.10")] + public uint8 app_get_data (); + [Version (since = "1.10")] + public uint16 app_get_data_length (); + [Version (since = "1.10")] + public unowned string app_get_name (); + [Version (since = "1.10")] + public uint32 app_get_ssrc (); + [Version (since = "1.10")] + public uint8 app_get_subtype (); + [Version (since = "1.10")] + public bool app_set_data_length (uint16 wordlen); + [Version (since = "1.10")] + public void app_set_name (string name); + [Version (since = "1.10")] + public void app_set_ssrc (uint32 ssrc); + [Version (since = "1.10")] + public void app_set_subtype (uint8 subtype); + public bool bye_add_ssrc (uint32 ssrc); + public bool bye_add_ssrcs ([CCode (array_length_cname = "len", array_length_pos = 1.1, array_length_type = "guint")] uint32[] ssrc); + public uint32 bye_get_nth_ssrc (uint nth); + public string bye_get_reason (); + public uint8 bye_get_reason_len (); + public uint bye_get_ssrc_count (); + public bool bye_set_reason (string reason); + [Version (since = "1.10")] + public bool copy_profile_specific_ext ([CCode (array_length_cname = "len", array_length_pos = 1.1, array_length_type = "guint")] out uint8[] data); + public uint8 fb_get_fci (); + public uint16 fb_get_fci_length (); + public uint32 fb_get_media_ssrc (); + public uint32 fb_get_sender_ssrc (); + public Gst.RTCP.FBType fb_get_type (); + public bool fb_set_fci_length (uint16 wordlen); + public void fb_set_media_ssrc (uint32 ssrc); + public void fb_set_sender_ssrc (uint32 ssrc); + public void fb_set_type (Gst.RTCP.FBType type); + public uint8 get_count (); + public uint16 get_length (); + public bool get_padding (); + [Version (since = "1.10")] + public bool get_profile_specific_ext ([CCode (array_length_cname = "len", array_length_pos = 1.1, array_length_type = "guint")] out unowned uint8[] data); + [Version (since = "1.10")] + public uint16 get_profile_specific_ext_length (); + public void get_rb (uint nth, out uint32 ssrc, out uint8 fractionlost, out int32 packetslost, out uint32 exthighestseq, out uint32 jitter, out uint32 lsr, out uint32 dlsr); + public uint get_rb_count (); + public Gst.RTCP.Type get_type (); + public bool move_to_next (); + public bool remove (); + public uint32 rr_get_ssrc (); + public void rr_set_ssrc (uint32 ssrc); + public bool sdes_add_entry (Gst.RTCP.SDESType type, [CCode (array_length_cname = "len", array_length_pos = 1.5, array_length_type = "guint8")] uint8[] data); + public bool sdes_add_item (uint32 ssrc); + public bool sdes_copy_entry (out Gst.RTCP.SDESType type, [CCode (array_length_cname = "len", array_length_pos = 1.5, array_length_type = "guint8")] out uint8[] data); + public bool sdes_first_entry (); + public bool sdes_first_item (); + public bool sdes_get_entry (out Gst.RTCP.SDESType type, [CCode (array_length_cname = "len", array_length_pos = 1.5, array_length_type = "guint8")] out unowned uint8[] data); + public uint sdes_get_item_count (); + public uint32 sdes_get_ssrc (); + public bool sdes_next_entry (); + public bool sdes_next_item (); + public void set_rb (uint nth, uint32 ssrc, uint8 fractionlost, int32 packetslost, uint32 exthighestseq, uint32 jitter, uint32 lsr, uint32 dlsr); + public void sr_get_sender_info (out uint32 ssrc, out uint64 ntptime, out uint32 rtptime, out uint32 packet_count, out uint32 octet_count); + public void sr_set_sender_info (uint32 ssrc, uint64 ntptime, uint32 rtptime, uint32 packet_count, uint32 octet_count); + [Version (since = "1.16")] + public bool xr_first_rb (); + [Version (since = "1.16")] + public uint16 xr_get_block_length (); + [Version (since = "1.16")] + public Gst.RTCP.XRType xr_get_block_type (); + [Version (since = "1.16")] + public bool xr_get_dlrr_block (uint nth, out uint32 ssrc, out uint32 last_rr, out uint32 delay); + [Version (since = "1.16")] + public bool xr_get_prt_by_seq (uint16 seq, out uint32 receipt_time); + [Version (since = "1.16")] + public bool xr_get_prt_info (out uint32 ssrc, out uint8 thinning, out uint16 begin_seq, out uint16 end_seq); + [Version (since = "1.16")] + public bool xr_get_rle_info (out uint32 ssrc, out uint8 thinning, out uint16 begin_seq, out uint16 end_seq, out uint32 chunk_count); + [Version (since = "1.16")] + public bool xr_get_rle_nth_chunk (uint nth, out uint16 chunk); + [Version (since = "1.16")] + public bool xr_get_rrt (out uint64 timestamp); + [Version (since = "1.16")] + public uint32 xr_get_ssrc (); + [Version (since = "1.16")] + public bool xr_get_summary_info (out uint32 ssrc, out uint16 begin_seq, out uint16 end_seq); + [Version (since = "1.16")] + public bool xr_get_summary_jitter (out uint32 min_jitter, out uint32 max_jitter, out uint32 mean_jitter, out uint32 dev_jitter); + [Version (since = "1.16")] + public bool xr_get_summary_pkt (out uint32 lost_packets, out uint32 dup_packets); + [Version (since = "1.16")] + public bool xr_get_summary_ttl (out bool is_ipv4, out uint8 min_ttl, out uint8 max_ttl, out uint8 mean_ttl, out uint8 dev_ttl); + [Version (since = "1.16")] + public bool xr_get_voip_burst_metrics (out uint8 burst_density, out uint8 gap_density, out uint16 burst_duration, out uint16 gap_duration); + [Version (since = "1.16")] + public bool xr_get_voip_configuration_params (out uint8 gmin, out uint8 rx_config); + [Version (since = "1.16")] + public bool xr_get_voip_delay_metrics (out uint16 roundtrip_delay, out uint16 end_system_delay); + [Version (since = "1.16")] + public bool xr_get_voip_jitter_buffer_params (out uint16 jb_nominal, out uint16 jb_maximum, out uint16 jb_abs_max); + [Version (since = "1.16")] + public bool xr_get_voip_metrics_ssrc (out uint32 ssrc); + [Version (since = "1.16")] + public bool xr_get_voip_packet_metrics (out uint8 loss_rate, out uint8 discard_rate); + [Version (since = "1.16")] + public bool xr_get_voip_quality_metrics (out uint8 r_factor, out uint8 ext_r_factor, out uint8 mos_lq, out uint8 mos_cq); + [Version (since = "1.16")] + public bool xr_get_voip_signal_metrics (out uint8 signal_level, out uint8 noise_level, out uint8 rerl, out uint8 gmin); + [Version (since = "1.16")] + public bool xr_next_rb (); + } + [CCode (cheader_filename = "gst/rtp/rtp.h", cprefix = "GST_RTCP_", type_id = "gst_rtcpfb_type_get_type ()")] + [GIR (name = "RTCPFBType")] + public enum FBType { + FB_TYPE_INVALID, + RTPFB_TYPE_NACK, + RTPFB_TYPE_TMMBR, + RTPFB_TYPE_TMMBN, + RTPFB_TYPE_RTCP_SR_REQ, + RTPFB_TYPE_TWCC, + PSFB_TYPE_PLI, + PSFB_TYPE_SLI, + PSFB_TYPE_RPSI, + PSFB_TYPE_AFB, + PSFB_TYPE_FIR, + PSFB_TYPE_TSTR, + PSFB_TYPE_TSTN, + PSFB_TYPE_VBCN + } + [CCode (cheader_filename = "gst/rtp/rtp.h", cprefix = "GST_RTCP_SDES_", type_id = "gst_rtcpsdes_type_get_type ()")] + [GIR (name = "RTCPSDESType")] + public enum SDESType { + INVALID, + END, + CNAME, + NAME, + EMAIL, + PHONE, + LOC, + TOOL, + NOTE, + PRIV; + [CCode (cname = "gst_rtcp_sdes_name_to_type")] + public static Gst.RTCP.SDESType from_string (string name); + [CCode (cname = "gst_rtcp_sdes_type_to_name")] + public unowned string to_string (); + } + [CCode (cheader_filename = "gst/rtp/rtp.h", cprefix = "GST_RTCP_TYPE_", type_id = "gst_rtcp_type_get_type ()")] + [GIR (name = "RTCPType")] + public enum Type { + INVALID, + SR, + RR, + SDES, + BYE, + APP, + RTPFB, + PSFB, + XR + } + [CCode (cheader_filename = "gst/rtp/rtp.h", cprefix = "GST_RTCP_XR_TYPE_", type_id = "gst_rtcpxr_type_get_type ()")] + [GIR (name = "RTCPXRType")] + [Version (since = "1.16")] + public enum XRType { + INVALID, + LRLE, + DRLE, + PRT, + RRT, + DLRR, + SSUMM, + VOIP_METRICS + } + [CCode (cheader_filename = "gst/rtp/rtp.h", cname = "GST_RTCP_MAX_BYE_SSRC_COUNT")] + public const int MAX_BYE_SSRC_COUNT; + [CCode (cheader_filename = "gst/rtp/rtp.h", cname = "GST_RTCP_MAX_RB_COUNT")] + public const int MAX_RB_COUNT; + [CCode (cheader_filename = "gst/rtp/rtp.h", cname = "GST_RTCP_MAX_SDES")] + public const int MAX_SDES; + [CCode (cheader_filename = "gst/rtp/rtp.h", cname = "GST_RTCP_MAX_SDES_ITEM_COUNT")] + public const int MAX_SDES_ITEM_COUNT; + [CCode (cheader_filename = "gst/rtp/rtp.h", cname = "GST_RTCP_REDUCED_SIZE_VALID_MASK")] + public const int REDUCED_SIZE_VALID_MASK; + [CCode (cheader_filename = "gst/rtp/rtp.h", cname = "GST_RTCP_VALID_MASK")] + public const int VALID_MASK; + [CCode (cheader_filename = "gst/rtp/rtp.h", cname = "GST_RTCP_VALID_VALUE")] + public const int VALID_VALUE; + [CCode (cheader_filename = "gst/rtp/rtp.h", cname = "GST_RTCP_VERSION")] + public const int VERSION; + [CCode (cheader_filename = "gst/rtp/rtp.h")] + public static uint64 ntp_to_unix (uint64 ntptime); + [CCode (cheader_filename = "gst/rtp/rtp.h")] + public static uint64 unix_to_ntp (uint64 unixtime); + } + namespace RTP { + [CCode (cheader_filename = "gst/rtp/rtp.h", type_id = "gst_rtp_base_audio_payload_get_type ()")] + [GIR (name = "RTPBaseAudioPayload")] + public class BaseAudioPayload : Gst.RTP.BasePayload { + public Gst.ClockTime base_ts; + public int frame_duration; + public int frame_size; + public int sample_size; + [CCode (has_construct_function = false)] + protected BaseAudioPayload (); + public Gst.FlowReturn flush (uint payload_len, Gst.ClockTime timestamp); + public Gst.Base.Adapter get_adapter (); + public Gst.FlowReturn push ([CCode (array_length_cname = "payload_len", array_length_pos = 1.5, array_length_type = "guint")] uint8[] data, Gst.ClockTime timestamp); + public void set_frame_based (); + public void set_frame_options (int frame_duration, int frame_size); + public void set_sample_based (); + public void set_sample_options (int sample_size); + public void set_samplebits_options (int sample_size); + [NoAccessorMethod] + public bool buffer_list { get; set; } + } + [CCode (cheader_filename = "gst/rtp/rtp.h", type_id = "gst_rtp_base_depayload_get_type ()")] + [GIR (name = "RTPBaseDepayload")] + public abstract class BaseDepayload : Gst.Element { + public uint clock_rate; + public bool need_newsegment; + public weak Gst.Segment segment; + public weak Gst.Pad sinkpad; + public weak Gst.Pad srcpad; + [CCode (has_construct_function = false)] + protected BaseDepayload (); + [NoWrapper] + public virtual bool handle_event (Gst.Event event); + [Version (since = "1.16")] + public bool is_source_info_enabled (); + [NoWrapper] + public virtual bool packet_lost (Gst.Event event); + [NoWrapper] + public virtual Gst.Buffer process (Gst.Buffer @in); + [NoWrapper] + public virtual Gst.Buffer process_rtp_packet (Gst.RTP.Buffer rtp_buffer); + public Gst.FlowReturn push (Gst.Buffer out_buf); + public Gst.FlowReturn push_list (Gst.BufferList out_list); + [NoWrapper] + public virtual bool set_caps (Gst.Caps caps); + [Version (since = "1.16")] + public void set_source_info_enabled (bool enable); + [NoAccessorMethod] + [Version (since = "1.20")] + public bool auto_header_extension { get; set; } + [NoAccessorMethod] + [Version (since = "1.18")] + public int max_reorder { get; set; } + [NoAccessorMethod] + [Version (since = "1.16")] + public bool source_info { get; set; } + [NoAccessorMethod] + public Gst.Structure stats { owned get; } + [Version (since = "1.20")] + public signal void add_extension (owned Gst.RTP.HeaderExtension ext); + [Version (since = "1.20")] + public signal void clear_extensions (); + [Version (since = "1.20")] + public signal Gst.RTP.HeaderExtension request_extension (uint ext_id, string? ext_uri); + } + [CCode (cheader_filename = "gst/rtp/rtp.h", type_id = "gst_rtp_base_payload_get_type ()")] + [GIR (name = "RTPBasePayload")] + public abstract class BasePayload : Gst.Element { + [CCode (has_construct_function = false)] + protected BasePayload (); + [Version (since = "1.16")] + public Gst.Buffer allocate_output_buffer (uint payload_len, uint8 pad_len, uint8 csrc_count); + [NoWrapper] + public virtual Gst.Caps get_caps (Gst.Pad pad, Gst.Caps filter); + [Version (since = "1.16")] + public uint get_source_count (Gst.Buffer buffer); + [NoWrapper] + public virtual Gst.FlowReturn handle_buffer (Gst.Buffer buffer); + public bool is_filled (uint size, Gst.ClockTime duration); + [Version (since = "1.16")] + public bool is_source_info_enabled (); + public Gst.FlowReturn push (Gst.Buffer buffer); + public Gst.FlowReturn push_list (Gst.BufferList list); + [NoWrapper] + public virtual bool query (Gst.Pad pad, Gst.Query query); + [NoWrapper] + public virtual bool set_caps (Gst.Caps caps); + public void set_options (string media, bool @dynamic, string encoding_name, uint32 clock_rate); + [Version (since = "1.20")] + public bool set_outcaps_structure (Gst.Structure? s); + [Version (since = "1.16")] + public void set_source_info_enabled (bool enable); + [NoWrapper] + public virtual bool sink_event (Gst.Event event); + [NoWrapper] + public virtual bool src_event (Gst.Event event); + [NoAccessorMethod] + [Version (since = "1.20")] + public bool auto_header_extension { get; set; } + [NoAccessorMethod] + public int64 max_ptime { get; set; } + [NoAccessorMethod] + public int64 min_ptime { get; set; } + [NoAccessorMethod] + public uint mtu { get; set; } + [NoAccessorMethod] + [Version (since = "1.16")] + public bool onvif_no_rate_control { get; set; } + [NoAccessorMethod] + public bool perfect_rtptime { get; set; } + [NoAccessorMethod] + public uint pt { get; set; } + [NoAccessorMethod] + public int64 ptime_multiple { get; set; } + [NoAccessorMethod] + [Version (since = "1.18")] + public bool scale_rtptime { get; set; } + [NoAccessorMethod] + public uint seqnum { get; } + [NoAccessorMethod] + public int seqnum_offset { get; set; } + [NoAccessorMethod] + [Version (since = "1.16")] + public bool source_info { get; set; } + [NoAccessorMethod] + public uint ssrc { get; set; } + [NoAccessorMethod] + public Gst.Structure stats { owned get; } + [NoAccessorMethod] + public uint timestamp { get; } + [NoAccessorMethod] + public uint timestamp_offset { get; set; } + [Version (since = "1.20")] + public signal void add_extension (owned Gst.RTP.HeaderExtension ext); + [Version (since = "1.20")] + public signal void clear_extensions (); + [Version (since = "1.20")] + public signal Gst.RTP.HeaderExtension request_extension (uint ext_id, string ext_uri); + } + [CCode (cheader_filename = "gst/rtp/rtp.h", type_id = "gst_rtp_header_extension_get_type ()")] + [GIR (name = "RTPHeaderExtension")] + [Version (since = "1.20")] + public abstract class HeaderExtension : Gst.Element { + public uint ext_id; + [CCode (has_construct_function = false)] + protected HeaderExtension (); + public static Gst.RTP.HeaderExtension? create_from_uri (string uri); + public uint get_id (); + public virtual size_t get_max_size (Gst.Buffer input_meta); + public string get_sdp_caps_field_name (); + public virtual Gst.RTP.HeaderExtensionFlags get_supported_flags (); + public unowned string get_uri (); + public virtual bool read (Gst.RTP.HeaderExtensionFlags read_flags, [CCode (array_length_cname = "size", array_length_pos = 2.5, array_length_type = "gsize", type = "const guint8*")] uint8[] data, Gst.Buffer buffer); + public virtual bool set_attributes_from_caps (Gst.Caps caps); + public bool set_attributes_from_caps_simple_sdp (Gst.Caps caps); + public virtual bool set_caps_from_attributes (Gst.Caps caps); + public bool set_caps_from_attributes_simple_sdp (Gst.Caps caps); + public void set_id (uint ext_id); + public virtual bool set_non_rtp_sink_caps (Gst.Caps caps); + [CCode (cname = "gst_rtp_header_extension_class_set_uri")] + public class void set_uri (string uri); + public void set_wants_update_non_rtp_src_caps (bool state); + public virtual bool update_non_rtp_src_caps (Gst.Caps caps); + public virtual size_t write (Gst.Buffer input_meta, Gst.RTP.HeaderExtensionFlags write_flags, Gst.Buffer output, [CCode (array_length_cname = "size", array_length_pos = 4.1, array_length_type = "gsize", type = "guint8*")] uint8[] data); + } + [CCode (cheader_filename = "gst/rtp/rtp.h", has_type_id = false)] + [GIR (name = "RTPBuffer")] + public struct Buffer { + public weak Gst.Buffer buffer; + public uint state; + [CCode (array_length = false)] + public weak void* data[4]; + [CCode (array_length = false)] + public weak size_t size[4]; + public bool add_extension_onebyte_header (uint8 id, [CCode (array_length_cname = "size", array_length_pos = 2.1, array_length_type = "guint")] uint8[] data); + public bool add_extension_twobytes_header (uint8 appbits, uint8 id, [CCode (array_length_cname = "size", array_length_pos = 3.1, array_length_type = "guint")] uint8[] data); + [CCode (cname = "gst_buffer_add_rtp_source_meta")] + [Version (since = "1.16")] + public static unowned Gst.RTP.SourceMeta? add_rtp_source_meta (Gst.Buffer buffer, uint32? ssrc, uint32? csrc, uint csrc_count); + public static void allocate_data (Gst.Buffer buffer, uint payload_len, uint8 pad_len, uint8 csrc_count); + public static uint calc_header_len (uint8 csrc_count); + public static uint calc_packet_len (uint payload_len, uint8 pad_len, uint8 csrc_count); + public static uint calc_payload_len (uint packet_len, uint8 pad_len, uint8 csrc_count); + public static int compare_seqnum (uint16 seqnum1, uint16 seqnum2); + public static uint32 default_clock_rate (uint8 payload_type); + public static uint64 ext_timestamp (ref uint64 exttimestamp, uint32 timestamp); + public uint32 get_csrc (uint8 idx); + public uint8 get_csrc_count (); + public bool get_extension (); + [Version (since = "1.2")] + public GLib.Bytes get_extension_bytes (out uint16 bits); + public bool get_extension_data (out uint16 bits, [CCode (array_length = false)] out unowned uint8[] data, out uint wordlen); + public bool get_extension_onebyte_header (uint8 id, uint nth, [CCode (array_length_cname = "size", array_length_pos = 3.1, array_length_type = "guint")] out unowned uint8[] data); + [Version (since = "1.18")] + public static bool get_extension_onebyte_header_from_bytes (GLib.Bytes bytes, uint16 bit_pattern, uint8 id, uint nth, [CCode (array_length_cname = "size", array_length_pos = 5.1, array_length_type = "guint")] out unowned uint8[] data); + public bool get_extension_twobytes_header (out uint8 appbits, uint8 id, uint nth, [CCode (array_length_cname = "size", array_length_pos = 4.1, array_length_type = "guint")] out unowned uint8[] data); + public uint get_header_len (); + public bool get_marker (); + public uint get_packet_len (); + public bool get_padding (); + [CCode (array_length = false)] + public unowned uint8[] get_payload (); + public Gst.Buffer get_payload_buffer (); + [Version (since = "1.2")] + public GLib.Bytes get_payload_bytes (); + public uint get_payload_len (); + public Gst.Buffer get_payload_subbuffer (uint offset, uint len); + public uint8 get_payload_type (); + [CCode (cname = "gst_buffer_get_rtp_source_meta")] + [Version (since = "1.16")] + public static unowned Gst.RTP.SourceMeta? get_rtp_source_meta (Gst.Buffer buffer); + public uint16 get_seq (); + public uint32 get_ssrc (); + public uint32 get_timestamp (); + public uint8 get_version (); + public static bool map (Gst.Buffer buffer, Gst.MapFlags flags, out Gst.RTP.Buffer rtp); + public static Gst.Buffer new_allocate (uint payload_len, uint8 pad_len, uint8 csrc_count); + public static Gst.Buffer new_allocate_len (uint packet_len, uint8 pad_len, uint8 csrc_count); + public static Gst.Buffer new_copy_data ([CCode (array_length_cname = "len", array_length_pos = 1.1, array_length_type = "gsize")] uint8[] data); + public static Gst.Buffer new_take_data ([CCode (array_length_cname = "len", array_length_pos = 1.1, array_length_type = "gsize")] owned uint8[] data); + public void pad_to (uint len); + public void set_csrc (uint8 idx, uint32 csrc); + public void set_extension (bool extension); + public bool set_extension_data (uint16 bits, uint16 length); + public void set_marker (bool marker); + public void set_packet_len (uint len); + public void set_padding (bool padding); + public void set_payload_type (uint8 payload_type); + public void set_seq (uint16 seq); + public void set_ssrc (uint32 ssrc); + public void set_timestamp (uint32 timestamp); + public void set_version (uint8 version); + public void unmap (); + } + [CCode (cheader_filename = "gst/rtp/rtp.h", has_type_id = false)] + [GIR (name = "RTPPayloadInfo")] + public struct PayloadInfo { + public uint8 payload_type; + public weak string media; + public weak string encoding_name; + public uint clock_rate; + public weak string encoding_parameters; + public uint bitrate; + } + [CCode (cheader_filename = "gst/rtp/rtp.h", has_type_id = false)] + [GIR (name = "RTPSourceMeta")] + [Version (since = "1.16")] + public struct SourceMeta { + public Gst.Meta meta; + public uint32 ssrc; + public bool ssrc_valid; + [CCode (array_length = false)] + public weak uint32 csrc[15]; + public uint csrc_count; + public bool append_csrc ([CCode (array_length_cname = "csrc_count", array_length_pos = 1.1, array_length_type = "guint", type = "const guint32*")] uint32[] csrc); + public uint get_source_count (); + public bool set_ssrc (uint32? ssrc); + } + [CCode (cheader_filename = "gst/rtp/rtp.h", cprefix = "GST_RTP_BUFFER_FLAG_", type_id = "gst_rtp_buffer_flags_get_type ()")] + [Flags] + [GIR (name = "RTPBufferFlags")] + [Version (since = "1.10")] + public enum BufferFlags { + RETRANSMISSION, + REDUNDANT, + LAST + } + [CCode (cheader_filename = "gst/rtp/rtp.h", cprefix = "GST_RTP_BUFFER_MAP_FLAG_", type_id = "gst_rtp_buffer_map_flags_get_type ()")] + [Flags] + [GIR (name = "RTPBufferMapFlags")] + [Version (since = "1.6.1")] + public enum BufferMapFlags { + SKIP_PADDING, + LAST + } + [CCode (cheader_filename = "gst/rtp/rtp.h", cprefix = "GST_RTP_HEADER_EXTENSION_", type_id = "gst_rtp_header_extension_flags_get_type ()")] + [Flags] + [GIR (name = "RTPHeaderExtensionFlags")] + [Version (since = "1.20")] + public enum HeaderExtensionFlags { + ONE_BYTE, + TWO_BYTE + } + [CCode (cheader_filename = "gst/rtp/rtp.h", cprefix = "GST_RTP_PAYLOAD_", type_id = "gst_rtp_payload_get_type ()")] + [GIR (name = "RTPPayload")] + public enum Payload { + PCMU, + @1016, + G721, + GSM, + G723, + DVI4_8000, + DVI4_16000, + LPC, + PCMA, + G722, + L16_STEREO, + L16_MONO, + QCELP, + CN, + MPA, + G728, + DVI4_11025, + DVI4_22050, + G729, + CELLB, + JPEG, + NV, + H261, + MPV, + MP2T, + H263; + public const string @1016_STRING; + public const string CELLB_STRING; + public const string CN_STRING; + public const string DVI4_11025_STRING; + public const string DVI4_16000_STRING; + public const string DVI4_22050_STRING; + public const string DVI4_8000_STRING; + public const string DYNAMIC_STRING; + public const string G721_STRING; + public const string G722_STRING; + public const int G723_53; + public const string G723_53_STRING; + public const int G723_63; + public const string G723_63_STRING; + public const string G723_STRING; + public const string G728_STRING; + public const string G729_STRING; + public const string GSM_STRING; + public const string H261_STRING; + public const string H263_STRING; + public const string JPEG_STRING; + public const string L16_MONO_STRING; + public const string L16_STEREO_STRING; + public const string LPC_STRING; + public const string MP2T_STRING; + public const string MPA_STRING; + public const string MPV_STRING; + public const string NV_STRING; + public const string PCMA_STRING; + public const string PCMU_STRING; + public const string QCELP_STRING; + public const int TS41; + public const string TS41_STRING; + public const int TS48; + public const string TS48_STRING; + } + [CCode (cheader_filename = "gst/rtp/rtp.h", cprefix = "GST_RTP_PROFILE_", type_id = "gst_rtp_profile_get_type ()")] + [GIR (name = "RTPProfile")] + [Version (since = "1.6")] + public enum Profile { + UNKNOWN, + AVP, + SAVP, + AVPF, + SAVPF + } + [CCode (cheader_filename = "gst/rtp/rtp.h", cname = "GST_RTP_HDREXT_BASE")] + public const string HDREXT_BASE; + [CCode (cheader_filename = "gst/rtp/rtp.h", cname = "GST_RTP_HDREXT_ELEMENT_CLASS")] + [Version (since = "1.20")] + public const string HDREXT_ELEMENT_CLASS; + [CCode (cheader_filename = "gst/rtp/rtp.h", cname = "GST_RTP_HDREXT_NTP_56")] + public const string HDREXT_NTP_56; + [CCode (cheader_filename = "gst/rtp/rtp.h", cname = "GST_RTP_HDREXT_NTP_56_SIZE")] + public const int HDREXT_NTP_56_SIZE; + [CCode (cheader_filename = "gst/rtp/rtp.h", cname = "GST_RTP_HDREXT_NTP_64")] + public const string HDREXT_NTP_64; + [CCode (cheader_filename = "gst/rtp/rtp.h", cname = "GST_RTP_HDREXT_NTP_64_SIZE")] + public const int HDREXT_NTP_64_SIZE; + [CCode (cheader_filename = "gst/rtp/rtp.h", cname = "GST_RTP_HEADER_EXTENSION_URI_METADATA_KEY")] + [Version (since = "1.20")] + public const string HEADER_EXTENSION_URI_METADATA_KEY; + [CCode (cheader_filename = "gst/rtp/rtp.h", cname = "GST_RTP_SOURCE_META_MAX_CSRC_COUNT")] + public const int SOURCE_META_MAX_CSRC_COUNT; + [CCode (cheader_filename = "gst/rtp/rtp.h", cname = "GST_RTP_VERSION")] + public const int VERSION; + [CCode (cheader_filename = "gst/rtp/rtp.h")] + [Version (since = "1.20")] + public static GLib.List get_header_extension_list (); + [CCode (cheader_filename = "gst/rtp/rtp.h")] + public static bool hdrext_get_ntp_56 ([CCode (array_length_cname = "size", array_length_pos = 1.5, array_length_type = "guint")] uint8[] data, out uint64 ntptime); + [CCode (cheader_filename = "gst/rtp/rtp.h")] + public static bool hdrext_get_ntp_64 ([CCode (array_length_cname = "size", array_length_pos = 1.5, array_length_type = "guint")] uint8[] data, out uint64 ntptime); + [CCode (cheader_filename = "gst/rtp/rtp.h")] + public static bool hdrext_set_ntp_56 (void* data, uint size, uint64 ntptime); + [CCode (cheader_filename = "gst/rtp/rtp.h")] + public static bool hdrext_set_ntp_64 (void* data, uint size, uint64 ntptime); + [CCode (cheader_filename = "gst/rtp/rtp.h")] + public static unowned Gst.RTP.PayloadInfo? payload_info_for_name (string media, string encoding_name); + [CCode (cheader_filename = "gst/rtp/rtp.h")] + public static unowned Gst.RTP.PayloadInfo? payload_info_for_pt (uint8 payload_type); + [CCode (cheader_filename = "gst/rtp/rtp.h")] + public static GLib.Type source_meta_api_get_type (); + [CCode (cheader_filename = "gst/rtp/rtp.h")] + public static unowned Gst.MetaInfo? source_meta_get_info (); + } +} diff --git a/xmpp-vala/src/module/xep/0167_jingle_rtp/content_parameters.vala b/xmpp-vala/src/module/xep/0167_jingle_rtp/content_parameters.vala index d6f1acd2..d4440169 100644 --- a/xmpp-vala/src/module/xep/0167_jingle_rtp/content_parameters.vala +++ b/xmpp-vala/src/module/xep/0167_jingle_rtp/content_parameters.vala @@ -17,6 +17,7 @@ public class Xmpp.Xep.JingleRtp.Parameters : Jingle.ContentParameters, Object { public bool encryption_required { get; private set; default = false; } public PayloadType? agreed_payload_type { get; private set; } public Gee.List payload_types = new ArrayList(PayloadType.equals_func); + public Gee.List header_extensions = new ArrayList(); public Gee.List remote_cryptos = new ArrayList(); public Crypto? local_crypto = null; public Crypto? remote_crypto = null; @@ -54,9 +55,12 @@ public class Xmpp.Xep.JingleRtp.Parameters : Jingle.ContentParameters, Object { this.remote_cryptos.add(Crypto.parse(crypto)); } } - foreach (StanzaNode payloadType in node.get_subnodes("payload-type")) { + foreach (StanzaNode payloadType in node.get_subnodes(PayloadType.NAME)) { this.payload_types.add(PayloadType.parse(payloadType)); } + foreach (StanzaNode subnode in node.get_subnodes(HeaderExtension.NAME, HeaderExtension.NS_URI)) { + this.header_extensions.add(HeaderExtension.parse(subnode)); + } } public async void handle_proposed_content(XmppStream stream, Jingle.Session session, Jingle.Content content) { @@ -66,6 +70,11 @@ public class Xmpp.Xep.JingleRtp.Parameters : Jingle.ContentParameters, Object { content.reject(); return; } + // Drop unsupported header extensions + var iter = header_extensions.iterator(); + while(iter.next()) { + if (!parent.is_header_extension_supported(media, iter.@get())) iter.remove(); + } remote_crypto = parent.pick_remote_crypto(remote_cryptos); if (local_crypto == null && remote_crypto != null) { local_crypto = parent.pick_local_crypto(remote_crypto); @@ -151,7 +160,7 @@ public class Xmpp.Xep.JingleRtp.Parameters : Jingle.ContentParameters, Object { Gee.List crypto_nodes = description_node.get_deep_subnodes("encryption", "crypto"); if (crypto_nodes.size == 0) { - warning("Counterpart didn't include any cryptos"); + debug("Counterpart didn't include any cryptos"); if (encryption_required) { return; } @@ -182,6 +191,9 @@ public class Xmpp.Xep.JingleRtp.Parameters : Jingle.ContentParameters, Object { ret.put_node(payload_type.to_xml()); } } + foreach (HeaderExtension ext in header_extensions) { + ret.put_node(ext.to_xml()); + } if (local_crypto != null) { ret.put_node(new StanzaNode.build("encryption", NS_URI) .put_node(local_crypto.to_xml())); @@ -191,4 +203,28 @@ public class Xmpp.Xep.JingleRtp.Parameters : Jingle.ContentParameters, Object { } return ret; } +} + +public class Xmpp.Xep.JingleRtp.HeaderExtension { + public const string NS_URI = "urn:xmpp:jingle:apps:rtp:rtp-hdrext:0"; + public const string NAME = "rtp-hdrext"; + + public uint8 id { get; private set; } + public string uri { get; private set; } + + public HeaderExtension(uint8 id, string uri) { + this.id = id; + this.uri = uri; + } + + public static HeaderExtension parse(StanzaNode node) { + return new HeaderExtension((uint8) node.get_attribute_int("id"), node.get_attribute("uri")); + } + + public StanzaNode to_xml() { + return new StanzaNode.build(NAME, NS_URI) + .add_self_xmlns() + .put_attribute("id", id.to_string()) + .put_attribute("uri", uri); + } } \ No newline at end of file diff --git a/xmpp-vala/src/module/xep/0167_jingle_rtp/jingle_rtp_module.vala b/xmpp-vala/src/module/xep/0167_jingle_rtp/jingle_rtp_module.vala index 3adad114..6eb6289b 100644 --- a/xmpp-vala/src/module/xep/0167_jingle_rtp/jingle_rtp_module.vala +++ b/xmpp-vala/src/module/xep/0167_jingle_rtp/jingle_rtp_module.vala @@ -24,6 +24,8 @@ public abstract class Module : XmppStreamModule { public abstract Crypto? pick_remote_crypto(Gee.List cryptos); public abstract Crypto? pick_local_crypto(Crypto? remote); public abstract Stream create_stream(Jingle.Content content); + public abstract bool is_header_extension_supported(string media, HeaderExtension ext); + public abstract Gee.List get_suggested_header_extensions(string media); public abstract void close_stream(Stream stream); public async Jingle.Session start_call(XmppStream stream, Jid receiver_full_jid, bool video, string? sid = null) throws Jingle.Error { @@ -40,6 +42,7 @@ public abstract class Module : XmppStreamModule { // Create audio content Parameters audio_content_parameters = new Parameters(this, "audio", yield get_supported_payloads("audio")); audio_content_parameters.local_crypto = generate_local_crypto(); + audio_content_parameters.header_extensions.add_all(get_suggested_header_extensions("audio")); Jingle.Transport? audio_transport = yield jingle_module.select_transport(stream, content_type.required_transport_type, content_type.required_components, receiver_full_jid, Set.empty()); if (audio_transport == null) { throw new Jingle.Error.NO_SHARED_PROTOCOLS("No suitable audio transports"); @@ -57,6 +60,7 @@ public abstract class Module : XmppStreamModule { // Create video content Parameters video_content_parameters = new Parameters(this, "video", yield get_supported_payloads("video")); video_content_parameters.local_crypto = generate_local_crypto(); + video_content_parameters.header_extensions.add_all(get_suggested_header_extensions("video")); Jingle.Transport? video_transport = yield stream.get_module(Jingle.Module.IDENTITY).select_transport(stream, content_type.required_transport_type, content_type.required_components, receiver_full_jid, Set.empty()); if (video_transport == null) { throw new Jingle.Error.NO_SHARED_PROTOCOLS("No suitable video transports"); @@ -98,6 +102,7 @@ public abstract class Module : XmppStreamModule { // Content for video does not yet exist -> create it Parameters video_content_parameters = new Parameters(this, "video", yield get_supported_payloads("video")); video_content_parameters.local_crypto = generate_local_crypto(); + video_content_parameters.header_extensions.add_all(get_suggested_header_extensions("video")); Jingle.Transport? video_transport = yield stream.get_module(Jingle.Module.IDENTITY).select_transport(stream, content_type.required_transport_type, content_type.required_components, receiver_full_jid, Set.empty()); if (video_transport == null) { throw new Jingle.Error.NO_SHARED_PROTOCOLS("No suitable video transports"); diff --git a/xmpp-vala/src/module/xep/0167_jingle_rtp/payload_type.vala b/xmpp-vala/src/module/xep/0167_jingle_rtp/payload_type.vala index 452f1d65..faba38c9 100644 --- a/xmpp-vala/src/module/xep/0167_jingle_rtp/payload_type.vala +++ b/xmpp-vala/src/module/xep/0167_jingle_rtp/payload_type.vala @@ -3,6 +3,8 @@ using Xmpp; using Xmpp.Xep; public class Xmpp.Xep.JingleRtp.PayloadType { + public const string NAME = "payload-type"; + public uint8 id { get; set; } public string? name { get; set; } public uint8 channels { get; set; default = 1; } @@ -10,6 +12,7 @@ public class Xmpp.Xep.JingleRtp.PayloadType { public uint32 maxptime { get; set; } public uint32 ptime { get; set; } public Map parameters = new HashMap(); + public Gee.List rtcp_fbs = new ArrayList(); public static PayloadType parse(StanzaNode node) { PayloadType payloadType = new PayloadType(); @@ -22,11 +25,14 @@ public class Xmpp.Xep.JingleRtp.PayloadType { foreach (StanzaNode parameter in node.get_subnodes("parameter")) { payloadType.parameters[parameter.get_attribute("name")] = parameter.get_attribute("value"); } + foreach (StanzaNode subnode in node.get_subnodes(RtcpFeedback.NAME, RtcpFeedback.NS_URI)) { + payloadType.rtcp_fbs.add(RtcpFeedback.parse(subnode)); + } return payloadType; } public StanzaNode to_xml() { - StanzaNode node = new StanzaNode.build("payload-type", NS_URI) + StanzaNode node = new StanzaNode.build(NAME, NS_URI) .put_attribute("id", id.to_string()); if (channels != 1) node.put_attribute("channels", channels.to_string()); if (clockrate != 0) node.put_attribute("clockrate", clockrate.to_string()); @@ -38,9 +44,25 @@ public class Xmpp.Xep.JingleRtp.PayloadType { .put_attribute("name", parameter) .put_attribute("value", parameters[parameter])); } + foreach (RtcpFeedback rtcp_fb in rtcp_fbs) { + node.put_node(rtcp_fb.to_xml()); + } return node; } + public PayloadType clone() { + PayloadType clone = new PayloadType(); + clone.id = id; + clone.name = name; + clone.channels = channels; + clone.clockrate = clockrate; + clone.maxptime = maxptime; + clone.ptime = ptime; + clone.parameters.set_all(parameters); + clone.rtcp_fbs.add_all(rtcp_fbs); + return clone; + } + public static bool equals_func(PayloadType a, PayloadType b) { return a.id == b.id && a.name == b.name && @@ -49,4 +71,29 @@ public class Xmpp.Xep.JingleRtp.PayloadType { a.maxptime == b.maxptime && a.ptime == b.ptime; } +} + +public class Xmpp.Xep.JingleRtp.RtcpFeedback { + public const string NS_URI = "urn:xmpp:jingle:apps:rtp:rtcp-fb:0"; + public const string NAME = "rtcp-fb"; + + public string type_ { get; private set; } + public string? subtype { get; private set; } + + public RtcpFeedback(string type, string? subtype = null) { + this.type_ = type; + this.subtype = subtype; + } + + public static RtcpFeedback parse(StanzaNode node) { + return new RtcpFeedback(node.get_attribute("type"), node.get_attribute("subtype")); + } + + public StanzaNode to_xml() { + StanzaNode node = new StanzaNode.build(NAME, NS_URI) + .add_self_xmlns() + .put_attribute("type", type_); + if (subtype != null) node.put_attribute("subtype", subtype); + return node; + } } \ No newline at end of file diff --git a/xmpp-vala/src/module/xep/0167_jingle_rtp/stream.vala b/xmpp-vala/src/module/xep/0167_jingle_rtp/stream.vala index adae11f5..65be8a0a 100644 --- a/xmpp-vala/src/module/xep/0167_jingle_rtp/stream.vala +++ b/xmpp-vala/src/module/xep/0167_jingle_rtp/stream.vala @@ -33,6 +33,13 @@ public abstract class Xmpp.Xep.JingleRtp.Stream : Object { } return null; }} + public Gee.List? header_extensions { get { + var content_params = content.content_params; + if (content_params is Parameters) { + return ((Parameters)content_params).header_extensions; + } + return null; + }} public bool sending { get { return content.session.senders_include_us(content.senders); }} -- cgit v1.2.3-54-g00ecf From 421f43dd8bd993eb88581e1b5011cc061ceb4fc8 Mon Sep 17 00:00:00 2001 From: fiaxh Date: Sun, 25 Apr 2021 19:49:10 +0200 Subject: Add support for OMEMO call encryption --- libdino/src/service/calls.vala | 44 ++- libdino/src/service/connection_manager.vala | 4 +- main/src/ui/call_window/call_bottom_bar.vala | 17 +- plugins/ice/src/transport_parameters.vala | 15 +- plugins/omemo/CMakeLists.txt | 4 +- .../omemo/src/dtls_srtp_verification_draft.vala | 195 +++++++++++++ plugins/omemo/src/jingle/jet_omemo.vala | 82 ++---- plugins/omemo/src/logic/decrypt.vala | 210 ++++++++++++++ plugins/omemo/src/logic/encrypt.vala | 131 +++++++++ plugins/omemo/src/logic/encrypt_state.vala | 24 -- plugins/omemo/src/logic/manager.vala | 16 +- plugins/omemo/src/logic/trust_manager.vala | 302 +-------------------- plugins/omemo/src/plugin.vala | 22 +- plugins/omemo/src/protocol/stream_module.vala | 6 +- xmpp-vala/CMakeLists.txt | 3 + xmpp-vala/src/module/iq/module.vala | 5 + xmpp-vala/src/module/xep/0166_jingle/content.vala | 3 +- .../module/xep/0166_jingle/content_transport.vala | 2 +- .../src/module/xep/0166_jingle/jingle_module.vala | 4 +- xmpp-vala/src/module/xep/0166_jingle/session.vala | 10 +- .../xep/0167_jingle_rtp/content_parameters.vala | 3 +- .../0176_jingle_ice_udp/jingle_ice_udp_module.vala | 2 +- .../0176_jingle_ice_udp/transport_parameters.vala | 6 +- .../module/xep/0260_jingle_socks5_bytestreams.vala | 2 +- .../xep/0261_jingle_in_band_bytestreams.vala | 2 +- .../module/xep/0353_jingle_message_initiation.vala | 2 + .../src/module/xep/0384_omemo/omemo_decryptor.vala | 62 +++++ .../src/module/xep/0384_omemo/omemo_encryptor.vala | 116 ++++++++ 28 files changed, 859 insertions(+), 435 deletions(-) create mode 100644 plugins/omemo/src/dtls_srtp_verification_draft.vala create mode 100644 plugins/omemo/src/logic/decrypt.vala create mode 100644 plugins/omemo/src/logic/encrypt.vala delete mode 100644 plugins/omemo/src/logic/encrypt_state.vala create mode 100644 xmpp-vala/src/module/xep/0384_omemo/omemo_decryptor.vala create mode 100644 xmpp-vala/src/module/xep/0384_omemo/omemo_encryptor.vala (limited to 'xmpp-vala/src/module/xep/0167_jingle_rtp') diff --git a/libdino/src/service/calls.vala b/libdino/src/service/calls.vala index 1d47823d..3615e24f 100644 --- a/libdino/src/service/calls.vala +++ b/libdino/src/service/calls.vala @@ -40,8 +40,8 @@ namespace Dino { private HashMap video_content_parameter = new HashMap(Call.hash_func, Call.equals_func); private HashMap audio_content = new HashMap(Call.hash_func, Call.equals_func); private HashMap video_content = new HashMap(Call.hash_func, Call.equals_func); - private HashMap video_encryption = new HashMap(Call.hash_func, Call.equals_func); - private HashMap audio_encryption = new HashMap(Call.hash_func, Call.equals_func); + private HashMap> video_encryptions = new HashMap>(Call.hash_func, Call.equals_func); + private HashMap> audio_encryptions = new HashMap>(Call.hash_func, Call.equals_func); public static void start(StreamInteractor stream_interactor, Database db) { Calls m = new Calls(stream_interactor, db); @@ -498,24 +498,46 @@ namespace Dino { } if (media == "audio") { - audio_encryption[call] = content.encryption; + audio_encryptions[call] = content.encryptions; } else if (media == "video") { - video_encryption[call] = content.encryption; + video_encryptions[call] = content.encryptions; } - if ((audio_encryption.has_key(call) && audio_encryption[call] == null) || (video_encryption.has_key(call) && video_encryption[call] == null)) { + if ((audio_encryptions.has_key(call) && audio_encryptions[call].is_empty) || (video_encryptions.has_key(call) && video_encryptions[call].is_empty)) { call.encryption = Encryption.NONE; encryption_updated(call, null); return; } - Xep.Jingle.ContentEncryption encryption = audio_encryption[call] ?? video_encryption[call]; - if (encryption.encryption_ns == Xep.JingleIceUdp.DTLS_NS_URI) { + HashMap encryptions = audio_encryptions[call] ?? video_encryptions[call]; + + Xep.Jingle.ContentEncryption? omemo_encryption = null, dtls_encryption = null, srtp_encryption = null; + foreach (string encr_name in encryptions.keys) { + if (video_encryptions.has_key(call) && !video_encryptions[call].has_key(encr_name)) continue; + + var encryption = encryptions[encr_name]; + if (encryption.encryption_ns == "http://gultsch.de/xmpp/drafts/omemo/dlts-srtp-verification") { + omemo_encryption = encryption; + } else if (encryption.encryption_ns == Xep.JingleIceUdp.DTLS_NS_URI) { + dtls_encryption = encryption; + } else if (encryption.encryption_name == "SRTP") { + srtp_encryption = encryption; + } + } + + if (omemo_encryption != null && dtls_encryption != null) { + call.encryption = Encryption.OMEMO; + encryption_updated(call, omemo_encryption); + } else if (dtls_encryption != null) { call.encryption = Encryption.DTLS_SRTP; - } else if (encryption.encryption_name == "SRTP") { + encryption_updated(call, dtls_encryption); + } else if (srtp_encryption != null) { call.encryption = Encryption.SRTP; + encryption_updated(call, srtp_encryption); + } else { + call.encryption = Encryption.NONE; + encryption_updated(call, null); } - encryption_updated(call, encryption); } private void remove_call_from_datastructures(Call call) { @@ -533,8 +555,8 @@ namespace Dino { video_content_parameter.unset(call); audio_content.unset(call); video_content.unset(call); - audio_encryption.unset(call); - video_encryption.unset(call); + audio_encryptions.unset(call); + video_encryptions.unset(call); } private void on_account_added(Account account) { diff --git a/libdino/src/service/connection_manager.vala b/libdino/src/service/connection_manager.vala index 1439c6f3..0eb6a6f5 100644 --- a/libdino/src/service/connection_manager.vala +++ b/libdino/src/service/connection_manager.vala @@ -350,7 +350,9 @@ public class ConnectionManager : Object { foreach (Account account in connections.keys) { try { make_offline(account); - yield connections[account].stream.disconnect(); + if (connections[account].stream != null) { + yield connections[account].stream.disconnect(); + } } catch (Error e) { debug("Error disconnecting stream %p: %s", connections[account].stream, e.message); } diff --git a/main/src/ui/call_window/call_bottom_bar.vala b/main/src/ui/call_window/call_bottom_bar.vala index c6375ea2..a9fee8c3 100644 --- a/main/src/ui/call_window/call_bottom_bar.vala +++ b/main/src/ui/call_window/call_bottom_bar.vala @@ -100,16 +100,25 @@ public class Dino.Ui.CallBottomBar : Gtk.Box { encryption_button.get_style_context().add_class("unencrypted"); popover.add(new Label("This call isn't encrypted.") { margin=10, visible=true } ); + } else if (encryption.encryption_name == "OMEMO") { + encryption_image.set_from_icon_name("changes-prevent-symbolic", IconSize.BUTTON); + encryption_button.get_style_context().remove_class("unencrypted"); + + popover.add(new Label("This call is encrypted with OMEMO.") { 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("This call is end-to-end encrypted.") { 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("" + format_fingerprint(encryption.peer_key) + "") { 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("" + format_fingerprint(encryption.our_key) + "") { use_markup=true, max_width_chars=25, ellipsize=EllipsizeMode.MIDDLE, xalign=0, hexpand=true, visible=true }, 2, 3, 1, 1); + if (encryption.peer_key.length > 0) { + encryption_info_grid.attach(new Label("Peer key") { xalign=0, visible=true }, 1, 2, 1, 1); + encryption_info_grid.attach(new Label("" + format_fingerprint(encryption.peer_key) + "") { 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) { + encryption_info_grid.attach(new Label("Your key") { xalign=0, visible=true }, 1, 3, 1, 1); + encryption_info_grid.attach(new Label("" + format_fingerprint(encryption.our_key) + "") { 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); } diff --git a/plugins/ice/src/transport_parameters.vala b/plugins/ice/src/transport_parameters.vala index 52451fcf..38652952 100644 --- a/plugins/ice/src/transport_parameters.vala +++ b/plugins/ice/src/transport_parameters.vala @@ -77,7 +77,10 @@ public class Dino.Plugins.Ice.TransportParameters : JingleIceUdp.IceUdpTransport own_setup = "actpass"; dtls_srtp_handler.mode = DtlsSrtp.Mode.SERVER; dtls_srtp_handler.setup_dtls_connection.begin((_, res) => { - this.content.encryption = dtls_srtp_handler.setup_dtls_connection.end(res) ?? this.content.encryption; + var content_encryption = dtls_srtp_handler.setup_dtls_connection.end(res); + if (content_encryption != null) { + this.content.encryptions[content_encryption.encryption_ns] = content_encryption; + } }); } } @@ -157,7 +160,10 @@ public class Dino.Plugins.Ice.TransportParameters : JingleIceUdp.IceUdpTransport dtls_srtp_handler.mode = DtlsSrtp.Mode.CLIENT; dtls_srtp_handler.stop_dtls_connection(); dtls_srtp_handler.setup_dtls_connection.begin((_, res) => { - this.content.encryption = dtls_srtp_handler.setup_dtls_connection.end(res) ?? this.content.encryption; + var content_encryption = dtls_srtp_handler.setup_dtls_connection.end(res); + if (content_encryption != null) { + this.content.encryptions[content_encryption.encryption_ns] = content_encryption; + } }); } } else { @@ -225,7 +231,10 @@ public class Dino.Plugins.Ice.TransportParameters : JingleIceUdp.IceUdpTransport may_consider_ready(stream_id, component_id); if (incoming && dtls_srtp_handler != null && !dtls_srtp_handler.ready && is_component_ready(agent, stream_id, component_id) && dtls_srtp_handler.mode == DtlsSrtp.Mode.CLIENT) { dtls_srtp_handler.setup_dtls_connection.begin((_, res) => { - this.content.encryption = dtls_srtp_handler.setup_dtls_connection.end(res) ?? this.content.encryption; + Jingle.ContentEncryption? encryption = dtls_srtp_handler.setup_dtls_connection.end(res); + if (encryption != null) { + this.content.encryptions[encryption.encryption_ns] = encryption; + } }); } } diff --git a/plugins/omemo/CMakeLists.txt b/plugins/omemo/CMakeLists.txt index c7a45069..944fc649 100644 --- a/plugins/omemo/CMakeLists.txt +++ b/plugins/omemo/CMakeLists.txt @@ -29,6 +29,7 @@ compile_gresources( vala_precompile(OMEMO_VALA_C SOURCES + src/dtls_srtp_verification_draft.vala src/plugin.vala src/register_plugin.vala src/trust_level.vala @@ -39,7 +40,8 @@ SOURCES src/jingle/jet_omemo.vala src/logic/database.vala - src/logic/encrypt_state.vala + src/logic/decrypt.vala + src/logic/encrypt.vala src/logic/manager.vala src/logic/pre_key_store.vala src/logic/session_store.vala diff --git a/plugins/omemo/src/dtls_srtp_verification_draft.vala b/plugins/omemo/src/dtls_srtp_verification_draft.vala new file mode 100644 index 00000000..e2441670 --- /dev/null +++ b/plugins/omemo/src/dtls_srtp_verification_draft.vala @@ -0,0 +1,195 @@ +using Signal; +using Gee; +using Xmpp; + +namespace Dino.Plugins.Omemo.DtlsSrtpVerificationDraft { + public const string NS_URI = "http://gultsch.de/xmpp/drafts/omemo/dlts-srtp-verification"; + + public class StreamModule : XmppStreamModule { + + public static Xmpp.ModuleIdentity IDENTITY = new Xmpp.ModuleIdentity(NS_URI, "dtls_srtp_omemo_verification_draft"); + + private VerificationSendListener send_listener = new VerificationSendListener(); + private HashMap device_id_by_jingle_sid = new HashMap(); + private HashMap> content_names_by_jingle_sid = new HashMap>(); + + private void on_preprocess_incoming_iq_set_get(XmppStream stream, Xmpp.Iq.Stanza iq) { + if (iq.type_ != Iq.Stanza.TYPE_SET) return; + + Gee.List content_nodes = iq.stanza.get_deep_subnodes(Xep.Jingle.NS_URI + ":jingle", Xep.Jingle.NS_URI + ":content"); + if (content_nodes.size == 0) return; + + string? jingle_sid = iq.stanza.get_deep_attribute(Xep.Jingle.NS_URI + ":jingle", "sid"); + if (jingle_sid == null) return; + + Xep.Omemo.OmemoDecryptor decryptor = stream.get_module(Xep.Omemo.OmemoDecryptor.IDENTITY); + + foreach (StanzaNode content_node in content_nodes) { + string? content_name = content_node.get_attribute("name"); + if (content_name == null) continue; + StanzaNode? transport_node = content_node.get_subnode("transport", Xep.JingleIceUdp.NS_URI); + if (transport_node == null) continue; + StanzaNode? fingerprint_node = transport_node.get_subnode("fingerprint", NS_URI); + if (fingerprint_node == null) continue; + StanzaNode? encrypted_node = fingerprint_node.get_subnode("encrypted", Omemo.NS_URI); + if (encrypted_node == null) continue; + + Xep.Omemo.ParsedData? parsed_data = decryptor.parse_node(encrypted_node); + if (parsed_data == null || parsed_data.ciphertext == null) continue; + + if (device_id_by_jingle_sid.has_key(jingle_sid) && device_id_by_jingle_sid[jingle_sid] != parsed_data.sid) { + warning("Expected DTLS fingerprint to be OMEMO encrypted from %s %d, but it was from %d", iq.from.to_string(), device_id_by_jingle_sid[jingle_sid], parsed_data.sid); + } + + foreach (Bytes encr_key in parsed_data.our_potential_encrypted_keys.keys) { + parsed_data.is_prekey = parsed_data.our_potential_encrypted_keys[encr_key]; + parsed_data.encrypted_key = encr_key.get_data(); + + try { + uint8[] key = decryptor.decrypt_key(parsed_data, iq.from.bare_jid); + string cleartext = decryptor.decrypt(parsed_data.ciphertext, key, parsed_data.iv); + + StanzaNode new_fingerprint_node = new StanzaNode.build("fingerprint", Xep.JingleIceUdp.DTLS_NS_URI).add_self_xmlns() + .put_node(new StanzaNode.text(cleartext)); + string? hash_attr = fingerprint_node.get_attribute("hash", NS_URI); + string? setup_attr = fingerprint_node.get_attribute("setup", NS_URI); + if (hash_attr != null) new_fingerprint_node.put_attribute("hash", hash_attr); + if (setup_attr != null) new_fingerprint_node.put_attribute("setup", setup_attr); + transport_node.put_node(new_fingerprint_node); + + device_id_by_jingle_sid[jingle_sid] = parsed_data.sid; + if (!content_names_by_jingle_sid.has_key(content_name)) { + content_names_by_jingle_sid[content_name] = new ArrayList(); + } + content_names_by_jingle_sid[content_name].add(content_name); + + 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) print(@"$(session.contents_map.has_key(content_name))\n"); + 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] }; + session.contents_map[content_name].encryptions[NS_URI] = encryption; + + if (iq.stanza.get_deep_attribute(Xep.Jingle.NS_URI + ":jingle", "action") == "session-accept") { + session.additional_content_add_incoming.connect(on_content_add_received); + } + }); + + break; + } catch (Error e) { + debug("Decrypting message from %s/%d failed: %s", iq.from.bare_jid.to_string(), parsed_data.sid, e.message); + } + } + } + } + + private void on_preprocess_outgoing_iq_set_get(XmppStream stream, Xmpp.Iq.Stanza iq) { + if (iq.type_ != Iq.Stanza.TYPE_SET) return; + + StanzaNode? jingle_node = iq.stanza.get_subnode("jingle", Xep.Jingle.NS_URI); + if (jingle_node == null) return; + + string? sid = jingle_node.get_attribute("sid", Xep.Jingle.NS_URI); + if (sid == null || !device_id_by_jingle_sid.has_key(sid)) return; + + Gee.List content_nodes = jingle_node.get_subnodes("content", Xep.Jingle.NS_URI); + if (content_nodes.size == 0) return; + + foreach (StanzaNode content_node in content_nodes) { + StanzaNode? transport_node = content_node.get_subnode("transport", Xep.JingleIceUdp.NS_URI); + if (transport_node == null) continue; + StanzaNode? fingerprint_node = transport_node.get_subnode("fingerprint", Xep.JingleIceUdp.DTLS_NS_URI); + if (fingerprint_node == null) continue; + string fingerprint = fingerprint_node.get_deep_string_content(); + + Xep.Omemo.OmemoEncryptor encryptor = stream.get_module(Xep.Omemo.OmemoEncryptor.IDENTITY); + Xep.Omemo.EncryptionData enc_data = encryptor.encrypt_plaintext(fingerprint); + encryptor.encrypt_key(enc_data, iq.to.bare_jid, device_id_by_jingle_sid[sid]); + + StanzaNode new_fingerprint_node = new StanzaNode.build("fingerprint", NS_URI).add_self_xmlns().put_node(enc_data.get_encrypted_node()); + string? hash_attr = fingerprint_node.get_attribute("hash", Xep.JingleIceUdp.DTLS_NS_URI); + string? setup_attr = fingerprint_node.get_attribute("setup", Xep.JingleIceUdp.DTLS_NS_URI); + if (hash_attr != null) new_fingerprint_node.put_attribute("hash", hash_attr); + if (setup_attr != null) new_fingerprint_node.put_attribute("setup", setup_attr); + transport_node.put_node(new_fingerprint_node); + + transport_node.sub_nodes.remove(fingerprint_node); + } + } + + private void on_message_received(XmppStream stream, Xmpp.MessageStanza message) { + StanzaNode? proceed_node = message.stanza.get_subnode("proceed", Xep.JingleMessageInitiation.NS_URI); + if (proceed_node == null) return; + + string? jingle_sid = proceed_node.get_attribute("id"); + if (jingle_sid == null) return; + + StanzaNode? device_node = proceed_node.get_subnode("device", NS_URI); + if (device_node == null) return; + + int device_id = device_node.get_attribute_int("id", -1); + if (device_id == -1) return; + + device_id_by_jingle_sid[jingle_sid] = device_id; + } + + private void on_session_initiate_received(XmppStream stream, Xep.Jingle.Session session) { + if (device_id_by_jingle_sid.has_key(session.sid)) { + foreach (Xep.Jingle.Content content in session.contents) { + on_content_add_received(stream, content); + } + } + session.additional_content_add_incoming.connect(on_content_add_received); + } + + 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] }; + content.encryptions[encryption.encryption_ns] = encryption; + } + } + + public override void attach(XmppStream stream) { + stream.get_module(Xmpp.MessageModule.IDENTITY).received_message.connect(on_message_received); + stream.get_module(Xmpp.MessageModule.IDENTITY).send_pipeline.connect(send_listener); + stream.get_module(Xmpp.Iq.Module.IDENTITY).preprocess_incoming_iq_set_get.connect(on_preprocess_incoming_iq_set_get); + stream.get_module(Xmpp.Iq.Module.IDENTITY).preprocess_outgoing_iq_set_get.connect(on_preprocess_outgoing_iq_set_get); + stream.get_module(Xep.Jingle.Module.IDENTITY).session_initiate_received.connect(on_session_initiate_received); + } + + public override void detach(XmppStream stream) { + stream.get_module(Xmpp.MessageModule.IDENTITY).received_message.disconnect(on_message_received); + stream.get_module(Xmpp.MessageModule.IDENTITY).send_pipeline.disconnect(send_listener); + stream.get_module(Xmpp.Iq.Module.IDENTITY).preprocess_incoming_iq_set_get.disconnect(on_preprocess_incoming_iq_set_get); + stream.get_module(Xmpp.Iq.Module.IDENTITY).preprocess_outgoing_iq_set_get.disconnect(on_preprocess_outgoing_iq_set_get); + stream.get_module(Xep.Jingle.Module.IDENTITY).session_initiate_received.disconnect(on_session_initiate_received); + } + + public override string get_ns() { return NS_URI; } + + public override string get_id() { return IDENTITY.id; } + } + + public class VerificationSendListener : StanzaListener { + + private const string[] after_actions_const = {}; + + public override string action_group { get { return "REWRITE_NODES"; } } + public override string[] after_actions { get { return after_actions_const; } } + + public override async bool run(XmppStream stream, MessageStanza message) { + StanzaNode? proceed_node = message.stanza.get_subnode("proceed", Xep.JingleMessageInitiation.NS_URI); + if (proceed_node == null) return false; + + StanzaNode device_node = new StanzaNode.build("device", NS_URI).add_self_xmlns() + .put_attribute("id", stream.get_module(Omemo.StreamModule.IDENTITY).store.local_registration_id.to_string()); + proceed_node.put_node(device_node); + return false; + } + } + + public class OmemoContentEncryption : Xep.Jingle.ContentEncryption { + public int peer_device_id { get; set; } + } +} + diff --git a/plugins/omemo/src/jingle/jet_omemo.vala b/plugins/omemo/src/jingle/jet_omemo.vala index 14307be2..afcdfcd6 100644 --- a/plugins/omemo/src/jingle/jet_omemo.vala +++ b/plugins/omemo/src/jingle/jet_omemo.vala @@ -7,18 +7,15 @@ using Xmpp; using Xmpp.Xep; namespace Dino.Plugins.JetOmemo { + private const string NS_URI = "urn:xmpp:jingle:jet-omemo:0"; private const string AES_128_GCM_URI = "urn:xmpp:ciphers:aes-128-gcm-nopadding"; + public class Module : XmppStreamModule, Jet.EnvelopEncoding { public static Xmpp.ModuleIdentity IDENTITY = new Xmpp.ModuleIdentity(NS_URI, "0396_jet_omemo"); - private Omemo.Plugin plugin; const uint KEY_SIZE = 16; const uint IV_SIZE = 12; - public Module(Omemo.Plugin plugin) { - this.plugin = plugin; - } - public override void attach(XmppStream stream) { if (stream.get_module(Jet.Module.IDENTITY) != null) { stream.get_module(ServiceDiscovery.Module.IDENTITY).add_feature(stream, NS_URI); @@ -44,71 +41,38 @@ public class Module : XmppStreamModule, Jet.EnvelopEncoding { } public Jet.TransportSecret decode_envolop(XmppStream stream, Jid local_full_jid, Jid peer_full_jid, StanzaNode security) throws Jingle.IqError { - Store store = stream.get_module(Omemo.StreamModule.IDENTITY).store; StanzaNode? encrypted = security.get_subnode("encrypted", Omemo.NS_URI); if (encrypted == null) throw new Jingle.IqError.BAD_REQUEST("Invalid JET-OMEMO envelop: missing encrypted element"); - StanzaNode? header = encrypted.get_subnode("header", Omemo.NS_URI); - if (header == null) throw new Jingle.IqError.BAD_REQUEST("Invalid JET-OMEMO envelop: missing header element"); - string? iv_node = header.get_deep_string_content("iv"); - if (header == null) throw new Jingle.IqError.BAD_REQUEST("Invalid JET-OMEMO envelop: missing iv element"); - uint8[] iv = Base64.decode((!)iv_node); - foreach (StanzaNode key_node in header.get_subnodes("key")) { - if (key_node.get_attribute_int("rid") == store.local_registration_id) { - string? key_node_content = key_node.get_string_content(); - - uint8[] key; - Address address = new Address(peer_full_jid.bare_jid.to_string(), header.get_attribute_int("sid")); - if (key_node.get_attribute_bool("prekey")) { - PreKeySignalMessage msg = Omemo.Plugin.get_context().deserialize_pre_key_signal_message(Base64.decode((!)key_node_content)); - SessionCipher cipher = store.create_session_cipher(address); - key = cipher.decrypt_pre_key_signal_message(msg); - } else { - SignalMessage msg = Omemo.Plugin.get_context().deserialize_signal_message(Base64.decode((!)key_node_content)); - SessionCipher cipher = store.create_session_cipher(address); - key = cipher.decrypt_signal_message(msg); - } - address.device_id = 0; // TODO: Hack to have address obj live longer - - uint8[] authtag = null; - if (key.length >= 32) { - int authtaglength = key.length - 16; - authtag = new uint8[authtaglength]; - uint8[] new_key = new uint8[16]; - Memory.copy(authtag, (uint8*)key + 16, 16); - Memory.copy(new_key, key, 16); - key = new_key; - } - // TODO: authtag? - return new Jet.TransportSecret(key, iv); + + Xep.Omemo.OmemoDecryptor decryptor = stream.get_module(Xep.Omemo.OmemoDecryptor.IDENTITY); + + Xmpp.Xep.Omemo.ParsedData? data = decryptor.parse_node(encrypted); + if (data == null) throw new Jingle.IqError.BAD_REQUEST("Invalid JET-OMEMO envelop: bad encrypted element"); + + foreach (Bytes encr_key in data.our_potential_encrypted_keys.keys) { + data.is_prekey = data.our_potential_encrypted_keys[encr_key]; + data.encrypted_key = encr_key.get_data(); + + try { + uint8[] key = decryptor.decrypt_key(data, peer_full_jid.bare_jid); + return new Jet.TransportSecret(key, data.iv); + } catch (GLib.Error e) { + debug("Decrypting JET key from %s/%d failed: %s", peer_full_jid.bare_jid.to_string(), data.sid, e.message); } } throw new Jingle.IqError.NOT_ACCEPTABLE("Not encrypted for targeted device"); } public void encode_envelop(XmppStream stream, Jid local_full_jid, Jid peer_full_jid, Jet.SecurityParameters security_params, StanzaNode security) { - ArrayList accounts = plugin.app.stream_interactor.get_accounts(); Store store = stream.get_module(Omemo.StreamModule.IDENTITY).store; - Account? account = null; - foreach (Account compare in accounts) { - if (compare.bare_jid.equals_bare(local_full_jid)) { - account = compare; - break; - } - } - if (account == null) { - // TODO - critical("Sending from offline account %s", local_full_jid.to_string()); - } - StanzaNode header_node; - StanzaNode encrypted_node = new StanzaNode.build("encrypted", Omemo.NS_URI).add_self_xmlns() - .put_node(header_node = new StanzaNode.build("header", Omemo.NS_URI) - .put_attribute("sid", store.local_registration_id.to_string()) - .put_node(new StanzaNode.build("iv", Omemo.NS_URI) - .put_node(new StanzaNode.text(Base64.encode(security_params.secret.initialization_vector))))); + var encryption_data = new Xep.Omemo.EncryptionData(store.local_registration_id); + encryption_data.iv = security_params.secret.initialization_vector; + encryption_data.keytag = security_params.secret.transport_key; + Xep.Omemo.OmemoEncryptor encryptor = stream.get_module(Xep.Omemo.OmemoEncryptor.IDENTITY); + encryptor.encrypt_key_to_recipient(stream, encryption_data, peer_full_jid.bare_jid); - plugin.trust_manager.encrypt_key(header_node, security_params.secret.transport_key, local_full_jid.bare_jid, new ArrayList.wrap(new Jid[] {peer_full_jid.bare_jid}), stream, account); - security.put_node(encrypted_node); + security.put_node(encryption_data.get_encrypted_node()); } public override string get_ns() { return NS_URI; } diff --git a/plugins/omemo/src/logic/decrypt.vala b/plugins/omemo/src/logic/decrypt.vala new file mode 100644 index 00000000..3cdacbf7 --- /dev/null +++ b/plugins/omemo/src/logic/decrypt.vala @@ -0,0 +1,210 @@ +using Dino.Entities; +using Qlite; +using Gee; +using Signal; +using Xmpp; + +namespace Dino.Plugins.Omemo { + + public class OmemoDecryptor : Xep.Omemo.OmemoDecryptor { + + private Account account; + private Store store; + private Database db; + private StreamInteractor stream_interactor; + private TrustManager trust_manager; + + public override uint32 own_device_id { get { return store.local_registration_id; }} + + public OmemoDecryptor(Account account, StreamInteractor stream_interactor, TrustManager trust_manager, Database db, Store store) { + this.account = account; + this.stream_interactor = stream_interactor; + this.trust_manager = trust_manager; + this.db = db; + this.store = store; + } + + public bool decrypt_message(Entities.Message message, Xmpp.MessageStanza stanza, Conversation conversation) { + StanzaNode? encrypted_node = stanza.stanza.get_subnode("encrypted", NS_URI); + if (encrypted_node == null || MessageFlag.get_flag(stanza) != null || stanza.from == null) return false; + + if (message.body == null && Xep.ExplicitEncryption.get_encryption_tag(stanza) == NS_URI) { + message.body = "[This message is OMEMO encrypted]"; // TODO temporary + } + if (!Plugin.ensure_context()) return false; + int identity_id = db.identity.get_id(conversation.account.id); + + MessageFlag flag = new MessageFlag(); + stanza.add_flag(flag); + + Xep.Omemo.ParsedData? data = parse_node(encrypted_node); + if (data == null || data.ciphertext == null) return false; + + + foreach (Bytes encr_key in data.our_potential_encrypted_keys.keys) { + data.is_prekey = data.our_potential_encrypted_keys[encr_key]; + data.encrypted_key = encr_key.get_data(); + Gee.List possible_jids = get_potential_message_jids(message, data, identity_id); + if (possible_jids.size == 0) { + debug("Received message from unknown entity with device id %d", data.sid); + } + + foreach (Jid possible_jid in possible_jids) { + try { + uint8[] key = decrypt_key(data, possible_jid); + string cleartext = arr_to_str(aes_decrypt(Cipher.AES_GCM_NOPADDING, key, data.iv, data.ciphertext)); + + // If we figured out which real jid a message comes from due to decryption working, save it + if (conversation.type_ == Conversation.Type.GROUPCHAT && message.real_jid == null) { + message.real_jid = possible_jid; + } + + trust_manager.message_device_id_map[message] = data.sid; + message.body = cleartext; + message.encryption = Encryption.OMEMO; + return true; + } catch (Error e) { + debug("Decrypting message from %s/%d failed: %s", possible_jid.to_string(), data.sid, e.message); + } + } + } + + if ( + encrypted_node.get_deep_string_content("payload") != null && // Ratchet forwarding doesn't contain payload and might not include us, which is ok + data.our_potential_encrypted_keys.size == 0 && // The message was not encrypted to us + stream_interactor.module_manager.get_module(message.account, StreamModule.IDENTITY).store.local_registration_id != data.sid // Message from this device. Never encrypted to itself. + ) { + db.identity_meta.update_last_message_undecryptable(identity_id, data.sid, message.time); + trust_manager.bad_message_state_updated(conversation.account, message.from, data.sid); + } + + debug("Received OMEMO encryped message that could not be decrypted."); + return false; + } + + public Gee.List get_potential_message_jids(Entities.Message message, Xmpp.Xep.Omemo.ParsedData data, int identity_id) { + Gee.List possible_jids = new ArrayList(); + if (message.type_ == Message.Type.CHAT) { + possible_jids.add(message.from.bare_jid); + } else { + if (message.real_jid != null) { + possible_jids.add(message.real_jid.bare_jid); + } else if (data.is_prekey) { + // pre key messages do store the identity key, so we can use that to find the real jid + PreKeySignalMessage msg = Plugin.get_context().deserialize_pre_key_signal_message(data.encrypted_key); + string identity_key = Base64.encode(msg.identity_key.serialize()); + foreach (Row row in db.identity_meta.get_with_device_id(identity_id, data.sid).with(db.identity_meta.identity_key_public_base64, "=", identity_key)) { + try { + possible_jids.add(new Jid(row[db.identity_meta.address_name])); + } catch (InvalidJidError e) { + warning("Ignoring invalid jid from database: %s", e.message); + } + } + } else { + // If we don't know the device name (MUC history w/o MAM), test decryption with all keys with fitting device id + foreach (Row row in db.identity_meta.get_with_device_id(identity_id, data.sid)) { + try { + possible_jids.add(new Jid(row[db.identity_meta.address_name])); + } catch (InvalidJidError e) { + warning("Ignoring invalid jid from database: %s", e.message); + } + } + } + } + return possible_jids; + } + + public override uint8[] decrypt_key(Xmpp.Xep.Omemo.ParsedData data, Jid from_jid) throws GLib.Error { + int sid = data.sid; + uint8[] ciphertext = data.ciphertext; + uint8[] encrypted_key = data.encrypted_key; + + Address address = new Address(from_jid.to_string(), sid); + uint8[] key; + + if (data.is_prekey) { + int identity_id = db.identity.get_id(account.id); + PreKeySignalMessage msg = Plugin.get_context().deserialize_pre_key_signal_message(encrypted_key); + string identity_key = Base64.encode(msg.identity_key.serialize()); + + bool ok = update_db_for_prekey(identity_id, identity_key, from_jid, sid); + if (!ok) return null; + + debug("Starting new session for decryption with device from %s/%d", from_jid.to_string(), sid); + SessionCipher cipher = store.create_session_cipher(address); + key = cipher.decrypt_pre_key_signal_message(msg); + // TODO: Finish session + } else { + debug("Continuing session for decryption with device from %s/%d", from_jid.to_string(), sid); + SignalMessage msg = Plugin.get_context().deserialize_signal_message(encrypted_key); + SessionCipher cipher = store.create_session_cipher(address); + key = cipher.decrypt_signal_message(msg); + } + + if (key.length >= 32) { + int authtaglength = key.length - 16; + uint8[] new_ciphertext = new uint8[ciphertext.length + authtaglength]; + uint8[] new_key = new uint8[16]; + Memory.copy(new_ciphertext, ciphertext, ciphertext.length); + Memory.copy((uint8*)new_ciphertext + ciphertext.length, (uint8*)key + 16, authtaglength); + Memory.copy(new_key, key, 16); + data.ciphertext = new_ciphertext; + key = new_key; + } + + return key; + } + + public override string decrypt(uint8[] ciphertext, uint8[] key, uint8[] iv) throws GLib.Error { + return arr_to_str(aes_decrypt(Cipher.AES_GCM_NOPADDING, key, iv, ciphertext)); + } + + private bool update_db_for_prekey(int identity_id, string identity_key, Jid from_jid, int sid) { + Row? device = db.identity_meta.get_device(identity_id, from_jid.to_string(), sid); + if (device != null && device[db.identity_meta.identity_key_public_base64] != null) { + if (device[db.identity_meta.identity_key_public_base64] != identity_key) { + critical("Tried to use a different identity key for a known device id."); + return false; + } + } else { + debug("Learn new device from incoming message from %s/%d", from_jid.to_string(), sid); + bool blind_trust = db.trust.get_blind_trust(identity_id, from_jid.to_string(), true); + if (db.identity_meta.insert_device_session(identity_id, from_jid.to_string(), sid, identity_key, blind_trust ? TrustLevel.TRUSTED : TrustLevel.UNKNOWN) < 0) { + critical("Failed learning a device."); + return false; + } + + XmppStream? stream = stream_interactor.get_stream(account); + if (device == null && stream != null) { + stream.get_module(StreamModule.IDENTITY).request_user_devicelist.begin(stream, from_jid); + } + } + return true; + } + + private string arr_to_str(uint8[] arr) { + // null-terminate the array + uint8[] rarr = new uint8[arr.length+1]; + Memory.copy(rarr, arr, arr.length); + return (string)rarr; + } + } + + public class DecryptMessageListener : MessageListener { + public string[] after_actions_const = new string[]{ }; + public override string action_group { get { return "DECRYPT"; } } + public override string[] after_actions { get { return after_actions_const; } } + + private HashMap decryptors; + + public DecryptMessageListener(HashMap decryptors) { + this.decryptors = decryptors; + } + + public override async bool run(Entities.Message message, Xmpp.MessageStanza stanza, Conversation conversation) { + decryptors[message.account].decrypt_message(message, stanza, conversation); + return false; + } + } +} + diff --git a/plugins/omemo/src/logic/encrypt.vala b/plugins/omemo/src/logic/encrypt.vala new file mode 100644 index 00000000..cd994c3a --- /dev/null +++ b/plugins/omemo/src/logic/encrypt.vala @@ -0,0 +1,131 @@ +using Gee; +using Signal; +using Dino.Entities; +using Xmpp; +using Xmpp.Xep.Omemo; + +namespace Dino.Plugins.Omemo { + + public class OmemoEncryptor : Xep.Omemo.OmemoEncryptor { + + private Account account; + private Store store; + private TrustManager trust_manager; + + public override uint32 own_device_id { get { return store.local_registration_id; }} + + public OmemoEncryptor(Account account, TrustManager trust_manager, Store store) { + this.account = account; + this.trust_manager = trust_manager; + this.store = store; + } + + public override Xep.Omemo.EncryptionData encrypt_plaintext(string plaintext) throws GLib.Error { + const uint KEY_SIZE = 16; + const uint IV_SIZE = 12; + + //Create a key and use it to encrypt the message + uint8[] key = new uint8[KEY_SIZE]; + Plugin.get_context().randomize(key); + uint8[] iv = new uint8[IV_SIZE]; + Plugin.get_context().randomize(iv); + + uint8[] aes_encrypt_result = aes_encrypt(Cipher.AES_GCM_NOPADDING, key, iv, plaintext.data); + uint8[] ciphertext = aes_encrypt_result[0:aes_encrypt_result.length - 16]; + uint8[] tag = aes_encrypt_result[aes_encrypt_result.length - 16:aes_encrypt_result.length]; + uint8[] keytag = new uint8[key.length + tag.length]; + Memory.copy(keytag, key, key.length); + Memory.copy((uint8*)keytag + key.length, tag, tag.length); + + var ret = new Xep.Omemo.EncryptionData(own_device_id); + ret.ciphertext = ciphertext; + ret.keytag = keytag; + ret.iv = iv; + return ret; + } + + public EncryptState encrypt(MessageStanza message, Jid self_jid, Gee.List recipients, XmppStream stream) { + + EncryptState status = new EncryptState(); + if (!Plugin.ensure_context()) return status; + if (message.to == null) return status; + + try { + EncryptionData enc_data = encrypt_plaintext(message.body); + status = encrypt_key_to_recipients(enc_data, self_jid, recipients, stream); + + message.stanza.put_node(enc_data.get_encrypted_node()); + Xep.ExplicitEncryption.add_encryption_tag_to_message(message, NS_URI, "OMEMO"); + message.body = "[This message is OMEMO encrypted]"; + status.encrypted = true; + } catch (Error e) { + warning(@"Signal error while encrypting message: $(e.message)\n"); + message.body = "[OMEMO encryption failed]"; + status.encrypted = false; + } + return status; + } + + internal EncryptState encrypt_key_to_recipients(EncryptionData enc_data, Jid self_jid, Gee.List recipients, XmppStream stream) throws Error { + EncryptState status = new EncryptState(); + + //Check we have the bundles and device lists needed to send the message + if (!trust_manager.is_known_address(account, self_jid)) return status; + status.own_list = true; + status.own_devices = trust_manager.get_trusted_devices(account, self_jid).size; + status.other_waiting_lists = 0; + status.other_devices = 0; + foreach (Jid recipient in recipients) { + if (!trust_manager.is_known_address(account, recipient)) { + status.other_waiting_lists++; + } + if (status.other_waiting_lists > 0) return status; + status.other_devices += trust_manager.get_trusted_devices(account, recipient).size; + } + if (status.own_devices == 0 || status.other_devices == 0) return status; + + + //Encrypt the key for each recipient's device individually + foreach (Jid recipient in recipients) { + EncryptionResult enc_res = encrypt_key_to_recipient(stream, enc_data, recipient); + status.add_result(enc_res, false); + } + + // Encrypt the key for each own device + EncryptionResult enc_res = encrypt_key_to_recipient(stream, enc_data, self_jid); + status.add_result(enc_res, true); + + return status; + } + + public override EncryptionResult encrypt_key_to_recipient(XmppStream stream, Xep.Omemo.EncryptionData enc_data, Jid recipient) throws GLib.Error { + var result = new EncryptionResult(); + StreamModule module = stream.get_module(StreamModule.IDENTITY); + + foreach(int32 device_id in trust_manager.get_trusted_devices(account, recipient)) { + if (module.is_ignored_device(recipient, device_id)) { + result.lost++; + continue; + } + try { + encrypt_key(enc_data, recipient, device_id); + result.success++; + } catch (Error e) { + if (e.code == ErrorCode.UNKNOWN) result.unknown++; + else result.failure++; + } + } + return result; + } + + public override void encrypt_key(Xep.Omemo.EncryptionData encryption_data, Jid jid, int32 device_id) throws GLib.Error { + Address address = new Address(jid.to_string(), device_id); + SessionCipher cipher = store.create_session_cipher(address); + CiphertextMessage device_key = cipher.encrypt(encryption_data.keytag); + address.device_id = 0; + debug("Created encrypted key for %s/%d", jid.to_string(), device_id); + + encryption_data.add_device_key(device_id, device_key.serialized, device_key.type == CiphertextType.PREKEY); + } + } +} \ No newline at end of file diff --git a/plugins/omemo/src/logic/encrypt_state.vala b/plugins/omemo/src/logic/encrypt_state.vala deleted file mode 100644 index fd72faf4..00000000 --- a/plugins/omemo/src/logic/encrypt_state.vala +++ /dev/null @@ -1,24 +0,0 @@ -namespace Dino.Plugins.Omemo { - -public class EncryptState { - 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 other_waiting_lists { 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; } - public bool own_list { get; internal set; } - - public string to_string() { - return @"EncryptState (encrypted=$encrypted, other=(devices=$other_devices, success=$other_success, lost=$other_lost, unknown=$other_unknown, failure=$other_failure, waiting_lists=$other_waiting_lists, own=(devices=$own_devices, success=$own_success, lost=$own_lost, unknown=$own_unknown, failure=$own_failure, list=$own_list))"; - } -} - -} diff --git a/plugins/omemo/src/logic/manager.vala b/plugins/omemo/src/logic/manager.vala index 64b117c7..5552e212 100644 --- a/plugins/omemo/src/logic/manager.vala +++ b/plugins/omemo/src/logic/manager.vala @@ -13,11 +13,12 @@ public class Manager : StreamInteractionModule, Object { private StreamInteractor stream_interactor; private Database db; private TrustManager trust_manager; + private HashMap encryptors; private Map message_states = new HashMap(Entities.Message.hash_func, Entities.Message.equals_func); private class MessageState { public Entities.Message msg { get; private set; } - public EncryptState last_try { get; private set; } + public Xep.Omemo.EncryptState last_try { get; private set; } public int waiting_other_sessions { get; set; } public int waiting_own_sessions { get; set; } public bool waiting_own_devicelist { get; set; } @@ -26,11 +27,11 @@ public class Manager : StreamInteractionModule, Object { public bool will_send_now { get; private set; } public bool active_send_attempt { get; set; } - public MessageState(Entities.Message msg, EncryptState last_try) { + public MessageState(Entities.Message msg, Xep.Omemo.EncryptState last_try) { update_from_encrypt_status(msg, last_try); } - public void update_from_encrypt_status(Entities.Message msg, EncryptState new_try) { + public void update_from_encrypt_status(Entities.Message msg, Xep.Omemo.EncryptState new_try) { this.msg = msg; this.last_try = new_try; this.waiting_other_sessions = new_try.other_unknown; @@ -59,10 +60,11 @@ public class Manager : StreamInteractionModule, Object { } } - private Manager(StreamInteractor stream_interactor, Database db, TrustManager trust_manager) { + private Manager(StreamInteractor stream_interactor, Database db, TrustManager trust_manager, HashMap encryptors) { this.stream_interactor = stream_interactor; this.db = db; this.trust_manager = trust_manager; + this.encryptors = encryptors; stream_interactor.stream_negotiated.connect(on_stream_negotiated); stream_interactor.get_module(MessageProcessor.IDENTITY).pre_message_send.connect(on_pre_message_send); @@ -125,7 +127,7 @@ public class Manager : StreamInteractionModule, Object { } //Attempt to encrypt the message - EncryptState enc_state = trust_manager.encrypt(message_stanza, conversation.account.bare_jid, recipients, stream, conversation.account); + Xep.Omemo.EncryptState enc_state = encryptors[conversation.account].encrypt(message_stanza, conversation.account.bare_jid, recipients, stream); MessageState state; lock (message_states) { if (message_states.has_key(message)) { @@ -411,8 +413,8 @@ public class Manager : StreamInteractionModule, Object { return true; // TODO wait for stream? } - public static void start(StreamInteractor stream_interactor, Database db, TrustManager trust_manager) { - Manager m = new Manager(stream_interactor, db, trust_manager); + public static void start(StreamInteractor stream_interactor, Database db, TrustManager trust_manager, HashMap encryptors) { + Manager m = new Manager(stream_interactor, db, trust_manager, encryptors); stream_interactor.add_module(m); } } diff --git a/plugins/omemo/src/logic/trust_manager.vala b/plugins/omemo/src/logic/trust_manager.vala index 1e61b201..20076a43 100644 --- a/plugins/omemo/src/logic/trust_manager.vala +++ b/plugins/omemo/src/logic/trust_manager.vala @@ -12,18 +12,15 @@ public class TrustManager { private StreamInteractor stream_interactor; private Database db; - private DecryptMessageListener decrypt_message_listener; private TagMessageListener tag_message_listener; - private HashMap message_device_id_map = new HashMap(Message.hash_func, Message.equals_func); + public HashMap message_device_id_map = new HashMap(Message.hash_func, Message.equals_func); public TrustManager(StreamInteractor stream_interactor, Database db) { this.stream_interactor = stream_interactor; this.db = db; - decrypt_message_listener = new DecryptMessageListener(stream_interactor, this, db, message_device_id_map); tag_message_listener = new TagMessageListener(stream_interactor, this, db, message_device_id_map); - stream_interactor.get_module(MessageProcessor.IDENTITY).received_pipeline.connect(decrypt_message_listener); stream_interactor.get_module(MessageProcessor.IDENTITY).received_pipeline.connect(tag_message_listener); } @@ -69,127 +66,6 @@ public class TrustManager { } } - private StanzaNode create_encrypted_key_node(uint8[] key, Address address, Store store) throws GLib.Error { - SessionCipher cipher = store.create_session_cipher(address); - CiphertextMessage device_key = cipher.encrypt(key); - debug("Created encrypted key for %s/%d", address.name, address.device_id); - StanzaNode key_node = new StanzaNode.build("key", NS_URI) - .put_attribute("rid", address.device_id.to_string()) - .put_node(new StanzaNode.text(Base64.encode(device_key.serialized))); - if (device_key.type == CiphertextType.PREKEY) key_node.put_attribute("prekey", "true"); - return key_node; - } - - internal EncryptState encrypt_key(StanzaNode header_node, uint8[] keytag, Jid self_jid, Gee.List recipients, XmppStream stream, Account account) throws Error { - EncryptState status = new EncryptState(); - StreamModule module = stream.get_module(StreamModule.IDENTITY); - - //Check we have the bundles and device lists needed to send the message - if (!is_known_address(account, self_jid)) return status; - status.own_list = true; - status.own_devices = get_trusted_devices(account, self_jid).size; - status.other_waiting_lists = 0; - status.other_devices = 0; - foreach (Jid recipient in recipients) { - if (!is_known_address(account, recipient)) { - status.other_waiting_lists++; - } - if (status.other_waiting_lists > 0) return status; - status.other_devices += get_trusted_devices(account, recipient).size; - } - if (status.own_devices == 0 || status.other_devices == 0) return status; - - - //Encrypt the key for each recipient's device individually - Address address = new Address("", 0); - foreach (Jid recipient in recipients) { - foreach(int32 device_id in get_trusted_devices(account, recipient)) { - if (module.is_ignored_device(recipient, device_id)) { - status.other_lost++; - continue; - } - try { - address.name = recipient.bare_jid.to_string(); - address.device_id = (int) device_id; - StanzaNode key_node = create_encrypted_key_node(keytag, address, module.store); - header_node.put_node(key_node); - status.other_success++; - } catch (Error e) { - if (e.code == ErrorCode.UNKNOWN) status.other_unknown++; - else status.other_failure++; - } - } - } - - // Encrypt the key for each own device - address.name = self_jid.bare_jid.to_string(); - foreach(int32 device_id in get_trusted_devices(account, self_jid)) { - if (module.is_ignored_device(self_jid, device_id)) { - status.own_lost++; - continue; - } - if (device_id != module.store.local_registration_id) { - address.device_id = (int) device_id; - try { - StanzaNode key_node = create_encrypted_key_node(keytag, address, module.store); - header_node.put_node(key_node); - status.own_success++; - } catch (Error e) { - if (e.code == ErrorCode.UNKNOWN) status.own_unknown++; - else status.own_failure++; - } - } - } - - return status; - } - - public EncryptState encrypt(MessageStanza message, Jid self_jid, Gee.List recipients, XmppStream stream, Account account) { - const uint KEY_SIZE = 16; - const uint IV_SIZE = 12; - EncryptState status = new EncryptState(); - if (!Plugin.ensure_context()) return status; - if (message.to == null) return status; - - StreamModule module = stream.get_module(StreamModule.IDENTITY); - - try { - //Create a key and use it to encrypt the message - uint8[] key = new uint8[KEY_SIZE]; - Plugin.get_context().randomize(key); - uint8[] iv = new uint8[IV_SIZE]; - Plugin.get_context().randomize(iv); - - uint8[] aes_encrypt_result = aes_encrypt(Cipher.AES_GCM_NOPADDING, key, iv, message.body.data); - uint8[] ciphertext = aes_encrypt_result[0:aes_encrypt_result.length-16]; - uint8[] tag = aes_encrypt_result[aes_encrypt_result.length-16:aes_encrypt_result.length]; - uint8[] keytag = new uint8[key.length + tag.length]; - Memory.copy(keytag, key, key.length); - Memory.copy((uint8*)keytag + key.length, tag, tag.length); - - StanzaNode header_node; - StanzaNode encrypted_node = new StanzaNode.build("encrypted", NS_URI).add_self_xmlns() - .put_node(header_node = new StanzaNode.build("header", NS_URI) - .put_attribute("sid", module.store.local_registration_id.to_string()) - .put_node(new StanzaNode.build("iv", NS_URI) - .put_node(new StanzaNode.text(Base64.encode(iv))))) - .put_node(new StanzaNode.build("payload", NS_URI) - .put_node(new StanzaNode.text(Base64.encode(ciphertext)))); - - status = encrypt_key(header_node, keytag, self_jid, recipients, stream, account); - - message.stanza.put_node(encrypted_node); - Xep.ExplicitEncryption.add_encryption_tag_to_message(message, NS_URI, "OMEMO"); - message.body = "[This message is OMEMO encrypted]"; - status.encrypted = true; - } catch (Error e) { - warning(@"Signal error while encrypting message: $(e.message)\n"); - message.body = "[OMEMO encryption failed]"; - status.encrypted = false; - } - return status; - } - public bool is_known_address(Account account, Jid jid) { int identity_id = db.identity.get_id(account.id); if (identity_id < 0) return false; @@ -260,182 +136,6 @@ public class TrustManager { return false; } } - - private class DecryptMessageListener : MessageListener { - public string[] after_actions_const = new string[]{ }; - public override string action_group { get { return "DECRYPT"; } } - public override string[] after_actions { get { return after_actions_const; } } - - private StreamInteractor stream_interactor; - private TrustManager trust_manager; - private Database db; - private HashMap message_device_id_map; - - public DecryptMessageListener(StreamInteractor stream_interactor, TrustManager trust_manager, Database db, HashMap message_device_id_map) { - this.stream_interactor = stream_interactor; - this.trust_manager = trust_manager; - this.db = db; - this.message_device_id_map = message_device_id_map; - } - - public override async bool run(Entities.Message message, Xmpp.MessageStanza stanza, Conversation conversation) { - StreamModule module = stream_interactor.module_manager.get_module(conversation.account, StreamModule.IDENTITY); - Store store = module.store; - - StanzaNode? _encrypted = stanza.stanza.get_subnode("encrypted", NS_URI); - if (_encrypted == null || MessageFlag.get_flag(stanza) != null || stanza.from == null) return false; - StanzaNode encrypted = (!)_encrypted; - if (message.body == null && Xep.ExplicitEncryption.get_encryption_tag(stanza) == NS_URI) { - message.body = "[This message is OMEMO encrypted]"; // TODO temporary - }; - if (!Plugin.ensure_context()) return false; - int identity_id = db.identity.get_id(conversation.account.id); - MessageFlag flag = new MessageFlag(); - stanza.add_flag(flag); - StanzaNode? _header = encrypted.get_subnode("header"); - if (_header == null) return false; - StanzaNode header = (!)_header; - int sid = header.get_attribute_int("sid"); - if (sid <= 0) return false; - - var our_nodes = new ArrayList(); - foreach (StanzaNode key_node in header.get_subnodes("key")) { - debug("Is ours? %d =? %u", key_node.get_attribute_int("rid"), store.local_registration_id); - if (key_node.get_attribute_int("rid") == store.local_registration_id) { - our_nodes.add(key_node); - } - } - - string? payload = encrypted.get_deep_string_content("payload"); - string? iv_node = header.get_deep_string_content("iv"); - - foreach (StanzaNode key_node in our_nodes) { - string? key_node_content = key_node.get_string_content(); - if (payload == null || iv_node == null || key_node_content == null) continue; - uint8[] key; - uint8[] ciphertext = Base64.decode((!)payload); - uint8[] iv = Base64.decode((!)iv_node); - Gee.List possible_jids = new ArrayList(); - if (conversation.type_ == Conversation.Type.CHAT) { - possible_jids.add(stanza.from.bare_jid); - } else { - Jid? real_jid = message.real_jid; - if (real_jid != null) { - possible_jids.add(real_jid.bare_jid); - } else if (key_node.get_attribute_bool("prekey")) { - // pre key messages do store the identity key, so we can use that to find the real jid - PreKeySignalMessage msg = Plugin.get_context().deserialize_pre_key_signal_message(Base64.decode((!)key_node_content)); - string identity_key = Base64.encode(msg.identity_key.serialize()); - foreach (Row row in db.identity_meta.get_with_device_id(identity_id, sid).with(db.identity_meta.identity_key_public_base64, "=", identity_key)) { - try { - possible_jids.add(new Jid(row[db.identity_meta.address_name])); - } catch (InvalidJidError e) { - warning("Ignoring invalid jid from database: %s", e.message); - } - } - if (possible_jids.size != 1) { - continue; - } - } else { - // If we don't know the device name (MUC history w/o MAM), test decryption with all keys with fitting device id - foreach (Row row in db.identity_meta.get_with_device_id(identity_id, sid)) { - try { - possible_jids.add(new Jid(row[db.identity_meta.address_name])); - } catch (InvalidJidError e) { - warning("Ignoring invalid jid from database: %s", e.message); - } - } - } - } - - if (possible_jids.size == 0) { - debug("Received message from unknown entity with device id %d", sid); - } - - foreach (Jid possible_jid in possible_jids) { - try { - Address address = new Address(possible_jid.to_string(), sid); - if (key_node.get_attribute_bool("prekey")) { - Row? device = db.identity_meta.get_device(identity_id, possible_jid.to_string(), sid); - PreKeySignalMessage msg = Plugin.get_context().deserialize_pre_key_signal_message(Base64.decode((!)key_node_content)); - string identity_key = Base64.encode(msg.identity_key.serialize()); - if (device != null && device[db.identity_meta.identity_key_public_base64] != null) { - if (device[db.identity_meta.identity_key_public_base64] != identity_key) { - critical("Tried to use a different identity key for a known device id."); - continue; - } - } else { - debug("Learn new device from incoming message from %s/%d", possible_jid.to_string(), sid); - bool blind_trust = db.trust.get_blind_trust(identity_id, possible_jid.to_string(), true); - if (db.identity_meta.insert_device_session(identity_id, possible_jid.to_string(), sid, identity_key, blind_trust ? TrustLevel.TRUSTED : TrustLevel.UNKNOWN) < 0) { - critical("Failed learning a device."); - continue; - } - XmppStream? stream = stream_interactor.get_stream(conversation.account); - if (device == null && stream != null) { - module.request_user_devicelist.begin(stream, possible_jid); - } - } - debug("Starting new session for decryption with device from %s/%d", possible_jid.to_string(), sid); - SessionCipher cipher = store.create_session_cipher(address); - key = cipher.decrypt_pre_key_signal_message(msg); - // TODO: Finish session - } else { - debug("Continuing session for decryption with device from %s/%d", possible_jid.to_string(), sid); - SignalMessage msg = Plugin.get_context().deserialize_signal_message(Base64.decode((!)key_node_content)); - SessionCipher cipher = store.create_session_cipher(address); - key = cipher.decrypt_signal_message(msg); - } - //address.device_id = 0; // TODO: Hack to have address obj live longer - - if (key.length >= 32) { - int authtaglength = key.length - 16; - uint8[] new_ciphertext = new uint8[ciphertext.length + authtaglength]; - uint8[] new_key = new uint8[16]; - Memory.copy(new_ciphertext, ciphertext, ciphertext.length); - Memory.copy((uint8*)new_ciphertext + ciphertext.length, (uint8*)key + 16, authtaglength); - Memory.copy(new_key, key, 16); - ciphertext = new_ciphertext; - key = new_key; - } - - message.body = arr_to_str(aes_decrypt(Cipher.AES_GCM_NOPADDING, key, iv, ciphertext)); - message_device_id_map[message] = address.device_id; - message.encryption = Encryption.OMEMO; - flag.decrypted = true; - } catch (Error e) { - debug("Decrypting message from %s/%d failed: %s", possible_jid.to_string(), sid, e.message); - continue; - } - - // If we figured out which real jid a message comes from due to decryption working, save it - if (conversation.type_ == Conversation.Type.GROUPCHAT && message.real_jid == null) { - message.real_jid = possible_jid; - } - return false; - } - } - - if ( - payload != null && // Ratchet forwarding doesn't contain payload and might not include us, which is ok - our_nodes.size == 0 && // The message was not encrypted to us - module.store.local_registration_id != sid // Message from this device. Never encrypted to itself. - ) { - db.identity_meta.update_last_message_undecryptable(identity_id, sid, message.time); - trust_manager.bad_message_state_updated(conversation.account, message.from, sid); - } - - debug("Received OMEMO encryped message that could not be decrypted."); - return false; - } - - private string arr_to_str(uint8[] arr) { - // null-terminate the array - uint8[] rarr = new uint8[arr.length+1]; - Memory.copy(rarr, arr, arr.length); - return (string)rarr; - } - } } } diff --git a/plugins/omemo/src/plugin.vala b/plugins/omemo/src/plugin.vala index e739fc4d..7a0304d1 100644 --- a/plugins/omemo/src/plugin.vala +++ b/plugins/omemo/src/plugin.vala @@ -1,3 +1,4 @@ +using Gee; using Dino.Entities; extern const string GETTEXT_PACKAGE; @@ -20,6 +21,7 @@ public class Plugin : RootInterface, Object { } return true; } catch (Error e) { + warning("Error initializing Signal Context %s", e.message); return false; } } @@ -33,6 +35,9 @@ 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 decryptors = new HashMap(Account.hash_func, Account.equals_func); + public HashMap encryptors = new HashMap(Account.hash_func, Account.equals_func); public void registered(Dino.Application app) { ensure_context(); @@ -43,22 +48,33 @@ public class Plugin : RootInterface, Object { this.contact_details_provider = new ContactDetailsProvider(this); this.device_notification_populator = new DeviceNotificationPopulator(this, this.app.stream_interactor); this.trust_manager = new TrustManager(this.app.stream_interactor, this.db); + this.app.plugin_registry.register_encryption_list_entry(list_entry); this.app.plugin_registry.register_account_settings_entry(settings_entry); 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.stream_interactor.module_manager.initialize_account_modules.connect((account, list) => { - list.add(new StreamModule()); - list.add(new JetOmemo.Module(this)); + Signal.Store signal_store = Plugin.get_context().create_store(); + list.add(new StreamModule(signal_store)); + decryptors[account] = new OmemoDecryptor(account, app.stream_interactor, trust_manager, db, signal_store); + list.add(decryptors[account]); + encryptors[account] = new OmemoEncryptor(account, trust_manager,signal_store); + list.add(encryptors[account]); + list.add(new JetOmemo.Module()); + list.add(new DtlsSrtpVerificationDraft.StreamModule()); 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(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)); - Manager.start(this.app.stream_interactor, db, trust_manager); + Manager.start(this.app.stream_interactor, db, trust_manager, encryptors); SimpleAction own_keys_action = new SimpleAction("own-keys", VariantType.INT32); own_keys_action.activate.connect((variant) => { diff --git a/plugins/omemo/src/protocol/stream_module.vala b/plugins/omemo/src/protocol/stream_module.vala index e4a2733c..39d9c448 100644 --- a/plugins/omemo/src/protocol/stream_module.vala +++ b/plugins/omemo/src/protocol/stream_module.vala @@ -25,10 +25,8 @@ public class StreamModule : XmppStreamModule { public signal void bundle_fetched(Jid jid, int device_id, Bundle bundle); public signal void bundle_fetch_failed(Jid jid, int device_id); - public StreamModule() { - if (Plugin.ensure_context()) { - this.store = Plugin.get_context().create_store(); - } + public StreamModule(Store store) { + this.store = store; } public override void attach(XmppStream stream) { diff --git a/xmpp-vala/CMakeLists.txt b/xmpp-vala/CMakeLists.txt index 3aa10caf..bf8f0068 100644 --- a/xmpp-vala/CMakeLists.txt +++ b/xmpp-vala/CMakeLists.txt @@ -109,6 +109,9 @@ SOURCES "src/module/xep/0176_jingle_ice_udp/jingle_ice_udp_module.vala" "src/module/xep/0176_jingle_ice_udp/transport_parameters.vala" + "src/module/xep/0384_omemo/omemo_encryptor.vala" + "src/module/xep/0384_omemo/omemo_decryptor.vala" + "src/module/xep/0184_message_delivery_receipts.vala" "src/module/xep/0191_blocking_command.vala" "src/module/xep/0198_stream_management.vala" diff --git a/xmpp-vala/src/module/iq/module.vala b/xmpp-vala/src/module/iq/module.vala index 56605d01..17cd3f0d 100644 --- a/xmpp-vala/src/module/iq/module.vala +++ b/xmpp-vala/src/module/iq/module.vala @@ -6,6 +6,9 @@ namespace Xmpp.Iq { public class Module : XmppStreamNegotiationModule { public static ModuleIdentity IDENTITY = new ModuleIdentity(NS_URI, "iq_module"); + public signal void preprocess_incoming_iq_set_get(XmppStream stream, Stanza iq_stanza); + public signal void preprocess_outgoing_iq_set_get(XmppStream stream, Stanza iq_stanza); + private HashMap responseListeners = new HashMap(); private HashMap> namespaceRegistrants = new HashMap>(); @@ -23,6 +26,7 @@ namespace Xmpp.Iq { public delegate void OnResult(XmppStream stream, Iq.Stanza iq); public void send_iq(XmppStream stream, Iq.Stanza iq, owned OnResult? listener = null) { + preprocess_outgoing_iq_set_get(stream, iq); stream.write(iq.stanza); if (listener != null) { responseListeners[iq.id] = new ResponseListener((owned) listener); @@ -70,6 +74,7 @@ namespace Xmpp.Iq { } else { Gee.List children = node.get_all_subnodes(); if (children.size == 1 && namespaceRegistrants.has_key(children[0].ns_uri)) { + preprocess_incoming_iq_set_get(stream, iq); Gee.List handlers = namespaceRegistrants[children[0].ns_uri]; foreach (Handler handler in handlers) { if (iq.type_ == Iq.Stanza.TYPE_GET) { diff --git a/xmpp-vala/src/module/xep/0166_jingle/content.vala b/xmpp-vala/src/module/xep/0166_jingle/content.vala index beb12183..befe02f4 100644 --- a/xmpp-vala/src/module/xep/0166_jingle/content.vala +++ b/xmpp-vala/src/module/xep/0166_jingle/content.vala @@ -34,9 +34,8 @@ public class Xmpp.Xep.Jingle.Content : Object { public weak Session session; public Map component_connections = new HashMap(); // TODO private - public ContentEncryption? encryption { get; set; } + public HashMap encryptions = new HashMap(); - // INITIATE_SENT | INITIATE_RECEIVED | CONNECTING public Set tried_transport_methods = new HashSet(); diff --git a/xmpp-vala/src/module/xep/0166_jingle/content_transport.vala b/xmpp-vala/src/module/xep/0166_jingle/content_transport.vala index cd74c836..2697a01c 100644 --- a/xmpp-vala/src/module/xep/0166_jingle/content_transport.vala +++ b/xmpp-vala/src/module/xep/0166_jingle/content_transport.vala @@ -21,7 +21,7 @@ namespace Xmpp.Xep.Jingle { public abstract uint8 components { get; } public abstract void set_content(Content content); - public abstract StanzaNode to_transport_stanza_node(); + public abstract StanzaNode to_transport_stanza_node(string action_type); public abstract void handle_transport_accept(StanzaNode transport) throws IqError; public abstract void handle_transport_info(StanzaNode transport) throws IqError; public abstract void create_transport_connection(XmppStream stream, Content content); diff --git a/xmpp-vala/src/module/xep/0166_jingle/jingle_module.vala b/xmpp-vala/src/module/xep/0166_jingle/jingle_module.vala index 7314ca6c..186848f6 100644 --- a/xmpp-vala/src/module/xep/0166_jingle/jingle_module.vala +++ b/xmpp-vala/src/module/xep/0166_jingle/jingle_module.vala @@ -3,7 +3,7 @@ using Xmpp; namespace Xmpp.Xep.Jingle { - internal const string NS_URI = "urn:xmpp:jingle:1"; + public const string NS_URI = "urn:xmpp:jingle:1"; private const string ERROR_NS_URI = "urn:xmpp:jingle:errors:1"; // This module can only be attached to one stream at a time. @@ -131,7 +131,7 @@ namespace Xmpp.Xep.Jingle { .put_attribute("name", content.content_name) .put_attribute("senders", content.senders.to_string()) .put_node(content.content_params.get_description_node()) - .put_node(content.transport_params.to_transport_stanza_node()); + .put_node(content.transport_params.to_transport_stanza_node("session-initiate")); if (content.security_params != null) { content_node.put_node(content.security_params.to_security_stanza_node(stream, my_jid, receiver_full_jid)); } diff --git a/xmpp-vala/src/module/xep/0166_jingle/session.vala b/xmpp-vala/src/module/xep/0166_jingle/session.vala index 5fe89415..4d04c8d5 100644 --- a/xmpp-vala/src/module/xep/0166_jingle/session.vala +++ b/xmpp-vala/src/module/xep/0166_jingle/session.vala @@ -221,7 +221,7 @@ public class Xmpp.Xep.Jingle.Session : Object { .put_attribute("name", content.content_name) .put_attribute("senders", content.senders.to_string()) .put_node(content.content_params.get_description_node()) - .put_node(content.transport_params.to_transport_stanza_node())); + .put_node(content.transport_params.to_transport_stanza_node("content-add"))); Iq.Stanza iq = new Iq.Stanza.set(content_add_node) { to=peer_full_jid }; yield stream.get_module(Iq.Module.IDENTITY).send_iq_async(stream, iq); @@ -343,7 +343,7 @@ public class Xmpp.Xep.Jingle.Session : Object { .put_attribute("name", content.content_name) .put_attribute("senders", content.senders.to_string()) .put_node(content.content_params.get_description_node()) - .put_node(content.transport_params.to_transport_stanza_node()); + .put_node(content.transport_params.to_transport_stanza_node("session-accept")); jingle.put_node(content_node); } @@ -379,7 +379,7 @@ public class Xmpp.Xep.Jingle.Session : Object { .put_attribute("name", content.content_name) .put_attribute("senders", content.senders.to_string()) .put_node(content.content_params.get_description_node()) - .put_node(content.transport_params.to_transport_stanza_node())); + .put_node(content.transport_params.to_transport_stanza_node("content-accept"))); Iq.Stanza iq = new Iq.Stanza.set(content_accept_node) { to=peer_full_jid }; stream.get_module(Iq.Module.IDENTITY).send_iq(stream, iq); @@ -477,7 +477,7 @@ public class Xmpp.Xep.Jingle.Session : Object { .put_node(new StanzaNode.build("content", NS_URI) .put_attribute("creator", "initiator") .put_attribute("name", content.content_name) - .put_node(transport_params.to_transport_stanza_node()) + .put_node(transport_params.to_transport_stanza_node("transport-accept")) ); Iq.Stanza iq_response = new Iq.Stanza.set(jingle_response) { to=peer_full_jid }; stream.get_module(Iq.Module.IDENTITY).send_iq(stream, iq_response); @@ -493,7 +493,7 @@ public class Xmpp.Xep.Jingle.Session : Object { .put_node(new StanzaNode.build("content", NS_URI) .put_attribute("creator", "initiator") .put_attribute("name", content.content_name) - .put_node(transport_params.to_transport_stanza_node()) + .put_node(transport_params.to_transport_stanza_node("transport-replace")) ); Iq.Stanza iq = new Iq.Stanza.set(jingle) { to=peer_full_jid }; stream.get_module(Iq.Module.IDENTITY).send_iq(stream, iq); diff --git a/xmpp-vala/src/module/xep/0167_jingle_rtp/content_parameters.vala b/xmpp-vala/src/module/xep/0167_jingle_rtp/content_parameters.vala index d4440169..344fe8b8 100644 --- a/xmpp-vala/src/module/xep/0167_jingle_rtp/content_parameters.vala +++ b/xmpp-vala/src/module/xep/0167_jingle_rtp/content_parameters.vala @@ -133,7 +133,8 @@ public class Xmpp.Xep.JingleRtp.Parameters : Jingle.ContentParameters, Object { 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 }; + var content_encryption = new Xmpp.Xep.Jingle.ContentEncryption() { encryption_ns = "", encryption_name = "SRTP", our_key=local_crypto.key, peer_key=remote_crypto.key }; + content.encryptions[content_encryption.encryption_name] = content_encryption; } this.stream = parent.create_stream(content); diff --git a/xmpp-vala/src/module/xep/0176_jingle_ice_udp/jingle_ice_udp_module.vala b/xmpp-vala/src/module/xep/0176_jingle_ice_udp/jingle_ice_udp_module.vala index 5211e3a9..87c010dd 100644 --- a/xmpp-vala/src/module/xep/0176_jingle_ice_udp/jingle_ice_udp_module.vala +++ b/xmpp-vala/src/module/xep/0176_jingle_ice_udp/jingle_ice_udp_module.vala @@ -4,7 +4,7 @@ using Xmpp; namespace Xmpp.Xep.JingleIceUdp { -private const string NS_URI = "urn:xmpp:jingle:transports:ice-udp:1"; +public 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 { diff --git a/xmpp-vala/src/module/xep/0176_jingle_ice_udp/transport_parameters.vala b/xmpp-vala/src/module/xep/0176_jingle_ice_udp/transport_parameters.vala index ed0fab50..83da296b 100644 --- a/xmpp-vala/src/module/xep/0176_jingle_ice_udp/transport_parameters.vala +++ b/xmpp-vala/src/module/xep/0176_jingle_ice_udp/transport_parameters.vala @@ -65,13 +65,13 @@ public abstract class Xmpp.Xep.JingleIceUdp.IceUdpTransportParameters : Jingle.T this.content = null; } - public StanzaNode to_transport_stanza_node() { + public StanzaNode to_transport_stanza_node(string action_type) { var node = new StanzaNode.build("transport", NS_URI) .add_self_xmlns() .put_attribute("ufrag", local_ufrag) .put_attribute("pwd", local_pwd); - if (own_fingerprint != null) { + if (own_fingerprint != null && action_type != "transport-info") { var fingerprint_node = new StanzaNode.build("fingerprint", DTLS_NS_URI) .add_self_xmlns() .put_attribute("hash", "sha-256") @@ -137,7 +137,7 @@ public abstract class Xmpp.Xep.JingleIceUdp.IceUdpTransportParameters : Jingle.T private void check_send_transport_info() { if (this.content != null && unsent_local_candidates.size > 0) { - content.send_transport_info(to_transport_stanza_node()); + content.send_transport_info(to_transport_stanza_node("transport-info")); } } diff --git a/xmpp-vala/src/module/xep/0260_jingle_socks5_bytestreams.vala b/xmpp-vala/src/module/xep/0260_jingle_socks5_bytestreams.vala index 1c4e0d38..6edebbbc 100644 --- a/xmpp-vala/src/module/xep/0260_jingle_socks5_bytestreams.vala +++ b/xmpp-vala/src/module/xep/0260_jingle_socks5_bytestreams.vala @@ -391,7 +391,7 @@ class Parameters : Jingle.TransportParameters, Object { } - public StanzaNode to_transport_stanza_node() { + public StanzaNode to_transport_stanza_node(string action_type) { StanzaNode transport = new StanzaNode.build("transport", NS_URI) .add_self_xmlns() .put_attribute("dstaddr", local_dstaddr); diff --git a/xmpp-vala/src/module/xep/0261_jingle_in_band_bytestreams.vala b/xmpp-vala/src/module/xep/0261_jingle_in_band_bytestreams.vala index 5bb71831..f7c77544 100644 --- a/xmpp-vala/src/module/xep/0261_jingle_in_band_bytestreams.vala +++ b/xmpp-vala/src/module/xep/0261_jingle_in_band_bytestreams.vala @@ -73,7 +73,7 @@ class Parameters : Jingle.TransportParameters, Object { } - public StanzaNode to_transport_stanza_node() { + public StanzaNode to_transport_stanza_node(string action_type) { return new StanzaNode.build("transport", NS_URI) .add_self_xmlns() .put_attribute("block-size", block_size.to_string()) diff --git a/xmpp-vala/src/module/xep/0353_jingle_message_initiation.vala b/xmpp-vala/src/module/xep/0353_jingle_message_initiation.vala index 08e803a2..e26be515 100644 --- a/xmpp-vala/src/module/xep/0353_jingle_message_initiation.vala +++ b/xmpp-vala/src/module/xep/0353_jingle_message_initiation.vala @@ -102,10 +102,12 @@ namespace Xmpp.Xep.JingleMessageInitiation { } public override void attach(XmppStream stream) { + stream.get_module(ServiceDiscovery.Module.IDENTITY).add_feature(stream, NS_URI); stream.get_module(MessageModule.IDENTITY).received_message.connect(on_received_message); } public override void detach(XmppStream stream) { + stream.get_module(ServiceDiscovery.Module.IDENTITY).remove_feature(stream, NS_URI); stream.get_module(MessageModule.IDENTITY).received_message.disconnect(on_received_message); } diff --git a/xmpp-vala/src/module/xep/0384_omemo/omemo_decryptor.vala b/xmpp-vala/src/module/xep/0384_omemo/omemo_decryptor.vala new file mode 100644 index 00000000..8e3213ae --- /dev/null +++ b/xmpp-vala/src/module/xep/0384_omemo/omemo_decryptor.vala @@ -0,0 +1,62 @@ +using Gee; +using Xmpp.Xep; +using Xmpp; + +namespace Xmpp.Xep.Omemo { + + public abstract class OmemoDecryptor : XmppStreamModule { + + public static Xmpp.ModuleIdentity IDENTITY = new Xmpp.ModuleIdentity(NS_URI, "0384_omemo_decryptor"); + + public abstract uint32 own_device_id { get; } + + public abstract string decrypt(uint8[] ciphertext, uint8[] key, uint8[] iv) throws GLib.Error; + + public abstract uint8[] decrypt_key(ParsedData data, Jid from_jid) throws GLib.Error; + + public ParsedData? parse_node(StanzaNode encrypted_node) { + ParsedData ret = new ParsedData(); + + StanzaNode? header_node = encrypted_node.get_subnode("header"); + if (header_node == null) return null; + + ret.sid = header_node.get_attribute_int("sid", -1); + if (ret.sid == -1) return null; + + string? payload_str = encrypted_node.get_deep_string_content("payload"); + if (payload_str != null) ret.ciphertext = Base64.decode(payload_str); + + string? iv_str = header_node.get_deep_string_content("iv"); + if (iv_str == null) return null; + ret.iv = Base64.decode(iv_str); + + foreach (StanzaNode key_node in header_node.get_subnodes("key")) { + debug("Is ours? %d =? %u", key_node.get_attribute_int("rid"), own_device_id); + if (key_node.get_attribute_int("rid") == own_device_id) { + string? key_node_content = key_node.get_string_content(); + if (key_node_content == null) continue; + uchar[] encrypted_key = Base64.decode(key_node_content); + ret.our_potential_encrypted_keys[new Bytes.take(encrypted_key)] = key_node.get_attribute_bool("prekey"); + } + } + + return ret; + } + + public override void attach(XmppStream stream) { } + public override void detach(XmppStream stream) { } + public override string get_ns() { return NS_URI; } + public override string get_id() { return IDENTITY.id; } + } + + public class ParsedData { + public int sid; + public uint8[] ciphertext; + public uint8[] iv; + public uchar[] encrypted_key; + public bool is_prekey; + + public HashMap our_potential_encrypted_keys = new HashMap(); + } +} + diff --git a/xmpp-vala/src/module/xep/0384_omemo/omemo_encryptor.vala b/xmpp-vala/src/module/xep/0384_omemo/omemo_encryptor.vala new file mode 100644 index 00000000..6509bfe3 --- /dev/null +++ b/xmpp-vala/src/module/xep/0384_omemo/omemo_encryptor.vala @@ -0,0 +1,116 @@ +using Gee; +using Xmpp.Xep; +using Xmpp; + +namespace Xmpp.Xep.Omemo { + + public const string NS_URI = "eu.siacs.conversations.axolotl"; + public const string NODE_DEVICELIST = NS_URI + ".devicelist"; + public const string NODE_BUNDLES = NS_URI + ".bundles"; + public const string NODE_VERIFICATION = NS_URI + ".verification"; + + public abstract class OmemoEncryptor : XmppStreamModule { + + public static Xmpp.ModuleIdentity IDENTITY = new Xmpp.ModuleIdentity(NS_URI, "0384_omemo_encryptor"); + + public abstract uint32 own_device_id { get; } + + public abstract EncryptionData encrypt_plaintext(string plaintext) throws GLib.Error; + + public abstract void encrypt_key(Xep.Omemo.EncryptionData encryption_data, Jid jid, int32 device_id) throws GLib.Error; + + public abstract EncryptionResult encrypt_key_to_recipient(XmppStream stream, Xep.Omemo.EncryptionData enc_data, Jid recipient) throws GLib.Error; + + public override void attach(XmppStream stream) { } + public override void detach(XmppStream stream) { } + public override string get_ns() { return NS_URI; } + public override string get_id() { return IDENTITY.id; } + } + + public class EncryptionData { + public uint32 own_device_id; + public uint8[] ciphertext; + public uint8[] keytag; + public uint8[] iv; + + public Gee.List key_nodes = new ArrayList(); + + public EncryptionData(uint32 own_device_id) { + this.own_device_id = own_device_id; + } + + public void add_device_key(int device_id, uint8[] device_key, bool prekey) { + StanzaNode key_node = new StanzaNode.build("key", NS_URI) + .put_attribute("rid", device_id.to_string()) + .put_node(new StanzaNode.text(Base64.encode(device_key))); + if (prekey) { + key_node.put_attribute("prekey", "true"); + } + key_nodes.add(key_node); + } + + public StanzaNode get_encrypted_node() { + StanzaNode encrypted_node = new StanzaNode.build("encrypted", NS_URI).add_self_xmlns(); + + StanzaNode header_node = new StanzaNode.build("header", NS_URI) + .put_attribute("sid", own_device_id.to_string()) + .put_node(new StanzaNode.build("iv", NS_URI).put_node(new StanzaNode.text(Base64.encode(iv)))); + encrypted_node.put_node(header_node); + + if (ciphertext != null) { + StanzaNode payload_node = new StanzaNode.build("payload", NS_URI) + .put_node(new StanzaNode.text(Base64.encode(ciphertext))); + encrypted_node.put_node(payload_node); + } + + foreach (StanzaNode key_node in key_nodes) { + header_node.put_node(key_node); + } + + return encrypted_node; + } + } + + public class EncryptionResult { + public int lost { get; internal set; } + public int success { get; internal set; } + public int unknown { get; internal set; } + public int failure { get; internal set; } + } + + public class EncryptState { + 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 other_waiting_lists { 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; } + public bool own_list { get; internal set; } + + public void add_result(EncryptionResult enc_res, bool own) { + if (own) { + own_lost += enc_res.lost; + own_success += enc_res.success; + own_unknown += enc_res.unknown; + own_failure += enc_res.failure; + } else { + other_lost += enc_res.lost; + other_success += enc_res.success; + other_unknown += enc_res.unknown; + other_failure += enc_res.failure; + } + } + + public string to_string() { + return @"EncryptState (encrypted=$encrypted, other=(devices=$other_devices, success=$other_success, lost=$other_lost, unknown=$other_unknown, failure=$other_failure, waiting_lists=$other_waiting_lists, own=(devices=$own_devices, success=$own_success, lost=$own_lost, unknown=$own_unknown, failure=$own_failure, list=$own_list))"; + } + } +} + -- cgit v1.2.3-54-g00ecf From 7d2e64769067c1b47e0500f6456dd7e6f4eb435a Mon Sep 17 00:00:00 2001 From: fiaxh Date: Thu, 29 Apr 2021 15:56:22 +0200 Subject: Improve call wording, cleanup --- main/src/ui/call_window/call_bottom_bar.vala | 1 - main/src/ui/call_window/call_window.vala | 4 +- .../src/ui/call_window/call_window_controller.vala | 30 +++--- .../ui/conversation_content_view/call_widget.vala | 2 +- main/src/ui/conversation_titlebar/call_entry.vala | 5 +- .../omemo/src/dtls_srtp_verification_draft.vala | 1 - plugins/rtp/src/module.vala | 12 +-- plugins/rtp/src/stream.vala | 4 +- .../src/module/xep/0166_jingle/component.vala | 12 ++- xmpp-vala/src/module/xep/0166_jingle/content.vala | 4 +- .../xep/0167_jingle_rtp/jingle_rtp_module.vala | 18 ++-- .../0176_jingle_ice_udp/transport_parameters.vala | 2 +- .../src/module/xep/0234_jingle_file_transfer.vala | 20 ++-- .../module/xep/0260_jingle_socks5_bytestreams.vala | 118 +++++++++++---------- .../xep/0261_jingle_in_band_bytestreams.vala | 2 +- 15 files changed, 125 insertions(+), 110 deletions(-) (limited to 'xmpp-vala/src/module/xep/0167_jingle_rtp') diff --git a/main/src/ui/call_window/call_bottom_bar.vala b/main/src/ui/call_window/call_bottom_bar.vala index a9fee8c3..a3e4b93b 100644 --- a/main/src/ui/call_window/call_bottom_bar.vala +++ b/main/src/ui/call_window/call_bottom_bar.vala @@ -25,7 +25,6 @@ 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 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 }; diff --git a/main/src/ui/call_window/call_window.vala b/main/src/ui/call_window/call_window.vala index 572f73b6..3b3d4dc2 100644 --- a/main/src/ui/call_window/call_window.vala +++ b/main/src/ui/call_window/call_window.vala @@ -158,13 +158,13 @@ namespace Dino.Ui { public void set_status(string state) { switch (state) { case "requested": - header_bar.subtitle = _("Sending a call request…"); + header_bar.subtitle = _("Calling…"); break; case "ringing": header_bar.subtitle = _("Ringing…"); break; case "establishing": - header_bar.subtitle = _("Establishing a (peer-to-peer) connection…"); + header_bar.subtitle = _("Connecting…"); break; default: header_bar.subtitle = null; diff --git a/main/src/ui/call_window/call_window_controller.vala b/main/src/ui/call_window/call_window_controller.vala index 616e341d..0a223d72 100644 --- a/main/src/ui/call_window/call_window_controller.vala +++ b/main/src/ui/call_window/call_window_controller.vala @@ -3,8 +3,6 @@ using Gtk; public class Dino.Ui.CallWindowController : Object { - public signal void terminated(); - private CallWindow call_window; private Call call; private Conversation conversation; @@ -40,8 +38,16 @@ public class Dino.Ui.CallWindowController : Object { call_window.set_status("requested"); } - call_window.bottom_bar.hang_up.connect(end_call); - call_window.destroy.connect(end_call); + call_window.bottom_bar.hang_up.connect(() => { + calls.end_call(conversation, call); + call_window.close(); + call_window.destroy(); + this.dispose(); + }); + call_window.destroy.connect(() => { + calls.end_call(conversation, call); + this.dispose(); + }); call_window.bottom_bar.notify["audio-enabled"].connect(() => { calls.mute_own_audio(call, !call_window.bottom_bar.audio_enabled); @@ -116,16 +122,6 @@ public class Dino.Ui.CallWindowController : Object { this.window_width = this.call_window.get_allocated_width(); } - private void end_call() { - call.notify["state"].disconnect(on_call_state_changed); - calls.call_terminated.disconnect(on_call_terminated); - - calls.end_call(conversation, call); - call_window.close(); - call_window.destroy(); - terminated(); - } - private void on_call_state_changed() { if (call.state == Call.State.IN_PROGRESS) { call_window.set_status(""); @@ -234,4 +230,10 @@ public class Dino.Ui.CallWindowController : Object { call_window.unset_own_video(); } } + + public override void dispose() { + base.dispose(); + call.notify["state"].disconnect(on_call_state_changed); + calls.call_terminated.disconnect(on_call_terminated); + } } \ No newline at end of file diff --git a/main/src/ui/conversation_content_view/call_widget.vala b/main/src/ui/conversation_content_view/call_widget.vala index 66788e28..74525d11 100644 --- a/main/src/ui/conversation_content_view/call_widget.vala +++ b/main/src/ui/conversation_content_view/call_widget.vala @@ -154,7 +154,7 @@ namespace Dino.Ui { case Call.State.FAILED: image.set_from_icon_name("dino-phone-hangup-symbolic", IconSize.LARGE_TOOLBAR); title_label.label = _("Call failed"); - subtitle_label.label = "This call failed to establish"; + subtitle_label.label = "Call failed to establish"; break; } } diff --git a/main/src/ui/conversation_titlebar/call_entry.vala b/main/src/ui/conversation_titlebar/call_entry.vala index e1d10e5c..9353f631 100644 --- a/main/src/ui/conversation_titlebar/call_entry.vala +++ b/main/src/ui/conversation_titlebar/call_entry.vala @@ -92,9 +92,6 @@ namespace Dino.Ui { call_window.present(); update_button_state(); - call_controller.terminated.connect(() => { - update_button_state(); - }); } public new void set_conversation(Conversation conversation) { @@ -119,7 +116,7 @@ namespace Dino.Ui { if (conversation.type_ == Conversation.Type.CHAT) { Conversation conv_bak = conversation; bool audio_works = yield stream_interactor.get_module(Calls.IDENTITY).can_do_audio_calls_async(conversation); - bool video_works = yield stream_interactor.get_module(Calls.IDENTITY).can_do_audio_calls_async(conversation); + bool video_works = yield stream_interactor.get_module(Calls.IDENTITY).can_do_video_calls_async(conversation); if (conv_bak != conversation) return; visible = audio_works; diff --git a/plugins/omemo/src/dtls_srtp_verification_draft.vala b/plugins/omemo/src/dtls_srtp_verification_draft.vala index e2441670..66a31954 100644 --- a/plugins/omemo/src/dtls_srtp_verification_draft.vala +++ b/plugins/omemo/src/dtls_srtp_verification_draft.vala @@ -65,7 +65,6 @@ 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) print(@"$(session.contents_map.has_key(content_name))\n"); 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] }; session.contents_map[content_name].encryptions[NS_URI] = encryption; diff --git a/plugins/rtp/src/module.vala b/plugins/rtp/src/module.vala index 13a21cd8..19a7501d 100644 --- a/plugins/rtp/src/module.vala +++ b/plugins/rtp/src/module.vala @@ -216,9 +216,9 @@ public class Dino.Plugins.Rtp.Module : JingleRtp.Module { } public override JingleRtp.Crypto? generate_local_crypto() { - uint8[] keyAndSalt = new uint8[30]; - Crypto.randomize(keyAndSalt); - return JingleRtp.Crypto.create(JingleRtp.Crypto.AES_CM_128_HMAC_SHA1_80, keyAndSalt); + uint8[] key_and_salt = new uint8[30]; + Crypto.randomize(key_and_salt); + return JingleRtp.Crypto.create(JingleRtp.Crypto.AES_CM_128_HMAC_SHA1_80, key_and_salt); } public override JingleRtp.Crypto? pick_remote_crypto(Gee.List cryptos) { @@ -230,8 +230,8 @@ public class Dino.Plugins.Rtp.Module : JingleRtp.Module { public override JingleRtp.Crypto? pick_local_crypto(JingleRtp.Crypto? remote) { if (remote == null || !remote.is_valid) return null; - uint8[] keyAndSalt = new uint8[30]; - Crypto.randomize(keyAndSalt); - return remote.rekey(keyAndSalt); + uint8[] key_and_salt = new uint8[30]; + Crypto.randomize(key_and_salt); + return remote.rekey(key_and_salt); } } \ No newline at end of file diff --git a/plugins/rtp/src/stream.vala b/plugins/rtp/src/stream.vala index 23634aa3..bd8a279f 100644 --- a/plugins/rtp/src/stream.vala +++ b/plugins/rtp/src/stream.vala @@ -256,7 +256,7 @@ public class Dino.Plugins.Rtp.Stream : Xmpp.Xep.JingleRtp.Stream { } private void prepare_local_crypto() { - if (local_crypto != null && !crypto_session.has_encrypt) { + if (local_crypto != null && local_crypto.is_valid && !crypto_session.has_encrypt) { crypto_session.set_encryption_key(local_crypto.crypto_suite, local_crypto.key, local_crypto.salt); debug("Setting up encryption with key params %s", local_crypto.key_params); } @@ -396,7 +396,7 @@ public class Dino.Plugins.Rtp.Stream : Xmpp.Xep.JingleRtp.Stream { } private void prepare_remote_crypto() { - if (remote_crypto != null && !crypto_session.has_decrypt) { + if (remote_crypto != null && remote_crypto.is_valid && !crypto_session.has_decrypt) { crypto_session.set_decryption_key(remote_crypto.crypto_suite, remote_crypto.key, remote_crypto.salt); debug("Setting up decryption with key params %s", remote_crypto.key_params); } diff --git a/xmpp-vala/src/module/xep/0166_jingle/component.vala b/xmpp-vala/src/module/xep/0166_jingle/component.vala index 544bcd69..5d573522 100644 --- a/xmpp-vala/src/module/xep/0166_jingle/component.vala +++ b/xmpp-vala/src/module/xep/0166_jingle/component.vala @@ -31,7 +31,11 @@ namespace Xmpp.Xep.Jingle { protected Gee.Promise promise = new Gee.Promise(); private string? terminated = null; - public async void init(IOStream stream) { + public async void set_stream(IOStream? stream) { + if (stream == null) { + promise.set_exception(new IOError.FAILED("Jingle connection failed")); + return; + } assert(!this.stream.ready); promise.set_value(stream); if (terminated != null) { @@ -39,11 +43,17 @@ namespace Xmpp.Xep.Jingle { } } + public void set_error(GLib.Error? e) { + promise.set_exception(e); + } + public override async void terminate(bool we_terminated, string? reason_name = null, string? reason_text = null) { if (terminated == null) { terminated = (reason_name ?? "") + " - " + (reason_text ?? "") + @"we terminated? $we_terminated"; if (stream.ready) { yield stream.value.close_async(); + } else { + promise.set_exception(new IOError.FAILED("Jingle connection failed")); } } } diff --git a/xmpp-vala/src/module/xep/0166_jingle/content.vala b/xmpp-vala/src/module/xep/0166_jingle/content.vala index befe02f4..41310aeb 100644 --- a/xmpp-vala/src/module/xep/0166_jingle/content.vala +++ b/xmpp-vala/src/module/xep/0166_jingle/content.vala @@ -36,7 +36,7 @@ public class Xmpp.Xep.Jingle.Content : Object { public HashMap encryptions = new HashMap(); - public Set tried_transport_methods = new HashSet(); + private Set tried_transport_methods = new HashSet(); public Content.initiate_sent(string content_name, Senders senders, @@ -109,7 +109,7 @@ public class Xmpp.Xep.Jingle.Content : Object { transport_params.dispose(); foreach (ComponentConnection connection in component_connections.values) { - connection.terminate(we_terminated, reason_name, reason_text); + connection.terminate.begin(we_terminated, reason_name, reason_text); } } diff --git a/xmpp-vala/src/module/xep/0167_jingle_rtp/jingle_rtp_module.vala b/xmpp-vala/src/module/xep/0167_jingle_rtp/jingle_rtp_module.vala index 6eb6289b..6b55cbe6 100644 --- a/xmpp-vala/src/module/xep/0167_jingle_rtp/jingle_rtp_module.vala +++ b/xmpp-vala/src/module/xep/0167_jingle_rtp/jingle_rtp_module.vala @@ -83,7 +83,7 @@ public abstract class Module : XmppStreamModule { } } - public async Jingle.Content add_outgoing_video_content(XmppStream stream, Jingle.Session session) { + public async Jingle.Content add_outgoing_video_content(XmppStream stream, Jingle.Session session) throws Jingle.Error { Jid my_jid = session.local_full_jid; Jid receiver_full_jid = session.peer_full_jid; @@ -168,7 +168,7 @@ public class Crypto { public string? session_params { get; private set; } public string tag { get; private set; } - public uint8[] key_and_salt { owned get { + public uint8[]? key_and_salt { owned get { if (!key_params.has_prefix("inline:")) return null; int endIndex = key_params.index_of("|"); if (endIndex < 0) endIndex = key_params.length; @@ -221,30 +221,30 @@ public class Crypto { case AES_CM_128_HMAC_SHA1_80: case AES_CM_128_HMAC_SHA1_32: case F8_128_HMAC_SHA1_80: - return key_and_salt.length == 30; + return key_and_salt != null && key_and_salt.length == 30; } return false; }} - public uint8[] key { owned get { - uint8[] key_and_salt = key_and_salt; + public uint8[]? key { owned get { + uint8[]? key_and_salt = key_and_salt; switch(crypto_suite) { case AES_CM_128_HMAC_SHA1_80: case AES_CM_128_HMAC_SHA1_32: case F8_128_HMAC_SHA1_80: - if (key_and_salt.length >= 16) return key_and_salt[0:16]; + if (key_and_salt != null && key_and_salt.length >= 16) return key_and_salt[0:16]; break; } return null; }} - public uint8[] salt { owned get { - uint8[] keyAndSalt = key_and_salt; + public uint8[]? salt { owned get { + uint8[]? key_and_salt = key_and_salt; switch(crypto_suite) { case AES_CM_128_HMAC_SHA1_80: case AES_CM_128_HMAC_SHA1_32: case F8_128_HMAC_SHA1_80: - if (keyAndSalt.length >= 30) return keyAndSalt[16:30]; + if (key_and_salt != null && key_and_salt.length >= 30) return key_and_salt[16:30]; break; } return null; diff --git a/xmpp-vala/src/module/xep/0176_jingle_ice_udp/transport_parameters.vala b/xmpp-vala/src/module/xep/0176_jingle_ice_udp/transport_parameters.vala index 83da296b..07b599ee 100644 --- a/xmpp-vala/src/module/xep/0176_jingle_ice_udp/transport_parameters.vala +++ b/xmpp-vala/src/module/xep/0176_jingle_ice_udp/transport_parameters.vala @@ -152,7 +152,7 @@ public abstract class Xmpp.Xep.JingleIceUdp.IceUdpTransportParameters : Jingle.T return sb.str; } - private uint8[] fingerprint_to_bytes(string? fingerprint_) { + private uint8[]? fingerprint_to_bytes(string? fingerprint_) { if (fingerprint_ == null) return null; string fingerprint = fingerprint_.replace(":", "").up(); diff --git a/xmpp-vala/src/module/xep/0234_jingle_file_transfer.vala b/xmpp-vala/src/module/xep/0234_jingle_file_transfer.vala index 07b158bc..4581019f 100644 --- a/xmpp-vala/src/module/xep/0234_jingle_file_transfer.vala +++ b/xmpp-vala/src/module/xep/0234_jingle_file_transfer.vala @@ -268,13 +268,19 @@ public class FileTransfer : Object { content.accept(); Jingle.StreamingConnection connection = content.component_connections.values.to_array()[0] as Jingle.StreamingConnection; - IOStream? io_stream = yield connection.stream.wait_async(); - FileTransferInputStream ft_stream = new FileTransferInputStream(io_stream.input_stream, size); - io_stream.output_stream.close(); - ft_stream.closed.connect(() => { - session.terminate(Jingle.ReasonElement.SUCCESS, null, null); - }); - this.stream = ft_stream; + try { + IOStream io_stream = yield connection.stream.wait_async(); + FileTransferInputStream ft_stream = new FileTransferInputStream(io_stream.input_stream, size); + io_stream.output_stream.close(); + ft_stream.closed.connect(() => { + session.terminate(Jingle.ReasonElement.SUCCESS, null, null); + }); + this.stream = ft_stream; + } catch (FutureError.EXCEPTION e) { + warning("Error accepting Jingle file-transfer: %s", connection.stream.exception.message); + } catch (FutureError e) { + warning("FutureError accepting Jingle file-transfer: %s", e.message); + } } public void reject(XmppStream stream) { diff --git a/xmpp-vala/src/module/xep/0260_jingle_socks5_bytestreams.vala b/xmpp-vala/src/module/xep/0260_jingle_socks5_bytestreams.vala index 6edebbbc..47c243e8 100644 --- a/xmpp-vala/src/module/xep/0260_jingle_socks5_bytestreams.vala +++ b/xmpp-vala/src/module/xep/0260_jingle_socks5_bytestreams.vala @@ -28,6 +28,7 @@ public class Module : Jingle.Transport, XmppStreamModule { public string ns_uri { get { return NS_URI; } } public Jingle.TransportType type_ { get { return Jingle.TransportType.STREAMING; } } public int priority { get { return 1; } } + private Gee.List get_proxies(XmppStream stream) { Gee.List result = new ArrayList(); int i = 1 << 15; @@ -37,6 +38,7 @@ public class Module : Jingle.Transport, XmppStreamModule { } return result; } + private Gee.List start_local_listeners(XmppStream stream, Jid local_full_jid, string dstaddr, out LocalListener? local_listener) { Gee.List result = new ArrayList(); SocketListener listener = new SocketListener(); @@ -62,15 +64,17 @@ public class Module : Jingle.Transport, XmppStreamModule { } return result; } + private void select_candidates(XmppStream stream, Jid local_full_jid, string dstaddr, Parameters result) { result.local_candidates.add_all(get_proxies(stream)); - //result.local_candidates.add_all(start_local_listeners(stream, local_full_jid, dstaddr, out result.listener)); + result.local_candidates.add_all(start_local_listeners(stream, local_full_jid, dstaddr, out result.listener)); result.local_candidates.sort((c1, c2) => { if (c1.priority < c2.priority) { return 1; } if (c1.priority > c2.priority) { return -1; } return 0; }); } + public Jingle.TransportParameters create_transport_parameters(XmppStream stream, uint8 components, Jid local_full_jid, Jid peer_full_jid) { assert(components == 1); Parameters result = new Parameters.create(local_full_jid, peer_full_jid, random_uuid()); @@ -78,6 +82,7 @@ public class Module : Jingle.Transport, XmppStreamModule { select_candidates(stream, local_full_jid, dstaddr, result); return result; } + public Jingle.TransportParameters parse_transport_parameters(XmppStream stream, uint8 components, Jid local_full_jid, Jid peer_full_jid, StanzaNode transport) throws Jingle.IqError { Parameters result = Parameters.parse(local_full_jid, peer_full_jid, transport); string dstaddr = calculate_dstaddr(result.sid, local_full_jid, peer_full_jid); @@ -146,6 +151,7 @@ public class Candidate : Socks5Bytestreams.Proxy { public Candidate.build(string cid, string host, Jid jid, int port, int local_priority, CandidateType type) { this(cid, host, jid, port, type.type_preference() + local_priority, type); } + public Candidate.proxy(string cid, Socks5Bytestreams.Proxy proxy, int local_priority) { this.build(cid, proxy.host, proxy.jid, proxy.port, local_priority, CandidateType.PROXY); } @@ -170,6 +176,7 @@ public class Candidate : Socks5Bytestreams.Proxy { return new Candidate(cid, host, jid, port, priority, type); } + public StanzaNode to_xml() { return new StanzaNode.build("candidate", NS_URI) .put_attribute("cid", cid) @@ -210,6 +217,7 @@ class LocalListener { this.inner = inner; this.dstaddr = dstaddr; } + public LocalListener.empty() { this.inner = null; this.dstaddr = ""; @@ -233,6 +241,7 @@ class LocalListener { handle_conn.begin(((StringWrapper)cid).str, conn); } } + async void handle_conn(string cid, SocketConnection conn) { conn.socket.timeout = NEGOTIATION_TIMEOUT; size_t read; @@ -418,39 +427,39 @@ class Parameters : Jingle.TransportParameters, Object { } public void handle_transport_info(StanzaNode transport) throws Jingle.IqError { - StanzaNode? candidate_error = transport.get_subnode("candidate-error", NS_URI); - StanzaNode? candidate_used = transport.get_subnode("candidate-used", NS_URI); - StanzaNode? activated = transport.get_subnode("activated", NS_URI); - StanzaNode? proxy_error = transport.get_subnode("proxy-error", NS_URI); - int num_children = 0; - if (candidate_error != null) { num_children += 1; } - if (candidate_used != null) { num_children += 1; } - if (activated != null) { num_children += 1; } - if (proxy_error != null) { num_children += 1; } - if (num_children == 0) { - throw new Jingle.IqError.UNSUPPORTED_INFO("unknown transport-info"); - } else if (num_children > 1) { - throw new Jingle.IqError.BAD_REQUEST("transport-info with more than one child"); - } - if (candidate_error != null) { - handle_remote_candidate(null); - } - if (candidate_used != null) { - string? cid = candidate_used.get_attribute("cid"); - if (cid == null) { - throw new Jingle.IqError.BAD_REQUEST("missing cid"); - } - handle_remote_candidate(cid); - } - if (activated != null) { - string? cid = activated.get_attribute("cid"); - if (cid == null) { - throw new Jingle.IqError.BAD_REQUEST("missing cid"); - } - handle_activated(cid); + ArrayList socks5_nodes = new ArrayList(); + foreach (StanzaNode node in transport.sub_nodes) { + if (node.ns_uri == NS_URI) socks5_nodes.add(node); } - if (proxy_error != null) { - handle_proxy_error(); + if (socks5_nodes.is_empty) { warning("No socks5 subnodes in transport node"); return; } + if (socks5_nodes.size > 1) { warning("Too many socks5 subnodes in transport node"); return; } + + StanzaNode node = socks5_nodes[0]; + + switch (node.name) { + case "activated": + string? cid = node.get_attribute("cid"); + if (cid == null) { + throw new Jingle.IqError.BAD_REQUEST("missing cid"); + } + handle_activated(cid); + break; + case "candidate-used": + string? cid = node.get_attribute("cid"); + if (cid == null) { + throw new Jingle.IqError.BAD_REQUEST("missing cid"); + } + handle_remote_candidate(cid); + break; + case "candidate-error": + handle_remote_candidate(null); + break; + case "proxy-error": + handle_proxy_error(); + break; + default: + warning("Unknown transport-info: %s", transport.to_string()); + break; } } @@ -499,32 +508,22 @@ class Parameters : Jingle.TransportParameters, Object { return; } - Candidate? remote = remote_selected_candidate; - Candidate? local = local_selected_candidate; - - int num_candidates = 0; - if (remote != null) { num_candidates += 1; } - if (local != null) { num_candidates += 1; } - - if (num_candidates == 0) { - // Notify Jingle of the failed transport. - content_set_transport_connection(null); + if (remote_selected_candidate == null && local_selected_candidate == null) { + content_set_transport_connection_error(new IOError.FAILED("No candidates")); return; } bool remote_wins; - if (num_candidates == 1) { - remote_wins = remote != null; - } else { - if (local.priority < remote.priority) { - remote_wins = true; - } else if (local.priority > remote.priority) { - remote_wins = false; - } else { + if (remote_selected_candidate != null && local_selected_candidate != null) { + if (local_selected_candidate.priority == remote_selected_candidate.priority) { // equal priority -> XEP-0260 says that the candidate offered // by the initiator wins, so the one that the remote chose remote_wins = role == Jingle.Role.INITIATOR; + } else { + remote_wins = local_selected_candidate.priority < remote_selected_candidate.priority; } + } else { + remote_wins = remote_selected_candidate != null; } if (!remote_wins) { @@ -545,8 +544,7 @@ class Parameters : Jingle.TransportParameters, Object { } SocketConnection? conn = listener.get_connection(remote_selected_candidate.cid); if (conn == null) { - // Remote hasn't actually connected to us?! - content_set_transport_connection(null); + content_set_transport_connection_error(new IOError.FAILED("Remote hasn't actually connected to us?!")); return; } content_set_transport_connection(conn); @@ -569,7 +567,7 @@ class Parameters : Jingle.TransportParameters, Object { if (!waiting_for_activation_error) { content_set_transport_connection(conn); } else { - content_set_transport_connection(null); + content_set_transport_connection_error(new IOError.FAILED("waiting_for_activation_error")); } } @@ -620,7 +618,7 @@ class Parameters : Jingle.TransportParameters, Object { .put_attribute("sid", sid) .put_node(new StanzaNode.build("proxy-error", NS_URI)) ); - content_set_transport_connection(null); + content_set_transport_connection_error(new IOError.FAILED("Connect to local candidate error: %s", e.message)); } } @@ -745,15 +743,19 @@ class Parameters : Jingle.TransportParameters, Object { private Jingle.StreamingConnection connection = new Jingle.StreamingConnection(); - private void content_set_transport_connection(IOStream? ios) { - IOStream? iostream = ios; + private void content_set_transport_connection(IOStream ios) { + IOStream iostream = ios; Jingle.Content? strong_content = content; if (strong_content == null) return; if (strong_content.security_params != null) { iostream = strong_content.security_params.wrap_stream(iostream); } - connection.init.begin(iostream); + connection.set_stream.begin(iostream); + } + + private void content_set_transport_connection_error(Error e) { + connection.set_error(e); } public void create_transport_connection(XmppStream stream, Jingle.Content content) { diff --git a/xmpp-vala/src/module/xep/0261_jingle_in_band_bytestreams.vala b/xmpp-vala/src/module/xep/0261_jingle_in_band_bytestreams.vala index f7c77544..09eaf711 100644 --- a/xmpp-vala/src/module/xep/0261_jingle_in_band_bytestreams.vala +++ b/xmpp-vala/src/module/xep/0261_jingle_in_band_bytestreams.vala @@ -98,7 +98,7 @@ class Parameters : Jingle.TransportParameters, Object { if (content.security_params != null) { iostream = content.security_params.wrap_stream(iostream); } - connection.init.begin(iostream); + connection.set_stream.begin(iostream); debug("set transport conn ibb"); content.set_transport_connection(connection, 1); } -- cgit v1.2.3-54-g00ecf