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 crate::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_all(
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 })
216 )
217 .build()
218 }
219}
220
221#[cfg(test)]
222mod tests {
223 use super::*;
224 use crate::Element;
225 use std::convert::TryFrom;
226
227 #[cfg(target_pointer_width = "32")]
228 #[test]
229 fn test_size() {
230 assert_size!(Mechanism, 1);
231 assert_size!(Auth, 16);
232 assert_size!(Challenge, 12);
233 assert_size!(Response, 12);
234 assert_size!(Abort, 0);
235 assert_size!(Success, 12);
236 assert_size!(DefinedCondition, 1);
237 assert_size!(Failure, 16);
238 }
239
240 #[cfg(target_pointer_width = "64")]
241 #[test]
242 fn test_size() {
243 assert_size!(Mechanism, 1);
244 assert_size!(Auth, 32);
245 assert_size!(Challenge, 24);
246 assert_size!(Response, 24);
247 assert_size!(Abort, 0);
248 assert_size!(Success, 24);
249 assert_size!(DefinedCondition, 1);
250 assert_size!(Failure, 32);
251 }
252
253 #[test]
254 fn test_simple() {
255 let elem: Element = "<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='PLAIN'/>"
256 .parse()
257 .unwrap();
258 let auth = Auth::try_from(elem).unwrap();
259 assert_eq!(auth.mechanism, Mechanism::Plain);
260 assert!(auth.data.is_empty());
261 }
262
263 #[test]
264 fn section_6_5_1() {
265 let elem: Element =
266 "<failure xmlns='urn:ietf:params:xml:ns:xmpp-sasl'><aborted/></failure>"
267 .parse()
268 .unwrap();
269 let failure = Failure::try_from(elem).unwrap();
270 assert_eq!(failure.defined_condition, DefinedCondition::Aborted);
271 assert!(failure.texts.is_empty());
272 }
273
274 #[test]
275 fn section_6_5_2() {
276 let elem: Element = "<failure xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>
277 <account-disabled/>
278 <text xml:lang='en'>Call 212-555-1212 for assistance.</text>
279 </failure>"
280 .parse()
281 .unwrap();
282 let failure = Failure::try_from(elem).unwrap();
283 assert_eq!(failure.defined_condition, DefinedCondition::AccountDisabled);
284 assert_eq!(
285 failure.texts["en"],
286 String::from("Call 212-555-1212 for assistance.")
287 );
288 }
289
290 /// Some servers apparently use a non-namespaced 'lang' attribute, which is invalid as not part
291 /// of the schema. This tests whether we can parse it when disabling validation.
292 #[cfg(feature = "disable-validation")]
293 #[test]
294 fn invalid_failure_with_non_prefixed_text_lang() {
295 let elem: Element = "<failure xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>
296 <not-authorized xmlns='urn:ietf:params:xml:ns:xmpp-sasl'/>
297 <text xmlns='urn:ietf:params:xml:ns:xmpp-sasl' lang='en'>Invalid username or password</text>
298 </failure>"
299 .parse()
300 .unwrap();
301 let failure = Failure::try_from(elem).unwrap();
302 assert_eq!(failure.defined_condition, DefinedCondition::NotAuthorized);
303 assert_eq!(
304 failure.texts[""],
305 String::from("Invalid username or password")
306 );
307 }
308}