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