diff options
Diffstat (limited to 'xmpp-vala/src')
-rw-r--r-- | xmpp-vala/src/module/sasl.vala | 203 | ||||
-rw-r--r-- | xmpp-vala/src/module/stanza_error.vala | 6 | ||||
-rw-r--r-- | xmpp-vala/src/module/tls.vala | 15 | ||||
-rw-r--r-- | xmpp-vala/src/module/xep/0004_data_forms.vala | 5 | ||||
-rw-r--r-- | xmpp-vala/src/module/xep/0045_muc/module.vala | 13 | ||||
-rw-r--r-- | xmpp-vala/src/module/xep/0077_in_band_registration.vala | 64 | ||||
-rw-r--r-- | xmpp-vala/src/module/xep/0368_srv_records_tls.vala | 5 |
7 files changed, 258 insertions, 53 deletions
diff --git a/xmpp-vala/src/module/sasl.vala b/xmpp-vala/src/module/sasl.vala index 4a427ce0..2e87e590 100644 --- a/xmpp-vala/src/module/sasl.vala +++ b/xmpp-vala/src/module/sasl.vala @@ -1,9 +1,27 @@ -namespace Xmpp.PlainSasl { +namespace Xmpp.Sasl { private const string NS_URI = "urn:ietf:params:xml:ns:xmpp-sasl"; + public class Flag : XmppStreamFlag { + public static FlagIdentity<Flag> IDENTITY = new FlagIdentity<Flag>(NS_URI, "sasl"); + public string mechanism; + public string name; + public string password; + public string client_nonce; + public uint8[] server_signature; + public bool finished = false; + + public override string get_ns() { return NS_URI; } + public override string get_id() { return IDENTITY.id; } + } + + namespace Mechanism { + public const string PLAIN = "PLAIN"; + public const string SCRAM_SHA_1 = "SCRAM-SHA-1"; + public const string SCRAM_SHA_1_PLUS = "SCRAM-SHA-1-PLUS"; + } + public class Module : XmppStreamNegotiationModule { - public static ModuleIdentity<Module> IDENTITY = new ModuleIdentity<Module>(NS_URI, "plain_module"); - private const string MECHANISM = "PLAIN"; + public static ModuleIdentity<Module> IDENTITY = new ModuleIdentity<Module>(NS_URI, "sasl"); public string name { get; set; } public string password { get; set; } @@ -26,14 +44,109 @@ namespace Xmpp.PlainSasl { stream.received_nonza.disconnect(this.received_nonza); } + private static size_t SHA1_SIZE = 20; + + private static uint8[] sha1(uint8[] data) { + Checksum checksum = new Checksum(ChecksumType.SHA1); + checksum.update(data, data.length); + uint8[] res = new uint8[SHA1_SIZE]; + checksum.get_digest(res, ref SHA1_SIZE); + return res; + } + + private static uint8[] hmac_sha1(uint8[] key, uint8[] data) { + Hmac hmac = new Hmac(ChecksumType.SHA1, key); + hmac.update(data); + uint8[] res = new uint8[SHA1_SIZE]; + hmac.get_digest(res, ref SHA1_SIZE); + return res; + } + + private static uint8[] pbkdf2_sha1(string password, uint8[] salt, uint iterations) { + uint8[] res = new uint8[SHA1_SIZE]; + uint8[] last = new uint8[salt.length + 4]; + for(int i = 0; i < salt.length; i++) { + last[i] = salt[i]; + } + last[salt.length + 3] = 1; + for(int i = 0; i < iterations; i++) { + last = hmac_sha1((uint8[]) password.to_utf8(), last); + xor_inplace(res, last); + } + return res; + } + + private static void xor_inplace(uint8[] mix, uint8[] a2) { + for(int i = 0; i < mix.length; i++) { + mix[i] = mix[i] ^ a2[i]; + } + } + + private static uint8[] xor(uint8[] a1, uint8[] a2) { + uint8[] mix = new uint8[a1.length]; + for(int i = 0; i < a1.length; i++) { + mix[i] = a1[i] ^ a2[i]; + } + return mix; + } + public void received_nonza(XmppStream stream, StanzaNode node) { if (node.ns_uri == NS_URI) { if (node.name == "success") { + Flag flag = stream.get_flag(Flag.IDENTITY); + if (flag.mechanism == Mechanism.SCRAM_SHA_1) { + string confirm = (string) Base64.decode(node.get_string_content()); + uint8[] server_signature = null; + foreach(string c in confirm.split(",")) { + string[] split = c.split("=", 2); + if (split.length != 2) continue; + switch(split[0]) { + case "v": server_signature = Base64.decode(split[1]); break; + } + } + if (server_signature == null) return; + if (server_signature.length != flag.server_signature.length) return; + for(int i = 0; i < server_signature.length; i++) { + if (server_signature[i] != flag.server_signature[i]) return; + } + } stream.require_setup(); - stream.get_flag(Flag.IDENTITY).finished = true; + flag.password = null; // Remove password from memory + flag.finished = true; } else if (node.name == "failure") { stream.remove_flag(stream.get_flag(Flag.IDENTITY)); received_auth_failure(stream, node); + } else if (node.name == "challenge" && stream.has_flag(Flag.IDENTITY)) { + Flag flag = stream.get_flag(Flag.IDENTITY); + if (flag.mechanism == Mechanism.SCRAM_SHA_1) { + string challenge = (string) Base64.decode(node.get_string_content()); + string? server_nonce = null; + uint8[] salt = null; + uint iterations = 0; + foreach(string c in challenge.split(",")) { + string[] split = c.split("=", 2); + if (split.length != 2) continue; + switch(split[0]) { + case "r": server_nonce = split[1]; break; + case "s": salt = Base64.decode(split[1]); break; + case "i": iterations = int.parse(split[1]); break; + } + } + if (server_nonce == null || salt == null || iterations == 0) return; + if (!server_nonce.has_prefix(flag.client_nonce)) return; + string client_final_message_bare = @"c=biws,r=$server_nonce"; + uint8[] salted_password = pbkdf2_sha1(flag.password, salt, iterations); + uint8[] client_key = hmac_sha1(salted_password, (uint8[]) "Client Key".to_utf8()); + uint8[] stored_key = sha1(client_key); + string auth_message = @"n=$(flag.name),r=$(flag.client_nonce),$challenge,$client_final_message_bare"; + uint8[] client_signature = hmac_sha1(stored_key, (uint8[]) auth_message.to_utf8()); + uint8[] client_proof = xor(client_key, client_signature); + uint8[] server_key = hmac_sha1(salted_password, (uint8[]) "Server Key".to_utf8()); + flag.server_signature = hmac_sha1(server_key, (uint8[]) auth_message.to_utf8()); + string client_final_message = @"$client_final_message_bare,p=$(Base64.encode(client_proof))"; + stream.write(new StanzaNode.build("response", NS_URI).add_self_xmlns() + .put_node(new StanzaNode.text(Base64.encode((uchar[]) (client_final_message).to_utf8())))); + } } } } @@ -44,45 +157,53 @@ namespace Xmpp.PlainSasl { if (!stream.has_flag(Tls.Flag.IDENTITY) || !stream.get_flag(Tls.Flag.IDENTITY).finished) return; var mechanisms = stream.features.get_subnode("mechanisms", NS_URI); - if (mechanisms != null) { - bool supportsPlain = false; - foreach (var mechanism in mechanisms.sub_nodes) { - if (mechanism.name != "mechanism" || mechanism.ns_uri != NS_URI) continue; - var text = mechanism.get_subnode("#text"); - if (text != null && text.val == MECHANISM) { - supportsPlain = true; - } - } - if (!supportsPlain) { - stderr.printf("Server at %s does not support %s auth, use full-features Sasl implementation!\n", stream.remote_name.to_string(), MECHANISM); - return; - } - - if (!name.contains("@")) { - name = "%s@%s".printf(name, stream.remote_name.to_string()); - } - if (!use_full_name && name.contains("@")) { - var split = name.split("@"); - if (split[1] == stream.remote_name.to_string()) { - name = split[0]; - } else { - use_full_name = true; - } + string[] supported_mechanisms = {}; + foreach (var mechanism in mechanisms.sub_nodes) { + if (mechanism.name != "mechanism" || mechanism.ns_uri != NS_URI) continue; + supported_mechanisms += mechanism.get_string_content(); + } + if (!name.contains("@")) { + name = "%s@%s".printf(name, stream.remote_name.to_string()); + } + if (!use_full_name && name.contains("@")) { + var split = name.split("@"); + if (split[1] == stream.remote_name.to_string()) { + name = split[0]; + } else { + use_full_name = true; } - var name = this.name; - if (!use_full_name && name.contains("@")) { - var split = name.split("@"); - if (split[1] == stream.remote_name.to_string()) { - name = split[0]; - } + } + string name = this.name; + if (!use_full_name && name.contains("@")) { + var split = name.split("@"); + if (split[1] == stream.remote_name.to_string()) { + name = split[0]; } + } + if (Mechanism.SCRAM_SHA_1 in supported_mechanisms) { + string normalized_password = password.normalize(-1, NormalizeMode.NFKC); + string client_nonce = Random.next_int().to_string("%.8x") + Random.next_int().to_string("%.8x") + Random.next_int().to_string("%.8x"); + string initial_message = @"n=$name,r=$client_nonce"; stream.write(new StanzaNode.build("auth", NS_URI).add_self_xmlns() - .put_attribute("mechanism", MECHANISM) + .put_attribute("mechanism", Mechanism.SCRAM_SHA_1) + .put_node(new StanzaNode.text(Base64.encode((uchar[]) ("n,,"+initial_message).to_utf8())))); + var flag = new Flag(); + flag.mechanism = Mechanism.SCRAM_SHA_1; + flag.name = name; + flag.password = normalized_password; + flag.client_nonce = client_nonce; + stream.add_flag(flag); + } else if (Mechanism.PLAIN in supported_mechanisms) { + stream.write(new StanzaNode.build("auth", NS_URI).add_self_xmlns() + .put_attribute("mechanism", Mechanism.PLAIN) .put_node(new StanzaNode.text(Base64.encode(get_plain_bytes(name, password))))); var flag = new Flag(); - flag.mechanism = MECHANISM; + flag.mechanism = Mechanism.PLAIN; flag.name = name; stream.add_flag(flag); + } else { + stderr.printf("No supported mechanism provided by server at %s\n", stream.remote_name.to_string()); + return; } } @@ -108,14 +229,4 @@ namespace Xmpp.PlainSasl { public override string get_ns() { return NS_URI; } public override string get_id() { return IDENTITY.id; } } - - public class Flag : XmppStreamFlag { - public static FlagIdentity<Flag> IDENTITY = new FlagIdentity<Flag>(NS_URI, "sasl"); - public string mechanism; - public string name; - public bool finished = false; - - public override string get_ns() { return NS_URI; } - public override string get_id() { return IDENTITY.id; } - } } diff --git a/xmpp-vala/src/module/stanza_error.vala b/xmpp-vala/src/module/stanza_error.vala index 51aa2629..c45ff4e3 100644 --- a/xmpp-vala/src/module/stanza_error.vala +++ b/xmpp-vala/src/module/stanza_error.vala @@ -36,6 +36,10 @@ namespace Xmpp { get { return error_node.get_attribute("by"); } } + public string? text { + get { return error_node.get_deep_string_content("urn:ietf:params:xml:ns:xmpp-stanzas:text"); } + } + public string condition { get { Gee.List<StanzaNode> subnodes = error_node.sub_nodes; @@ -57,7 +61,7 @@ namespace Xmpp { } public StanzaNode stanza; - private StanzaNode error_node; + public StanzaNode error_node; public ErrorStanza.from_stanza(StanzaNode stanza) { this.stanza = stanza; diff --git a/xmpp-vala/src/module/tls.vala b/xmpp-vala/src/module/tls.vala index 7118a321..f2d58d32 100644 --- a/xmpp-vala/src/module/tls.vala +++ b/xmpp-vala/src/module/tls.vala @@ -4,6 +4,7 @@ namespace Xmpp.Tls { public class Module : XmppStreamNegotiationModule { public static ModuleIdentity<Module> IDENTITY = new ModuleIdentity<Module>(NS_URI, "tls_module"); + public signal void invalid_certificate(TlsCertificate peer_cert, TlsCertificateFlags errors); public bool require { get; set; default = true; } public bool server_supports_tls = false; public bool server_requires_tls = false; @@ -27,6 +28,7 @@ namespace Xmpp.Tls { var conn = TlsClientConnection.new(io_stream, identity); stream.reset_stream(conn); + conn.accept_certificate.connect(on_invalid_certificate); var flag = stream.get_flag(Flag.IDENTITY); flag.peer_certificate = conn.get_peer_certificate(); flag.finished = true; @@ -56,6 +58,19 @@ namespace Xmpp.Tls { } } + public static bool on_invalid_certificate(TlsCertificate peer_cert, TlsCertificateFlags errors) { + string error_str = ""; + foreach (var f in new TlsCertificateFlags[]{TlsCertificateFlags.UNKNOWN_CA, TlsCertificateFlags.BAD_IDENTITY, + TlsCertificateFlags.NOT_ACTIVATED, TlsCertificateFlags.EXPIRED, TlsCertificateFlags.REVOKED, + TlsCertificateFlags.INSECURE, TlsCertificateFlags.GENERIC_ERROR, TlsCertificateFlags.VALIDATE_ALL}) { + if (f in errors) { + error_str += @"$(f), "; + } + } + warning(@"Tls Certificate Errors: $(error_str)"); + return false; + } + public override bool mandatory_outstanding(XmppStream stream) { return require && (!stream.has_flag(Flag.IDENTITY) || !stream.get_flag(Flag.IDENTITY).finished); } diff --git a/xmpp-vala/src/module/xep/0004_data_forms.vala b/xmpp-vala/src/module/xep/0004_data_forms.vala index 69c14b08..9456197c 100644 --- a/xmpp-vala/src/module/xep/0004_data_forms.vala +++ b/xmpp-vala/src/module/xep/0004_data_forms.vala @@ -53,7 +53,6 @@ public class DataForm { public string? label { get { return node.get_attribute("label", NS_URI); } set { node.set_attribute("label", value); } - default = null; } public virtual Type? type_ { get; internal set; default=null; } public string? var { @@ -78,7 +77,7 @@ public class DataForm { return ret; } - internal string get_value_string() { + public string get_value_string() { Gee.List<string> values = get_values(); return values.size > 0 ? values[0] : ""; } @@ -197,7 +196,7 @@ public class DataForm { // TODO text-multi - internal DataForm.from_node(StanzaNode node, XmppStream stream, owned OnResult listener) { + internal DataForm.from_node(StanzaNode node, XmppStream stream, owned OnResult? listener = null) { this.stanza_node = node; this.stream = stream; this.on_result = (owned)listener; diff --git a/xmpp-vala/src/module/xep/0045_muc/module.vala b/xmpp-vala/src/module/xep/0045_muc/module.vala index b0a22d6b..955ea89b 100644 --- a/xmpp-vala/src/module/xep/0045_muc/module.vala +++ b/xmpp-vala/src/module/xep/0045_muc/module.vala @@ -60,7 +60,7 @@ public class Module : XmppStreamModule { public signal void received_occupant_jid(XmppStream stream, Jid jid, Jid? real_jid); public signal void received_occupant_role(XmppStream stream, Jid jid, Role? role); public signal void subject_set(XmppStream stream, string? subject, Jid jid); - public signal void room_configuration_changed(XmppStream stream, Jid jid, StatusCode code); + public signal void room_name_set(XmppStream stream, Jid jid, string? room_name); public signal void room_entered(XmppStream stream, Jid jid, string nick); public signal void room_enter_error(XmppStream stream, Jid jid, MucEnterError? error); // TODO "?" shoudln't be necessary (vala bug), remove someday @@ -207,6 +207,16 @@ public class Module : XmppStreamModule { stream.get_flag(Flag.IDENTITY).set_muc_subject(message.from, subject); subject_set(stream, subject, message.from); } + + StanzaNode? x_node = message.stanza.get_subnode("x", NS_URI_USER); + if (x_node != null) { + StanzaNode? status_node = x_node.get_subnode("status", NS_URI_USER); + if (status_node != null && status_node.get_attribute_int("code") == 104) { + // room configuration has changed (e.g. room name) + // https://xmpp.org/extensions/xep-0045.html#roomconfig-notify + query_room_info(stream, message.from.bare_jid); + } + } } } @@ -320,6 +330,7 @@ public class Module : XmppStreamModule { foreach (ServiceDiscovery.Identity identity in query_result.identities) { if (identity.category == "conference") { stream.get_flag(Flag.IDENTITY).set_room_name(jid, identity.name); + room_name_set(stream, jid, identity.name); } } diff --git a/xmpp-vala/src/module/xep/0077_in_band_registration.vala b/xmpp-vala/src/module/xep/0077_in_band_registration.vala new file mode 100644 index 00000000..1c544c18 --- /dev/null +++ b/xmpp-vala/src/module/xep/0077_in_band_registration.vala @@ -0,0 +1,64 @@ +using Gee; + +namespace Xmpp.Xep.InBandRegistration { + +public const string NS_URI = "jabber:iq:register"; + +public class Module : XmppStreamNegotiationModule { + public static ModuleIdentity<Module> IDENTITY = new ModuleIdentity<Module>(NS_URI, "0077_in_band_registration"); + + public async Form? get_from_server(XmppStream stream, Jid jid) { + Iq.Stanza request_form_iq = new Iq.Stanza.get(new StanzaNode.build("query", NS_URI).add_self_xmlns()); + request_form_iq.to = jid; + SourceFunc callback = get_from_server.callback; + Form? form = null; + stream.get_module(Iq.Module.IDENTITY).send_iq(stream, request_form_iq, (stream, response_iq) => { + form = new Form.from_node(stream, response_iq); + Idle.add((owned)callback); + }); + yield; + return form; + } + + public async string submit_to_server(XmppStream stream, Jid jid, Form form) { + StanzaNode query_node = new StanzaNode.build("query", NS_URI).add_self_xmlns(); + query_node.put_node(form.get_submit_node()); + Iq.Stanza iq = new Iq.Stanza.set(query_node); + iq.to = jid; + string? error_message = null; + SourceFunc callback = submit_to_server.callback; + stream.get_module(Iq.Module.IDENTITY).send_iq(stream, iq, (stream, response_iq) => { + if (response_iq.is_error()) { + ErrorStanza? error_stanza = response_iq.get_error(); + error_message = error_stanza.text ?? "Error"; + } + Idle.add((owned)callback); + }); + yield; + return error_message; + } + + public override bool mandatory_outstanding(XmppStream stream) { return false; } + + public override bool negotiation_active(XmppStream stream) { return false; } + + 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 Form : DataForms.DataForm { + public string? oob = null; + + internal Form.from_node(XmppStream stream, Iq.Stanza iq) { + StanzaNode? x_node = iq.stanza.get_deep_subnode(NS_URI + ":query", DataForms.NS_URI + ":x"); + base.from_node(x_node ?? new StanzaNode.build("x", NS_URI).add_self_xmlns(), stream); + + oob = iq.stanza.get_deep_string_content(NS_URI + ":query", "jabber:x:oob:x", "url"); + } +} + +} diff --git a/xmpp-vala/src/module/xep/0368_srv_records_tls.vala b/xmpp-vala/src/module/xep/0368_srv_records_tls.vala index 8da8ba0c..87c8e433 100644 --- a/xmpp-vala/src/module/xep/0368_srv_records_tls.vala +++ b/xmpp-vala/src/module/xep/0368_srv_records_tls.vala @@ -37,9 +37,10 @@ public class TlsConnectionProvider : ConnectionProvider { SocketClient client = new SocketClient(); try { IOStream? io_stream = yield client.connect_to_host_async(srv_target.get_hostname(), srv_target.get_port()); - io_stream = TlsClientConnection.new(io_stream, new NetworkAddress(stream.remote_name.to_string(), srv_target.get_port())); + TlsConnection tls_connection = TlsClientConnection.new(io_stream, new NetworkAddress(stream.remote_name.to_string(), srv_target.get_port())); + tls_connection.accept_certificate.connect(Tls.Module.on_invalid_certificate); stream.add_flag(new Tls.Flag() { finished=true }); - return io_stream; + return tls_connection; } catch (Error e) { return null; } |