diff --git a/libdino/src/service/connection_manager.vala b/libdino/src/service/connection_manager.vala index 4413dfd7..b73cd117 100644 --- a/libdino/src/service/connection_manager.vala +++ b/libdino/src/service/connection_manager.vala @@ -162,7 +162,7 @@ public class ConnectionManager { stream.attached_modules.connect((stream) => { change_connection_state(account, ConnectionState.CONNECTED); }); - stream.get_module(PlainSasl.Module.IDENTITY).received_auth_failure.connect((stream, node) => { + stream.get_module(Sasl.Module.IDENTITY).received_auth_failure.connect((stream, node) => { set_connection_error(account, new ConnectionError(ConnectionError.Source.SASL, null)); change_connection_state(account, ConnectionState.DISCONNECTED); }); diff --git a/libdino/src/service/module_manager.vala b/libdino/src/service/module_manager.vala index dac5aef2..d16dc935 100644 --- a/libdino/src/service/module_manager.vala +++ b/libdino/src/service/module_manager.vala @@ -41,8 +41,8 @@ public class ModuleManager { foreach (XmppStreamModule module in module_map[account]) { if (module.get_id() == Bind.Module.IDENTITY.id) { (module as Bind.Module).requested_resource = resource ?? account.resourcepart; - } else if (module.get_id() == PlainSasl.Module.IDENTITY.id) { - (module as PlainSasl.Module).password = account.password; + } else if (module.get_id() == Sasl.Module.IDENTITY.id) { + (module as Sasl.Module).password = account.password; } } return modules; @@ -54,7 +54,7 @@ public class ModuleManager { module_map[account].add(new Iq.Module()); module_map[account].add(new Tls.Module()); module_map[account].add(new Xep.SrvRecordsTls.Module()); - module_map[account].add(new PlainSasl.Module(account.bare_jid.to_string(), account.password)); + module_map[account].add(new Sasl.Module(account.bare_jid.to_string(), account.password)); module_map[account].add(new Xep.StreamManagement.Module()); module_map[account].add(new Bind.Module(account.resourcepart)); module_map[account].add(new Session.Module()); 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 IDENTITY = new FlagIdentity(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 IDENTITY = new ModuleIdentity(NS_URI, "plain_module"); - private const string MECHANISM = "PLAIN"; + public static ModuleIdentity IDENTITY = new ModuleIdentity(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; - } + 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; } - 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; - } - } - 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 IDENTITY = new FlagIdentity(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; } - } }