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}