From 2b90fcc39a1079346d6c5e2bfff8987104da737a Mon Sep 17 00:00:00 2001 From: fiaxh Date: Fri, 19 Mar 2021 22:46:39 +0100 Subject: Improve & refactor Jingle base implementation Co-authored-by: Marvin W --- .../src/module/xep/0166_jingle/component.vala | 52 ++ xmpp-vala/src/module/xep/0166_jingle/content.vala | 236 +++++++++ .../xep/0166_jingle/content_description.vala | 27 + .../src/module/xep/0166_jingle/content_node.vala | 112 +++++ .../module/xep/0166_jingle/content_security.vala | 18 + .../module/xep/0166_jingle/content_transport.vala | 29 ++ .../src/module/xep/0166_jingle/jingle_flag.vala | 38 ++ .../src/module/xep/0166_jingle/jingle_module.vala | 235 +++++++++ .../src/module/xep/0166_jingle/jingle_structs.vala | 73 +++ .../src/module/xep/0166_jingle/reason_element.vala | 29 ++ xmpp-vala/src/module/xep/0166_jingle/session.vala | 559 +++++++++++++++++++++ .../src/module/xep/0166_jingle/session_info.vala | 12 + 12 files changed, 1420 insertions(+) create mode 100644 xmpp-vala/src/module/xep/0166_jingle/component.vala create mode 100644 xmpp-vala/src/module/xep/0166_jingle/content.vala create mode 100644 xmpp-vala/src/module/xep/0166_jingle/content_description.vala create mode 100644 xmpp-vala/src/module/xep/0166_jingle/content_node.vala create mode 100644 xmpp-vala/src/module/xep/0166_jingle/content_security.vala create mode 100644 xmpp-vala/src/module/xep/0166_jingle/content_transport.vala create mode 100644 xmpp-vala/src/module/xep/0166_jingle/jingle_flag.vala create mode 100644 xmpp-vala/src/module/xep/0166_jingle/jingle_module.vala create mode 100644 xmpp-vala/src/module/xep/0166_jingle/jingle_structs.vala create mode 100644 xmpp-vala/src/module/xep/0166_jingle/reason_element.vala create mode 100644 xmpp-vala/src/module/xep/0166_jingle/session.vala create mode 100644 xmpp-vala/src/module/xep/0166_jingle/session_info.vala (limited to 'xmpp-vala/src/module/xep/0166_jingle') diff --git a/xmpp-vala/src/module/xep/0166_jingle/component.vala b/xmpp-vala/src/module/xep/0166_jingle/component.vala new file mode 100644 index 00000000..544bcd69 --- /dev/null +++ b/xmpp-vala/src/module/xep/0166_jingle/component.vala @@ -0,0 +1,52 @@ +namespace Xmpp.Xep.Jingle { + + public abstract class ComponentConnection : Object { + public uint8 component_id { get; set; default = 0; } + public abstract async void terminate(bool we_terminated, string? reason_name = null, string? reason_text = null); + public signal void connection_closed(); + public signal void connection_error(IOError e); + } + + public abstract class DatagramConnection : ComponentConnection { + public bool ready { get; set; default = false; } + private string? terminate_reason_name = null; + private string? terminate_reason_text = null; + private bool terminated = false; + + public override async void terminate(bool we_terminated, string? reason_string = null, string? reason_text = null) { + if (!terminated) { + terminated = true; + terminate_reason_name = reason_string; + terminate_reason_text = reason_text; + connection_closed(); + } + } + + public signal void datagram_received(Bytes datagram); + public abstract void send_datagram(Bytes datagram); + } + + public class StreamingConnection : ComponentConnection { + public Gee.Future stream { get { return promise.future; } } + protected Gee.Promise promise = new Gee.Promise(); + private string? terminated = null; + + public async void init(IOStream stream) { + assert(!this.stream.ready); + promise.set_value(stream); + if (terminated != null) { + yield stream.close_async(); + } + } + + 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(); + } + } + } + } +} + diff --git a/xmpp-vala/src/module/xep/0166_jingle/content.vala b/xmpp-vala/src/module/xep/0166_jingle/content.vala new file mode 100644 index 00000000..67c13dd8 --- /dev/null +++ b/xmpp-vala/src/module/xep/0166_jingle/content.vala @@ -0,0 +1,236 @@ +using Gee; +using Xmpp; + +public class Xmpp.Xep.Jingle.Content : Object { + + public signal void senders_modify_incoming(Senders proposed_senders); + + // INITIATE_SENT -> CONNECTING -> [REPLACING_TRANSPORT -> CONNECTING ->]... ACTIVE -> ENDED + // INITIATE_RECEIVED -> CONNECTING -> [WAITING_FOR_TRANSPORT_REPLACE -> CONNECTING ->].. ACTIVE -> ENDED + public enum State { + PENDING, + WANTS_TO_BE_ACCEPTED, + ACCEPTED, + REPLACING_TRANSPORT, + WAITING_FOR_TRANSPORT_REPLACE + } + + public State state { get; set; } + + public Role role { get; private set; } + public Jid local_full_jid { get; private set; } + public Jid peer_full_jid { get; private set; } + public Role content_creator { get; private set; } + public string content_name { get; private set; } + public Senders senders { get; private set; } + + public ContentType content_type; + public ContentParameters content_params; + public Transport transport; + public TransportParameters? transport_params; + public SecurityPrecondition security_precondition; + public SecurityParameters? security_params; + + public weak Session session; + public Map component_connections = new HashMap(); // TODO private + + // INITIATE_SENT | INITIATE_RECEIVED | CONNECTING + public Set tried_transport_methods = new HashSet(); + + + public Content.initiate_sent(string content_name, Senders senders, + ContentType content_type, ContentParameters content_params, + Transport transport, TransportParameters? transport_params, + SecurityPrecondition? security_precondition, SecurityParameters? security_params, + Jid local_full_jid, Jid peer_full_jid) { + this.content_name = content_name; + this.senders = senders; + this.role = Role.INITIATOR; + this.local_full_jid = local_full_jid; + this.peer_full_jid = peer_full_jid; + this.content_creator = Role.INITIATOR; + + this.content_type = content_type; + this.content_params = content_params; + this.transport = transport; + this.transport_params = transport_params; + this.security_precondition = security_precondition; + this.security_params = security_params; + + this.tried_transport_methods.add(transport.ns_uri); + + state = State.PENDING; + } + + public Content.initiate_received(string content_name, Senders senders, + ContentType content_type, ContentParameters content_params, + Transport transport, TransportParameters? transport_params, + SecurityPrecondition? security_precondition, SecurityParameters? security_params, + Jid local_full_jid, Jid peer_full_jid) throws Error { + this.content_name = content_name; + this.senders = senders; + this.role = Role.RESPONDER; + this.local_full_jid = local_full_jid; + this.peer_full_jid = peer_full_jid; + this.content_creator = Role.INITIATOR; + + this.content_type = content_type; + this.content_params = content_params; + this.transport = transport; + this.transport_params = transport_params; + this.security_precondition = security_precondition; + this.security_params = security_params; + + if (transport != null) { + this.tried_transport_methods.add(transport.ns_uri); + } + + state = State.PENDING; + } + + public void set_session(Session session) { + this.session = session; + this.transport_params.set_content(this); + } + + public void accept() { + state = State.WANTS_TO_BE_ACCEPTED; + + session.accept_content(this); + } + + public void reject() { + session.reject_content(this); + } + + public void terminate(bool we_terminated, string? reason_name, string? reason_text) { + content_params.terminate(we_terminated, reason_name, reason_text); + + foreach (ComponentConnection connection in component_connections.values) { + connection.terminate(we_terminated, reason_name, reason_text); + } + } + + public void modify(Senders new_sender) { + session.send_content_modify(this, new_sender); + this.senders = new_sender; + } + + public void accept_content_modify(Senders senders) { + this.senders = senders; + } + + internal void handle_content_modify(XmppStream stream, Senders proposed_senders) { + senders_modify_incoming(proposed_senders); + } + + internal void on_accept(XmppStream stream) { + this.transport_params.create_transport_connection(stream, this); + this.content_params.accept(stream, session, this); + } + + internal void handle_accept(XmppStream stream, ContentNode content_node) { + this.transport_params.handle_transport_accept(content_node.transport); + this.transport_params.create_transport_connection(stream, this); + this.content_params.handle_accept(stream, this.session, this, content_node.description); + } + + private async void select_new_transport() { + XmppStream stream = session.stream; + Transport? new_transport = yield stream.get_module(Module.IDENTITY).select_transport(stream, transport.type_, transport_params.components, peer_full_jid, tried_transport_methods); + if (new_transport == null) { + session.terminate(ReasonElement.FAILED_TRANSPORT, null, "failed transport"); + // TODO should we only terminate this content or really the whole session? + return; + } + tried_transport_methods.add(new_transport.ns_uri); + transport_params = new_transport.create_transport_parameters(stream, transport_params.components, local_full_jid, peer_full_jid); + set_transport_params(transport_params); + session.send_transport_replace(this, transport_params); + state = State.REPLACING_TRANSPORT; + } + + public void handle_transport_accept(XmppStream stream, StanzaNode transport_node, StanzaNode jingle, Iq.Stanza iq) throws IqError { + if (state != State.REPLACING_TRANSPORT) { + throw new IqError.OUT_OF_ORDER("no outstanding transport-replace request"); + } + if (transport_node.ns_uri != transport.ns_uri) { + throw new IqError.BAD_REQUEST("transport-accept with unnegotiated transport method"); + } + transport_params.handle_transport_accept(transport_node); + stream.get_module(Iq.Module.IDENTITY).send_iq(stream, new Iq.Stanza.result(iq)); + transport_params.create_transport_connection(stream, this); + } + + public void handle_transport_reject(XmppStream stream, StanzaNode jingle, Iq.Stanza iq) throws IqError { + if (state != State.REPLACING_TRANSPORT) { + throw new IqError.OUT_OF_ORDER("no outstanding transport-replace request"); + } + stream.get_module(Iq.Module.IDENTITY).send_iq(stream, new Iq.Stanza.result(iq)); + select_new_transport.begin(); + } + + public void handle_transport_replace(XmppStream stream, StanzaNode transport_node, StanzaNode jingle, Iq.Stanza iq) throws IqError { + Transport? transport = stream.get_module(Module.IDENTITY).get_transport(transport_node.ns_uri); + TransportParameters? parameters = null; + if (transport != null) { + // Just parse the transport info for the errors. + parameters = transport.parse_transport_parameters(stream, content_type.required_components, local_full_jid, peer_full_jid, transport_node); + } + stream.get_module(Iq.Module.IDENTITY).send_iq(stream, new Iq.Stanza.result(iq)); + if (state != State.WAITING_FOR_TRANSPORT_REPLACE || transport == null) { + session.send_transport_reject(this, transport_node); + return; + } + set_transport_params(parameters); + session.send_transport_accept(this, parameters); + + this.transport_params.create_transport_connection(stream, this); + } + + public void handle_transport_info(XmppStream stream, StanzaNode transport, StanzaNode jingle, Iq.Stanza iq) throws IqError { + this.transport_params.handle_transport_info(transport); + stream.get_module(Iq.Module.IDENTITY).send_iq(stream, new Iq.Stanza.result(iq)); + } + + public void on_description_info(XmppStream stream, StanzaNode description, StanzaNode jinglq, Iq.Stanza iq) throws IqError { + // TODO: do something. + stream.get_module(Iq.Module.IDENTITY).send_iq(stream, new Iq.Stanza.result(iq)); + } + + void verify_content(ContentNode content) throws IqError { + if (content.name != content_name || content.creator != content_creator) { + throw new IqError.BAD_REQUEST("unknown content"); + } + } + + public void set_transport_connection(ComponentConnection? conn, uint8 component = 1) { + debug(@"set_transport_connection: %s, %s, %i, %s, overwrites: %s", this.content_name, this.state.to_string(), component, (conn != null).to_string(), component_connections.has_key(component).to_string()); + + if (conn != null) { + component_connections[component] = conn; + if (transport_params.components == component) { + state = State.ACCEPTED; + tried_transport_methods.clear(); + } + } else { + if (role == Role.INITIATOR) { + select_new_transport.begin(); + } else { + state = State.WAITING_FOR_TRANSPORT_REPLACE; + } + } + } + + private void set_transport_params(TransportParameters transport_params) { + this.transport_params = transport_params; + } + + public ComponentConnection? get_transport_connection(uint8 component = 1) { + return component_connections[component]; + } + + public void send_transport_info(StanzaNode transport) { + session.send_transport_info(this, transport); + } +} \ No newline at end of file diff --git a/xmpp-vala/src/module/xep/0166_jingle/content_description.vala b/xmpp-vala/src/module/xep/0166_jingle/content_description.vala new file mode 100644 index 00000000..1a24e52e --- /dev/null +++ b/xmpp-vala/src/module/xep/0166_jingle/content_description.vala @@ -0,0 +1,27 @@ +using Gee; +using Xmpp.Xep; +using Xmpp; + +namespace Xmpp.Xep.Jingle { + + public interface ContentType : Object { + public abstract string ns_uri { get; } + public abstract TransportType required_transport_type { get; } + public abstract uint8 required_components { get; } + public abstract ContentParameters parse_content_parameters(StanzaNode description) throws IqError; + } + + public interface ContentParameters : Object { + /** Called when the counterpart proposes the content */ + public abstract async void handle_proposed_content(XmppStream stream, Jingle.Session session, Content content); + + /** Called when we accept the content that was proposed by the counterpart */ + public abstract void accept(XmppStream stream, Jingle.Session session, Jingle.Content content); + /** Called when the counterpart accepts the content that was proposed by us*/ + public abstract void handle_accept(XmppStream stream, Jingle.Session session, Jingle.Content content, StanzaNode description_node); + + public abstract void terminate(bool we_terminated, string? reason_name, string? reason_text); + + public abstract StanzaNode get_description_node(); + } +} \ No newline at end of file diff --git a/xmpp-vala/src/module/xep/0166_jingle/content_node.vala b/xmpp-vala/src/module/xep/0166_jingle/content_node.vala new file mode 100644 index 00000000..7d8d56c8 --- /dev/null +++ b/xmpp-vala/src/module/xep/0166_jingle/content_node.vala @@ -0,0 +1,112 @@ +using Gee; +using Xmpp.Xep; +using Xmpp; + +namespace Xmpp.Xep.Jingle { + + class ContentNode { + public Role creator; + public string name; + public Senders senders; + public StanzaNode? description; + public StanzaNode? transport; + public StanzaNode? security; + } + + [Version(deprecated = true)] + ContentNode get_single_content_node(StanzaNode jingle) throws IqError { + Gee.List contents = jingle.get_subnodes("content"); + if (contents.size == 0) { + throw new IqError.BAD_REQUEST("missing content node"); + } + if (contents.size > 1) { + throw new IqError.NOT_IMPLEMENTED("can't process multiple content nodes"); + } + StanzaNode content = contents[0]; + string? creator_str = content.get_attribute("creator"); + // Vala can't typecheck the ternary operator here. + Role? creator = null; + if (creator_str != null) { + creator = Role.parse(creator_str); + } else { + // TODO(hrxi): now, is the creator attribute optional or not (XEP-0166 + // Jingle)? + creator = Role.INITIATOR; + } + + string? name = content.get_attribute("name"); + + Senders senders = Senders.parse(content.get_attribute("senders")); + + StanzaNode? description = get_single_node_anyns(content, "description"); + StanzaNode? transport = get_single_node_anyns(content, "transport"); + StanzaNode? security = get_single_node_anyns(content, "security"); + if (name == null || creator == null) { + throw new IqError.BAD_REQUEST("missing name or creator"); + } + + return new ContentNode() { + creator=creator, + name=name, + senders=senders, + description=description, + transport=transport, + security=security + }; + } + + Gee.List get_content_nodes(StanzaNode jingle) throws IqError { + Gee.List contents = jingle.get_subnodes("content"); + if (contents.size == 0) { + throw new IqError.BAD_REQUEST("missing content node"); + } + Gee.List list = new ArrayList(); + foreach (StanzaNode content in contents) { + string? creator_str = content.get_attribute("creator"); + // Vala can't typecheck the ternary operator here. + Role? creator = null; + if (creator_str != null) { + creator = Role.parse(creator_str); + } else { + // TODO(hrxi): now, is the creator attribute optional or not (XEP-0166 + // Jingle)? + creator = Role.INITIATOR; + } + + string? name = content.get_attribute("name"); + Senders senders = Senders.parse(content.get_attribute("senders")); + StanzaNode? description = get_single_node_anyns(content, "description"); + StanzaNode? transport = get_single_node_anyns(content, "transport"); + StanzaNode? security = get_single_node_anyns(content, "security"); + if (name == null || creator == null) { + throw new IqError.BAD_REQUEST("missing name or creator"); + } + list.add(new ContentNode() { + creator=creator, + name=name, + senders=senders, + description=description, + transport=transport, + security=security + }); + } + return list; + } + + StanzaNode? get_single_node_anyns(StanzaNode parent, string? node_name = null) throws IqError { + StanzaNode? result = null; + foreach (StanzaNode child in parent.get_all_subnodes()) { + if (node_name == null || child.name == node_name) { + if (result != null) { + if (node_name != null) { + throw new IqError.BAD_REQUEST(@"multiple $(node_name) nodes"); + } else { + throw new IqError.BAD_REQUEST(@"expected single subnode"); + } + } + result = child; + } + } + return result; + } +} \ No newline at end of file diff --git a/xmpp-vala/src/module/xep/0166_jingle/content_security.vala b/xmpp-vala/src/module/xep/0166_jingle/content_security.vala new file mode 100644 index 00000000..0e10311d --- /dev/null +++ b/xmpp-vala/src/module/xep/0166_jingle/content_security.vala @@ -0,0 +1,18 @@ +using Gee; +using Xmpp.Xep; +using Xmpp; + +namespace Xmpp.Xep.Jingle { + + public interface SecurityPrecondition : Object { + public abstract string security_ns_uri(); + public abstract SecurityParameters? create_security_parameters(XmppStream stream, Jid local_full_jid, Jid peer_full_jid, Object options) throws Jingle.Error; + public abstract SecurityParameters? parse_security_parameters(XmppStream stream, Jid local_full_jid, Jid peer_full_jid, StanzaNode security) throws IqError; + } + + public interface SecurityParameters : Object { + public abstract string security_ns_uri(); + public abstract StanzaNode to_security_stanza_node(XmppStream stream, Jid local_full_jid, Jid peer_full_jid); + public abstract IOStream wrap_stream(IOStream stream); + } +} \ No newline at end of file diff --git a/xmpp-vala/src/module/xep/0166_jingle/content_transport.vala b/xmpp-vala/src/module/xep/0166_jingle/content_transport.vala new file mode 100644 index 00000000..cd74c836 --- /dev/null +++ b/xmpp-vala/src/module/xep/0166_jingle/content_transport.vala @@ -0,0 +1,29 @@ +namespace Xmpp.Xep.Jingle { + + public interface Transport : Object { + public abstract string ns_uri { get; } + public async abstract bool is_transport_available(XmppStream stream, uint8 components, Jid full_jid); + public abstract TransportType type_ { get; } + public abstract int priority { get; } + public abstract TransportParameters create_transport_parameters(XmppStream stream, uint8 components, Jid local_full_jid, Jid peer_full_jid) throws Error; + public abstract TransportParameters parse_transport_parameters(XmppStream stream, uint8 components, Jid local_full_jid, Jid peer_full_jid, StanzaNode transport) throws IqError; + } + + public enum TransportType { + DATAGRAM, + STREAMING, + } + + // Gets a null `stream` if connection setup was unsuccessful and another + // transport method should be tried. + public interface TransportParameters : Object { + public abstract string ns_uri { get; } + public abstract uint8 components { get; } + + public abstract void set_content(Content content); + public abstract StanzaNode to_transport_stanza_node(); + 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); + } +} \ No newline at end of file diff --git a/xmpp-vala/src/module/xep/0166_jingle/jingle_flag.vala b/xmpp-vala/src/module/xep/0166_jingle/jingle_flag.vala new file mode 100644 index 00000000..9f0acd27 --- /dev/null +++ b/xmpp-vala/src/module/xep/0166_jingle/jingle_flag.vala @@ -0,0 +1,38 @@ +using Gee; +using Xmpp; + +public class Xmpp.Xep.Jingle.Flag : XmppStreamFlag { + public static FlagIdentity IDENTITY = new FlagIdentity(NS_URI, "jingle"); + + public HashMap sessions = new HashMap(); + public HashMap> promises = new HashMap>(); + + // We might get transport-infos about a session before we finished fully creating the session. (e.g. telepathy outgoing calls) + // Thus, we "pre add" the session as soon as possible and can then await it. + public void pre_add_session(string sid) { + var promise = new Promise(); + promises[sid] = promise; + } + + public void add_session(Session session) { + if (promises.has_key(session.sid)) { + promises[session.sid].set_value(session); + promises.unset(session.sid); + } + sessions[session.sid] = session; + } + + public async Session? get_session(string sid) { + if (promises.has_key(sid)) { + return yield promises[sid].future.wait_async(); + } + return sessions[sid]; + } + + public void remove_session(string sid) { + sessions.unset(sid); + } + + public override string get_ns() { return NS_URI; } + public override string get_id() { return IDENTITY.id; } +} \ 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 new file mode 100644 index 00000000..1e8a36d1 --- /dev/null +++ b/xmpp-vala/src/module/xep/0166_jingle/jingle_module.vala @@ -0,0 +1,235 @@ +using Gee; +using Xmpp; + +namespace Xmpp.Xep.Jingle { + + internal 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. + public class Module : XmppStreamModule, Iq.Handler { + public static Xmpp.ModuleIdentity IDENTITY = new Xmpp.ModuleIdentity(NS_URI, "0166_jingle"); + + public signal void session_initiate_received(XmppStream stream, Session session); + + private HashMap content_types = new HashMap(); + private HashMap session_info_types = new HashMap(); + private HashMap transports = new HashMap(); + private HashMap security_preconditions = new HashMap(); + + public override void attach(XmppStream stream) { + stream.add_flag(new Flag()); + stream.get_module(ServiceDiscovery.Module.IDENTITY).add_feature(stream, NS_URI); + stream.get_module(Iq.Module.IDENTITY).register_for_namespace(NS_URI, this); + + // TODO update stream in all sessions + } + + public override void detach(XmppStream stream) { + stream.get_module(ServiceDiscovery.Module.IDENTITY).remove_feature(stream, NS_URI); + stream.get_module(Iq.Module.IDENTITY).unregister_from_namespace(NS_URI, this); + } + + public void register_content_type(ContentType content_type) { + content_types[content_type.ns_uri] = content_type; + } + + public void register_session_info_type(SessionInfoNs info_ns) { + session_info_types[info_ns.ns_uri] = info_ns; + } + + public ContentType? get_content_type(string ns_uri) { + if (!content_types.has_key(ns_uri)) { + return null; + } + return content_types[ns_uri]; + } + + public SessionInfoNs? get_session_info_type(string ns_uri) { + return session_info_types[ns_uri]; + } + + public void register_transport(Transport transport) { + transports[transport.ns_uri] = transport; + } + + public Transport? get_transport(string ns_uri) { + if (!transports.has_key(ns_uri)) { + return null; + } + return transports[ns_uri]; + } + + public async Transport? select_transport(XmppStream stream, TransportType type, uint8 components, Jid receiver_full_jid, Set blacklist) { + Transport? result = null; + foreach (Transport transport in transports.values) { + if (transport.type_ != type) { + continue; + } + if (transport.ns_uri in blacklist) { + continue; + } + if (yield transport.is_transport_available(stream, components, receiver_full_jid)) { + if (result != null) { + if (result.priority >= transport.priority) { + continue; + } + } + result = transport; + } + } + return result; + } + + public void register_security_precondition(SecurityPrecondition precondition) { + security_preconditions[precondition.security_ns_uri()] = precondition; + } + + public SecurityPrecondition? get_security_precondition(string? ns_uri) { + if (ns_uri == null) return null; + if (!security_preconditions.has_key(ns_uri)) { + return null; + } + return security_preconditions[ns_uri]; + } + + private async bool is_jingle_available(XmppStream stream, Jid full_jid) { + bool? has_jingle = yield stream.get_module(ServiceDiscovery.Module.IDENTITY).has_entity_feature(stream, full_jid, NS_URI); + return has_jingle != null && has_jingle; + } + + public async bool is_available(XmppStream stream, TransportType type, uint8 components, Jid full_jid) { + 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 { + if (!yield is_jingle_available(stream, receiver_full_jid)) { + throw new Error.NO_SHARED_PROTOCOLS("No Jingle support"); + } + Jid? my_jid = stream.get_flag(Bind.Flag.IDENTITY).my_jid; + if (my_jid == null) { + throw new Error.GENERAL("Couldn't determine own JID"); + } + + Session session = new Session.initiate_sent(stream, sid, 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) { + session.insert_content(content); + } + + // Build & send session-initiate iq stanza + StanzaNode initiate_jingle_iq = new StanzaNode.build("jingle", NS_URI) + .add_self_xmlns() + .put_attribute("action", "session-initiate") + .put_attribute("initiator", my_jid.to_string()) + .put_attribute("sid", session.sid); + + foreach (Content content in contents) { + StanzaNode content_node = new StanzaNode.build("content", NS_URI) + .put_attribute("creator", "initiator") + .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()); + if (content.security_params != null) { + content_node.put_node(content.security_params.to_security_stanza_node(stream, my_jid, receiver_full_jid)); + } + initiate_jingle_iq.put_node(content_node); + } + + Iq.Stanza iq = new Iq.Stanza.set(initiate_jingle_iq) { to=receiver_full_jid }; + + stream.get_flag(Flag.IDENTITY).add_session(session); + // We might get a follow-up before the ack => add_session before send_iq returns + stream.get_module(Iq.Module.IDENTITY).send_iq(stream, iq, (stream, iq) => { + if (iq.is_error()) warning("Jingle session-initiate got error: %s", iq.stanza.to_string()); + }); + + return session; + } + + public async void handle_session_initiate(XmppStream stream, string sid, StanzaNode jingle, Iq.Stanza iq) throws IqError { + Jid? my_jid = stream.get_flag(Bind.Flag.IDENTITY).my_jid; + if (my_jid == null) { + throw new IqError.RESOURCE_CONSTRAINT("Couldn't determine own JID"); + } + + Session session = new Session.initiate_received(stream, sid, my_jid, iq.from); + session.terminated.connect((stream) => { stream.get_flag(Flag.IDENTITY).remove_session(sid); }); + + stream.get_flag(Flag.IDENTITY).pre_add_session(session.sid); + + foreach (ContentNode content_node in get_content_nodes(jingle)) { + yield session.insert_content_node(content_node, iq.from); + } + + stream.get_flag(Flag.IDENTITY).add_session(session); + + stream.get_module(Iq.Module.IDENTITY).send_iq(stream, new Iq.Stanza.result(iq)); + + session_initiate_received(stream, session); + } + + public async void on_iq_set(XmppStream stream, Iq.Stanza iq) { + try { + yield handle_iq_set(stream, iq); + } catch (IqError e) { + send_iq_error(e, stream, iq); + } + } + + public async void handle_iq_set(XmppStream stream, Iq.Stanza iq) throws IqError { + StanzaNode? jingle_node = iq.stanza.get_subnode("jingle", NS_URI); + if (jingle_node == null) { + throw new IqError.BAD_REQUEST("missing jingle node"); + } + string? sid = jingle_node.get_attribute("sid"); + string? action = jingle_node.get_attribute("action"); + if (sid == null || action == null) { + throw new IqError.BAD_REQUEST("missing jingle node, sid or action"); + } + + Session? session = yield stream.get_flag(Flag.IDENTITY).get_session(sid); + if (action == "session-initiate") { + if (session != null) { + stream.get_module(Iq.Module.IDENTITY).send_iq(stream, new Iq.Stanza.error(iq, new ErrorStanza.build(ErrorStanza.TYPE_MODIFY, ErrorStanza.CONDITION_CONFLICT, "session ID already in use", null)) { to=iq.from }); + return; + } + yield handle_session_initiate(stream, sid, jingle_node, iq); + return; + } + if (session == null) { + StanzaNode unknown_session = new StanzaNode.build("unknown-session", ERROR_NS_URI).add_self_xmlns(); + stream.get_module(Iq.Module.IDENTITY).send_iq(stream, new Iq.Stanza.error(iq, new ErrorStanza.item_not_found(unknown_session)) { to=iq.from }); + return; + } + session.handle_iq_set(action, jingle_node, iq); + } + + public override string get_ns() { return NS_URI; } + public override string get_id() { return IDENTITY.id; } + } + + void send_iq_error(IqError iq_error, XmppStream stream, Iq.Stanza iq) { + ErrorStanza error; + if (iq_error is IqError.BAD_REQUEST) { + error = new ErrorStanza.bad_request(iq_error.message); + } else if (iq_error is IqError.NOT_ACCEPTABLE) { + error = new ErrorStanza.not_acceptable(iq_error.message); + } else if (iq_error is IqError.NOT_IMPLEMENTED) { + error = new ErrorStanza.feature_not_implemented(iq_error.message); + } else if (iq_error is IqError.UNSUPPORTED_INFO) { + StanzaNode unsupported_info = new StanzaNode.build("unsupported-info", ERROR_NS_URI).add_self_xmlns(); + error = new ErrorStanza.build(ErrorStanza.TYPE_CANCEL, ErrorStanza.CONDITION_FEATURE_NOT_IMPLEMENTED, iq_error.message, unsupported_info); + } else if (iq_error is IqError.OUT_OF_ORDER) { + StanzaNode out_of_order = new StanzaNode.build("out-of-order", ERROR_NS_URI).add_self_xmlns(); + error = new ErrorStanza.build(ErrorStanza.TYPE_MODIFY, ErrorStanza.CONDITION_UNEXPECTED_REQUEST, iq_error.message, out_of_order); + } else if (iq_error is IqError.RESOURCE_CONSTRAINT) { + error = new ErrorStanza.resource_constraint(iq_error.message); + } else { + assert_not_reached(); + } + stream.get_module(Iq.Module.IDENTITY).send_iq(stream, new Iq.Stanza.error(iq, error) { to=iq.from }); + } +} \ No newline at end of file diff --git a/xmpp-vala/src/module/xep/0166_jingle/jingle_structs.vala b/xmpp-vala/src/module/xep/0166_jingle/jingle_structs.vala new file mode 100644 index 00000000..0f283e0e --- /dev/null +++ b/xmpp-vala/src/module/xep/0166_jingle/jingle_structs.vala @@ -0,0 +1,73 @@ +using Gee; +using Xmpp; + +namespace Xmpp.Xep.Jingle { + + public errordomain IqError { + BAD_REQUEST, + NOT_ACCEPTABLE, + NOT_IMPLEMENTED, + UNSUPPORTED_INFO, + OUT_OF_ORDER, + RESOURCE_CONSTRAINT, + } + + public errordomain Error { + GENERAL, + BAD_REQUEST, + INVALID_PARAMETERS, + UNSUPPORTED_TRANSPORT, + UNSUPPORTED_SECURITY, + NO_SHARED_PROTOCOLS, + TRANSPORT_ERROR, + } + + public enum Senders { + BOTH, + INITIATOR, + NONE, + RESPONDER; + + public string to_string() { + switch (this) { + case BOTH: return "both"; + case INITIATOR: return "initiator"; + case NONE: return "none"; + case RESPONDER: return "responder"; + } + assert_not_reached(); + } + + public static Senders parse(string? senders) throws IqError { + if (senders == null) return Senders.BOTH; + switch (senders) { + case "initiator": return Senders.INITIATOR; + case "responder": return Senders.RESPONDER; + case "both": return Senders.BOTH; + } + throw new IqError.BAD_REQUEST(@"invalid role $(senders)"); + } + } + + public enum Role { + INITIATOR, + RESPONDER; + + public string to_string() { + switch (this) { + case INITIATOR: return "initiator"; + case RESPONDER: return "responder"; + } + assert_not_reached(); + } + + public static Role parse(string role) throws IqError { + switch (role) { + case "initiator": return INITIATOR; + case "responder": return RESPONDER; + } + throw new IqError.BAD_REQUEST(@"invalid role $(role)"); + } + } + +} \ No newline at end of file diff --git a/xmpp-vala/src/module/xep/0166_jingle/reason_element.vala b/xmpp-vala/src/module/xep/0166_jingle/reason_element.vala new file mode 100644 index 00000000..1cbdf936 --- /dev/null +++ b/xmpp-vala/src/module/xep/0166_jingle/reason_element.vala @@ -0,0 +1,29 @@ +using Gee; +using Xmpp; + +namespace Xmpp.Xep.Jingle.ReasonElement { + public const string ALTERNATIVE_SESSION = "alternative-session"; + public const string BUSY = "busy"; + public const string CANCEL = "cancel"; + public const string CONNECTIVITY_ERROR = "connectivity-error"; + public const string DECLINE = "decline"; + public const string EXPIRED = "expired"; + public const string FAILED_APPLICATION = "failed_application"; + public const string FAILED_TRANSPORT = "failed_transport"; + public const string GENERAL_ERROR = "general-error"; + public const string GONE = "gone"; + public const string INCOMPATIBLE_PARAMETERS = "incompatible-parameters"; + public const string MEDIA_ERROR = "media-error"; + public const string SECURITY_ERROR = "security-error"; + public const string SUCCESS = "success"; + public const string TIMEOUT = "timeout"; + public const string UNSUPPORTED_APPLICATIONS = "unsupported-applications"; + public const string UNSUPPORTED_TRANSPORTS = "unsupported-transports"; + + public const string[] NORMAL_TERMINATE_REASONS = { + BUSY, + CANCEL, + DECLINE, + 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 new file mode 100644 index 00000000..e9ad9169 --- /dev/null +++ b/xmpp-vala/src/module/xep/0166_jingle/session.vala @@ -0,0 +1,559 @@ +using Gee; +using Xmpp; + + +public delegate void Xmpp.Xep.Jingle.SessionTerminate(Jid to, string sid, StanzaNode reason); + +public class Xmpp.Xep.Jingle.Session : Object { + + public signal void terminated(XmppStream stream, bool we_terminated, string? reason_name, string? reason_text); + public signal void additional_content_add_incoming(XmppStream stream, Content content); + + // INITIATE_SENT/INITIATE_RECEIVED -> CONNECTING -> PENDING -> ACTIVE -> ENDED + public enum State { + INITIATE_SENT, + INITIATE_RECEIVED, + ACTIVE, + ENDED, + } + + public XmppStream stream { get; set; } + public State state { get; set; } + public string sid { get; private set; } + public Jid local_full_jid { get; private set; } + public Jid peer_full_jid { get; private set; } + public bool we_initiated { get; private set; } + + public HashMap contents = new HashMap(); + + public SecurityParameters? security { get { return contents.values.to_array()[0].security_params; } } + + public Session.initiate_sent(XmppStream stream, string sid, Jid local_full_jid, Jid peer_full_jid) { + this.stream = stream; + this.sid = sid; + this.local_full_jid = local_full_jid; + this.peer_full_jid = peer_full_jid; + this.state = State.INITIATE_SENT; + this.we_initiated = true; + } + + public Session.initiate_received(XmppStream stream, string sid, Jid local_full_jid, Jid peer_full_jid) { + this.stream = stream; + this.sid = sid; + this.local_full_jid = local_full_jid; + this.peer_full_jid = peer_full_jid; + this.state = State.INITIATE_RECEIVED; + this.we_initiated = false; + } + + public void handle_iq_set(string action, StanzaNode jingle, Iq.Stanza iq) throws IqError { + + if (action.has_prefix("session-")) { + switch (action) { + case "session-accept": + Gee.List content_nodes = get_content_nodes(jingle); + + if (state != State.INITIATE_SENT) { + throw new IqError.OUT_OF_ORDER("got session-accept while not waiting for one"); + } + handle_session_accept(content_nodes, jingle, iq); + break; + case "session-info": + handle_session_info.begin(jingle, iq); + break; + case "session-terminate": + handle_session_terminate(jingle, iq); + break; + default: + throw new IqError.BAD_REQUEST("invalid action"); + } + + + } else if (action.has_prefix("content-")) { + switch (action) { + case "content-accept": + ContentNode content_node = get_single_content_node(jingle); + handle_content_accept(content_node); + stream.get_module(Iq.Module.IDENTITY).send_iq(stream, new Iq.Stanza.result(iq)); + break; + case "content-add": + ContentNode content_node = get_single_content_node(jingle); + insert_content_node.begin(content_node, peer_full_jid); + stream.get_module(Iq.Module.IDENTITY).send_iq(stream, new Iq.Stanza.result(iq)); + break; + case "content-modify": + handle_content_modify(stream, jingle, iq); + break; + case "content-reject": + case "content-remove": + throw new IqError.NOT_IMPLEMENTED(@"$(action) is not implemented"); + default: + throw new IqError.BAD_REQUEST("invalid action"); + } + + + } else if (action.has_prefix("transport-")) { + ContentNode content_node = get_single_content_node(jingle); + if (!contents.has_key(content_node.name)) { + throw new IqError.BAD_REQUEST("unknown content"); + } + + if (content_node.transport == null) { + throw new IqError.BAD_REQUEST("missing transport node"); + } + + Content content = contents[content_node.name]; + + if (content_node.creator != content.content_creator) { + throw new IqError.BAD_REQUEST("unknown content; creator"); + } + + switch (action) { + case "transport-accept": + content.handle_transport_accept(stream, content_node.transport, jingle, iq); + break; + case "transport-info": + content.handle_transport_info(stream, content_node.transport, jingle, iq); + break; + case "transport-reject": + content.handle_transport_reject(stream, jingle, iq); + break; + case "transport-replace": + content.handle_transport_replace(stream, content_node.transport, jingle, iq); + break; + default: + throw new IqError.BAD_REQUEST("invalid action"); + } + + + } else if (action == "description-info") { + ContentNode content_node = get_single_content_node(jingle); + if (!contents.has_key(content_node.name)) { + throw new IqError.BAD_REQUEST("unknown content"); + } + + Content content = contents[content_node.name]; + + if (content_node.creator != content.content_creator) { + throw new IqError.BAD_REQUEST("unknown content; creator"); + } + + content.on_description_info(stream, content_node.description, jingle, iq); + } else if (action == "security-info") { + throw new IqError.NOT_IMPLEMENTED(@"$(action) is not implemented"); + + + } else { + throw new IqError.BAD_REQUEST("invalid action"); + } + } + + internal void insert_content(Content content) { + this.contents[content.content_name] = content; + content.set_session(this); + } + + internal async void insert_content_node(ContentNode content_node, Jid peer_full_jid) throws IqError { + if (content_node.description == null || content_node.transport == null) { + throw new IqError.BAD_REQUEST("missing description or transport node"); + } + + Jid? my_jid = stream.get_flag(Bind.Flag.IDENTITY).my_jid; + + Transport? transport = stream.get_module(Jingle.Module.IDENTITY).get_transport(content_node.transport.ns_uri); + ContentType? content_type = stream.get_module(Jingle.Module.IDENTITY).get_content_type(content_node.description.ns_uri); + + if (content_type == null) { + // TODO(hrxi): how do we signal an unknown content type? + throw new IqError.NOT_IMPLEMENTED("unknown content type"); + } + + TransportParameters? transport_params = null; + if (transport != null) { + transport_params = transport.parse_transport_parameters(stream, content_type.required_components, my_jid, peer_full_jid, content_node.transport); + } else { + // terminate the session below + } + + ContentParameters content_params = content_type.parse_content_parameters(content_node.description); + + SecurityPrecondition? precondition = content_node.security != null ? stream.get_module(Jingle.Module.IDENTITY).get_security_precondition(content_node.security.ns_uri) : null; + SecurityParameters? security_params = null; + if (precondition != null) { + debug("Using precondition %s", precondition.security_ns_uri()); + security_params = precondition.parse_security_parameters(stream, my_jid, peer_full_jid, content_node.security); + } else if (content_node.security != null) { + throw new IqError.NOT_IMPLEMENTED("unknown security precondition"); + } + + TransportType type = content_type.required_transport_type; + + if (transport == null || transport.type_ != type) { + terminate(ReasonElement.UNSUPPORTED_TRANSPORTS, null, "unsupported transports"); + throw new IqError.NOT_IMPLEMENTED("unsupported transports"); + } + + Content content = new Content.initiate_received(content_node.name, content_node.senders, + content_type, content_params, + transport, transport_params, + precondition, security_params, + my_jid, peer_full_jid); + insert_content(content); + + yield content_params.handle_proposed_content(stream, this, content); + + if (this.state == State.ACTIVE) { + additional_content_add_incoming(stream, content); + } + } + + public async void add_content(Content content) { + content.session = this; + this.contents[content.content_name] = content; + + StanzaNode content_add_node = new StanzaNode.build("jingle", NS_URI) + .add_self_xmlns() + .put_attribute("action", "content-add") + .put_attribute("sid", sid) + .put_node(new StanzaNode.build("content", NS_URI) + .put_attribute("creator", "initiator") + .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())); + + 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); + } + + 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"); + + Content content = contents[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`"); + if (content_node.transport.ns_uri != content.transport_params.ns_uri) throw new IqError.BAD_REQUEST("session-accept with unnegotiated transport method"); + + content.handle_accept(stream, content_node); + } + + 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]; + + 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"); + + Iq.Stanza result_iq = new Iq.Stanza.result(iq) { to=peer_full_jid }; + stream.get_module(Iq.Module.IDENTITY).send_iq(stream, result_iq); + + content.handle_content_modify(stream, content_node.senders); + } + + private void handle_session_accept(Gee.List content_nodes, StanzaNode jingle, Iq.Stanza iq) throws IqError { + string? responder_str = jingle.get_attribute("responder"); + Jid responder = iq.from; + if (responder_str != null) { + try { + responder = new Jid(responder_str); + } catch (InvalidJidError e) { + warning("Received invalid session accept: %s", e.message); + } + } + // TODO(hrxi): more sanity checking, perhaps replace who we're talking to + if (!responder.is_full()) { + throw new IqError.BAD_REQUEST("invalid responder JID"); + } + foreach (ContentNode content_node in content_nodes) { + handle_content_accept(content_node); + } + stream.get_module(Iq.Module.IDENTITY).send_iq(stream, new Iq.Stanza.result(iq)); + + state = State.ACTIVE; + } + + private void handle_session_terminate(StanzaNode jingle, Iq.Stanza iq) throws IqError { + string? reason_text = null; + string? reason_name = null; + StanzaNode? reason_node = iq.stanza.get_deep_subnode(NS_URI + ":jingle", NS_URI + ":reason"); + if (reason_node != null) { + if (reason_node.sub_nodes.size > 2) warning("Jingle session-terminate reason node w/ >2 subnodes: %s", iq.stanza.to_string()); + + StanzaNode? specific_reason_node = null; + StanzaNode? text_node = null; + foreach (StanzaNode node in reason_node.sub_nodes) { + if (node.name == "text") { + text_node = node; + } else if (node.ns_uri == NS_URI) { + specific_reason_node = node; + } + } + reason_name = specific_reason_node != null ? specific_reason_node.name : null; + reason_text = text_node != null ? text_node.get_string_content() : null; + + if (reason_name != null && !(specific_reason_node.name in ReasonElement.NORMAL_TERMINATE_REASONS)) { + warning("Jingle session terminated: %s : %s", reason_name ?? "", reason_text ?? ""); + } else { + debug("Jingle session terminated: %s : %s", reason_name ?? "", reason_text ?? ""); + } + } + + foreach (Content content in contents.values) { + content.terminate(false, reason_name, reason_text); + } + + stream.get_module(Iq.Module.IDENTITY).send_iq(stream, new Iq.Stanza.result(iq)); + // TODO(hrxi): also handle presence type=unavailable + + state = State.ENDED; + terminated(stream, false, reason_name, reason_text); + } + + private async void handle_session_info(StanzaNode jingle, Iq.Stanza iq) throws IqError { + StanzaNode? info = get_single_node_anyns(jingle); + if (info == null) { + // Jingle session ping + stream.get_module(Iq.Module.IDENTITY).send_iq(stream, new Iq.Stanza.result(iq)); + return; + } + SessionInfoNs? info_ns = stream.get_module(Module.IDENTITY).get_session_info_type(info.ns_uri); + if (info_ns == null) { + throw new IqError.UNSUPPORTED_INFO("unknown session-info namespace"); + } + info_ns.handle_content_session_info(stream, this, info, iq); + + Iq.Stanza result_iq = new Iq.Stanza.result(iq) { to=peer_full_jid }; + stream.get_module(Iq.Module.IDENTITY).send_iq(stream, result_iq); + } + + private void accept() { + if (state != State.INITIATE_RECEIVED) critical("Accepting a stream, but we're the initiator"); + + StanzaNode jingle = new StanzaNode.build("jingle", NS_URI) + .add_self_xmlns() + .put_attribute("action", "session-accept") + .put_attribute("sid", sid); + foreach (Content content in contents.values) { + StanzaNode content_node = new StanzaNode.build("content", NS_URI) + .put_attribute("creator", "initiator") + .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()); + 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); + } + + state = State.ACTIVE; + } + + internal void accept_content(Content content) { + if (state == State.INITIATE_RECEIVED) { + bool all_accepted = true; + foreach (Content c in contents.values) { + if (c.state != Content.State.WANTS_TO_BE_ACCEPTED) { + all_accepted = false; + } + } + if (all_accepted) { + accept(); + } + } else if (state == State.ACTIVE) { + StanzaNode content_accept_node = new StanzaNode.build("jingle", NS_URI) + .add_self_xmlns() + .put_attribute("action", "content-accept") + .put_attribute("sid", sid) + .put_node(new StanzaNode.build("content", NS_URI) + .put_attribute("creator", "initiator") + .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())); + + Iq.Stanza iq = new Iq.Stanza.set(content_accept_node) { to=peer_full_jid }; + stream.get_module(Iq.Module.IDENTITY).send_iq(stream, iq); + + content.on_accept(stream); + } + } + + private void reject() { + if (state != State.INITIATE_RECEIVED) critical("Accepting a stream, but we're the initiator"); + terminate(ReasonElement.DECLINE, null, "declined"); + } + + internal void reject_content(Content content) { + if (state == State.INITIATE_RECEIVED) { + reject(); + } else { + warning("not really handeling content rejects"); + } + } + + public void set_application_error(StanzaNode? application_reason = null) { + terminate(ReasonElement.FAILED_APPLICATION, null, "application error"); + } + + public void terminate(string? reason_name, string? reason_text, string? local_reason) { + if (state == State.ENDED) return; + + if (state == State.ACTIVE) { + string reason_str; + if (local_reason != null) { + reason_str = @"local session-terminate: $(local_reason)"; + } else { + reason_str = "local session-terminate"; + } + foreach (Content content in contents.values) { + content.terminate(true, reason_name, reason_text); + } + } + + StanzaNode terminate_iq = new StanzaNode.build("jingle", NS_URI) + .add_self_xmlns() + .put_attribute("action", "session-terminate") + .put_attribute("sid", sid); + if (reason_name != null || reason_text != null) { + StanzaNode reason_node = new StanzaNode.build("reason", NS_URI); + if (reason_name != null) { + reason_node.put_node(new StanzaNode.build(reason_name, NS_URI)); + } + if (reason_text != null) { + reason_node.put_node(new StanzaNode.text(reason_text)); + } + terminate_iq.put_node(reason_node); + } + Iq.Stanza iq = new Iq.Stanza.set(terminate_iq) { to=peer_full_jid }; + stream.get_module(Iq.Module.IDENTITY).send_iq(stream, iq); + + state = State.ENDED; + terminated(stream, true, reason_name, reason_text); + } + + internal void send_session_info(StanzaNode child_node) { + if (state == State.ENDED) return; + + StanzaNode node = new StanzaNode.build("jingle", NS_URI).add_self_xmlns() + .put_attribute("action", "session-info") + .put_attribute("sid", sid) + // TODO put `initiator`? + .put_node(child_node); + Iq.Stanza iq = new Iq.Stanza.set(node) { to=peer_full_jid }; + stream.get_module(Iq.Module.IDENTITY).send_iq(stream, iq); + } + + internal void send_content_modify(Content content, Senders senders) { + if (state == State.ENDED) return; + + StanzaNode node = new StanzaNode.build("jingle", NS_URI).add_self_xmlns() + .put_attribute("action", "content-modify") + .put_attribute("sid", sid) + .put_node(new StanzaNode.build("content", NS_URI) + .put_attribute("creator", content.content_creator.to_string()) + .put_attribute("name", content.content_name) + .put_attribute("senders", senders.to_string())); + Iq.Stanza iq = new Iq.Stanza.set(node) { to=peer_full_jid }; + stream.get_module(Iq.Module.IDENTITY).send_iq(stream, iq); + } + + internal void send_transport_accept(Content content, TransportParameters transport_params) { + if (state == State.ENDED) return; + + StanzaNode jingle_response = new StanzaNode.build("jingle", NS_URI) + .add_self_xmlns() + .put_attribute("action", "transport-accept") + .put_attribute("sid", sid) + .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()) + ); + 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); + } + + internal void send_transport_replace(Content content, TransportParameters transport_params) { + if (state == State.ENDED) return; + + StanzaNode jingle = new StanzaNode.build("jingle", NS_URI) + .add_self_xmlns() + .put_attribute("action", "transport-replace") + .put_attribute("sid", sid) + .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()) + ); + Iq.Stanza iq = new Iq.Stanza.set(jingle) { to=peer_full_jid }; + stream.get_module(Iq.Module.IDENTITY).send_iq(stream, iq); + } + + internal void send_transport_reject(Content content, StanzaNode transport_node) { + if (state == State.ENDED) return; + + StanzaNode jingle_response = new StanzaNode.build("jingle", NS_URI) + .add_self_xmlns() + .put_attribute("action", "transport-reject") + .put_attribute("sid", sid) + .put_node(new StanzaNode.build("content", NS_URI) + .put_attribute("creator", "initiator") + .put_attribute("name", content.content_name) + .put_node(transport_node) + ); + 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); + } + + internal void send_transport_info(Content content, StanzaNode transport) { + if (state == State.ENDED) return; + + StanzaNode jingle = new StanzaNode.build("jingle", NS_URI) + .add_self_xmlns() + .put_attribute("action", "transport-info") + .put_attribute("sid", sid) + .put_node(new StanzaNode.build("content", NS_URI) + .put_attribute("creator", "initiator") + .put_attribute("name", content.content_name) + .put_node(transport) + ); + Iq.Stanza iq = new Iq.Stanza.set(jingle) { to=peer_full_jid }; + stream.get_module(Iq.Module.IDENTITY).send_iq(stream, iq); + } + + public bool senders_include_us(Senders senders) { + switch (senders) { + case Senders.BOTH: + return true; + case Senders.NONE: + return false; + case Senders.INITIATOR: + return we_initiated; + case Senders.RESPONDER: + return !we_initiated; + } + assert_not_reached(); + } + + public bool senders_include_counterpart(Senders senders) { + switch (senders) { + case Senders.BOTH: + return true; + case Senders.NONE: + return false; + case Senders.INITIATOR: + return !we_initiated; + case Senders.RESPONDER: + return we_initiated; + } + assert_not_reached(); + } +} \ No newline at end of file diff --git a/xmpp-vala/src/module/xep/0166_jingle/session_info.vala b/xmpp-vala/src/module/xep/0166_jingle/session_info.vala new file mode 100644 index 00000000..fcf7584f --- /dev/null +++ b/xmpp-vala/src/module/xep/0166_jingle/session_info.vala @@ -0,0 +1,12 @@ +using Gee; +using Xmpp.Xep; +using Xmpp; + +namespace Xmpp.Xep.Jingle { + + public interface SessionInfoNs : Object { + public abstract string ns_uri { get; } + + public abstract void handle_content_session_info(XmppStream stream, Session session, StanzaNode info, Iq.Stanza iq) throws IqError; + } +} \ No newline at end of file -- 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/0166_jingle') 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 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/0166_jingle') 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/0166_jingle') 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 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/0166_jingle') 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 dfffa08ec16e16157df6e7036e09073a546d7552 Mon Sep 17 00:00:00 2001 From: fiaxh Date: Fri, 9 Apr 2021 23:59:03 +0200 Subject: Fix warnings --- libdino/src/service/connection_manager.vala | 2 +- libdino/src/service/notification_events.vala | 3 +-- libdino/src/util/display_name.vala | 6 +++--- main/src/ui/util/helper.vala | 6 +++--- plugins/ice/src/dtls_srtp.vala | 13 ++++++------- plugins/ice/src/transport_parameters.vala | 2 +- xmpp-vala/src/module/xep/0166_jingle/content.vala | 6 ------ .../xep/0176_jingle_ice_udp/transport_parameters.vala | 1 - 8 files changed, 15 insertions(+), 24 deletions(-) (limited to 'xmpp-vala/src/module/xep/0166_jingle') diff --git a/libdino/src/service/connection_manager.vala b/libdino/src/service/connection_manager.vala index 454bcc2c..1439c6f3 100644 --- a/libdino/src/service/connection_manager.vala +++ b/libdino/src/service/connection_manager.vala @@ -170,7 +170,7 @@ public class ConnectionManager : Object { public async void disconnect_account(Account account) { if (connections.has_key(account)) { make_offline(account); - connections[account].disconnect_account(); + connections[account].disconnect_account.begin(); connections.unset(account); } } diff --git a/libdino/src/service/notification_events.vala b/libdino/src/service/notification_events.vala index 0243dbe5..7039d1cf 100644 --- a/libdino/src/service/notification_events.vala +++ b/libdino/src/service/notification_events.vala @@ -112,9 +112,8 @@ public class NotificationEvents : StreamInteractionModule, Object { notifier.notify_call.begin(call, conversation, video, conversation_display_name); call.notify["state"].connect(() => { - if (call.state != Call.State.RINGING) { - notifier.retract_call_notification(call, conversation); + notifier.retract_call_notification.begin(call, conversation); } }); } diff --git a/libdino/src/util/display_name.vala b/libdino/src/util/display_name.vala index 7fa741af..0c05eda8 100644 --- a/libdino/src/util/display_name.vala +++ b/libdino/src/util/display_name.vala @@ -36,7 +36,7 @@ namespace Dino { return participant.bare_jid.to_string(); } - private static string? get_real_display_name(StreamInteractor stream_interactor, Account account, Jid jid, string? self_word = null) { + public static string? get_real_display_name(StreamInteractor stream_interactor, Account account, Jid jid, string? self_word = null) { if (jid.equals_bare(account.bare_jid)) { if (self_word != null || account.alias == null || account.alias.length == 0) { return self_word; @@ -50,7 +50,7 @@ namespace Dino { return null; } - private static string get_groupchat_display_name(StreamInteractor stream_interactor, Account account, Jid jid) { + public static string get_groupchat_display_name(StreamInteractor stream_interactor, Account account, Jid jid) { MucManager muc_manager = stream_interactor.get_module(MucManager.IDENTITY); string? room_name = muc_manager.get_room_name(account, jid); if (room_name != null && room_name != jid.localpart) { @@ -72,7 +72,7 @@ namespace Dino { return jid.to_string(); } - private static string get_occupant_display_name(StreamInteractor stream_interactor, Conversation conversation, Jid jid, string? self_word = null, bool muc_real_name = false) { + public static string get_occupant_display_name(StreamInteractor stream_interactor, Conversation conversation, Jid jid, string? self_word = null, bool muc_real_name = false) { if (muc_real_name) { MucManager muc_manager = stream_interactor.get_module(MucManager.IDENTITY); if (muc_manager.is_private_room(conversation.account, jid.bare_jid)) { diff --git a/main/src/ui/util/helper.vala b/main/src/ui/util/helper.vala index d3ca063b..17dfd334 100644 --- a/main/src/ui/util/helper.vala +++ b/main/src/ui/util/helper.vala @@ -122,15 +122,15 @@ public static string get_participant_display_name(StreamInteractor stream_intera return Dino.get_participant_display_name(stream_interactor, conversation, participant, me_is_me ? _("Me") : null); } -private static string? get_real_display_name(StreamInteractor stream_interactor, Account account, Jid jid, bool me_is_me = false) { +public static string? get_real_display_name(StreamInteractor stream_interactor, Account account, Jid jid, bool me_is_me = false) { return Dino.get_real_display_name(stream_interactor, account, jid, me_is_me ? _("Me") : null); } -private static string get_groupchat_display_name(StreamInteractor stream_interactor, Account account, Jid jid) { +public static string get_groupchat_display_name(StreamInteractor stream_interactor, Account account, Jid jid) { return Dino.get_groupchat_display_name(stream_interactor, account, jid); } -private static string get_occupant_display_name(StreamInteractor stream_interactor, Conversation conversation, Jid jid, bool me_is_me = false, bool muc_real_name = false) { +public static string get_occupant_display_name(StreamInteractor stream_interactor, Conversation conversation, Jid jid, bool me_is_me = false, bool muc_real_name = false) { return Dino.get_occupant_display_name(stream_interactor, conversation, jid, me_is_me ? _("Me") : null); } diff --git a/plugins/ice/src/dtls_srtp.vala b/plugins/ice/src/dtls_srtp.vala index e8fc01c7..6701fe89 100644 --- a/plugins/ice/src/dtls_srtp.vala +++ b/plugins/ice/src/dtls_srtp.vala @@ -23,10 +23,9 @@ public class Handler { private X509.Certificate[] own_cert; private X509.PrivateKey private_key; - private Cond buffer_cond = new Cond(); - private Mutex buffer_mutex = new Mutex(); + private Cond buffer_cond = Cond(); + private Mutex buffer_mutex = Mutex(); private Gee.LinkedList buffer_queue = new Gee.LinkedList(); - private uint pull_timeout = uint.MAX; private bool running = false; private bool stop = false; @@ -34,7 +33,7 @@ public class Handler { private Crypto.Srtp.Session srtp_session = new Crypto.Srtp.Session(); - public uint8[] process_incoming_data(uint component_id, uint8[] data) { + public uint8[]? process_incoming_data(uint component_id, uint8[] data) { if (srtp_session.has_decrypt) { try { if (component_id == 1) { @@ -54,7 +53,7 @@ public class Handler { return null; } - public uint8[] process_outgoing_data(uint component_id, uint8[] data) { + public uint8[]? process_outgoing_data(uint component_id, uint8[] data) { if (srtp_session.has_encrypt) { try { if (component_id == 1) { @@ -127,7 +126,7 @@ public class Handler { 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()); + debug("Setting up DTLS connection. We're %s", mode.to_string()); CertificateCredentials cert_cred = CertificateCredentials.create(); int err = cert_cred.set_x509_key(own_cert, private_key); @@ -181,7 +180,7 @@ public class Handler { warning("SRTP client/server key/salt null"); } - debug("Finished DTLS connection. We're %s", server_or_client.to_string()); + debug("Finished DTLS connection. We're %s", mode.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()); diff --git a/plugins/ice/src/transport_parameters.vala b/plugins/ice/src/transport_parameters.vala index f854a367..6d160c62 100644 --- a/plugins/ice/src/transport_parameters.vala +++ b/plugins/ice/src/transport_parameters.vala @@ -111,7 +111,7 @@ public class Dino.Plugins.Ice.TransportParameters : JingleIceUdp.IceUdpTransport } private static DtlsSrtp.Handler setup_dtls(TransportParameters tp) { - var weak_self = new WeakRef(tp); + var weak_self = WeakRef(tp); DtlsSrtp.Handler dtls_srtp = DtlsSrtp.setup(); dtls_srtp.send_data.connect((data) => { TransportParameters self = (TransportParameters) weak_self.get(); diff --git a/xmpp-vala/src/module/xep/0166_jingle/content.vala b/xmpp-vala/src/module/xep/0166_jingle/content.vala index 67510c36..beb12183 100644 --- a/xmpp-vala/src/module/xep/0166_jingle/content.vala +++ b/xmpp-vala/src/module/xep/0166_jingle/content.vala @@ -201,12 +201,6 @@ public class Xmpp.Xep.Jingle.Content : Object { stream.get_module(Iq.Module.IDENTITY).send_iq(stream, new Iq.Stanza.result(iq)); } - void verify_content(ContentNode content) throws IqError { - if (content.name != content_name || content.creator != content_creator) { - throw new IqError.BAD_REQUEST("unknown content"); - } - } - public void set_transport_connection(ComponentConnection? conn, uint8 component = 1) { debug(@"set_transport_connection: %s, %s, %i, %s, overwrites: %s", this.content_name, this.state.to_string(), component, (conn != null).to_string(), component_connections.has_key(component).to_string()); 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 4976f560..6684ddc2 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 @@ -109,7 +109,6 @@ public abstract class Xmpp.Xep.JingleIceUdp.IceUdpTransportParameters : Jingle.T string? ufrag = node.get_attribute("ufrag"); if (pwd != null) remote_pwd = pwd; if (ufrag != null) remote_ufrag = ufrag; - uint8 components = 0; foreach (StanzaNode candidateNode in node.get_subnodes("candidate")) { remote_candidates.add(Candidate.parse(candidateNode)); } -- 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/0166_jingle') 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/0166_jingle') 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