aboutsummaryrefslogtreecommitdiff
path: root/xmpp-vala/src
diff options
context:
space:
mode:
Diffstat (limited to 'xmpp-vala/src')
-rw-r--r--xmpp-vala/src/module/sasl.vala203
-rw-r--r--xmpp-vala/src/module/stanza_error.vala6
-rw-r--r--xmpp-vala/src/module/tls.vala15
-rw-r--r--xmpp-vala/src/module/xep/0004_data_forms.vala5
-rw-r--r--xmpp-vala/src/module/xep/0045_muc/module.vala13
-rw-r--r--xmpp-vala/src/module/xep/0077_in_band_registration.vala64
-rw-r--r--xmpp-vala/src/module/xep/0368_srv_records_tls.vala5
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;
}