aboutsummaryrefslogtreecommitdiff
path: root/xmpp-vala/src/module/xep/0388_extensible_sasl_profile.vala
blob: 54d00482144308c9433b6599d07a09d5c9f34d9c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
using Gee;
using Xmpp.Sasl;

namespace Xmpp.Xep.ExtensibleSaslProfile {
    public const string NS_URI = "urn:xmpp:sasl:2";

    public class Module : XmppStreamNegotiationModule {
        public static ModuleIdentity<Module> IDENTITY = new ModuleIdentity<Module>(NS_URI, "sasl2");

        public HashMap<string, Sasl2InlineActivation> inline_activation_providers = new HashMap<string, Sasl2InlineActivation>();

        public string name { get; set; }
        public string password { get; set; }
        public string user_agent_id { get; set; }
        public string? user_agent_software { get; set; default = "Dino"; }
        public string? user_agent_device { get; set; }

        public signal void received_auth_failure(XmppStream stream, StanzaNode node);

        public Module(string name, string password, string user_agent_id) {
            this.name = name;
            this.password = password;
            this.user_agent_id = user_agent_id;
        }

        public override void attach(XmppStream stream) {
            stream.received_features_node.connect(this.received_features_node);
            stream.received_nonza.connect(this.received_nonza);
        }

        public override void detach(XmppStream stream) {
            stream.received_features_node.disconnect(this.received_features_node);
            stream.received_nonza.disconnect(this.received_nonza);
        }

        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);
                    flag.password = null; // Remove password from memory
                    flag.finished = true;

                    stream.expect_further_negotiation_stanzas = true;

                    var authorisation_identifier = node.get_subnode("authorization-identifier", NS_URI);
                    string jid_string = authorisation_identifier != null ? authorisation_identifier.get_string_content() : "";
                    try {
                        Jid jid = Jid.from_string(jid_string);

                        foreach (var inline_provider in inline_activation_providers.values) {
                            inline_provider.on_bound(stream, jid, node);
                        }
                    } catch (InvalidJidError e) {
                        warning("Server provided invalid authorization-identifier \"%s\": %s", jid_string, e.message);
                    }
                } else if (node.name == "challenge" && stream.has_flag(Flag.IDENTITY)) {
                    Flag flag = stream.get_flag(Flag.IDENTITY);
                    if (flag.mechanism == Mechanism.SCRAM_SHA_1) {
                        try {
                            var challenge_response = compute_challenge_response(node.get_string_content(), flag.password, flag.client_nonce, flag.name);
                            flag.server_signature = challenge_response.expected_server_signature;
                            stream.write(new StanzaNode.build("response", NS_URI).add_self_xmlns()
                                    .put_node(new StanzaNode.text(Base64.encode((uchar[]) (challenge_response.response).to_utf8()))));
                        } catch (Error e) {
                            warning("Error computing challenge response: %s", e.message);
                        }
                    }
                } else if (node.name == "failure") {
                    stream.remove_flag(stream.get_flag(Flag.IDENTITY));
                    received_auth_failure(stream, node);
                }
            }
        }

        public void received_features_node(XmppStream stream) {
            if (stream.has_flag(Flag.IDENTITY)) return;
            if (stream.is_setup_needed()) return;

            var authentication_node = stream.features.get_subnode("authentication", NS_URI);
            if (authentication_node == null) return;

            var mechanism_nodes = authentication_node.get_subnodes("mechanism", NS_URI);
            string[] supported_mechanisms = {};
            foreach (var mechanism in mechanism_nodes) {
                supported_mechanisms += mechanism.get_string_content();
            }

            var authenticate_node = new StanzaNode.build("authenticate", NS_URI).add_self_xmlns();

            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";
                string response = Base64.encode((uchar[]) ("n,,"+initial_message).to_utf8());
                authenticate_node
                        .put_attribute("mechanism", Mechanism.SCRAM_SHA_1)
                        .put_node(new StanzaNode.build("initial-response", NS_URI).put_node(new StanzaNode.text(response)));
                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) {
                authenticate_node
                        .put_attribute("mechanism", Mechanism.PLAIN)
                        .put_node(new StanzaNode.build("initial-response", NS_URI)
                            .put_node(new StanzaNode.text(Base64.encode(get_plain_bytes(name, password)))));
                var flag = new Flag();
                flag.mechanism = Mechanism.PLAIN;
                flag.name = name;
                stream.add_flag(flag);
            }

            StanzaNode? inline_node = authentication_node.get_subnode("inline", NS_URI);
            if (inline_node != null) {
                StanzaNode[] inline_activation_nodes = {};
                foreach (StanzaNode inline_feature_node in inline_node.sub_nodes) {
                    if (inline_activation_providers.has_key(inline_feature_node.ns_uri)) {
                        var activation_node = inline_activation_providers[inline_feature_node.ns_uri].get_activation_node(stream, inline_node);
                        if (activation_node != null) {
                            inline_activation_nodes += activation_node;
                        }
                    }
                }

                foreach (var inline_activation_node in inline_activation_nodes) {
                    authenticate_node.put_node(inline_activation_node);
                }
            }

            StanzaNode user_agent_node = new StanzaNode.build("user-agent", NS_URI).put_attribute("id", user_agent_id);
            if (user_agent_software != null) user_agent_node.put_node(new StanzaNode.build("software", NS_URI).put_node(new StanzaNode.text(user_agent_software)));
            if (user_agent_device != null) user_agent_node.put_node(new StanzaNode.build("device", NS_URI).put_node(new StanzaNode.text(user_agent_device)));
            authenticate_node.put_node(user_agent_node);

            stream.write(authenticate_node);
        }

        public override bool mandatory_outstanding(XmppStream stream) {
            return !stream.has_flag(Flag.IDENTITY) || !stream.get_flag(Flag.IDENTITY).finished;
        }

        public override bool negotiation_active(XmppStream stream) {
            return stream.has_flag(Flag.IDENTITY) && !stream.get_flag(Flag.IDENTITY).finished;
        }

        public override string get_ns() { return NS_URI; }
        public override string get_id() { return IDENTITY.id; }
    }

    public abstract class Sasl2InlineActivation : Object {
        public abstract StanzaNode? get_activation_node(XmppStream stream, StanzaNode inline_node);
        public abstract void on_bound(XmppStream stream, Jid authorization_identifier, StanzaNode success_node);
    }
}