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