From e899668213ee8f7d3566bb5754b488d8633c30c7 Mon Sep 17 00:00:00 2001 From: Marvin W Date: Tue, 10 Sep 2019 20:56:00 +0200 Subject: Add JET support --- xmpp-vala/src/module/xep/0166_jingle.vala | 68 ++++++++-- .../src/module/xep/0234_jingle_file_transfer.vala | 19 ++- .../module/xep/0260_jingle_socks5_bytestreams.vala | 7 + .../xep/0391_jingle_encrypted_transports.vala | 142 +++++++++++++++++++++ 4 files changed, 222 insertions(+), 14 deletions(-) create mode 100644 xmpp-vala/src/module/xep/0391_jingle_encrypted_transports.vala (limited to 'xmpp-vala/src') diff --git a/xmpp-vala/src/module/xep/0166_jingle.vala b/xmpp-vala/src/module/xep/0166_jingle.vala index 86396f30..85663929 100644 --- a/xmpp-vala/src/module/xep/0166_jingle.vala +++ b/xmpp-vala/src/module/xep/0166_jingle.vala @@ -43,6 +43,7 @@ public errordomain Error { BAD_REQUEST, INVALID_PARAMETERS, UNSUPPORTED_TRANSPORT, + UNSUPPORTED_SECURITY, NO_SHARED_PROTOCOLS, TRANSPORT_ERROR, } @@ -69,6 +70,7 @@ class ContentNode { public string name; public StanzaNode? description; public StanzaNode? transport; + public StanzaNode? security; } ContentNode get_single_content_node(StanzaNode jingle) throws IqError { @@ -94,6 +96,7 @@ ContentNode get_single_content_node(StanzaNode jingle) throws IqError { string? name = content.get_attribute("name"); 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"); } @@ -102,7 +105,8 @@ ContentNode get_single_content_node(StanzaNode jingle) throws IqError { creator=creator, name=name, description=description, - transport=transport + transport=transport, + security=security }; } @@ -112,6 +116,7 @@ public class Module : XmppStreamModule, Iq.Handler { private HashMap content_types = new HashMap(); private HashMap transports = new HashMap(); + private HashMap security_preconditions = new HashMap(); private XmppStream? current_stream = null; @@ -163,6 +168,16 @@ public class Module : XmppStreamModule, Iq.Handler { } 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 bool is_jingle_available(XmppStream stream, Jid full_jid) { bool? has_jingle = stream.get_flag(ServiceDiscovery.Flag.IDENTITY).has_entity_feature(full_jid, NS_URI); @@ -173,7 +188,7 @@ public class Module : XmppStreamModule, Iq.Handler { return is_jingle_available(stream, full_jid) && select_transport(stream, type, full_jid, Set.empty()) != null; } - public Session create_session(XmppStream stream, TransportType type, Jid receiver_full_jid, Senders senders, string content_name, StanzaNode description) throws Error { + public Session create_session(XmppStream stream, TransportType type, Jid receiver_full_jid, Senders senders, string content_name, StanzaNode description, string? precondition_name = null, Object? precondation_options = null) throws Error { if (!is_jingle_available(stream, receiver_full_jid)) { throw new Error.NO_SHARED_PROTOCOLS("No Jingle support"); } @@ -181,18 +196,26 @@ public class Module : XmppStreamModule, Iq.Handler { if (transport == null) { throw new Error.NO_SHARED_PROTOCOLS("No suitable transports"); } + SecurityPrecondition? precondition = get_security_precondition(precondition_name); + if (precondition_name != null && precondition == null) { + throw new Error.UNSUPPORTED_SECURITY("No suitable security precondiiton found"); + } Jid? my_jid = stream.get_flag(Bind.Flag.IDENTITY).my_jid; if (my_jid == null) { throw new Error.GENERAL("Couldn't determine own JID"); } TransportParameters transport_params = transport.create_transport_parameters(stream, my_jid, receiver_full_jid); - Session session = new Session.initiate_sent(random_uuid(), type, transport_params, my_jid, receiver_full_jid, content_name, send_terminate_and_remove_session); + SecurityParameters? security_params = precondition != null ? precondition.create_security_parameters(stream, my_jid, receiver_full_jid, precondation_options) : null; + Session session = new Session.initiate_sent(random_uuid(), type, transport_params, security_params, my_jid, receiver_full_jid, content_name, send_terminate_and_remove_session); StanzaNode content = new StanzaNode.build("content", NS_URI) .put_attribute("creator", "initiator") .put_attribute("name", content_name) .put_attribute("senders", senders.to_string()) .put_node(description) .put_node(transport_params.to_transport_stanza_node()); + if (security_params != null) { + content.put_node(security_params.to_security_stanza_node(stream, my_jid, receiver_full_jid)); + } StanzaNode jingle = new StanzaNode.build("jingle", NS_URI) .add_self_xmlns() .put_attribute("action", "session-initiate") @@ -233,8 +256,17 @@ public class Module : XmppStreamModule, Iq.Handler { } ContentParameters content_params = content_type.parse_content_parameters(content.description); + SecurityPrecondition? precondition = content.security != null ? get_security_precondition(content.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, iq.from, content.security); + } else if (content.security != null) { + throw new IqError.NOT_IMPLEMENTED("unknown security precondition"); + } + TransportType type = content_type.content_type_transport_type(); - Session session = new Session.initiate_received(sid, type, transport_params, my_jid, iq.from, content.name, send_terminate_and_remove_session); + Session session = new Session.initiate_received(sid, type, transport_params, security_params, my_jid, iq.from, content.name, send_terminate_and_remove_session); stream.get_flag(Flag.IDENTITY).add_session(session); stream.get_module(Iq.Module.IDENTITY).send_iq(stream, new Iq.Stanza.result(iq)); @@ -328,7 +360,7 @@ public interface Transport : Object { public abstract bool is_transport_available(XmppStream stream, Jid full_jid); public abstract TransportType transport_type(); public abstract int transport_priority(); - public abstract TransportParameters create_transport_parameters(XmppStream stream, Jid local_full_jid, Jid peer_full_jid); + public abstract TransportParameters create_transport_parameters(XmppStream stream, Jid local_full_jid, Jid peer_full_jid) throws Error; public abstract TransportParameters parse_transport_parameters(XmppStream stream, Jid local_full_jid, Jid peer_full_jid, StanzaNode transport) throws IqError; } @@ -375,6 +407,17 @@ public interface ContentParameters : Object { public abstract void on_session_initiate(XmppStream stream, Session session); } +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); +} public class Session { // INITIATE_SENT -> CONNECTING -> [REPLACING_TRANSPORT -> CONNECTING ->]... ACTIVE -> ENDED @@ -398,6 +441,7 @@ public class Session { public Jid peer_full_jid { get; private set; } public Role content_creator { get; private set; } public string content_name { get; private set; } + public SecurityParameters? security { get; private set; } private Connection connection; public IOStream conn { get { return connection; } } @@ -410,7 +454,7 @@ public class Session { SessionTerminate session_terminate_handler; - public Session.initiate_sent(string sid, TransportType type, TransportParameters transport, Jid local_full_jid, Jid peer_full_jid, string content_name, owned SessionTerminate session_terminate_handler) { + public Session.initiate_sent(string sid, TransportType type, TransportParameters transport, SecurityParameters? security, Jid local_full_jid, Jid peer_full_jid, string content_name, owned SessionTerminate session_terminate_handler) { this.state = State.INITIATE_SENT; this.role = Role.INITIATOR; this.sid = sid; @@ -422,12 +466,13 @@ public class Session { this.tried_transport_methods = new HashSet(); this.tried_transport_methods.add(transport.transport_ns_uri()); this.transport = transport; + this.security = security; this.connection = new Connection(this); this.session_terminate_handler = (owned)session_terminate_handler; this.terminate_on_connection_close = true; } - public Session.initiate_received(string sid, TransportType type, TransportParameters? transport, Jid local_full_jid, Jid peer_full_jid, string content_name, owned SessionTerminate session_terminate_handler) { + public Session.initiate_received(string sid, TransportType type, TransportParameters? transport, SecurityParameters? security, Jid local_full_jid, Jid peer_full_jid, string content_name, owned SessionTerminate session_terminate_handler) { this.state = State.INITIATE_RECEIVED; this.role = Role.RESPONDER; this.sid = sid; @@ -437,6 +482,7 @@ public class Session { this.content_creator = Role.INITIATOR; this.content_name = content_name; this.transport = transport; + this.security = security; this.tried_transport_methods = new HashSet(); if (transport != null) { this.tried_transport_methods.add(transport.transport_ns_uri()); @@ -557,7 +603,12 @@ public class Session { state = State.ACTIVE; transport = null; tried_transport_methods.clear(); - connection.set_inner(conn); + if (security != null) { + connection.set_inner(security.wrap_stream(conn)); + } else { + connection.set_inner(conn); + } + } else { if (role == Role.INITIATOR) { select_new_transport(stream); @@ -913,6 +964,7 @@ public class Connection : IOStream { return true; } public async bool close_read_async(int io_priority = GLib.Priority.DEFAULT, Cancellable? cancellable = null) throws IOError { + debug("Closing Jingle input stream"); yield wait_and_check_for_errors(io_priority, cancellable); if (read_closed) { return true; 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 951ea7b7..7780d973 100644 --- a/xmpp-vala/src/module/xep/0234_jingle_file_transfer.vala +++ b/xmpp-vala/src/module/xep/0234_jingle_file_transfer.vala @@ -48,18 +48,24 @@ public class Module : Jingle.ContentType, XmppStreamModule { return stream.get_module(Jingle.Module.IDENTITY).is_available(stream, Jingle.TransportType.STREAMING, full_jid); } - public async void offer_file_stream(XmppStream stream, Jid receiver_full_jid, InputStream input_stream, string basename, int64 size) throws IOError { + public async void offer_file_stream(XmppStream stream, Jid receiver_full_jid, InputStream input_stream, string basename, int64 size, string? precondition_name = null, Object? precondition_options = null) throws IOError { + StanzaNode file_node; StanzaNode description = new StanzaNode.build("description", NS_URI) .add_self_xmlns() - .put_node(new StanzaNode.build("file", NS_URI) - .put_node(new StanzaNode.build("name", NS_URI).put_node(new StanzaNode.text(basename))) - .put_node(new StanzaNode.build("size", NS_URI).put_node(new StanzaNode.text(size.to_string())))); + .put_node(file_node = new StanzaNode.build("file", NS_URI) + .put_node(new StanzaNode.build("name", NS_URI).put_node(new StanzaNode.text(basename)))); // TODO(hrxi): Add the mandatory hash field + if (size > 0) { + file_node.put_node(new StanzaNode.build("size", NS_URI).put_node(new StanzaNode.text(size.to_string()))); + } else { + warning("Sending file %s without size, likely going to cause problems down the road...", basename); + } + Jingle.Session session; try { session = stream.get_module(Jingle.Module.IDENTITY) - .create_session(stream, Jingle.TransportType.STREAMING, receiver_full_jid, Jingle.Senders.INITIATOR, "a-file-offer", description); // TODO(hrxi): Why "a-file-offer"? + .create_session(stream, Jingle.TransportType.STREAMING, receiver_full_jid, Jingle.Senders.INITIATOR, "a-file-offer", description, precondition_name, precondition_options); // TODO(hrxi): Why "a-file-offer"? } catch (Jingle.Error e) { throw new IOError.FAILED(@"couldn't create Jingle session: $(e.message)"); } @@ -172,13 +178,14 @@ public class FileTransfer : Object { public Jid peer { get { return session.peer_full_jid; } } public string? file_name { get { return parameters.name; } } public int64 size { get { return parameters.size; } } + public Jingle.SecurityParameters? security { get { return session.security; } } public InputStream? stream { get; private set; } public FileTransfer(Jingle.Session session, Parameters parameters) { this.session = session; this.parameters = parameters; - this.stream = new FileTransferInputStream(session.conn.input_stream, parameters.size); + this.stream = new FileTransferInputStream(session.conn.input_stream, size); } public void accept(XmppStream stream) throws IOError { 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 a5eb1250..991ea141 100644 --- a/xmpp-vala/src/module/xep/0260_jingle_socks5_bytestreams.vala +++ b/xmpp-vala/src/module/xep/0260_jingle_socks5_bytestreams.vala @@ -287,6 +287,7 @@ class Parameters : Jingle.TransportParameters, Object { } remote_sent_selected_candidate = true; remote_selected_candidate = candidate; + debug("Remote selected candidate %s", candidate.cid); try_completing_negotiation(); } private void handle_activated(string cid) throws Jingle.IqError { @@ -353,6 +354,7 @@ class Parameters : Jingle.TransportParameters, Object { } } public async void wait_for_remote_activation(Candidate candidate, SocketConnection conn) { + debug("Waiting for remote activation of %s", candidate.cid); waiting_for_activation_cid = candidate.cid; waiting_for_activation_callback = wait_for_remote_activation.callback; yield; @@ -368,6 +370,7 @@ class Parameters : Jingle.TransportParameters, Object { } } public async void connect_to_local_candidate(Candidate candidate) { + debug("Connecting to candidate %s", candidate.cid); try { SocketConnection conn = yield connect_to_socks5(candidate, local_dstaddr); @@ -420,6 +423,7 @@ class Parameters : Jingle.TransportParameters, Object { SocketClient socket_client = new SocketClient() { timeout=3 }; string address = @"[$(candidate.host)]:$(candidate.port)"; + debug("Connecting to SOCKS5 server at %s", address); size_t written; size_t read; @@ -500,6 +504,7 @@ class Parameters : Jingle.TransportParameters, Object { local_determined_selected_candidate = true; local_selected_candidate = candidate; local_selected_candidate_conn = conn; + debug("Selected candidate %s", candidate.cid); session.send_transport_info(stream, new StanzaNode.build("transport", NS_URI) .add_self_xmlns() .put_attribute("sid", sid) @@ -522,6 +527,8 @@ class Parameters : Jingle.TransportParameters, Object { .put_attribute("sid", sid) .put_node(new StanzaNode.build("candidate-error", NS_URI)) ); + // Try remote candidates + try_completing_negotiation(); } public void create_transport_connection(XmppStream stream, Jingle.Session session) { this.session = session; diff --git a/xmpp-vala/src/module/xep/0391_jingle_encrypted_transports.vala b/xmpp-vala/src/module/xep/0391_jingle_encrypted_transports.vala new file mode 100644 index 00000000..e2b1326b --- /dev/null +++ b/xmpp-vala/src/module/xep/0391_jingle_encrypted_transports.vala @@ -0,0 +1,142 @@ +using Gee; +using Xmpp.Xep.Jingle; + +namespace Xmpp.Xep.Jet { +public const string NS_URI = "urn:xmpp:jingle:jet:0"; + +public class Module : XmppStreamModule, SecurityPrecondition { + public static Xmpp.ModuleIdentity IDENTITY = new Xmpp.ModuleIdentity(NS_URI, "0391_jet"); + private HashMap envelop_encodings = new HashMap(); + private HashMap ciphers = new HashMap(); + + public override void attach(XmppStream stream) { + stream.get_module(ServiceDiscovery.Module.IDENTITY).add_feature(stream, NS_URI); + stream.get_module(Jingle.Module.IDENTITY).register_security_precondition(this); + } + + public override void detach(XmppStream stream) { + } + + public bool is_available(XmppStream stream, Jid full_jid) { + bool? has_feature = stream.get_flag(ServiceDiscovery.Flag.IDENTITY).has_entity_feature(full_jid, NS_URI); + return has_feature != null && (!)has_feature; + } + + public void register_envelop_encoding(EnvelopEncoding encoding) { + envelop_encodings[encoding.get_type_uri()] = encoding; + } + + public void register_cipher(Cipher cipher) { + ciphers[cipher.get_cipher_uri()] = cipher; + } + + public string security_ns_uri() { + return NS_URI; + } + + public Jingle.SecurityParameters? create_security_parameters(XmppStream stream, Jid local_full_jid, Jid peer_full_jid, Object options) throws Jingle.Error requires (options is Options) { + Options jet_options = (Options) options; + string cipher = jet_options.cipher_uri; + string type = jet_options.type_uri; + if (!envelop_encodings.has_key(type) || !ciphers.has_key(cipher)) { + throw new IqError.NOT_IMPLEMENTED("JET cipher or type unknown"); + } + EnvelopEncoding encoding = envelop_encodings[type]; + return new SecurityParameters(ciphers[cipher], encoding, ciphers[cipher].generate_random_secret(), jet_options); + } + + public Jingle.SecurityParameters? parse_security_parameters(XmppStream stream, Jid local_full_jid, Jid peer_full_jid, StanzaNode security) throws IqError { + string? cipher = security.get_attribute("cipher"); + string? type = security.get_attribute("type"); + if (cipher == null || type == null) { + throw new IqError.BAD_REQUEST("No cipher or type specified for JET"); + } + if (!envelop_encodings.has_key(type) || !ciphers.has_key(cipher)) { + throw new IqError.NOT_IMPLEMENTED("JET cipher or type unknown"); + } + EnvelopEncoding encoding = envelop_encodings[type]; + TransportSecret secret = encoding.decode_envolop(stream, local_full_jid, peer_full_jid, security); + return new SecurityParameters(ciphers[cipher], encoding, secret); + } + + public override string get_ns() { return NS_URI; } + public override string get_id() { return IDENTITY.id; } +} + +public class Options : Object { + public string type_uri { get; private set; } + public string cipher_uri { get; private set; } + + public Options(string type_uri, string cipher_uri) { + this.type_uri = type_uri; + this.cipher_uri = cipher_uri; + } +} + +public class SecurityParameters : Jingle.SecurityParameters, Object { + public Cipher cipher { get; private set; } + public EnvelopEncoding encoding { get; private set; } + public TransportSecret secret { get; private set; } + public Options? options { get; private set; } + + public SecurityParameters(Cipher cipher, EnvelopEncoding encoding, TransportSecret secret, Options? options = null) { + this.cipher = cipher; + this.encoding = encoding; + this.secret = secret; + this.options = options; + } + + public string security_ns_uri() { + return NS_URI; + } + public IOStream wrap_stream(IOStream stream) { + debug("Wrapping stream into encrypted stream for %s/%s", encoding.get_type_uri(), cipher.get_cipher_uri()); + return new EncryptedStream(cipher, secret, stream); + } + public StanzaNode to_security_stanza_node(XmppStream stream, Jid local_full_jid, Jid peer_full_jid) { + StanzaNode security = new StanzaNode.build("security", NS_URI) + .add_self_xmlns() + .put_attribute("cipher", cipher.get_cipher_uri()) + .put_attribute("type", encoding.get_type_uri()); + encoding.encode_envelop(stream, local_full_jid, peer_full_jid, this, security); + return security; + } +} + +public interface Cipher : Object { + public abstract string get_cipher_uri(); + public abstract TransportSecret generate_random_secret(); + public abstract InputStream wrap_input_stream(InputStream input, TransportSecret secret); + public abstract OutputStream wrap_output_stream(OutputStream output, TransportSecret secret); +} + +private class EncryptedStream : IOStream { + private IOStream stream; + private InputStream input; + private OutputStream output; + public override InputStream input_stream { get { return input; } } + public override OutputStream output_stream { get { return output; } } + + public EncryptedStream(Cipher cipher, TransportSecret secret, IOStream stream) { + this.stream = stream; + input = cipher.wrap_input_stream(stream.input_stream, secret); + output = cipher.wrap_output_stream(stream.output_stream, secret); + } +} + +public class TransportSecret { + public uint8[] transport_key { get; private set; } + public uint8[] initialization_vector { get; private set; } + public TransportSecret(uint8[] transport_key, uint8[] initialization_vector) { + this.transport_key = transport_key; + this.initialization_vector = initialization_vector; + } +} + +public interface EnvelopEncoding : Object { + public abstract string get_type_uri(); + public abstract TransportSecret decode_envolop(XmppStream stream, Jid local_full_jid, Jid peer_full_jid, StanzaNode security) throws IqError; + public abstract void encode_envelop(XmppStream stream, Jid local_full_jid, Jid peer_full_jid, SecurityParameters security_params, StanzaNode security); +} + +} \ No newline at end of file -- cgit v1.2.3-70-g09d2