xmpp_parsers/
sasl.rs

1// Copyright (c) 2018 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
2//
3// This Source Code Form is subject to the terms of the Mozilla Public
4// License, v. 2.0. If a copy of the MPL was not distributed with this
5// file, You can obtain one at http://mozilla.org/MPL/2.0/.
6
7use xso::{
8    error::{Error, FromElementError},
9    text::Base64,
10    AsXml, FromXml,
11};
12
13use crate::ns;
14use minidom::Element;
15use std::collections::BTreeMap;
16
17generate_attribute!(
18    /// The list of available SASL mechanisms.
19    Mechanism, "mechanism", {
20        /// Uses no hashing mechanism and transmit the password in clear to the
21        /// server, using a single step.
22        Plain => "PLAIN",
23
24        /// Challenge-based mechanism using HMAC and SHA-1, allows both the
25        /// client and the server to avoid having to store the password in
26        /// clear.
27        ///
28        /// See <https://www.rfc-editor.org/rfc/rfc5802>
29        ScramSha1 => "SCRAM-SHA-1",
30
31        /// Same as [ScramSha1](#structfield.ScramSha1), with the addition of
32        /// channel binding.
33        ScramSha1Plus => "SCRAM-SHA-1-PLUS",
34
35        /// Same as [ScramSha1](#structfield.ScramSha1), but using SHA-256
36        /// instead of SHA-1 as the hash function.
37        ScramSha256 => "SCRAM-SHA-256",
38
39        /// Same as [ScramSha256](#structfield.ScramSha256), with the addition
40        /// of channel binding.
41        ScramSha256Plus => "SCRAM-SHA-256-PLUS",
42
43        /// Creates a temporary JID on login, which will be destroyed on
44        /// disconnect.
45        Anonymous => "ANONYMOUS",
46    }
47);
48
49/// The first step of the SASL process, selecting the mechanism and sending
50/// the first part of the handshake.
51#[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
52#[xml(namespace = ns::SASL, name = "auth")]
53pub struct Auth {
54    /// The mechanism used.
55    #[xml(attribute)]
56    pub mechanism: Mechanism,
57
58    /// The content of the handshake.
59    #[xml(text = Base64)]
60    pub data: Vec<u8>,
61}
62
63/// In case the mechanism selected at the [auth](struct.Auth.html) step
64/// requires a second step, the server sends this element with additional
65/// data.
66#[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
67#[xml(namespace = ns::SASL, name = "challenge")]
68pub struct Challenge {
69    /// The challenge data.
70    #[xml(text = Base64)]
71    pub data: Vec<u8>,
72}
73
74/// In case the mechanism selected at the [auth](struct.Auth.html) step
75/// requires a second step, this contains the client’s response to the
76/// server’s [challenge](struct.Challenge.html).
77#[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
78#[xml(namespace = ns::SASL, name = "response")]
79pub struct Response {
80    /// The response data.
81    #[xml(text = Base64)]
82    pub data: Vec<u8>,
83}
84
85/// Sent by the client at any point after [auth](struct.Auth.html) if it
86/// wants to cancel the current authentication process.
87#[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
88#[xml(namespace = ns::SASL, name = "abort")]
89pub struct Abort;
90
91/// Sent by the server on SASL success.
92#[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
93#[xml(namespace = ns::SASL, name = "success")]
94pub struct Success {
95    /// Possible data sent on success.
96    #[xml(text = Base64)]
97    pub data: Vec<u8>,
98}
99
100generate_element_enum!(
101    /// List of possible failure conditions for SASL.
102    DefinedCondition, "defined-condition", SASL, {
103        /// The client aborted the authentication with
104        /// [abort](struct.Abort.html).
105        Aborted => "aborted",
106
107        /// The account the client is trying to authenticate against has been
108        /// disabled.
109        AccountDisabled => "account-disabled",
110
111        /// The credentials for this account have expired.
112        CredentialsExpired => "credentials-expired",
113
114        /// You must enable StartTLS or use direct TLS before using this
115        /// authentication mechanism.
116        EncryptionRequired => "encryption-required",
117
118        /// The base64 data sent by the client is invalid.
119        IncorrectEncoding => "incorrect-encoding",
120
121        /// The authzid provided by the client is invalid.
122        InvalidAuthzid => "invalid-authzid",
123
124        /// The client tried to use an invalid mechanism, or none.
125        InvalidMechanism => "invalid-mechanism",
126
127        /// The client sent a bad request.
128        MalformedRequest => "malformed-request",
129
130        /// The mechanism selected is weaker than what the server allows.
131        MechanismTooWeak => "mechanism-too-weak",
132
133        /// The credentials provided are invalid.
134        NotAuthorized => "not-authorized",
135
136        /// The server encountered an issue which may be fixed later, the
137        /// client should retry at some point.
138        TemporaryAuthFailure => "temporary-auth-failure",
139    }
140);
141
142type Lang = String;
143
144/// Sent by the server on SASL failure.
145#[derive(Debug, Clone)]
146pub struct Failure {
147    /// One of the allowed defined-conditions for SASL.
148    pub defined_condition: DefinedCondition,
149
150    /// A human-readable explanation for the failure.
151    pub texts: BTreeMap<Lang, String>,
152}
153
154impl TryFrom<Element> for Failure {
155    type Error = FromElementError;
156
157    fn try_from(root: Element) -> Result<Failure, FromElementError> {
158        check_self!(root, "failure", SASL);
159        check_no_attributes!(root, "failure");
160
161        let mut defined_condition = None;
162        let mut texts = BTreeMap::new();
163
164        for child in root.children() {
165            if child.is("text", ns::SASL) {
166                check_no_unknown_attributes!(child, "text", ["xml:lang"]);
167                check_no_children!(child, "text");
168                let lang = get_attr!(child, "xml:lang", Default);
169                if texts.insert(lang, child.text()).is_some() {
170                    return Err(Error::Other(
171                        "Text element present twice for the same xml:lang in failure element.",
172                    )
173                    .into());
174                }
175            } else if child.has_ns(ns::SASL) {
176                if defined_condition.is_some() {
177                    return Err(Error::Other(
178                        "Failure must not have more than one defined-condition.",
179                    )
180                    .into());
181                }
182                check_no_attributes!(child, "defined-condition");
183                check_no_children!(child, "defined-condition");
184                let condition = match DefinedCondition::try_from(child.clone()) {
185                    Ok(condition) => condition,
186                    // TODO: do we really want to eat this error?
187                    Err(_) => DefinedCondition::NotAuthorized,
188                };
189                defined_condition = Some(condition);
190            } else {
191                return Err(Error::Other("Unknown element in Failure.").into());
192            }
193        }
194        let defined_condition =
195            defined_condition.ok_or(Error::Other("Failure must have a defined-condition."))?;
196
197        Ok(Failure {
198            defined_condition,
199            texts,
200        })
201    }
202}
203
204impl From<Failure> for Element {
205    fn from(failure: Failure) -> Element {
206        Element::builder("failure", ns::SASL)
207            .append(failure.defined_condition)
208            .append_all(failure.texts.into_iter().map(|(lang, text)| {
209                Element::builder("text", ns::SASL)
210                    .attr("xml:lang", lang)
211                    .append(text)
212            }))
213            .build()
214    }
215}
216
217#[cfg(test)]
218mod tests {
219    use super::*;
220
221    #[cfg(target_pointer_width = "32")]
222    #[test]
223    fn test_size() {
224        assert_size!(Mechanism, 1);
225        assert_size!(Auth, 16);
226        assert_size!(Challenge, 12);
227        assert_size!(Response, 12);
228        assert_size!(Abort, 0);
229        assert_size!(Success, 12);
230        assert_size!(DefinedCondition, 1);
231        assert_size!(Failure, 16);
232    }
233
234    #[cfg(target_pointer_width = "64")]
235    #[test]
236    fn test_size() {
237        assert_size!(Mechanism, 1);
238        assert_size!(Auth, 32);
239        assert_size!(Challenge, 24);
240        assert_size!(Response, 24);
241        assert_size!(Abort, 0);
242        assert_size!(Success, 24);
243        assert_size!(DefinedCondition, 1);
244        assert_size!(Failure, 32);
245    }
246
247    #[test]
248    fn test_simple() {
249        let elem: Element = "<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='PLAIN'/>"
250            .parse()
251            .unwrap();
252        let auth = Auth::try_from(elem).unwrap();
253        assert_eq!(auth.mechanism, Mechanism::Plain);
254        assert!(auth.data.is_empty());
255    }
256
257    #[test]
258    fn section_6_5_1() {
259        let elem: Element =
260            "<failure xmlns='urn:ietf:params:xml:ns:xmpp-sasl'><aborted/></failure>"
261                .parse()
262                .unwrap();
263        let failure = Failure::try_from(elem).unwrap();
264        assert_eq!(failure.defined_condition, DefinedCondition::Aborted);
265        assert!(failure.texts.is_empty());
266    }
267
268    #[test]
269    fn section_6_5_2() {
270        let elem: Element = "<failure xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>
271            <account-disabled/>
272            <text xml:lang='en'>Call 212-555-1212 for assistance.</text>
273        </failure>"
274            .parse()
275            .unwrap();
276        let failure = Failure::try_from(elem).unwrap();
277        assert_eq!(failure.defined_condition, DefinedCondition::AccountDisabled);
278        assert_eq!(
279            failure.texts["en"],
280            String::from("Call 212-555-1212 for assistance.")
281        );
282    }
283
284    /// Some servers apparently use a non-namespaced 'lang' attribute, which is invalid as not part
285    /// of the schema.  This tests whether we can parse it when disabling validation.
286    #[cfg(feature = "disable-validation")]
287    #[test]
288    fn invalid_failure_with_non_prefixed_text_lang() {
289        let elem: Element = "<failure xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>
290            <not-authorized xmlns='urn:ietf:params:xml:ns:xmpp-sasl'/>
291            <text xmlns='urn:ietf:params:xml:ns:xmpp-sasl' lang='en'>Invalid username or password</text>
292        </failure>"
293            .parse()
294            .unwrap();
295        let failure = Failure::try_from(elem).unwrap();
296        assert_eq!(failure.defined_condition, DefinedCondition::NotAuthorized);
297        assert_eq!(
298            failure.texts[""],
299            String::from("Invalid username or password")
300        );
301    }
302}