2018-02-20 16:01:12 +00:00
|
|
|
|
// Copyright (c) 2018 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
|
|
|
//
|
|
|
|
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
|
|
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
|
|
|
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
|
|
|
|
2018-12-18 14:27:30 +00:00
|
|
|
|
use crate::error::Error;
|
2018-12-18 15:07:46 +00:00
|
|
|
|
use crate::helpers::Base64;
|
2018-12-18 14:27:30 +00:00
|
|
|
|
use crate::ns;
|
2018-12-18 14:32:05 +00:00
|
|
|
|
use minidom::Element;
|
2018-12-18 15:07:46 +00:00
|
|
|
|
use std::collections::BTreeMap;
|
2018-12-18 14:32:05 +00:00
|
|
|
|
use try_from::TryFrom;
|
2018-08-02 22:53:00 +00:00
|
|
|
|
|
|
|
|
|
generate_attribute!(
|
|
|
|
|
/// The list of available SASL mechanisms.
|
|
|
|
|
Mechanism, "mechanism", {
|
|
|
|
|
/// Uses no hashing mechanism and transmit the password in clear to the
|
|
|
|
|
/// server, using a single step.
|
|
|
|
|
Plain => "PLAIN",
|
|
|
|
|
|
|
|
|
|
/// Challenge-based mechanism using HMAC and SHA-1, allows both the
|
|
|
|
|
/// client and the server to avoid having to store the password in
|
|
|
|
|
/// clear.
|
|
|
|
|
///
|
|
|
|
|
/// See https://tools.ietf.org/html/rfc5802
|
|
|
|
|
ScramSha1 => "SCRAM-SHA-1",
|
|
|
|
|
|
|
|
|
|
/// Same as [ScramSha1](#structfield.ScramSha1), with the addition of
|
|
|
|
|
/// channel binding.
|
|
|
|
|
ScramSha1Plus => "SCRAM-SHA-1-PLUS",
|
|
|
|
|
|
|
|
|
|
/// Same as [ScramSha1](#structfield.ScramSha1), but using SHA-256
|
|
|
|
|
/// instead of SHA-1 as the hash function.
|
|
|
|
|
ScramSha256 => "SCRAM-SHA-256",
|
|
|
|
|
|
|
|
|
|
/// Same as [ScramSha256](#structfield.ScramSha256), with the addition
|
|
|
|
|
/// of channel binding.
|
|
|
|
|
ScramSha256Plus => "SCRAM-SHA-256-PLUS",
|
|
|
|
|
|
|
|
|
|
/// Creates a temporary JID on login, which will be destroyed on
|
|
|
|
|
/// disconnect.
|
|
|
|
|
Anonymous => "ANONYMOUS",
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
generate_element!(
|
|
|
|
|
/// The first step of the SASL process, selecting the mechanism and sending
|
|
|
|
|
/// the first part of the handshake.
|
|
|
|
|
Auth, "auth", SASL,
|
2018-08-02 17:07:07 +00:00
|
|
|
|
attributes: [
|
2018-08-02 22:53:00 +00:00
|
|
|
|
/// The mechanism used.
|
2018-02-20 16:01:12 +00:00
|
|
|
|
mechanism: Mechanism = "mechanism" => required
|
|
|
|
|
],
|
2018-08-02 17:07:07 +00:00
|
|
|
|
text: (
|
2018-08-02 22:53:00 +00:00
|
|
|
|
/// The content of the handshake.
|
2018-08-02 17:07:07 +00:00
|
|
|
|
data: Base64<Vec<u8>>
|
|
|
|
|
)
|
2018-02-20 16:01:12 +00:00
|
|
|
|
);
|
|
|
|
|
|
2018-08-02 22:53:00 +00:00
|
|
|
|
generate_element!(
|
|
|
|
|
/// In case the mechanism selected at the [auth](struct.Auth.html) step
|
|
|
|
|
/// requires a second step, the server sends this element with additional
|
|
|
|
|
/// data.
|
|
|
|
|
Challenge, "challenge", SASL,
|
2018-08-02 17:07:07 +00:00
|
|
|
|
text: (
|
2018-08-02 22:53:00 +00:00
|
|
|
|
/// The challenge data.
|
2018-08-02 17:07:07 +00:00
|
|
|
|
data: Base64<Vec<u8>>
|
|
|
|
|
)
|
2018-02-20 16:01:12 +00:00
|
|
|
|
);
|
|
|
|
|
|
2018-08-02 22:53:00 +00:00
|
|
|
|
generate_element!(
|
|
|
|
|
/// In case the mechanism selected at the [auth](struct.Auth.html) step
|
|
|
|
|
/// requires a second step, this contains the client’s response to the
|
|
|
|
|
/// server’s [challenge](struct.Challenge.html).
|
|
|
|
|
Response, "response", SASL,
|
2018-08-02 17:07:07 +00:00
|
|
|
|
text: (
|
2018-08-02 22:53:00 +00:00
|
|
|
|
/// The response data.
|
2018-08-02 17:07:07 +00:00
|
|
|
|
data: Base64<Vec<u8>>
|
|
|
|
|
)
|
2018-02-20 16:01:12 +00:00
|
|
|
|
);
|
|
|
|
|
|
2018-08-02 22:53:00 +00:00
|
|
|
|
generate_empty_element!(
|
|
|
|
|
/// Sent by the client at any point after [auth](struct.Auth.html) if it
|
|
|
|
|
/// wants to cancel the current authentication process.
|
2018-12-18 14:32:05 +00:00
|
|
|
|
Abort,
|
|
|
|
|
"abort",
|
|
|
|
|
SASL
|
2018-08-02 22:53:00 +00:00
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
generate_element!(
|
|
|
|
|
/// Sent by the server on SASL success.
|
|
|
|
|
Success, "success", SASL,
|
2018-08-02 17:07:07 +00:00
|
|
|
|
text: (
|
2018-08-02 22:53:00 +00:00
|
|
|
|
/// Possible data sent on success.
|
2018-08-02 17:07:07 +00:00
|
|
|
|
data: Base64<Vec<u8>>
|
|
|
|
|
)
|
2018-02-20 16:01:12 +00:00
|
|
|
|
);
|
|
|
|
|
|
2018-08-02 22:53:00 +00:00
|
|
|
|
generate_element_enum!(
|
|
|
|
|
/// List of possible failure conditions for SASL.
|
|
|
|
|
DefinedCondition, "defined-condition", SASL, {
|
|
|
|
|
/// The client aborted the authentication with
|
|
|
|
|
/// [abort](struct.Abort.html).
|
|
|
|
|
Aborted => "aborted",
|
|
|
|
|
|
|
|
|
|
/// The account the client is trying to authenticate against has been
|
|
|
|
|
/// disabled.
|
|
|
|
|
AccountDisabled => "account-disabled",
|
|
|
|
|
|
|
|
|
|
/// The credentials for this account have expired.
|
|
|
|
|
CredentialsExpired => "credentials-expired",
|
|
|
|
|
|
|
|
|
|
/// You must enable StartTLS or use direct TLS before using this
|
|
|
|
|
/// authentication mechanism.
|
|
|
|
|
EncryptionRequired => "encryption-required",
|
|
|
|
|
|
|
|
|
|
/// The base64 data sent by the client is invalid.
|
|
|
|
|
IncorrectEncoding => "incorrect-encoding",
|
|
|
|
|
|
|
|
|
|
/// The authzid provided by the client is invalid.
|
|
|
|
|
InvalidAuthzid => "invalid-authzid",
|
|
|
|
|
|
|
|
|
|
/// The client tried to use an invalid mechanism, or none.
|
|
|
|
|
InvalidMechanism => "invalid-mechanism",
|
|
|
|
|
|
|
|
|
|
/// The client sent a bad request.
|
|
|
|
|
MalformedRequest => "malformed-request",
|
|
|
|
|
|
|
|
|
|
/// The mechanism selected is weaker than what the server allows.
|
|
|
|
|
MechanismTooWeak => "mechanism-too-weak",
|
|
|
|
|
|
|
|
|
|
/// The credentials provided are invalid.
|
|
|
|
|
NotAuthorized => "not-authorized",
|
|
|
|
|
|
|
|
|
|
/// The server encountered an issue which may be fixed later, the
|
|
|
|
|
/// client should retry at some point.
|
|
|
|
|
TemporaryAuthFailure => "temporary-auth-failure",
|
|
|
|
|
}
|
2018-08-02 16:16:14 +00:00
|
|
|
|
);
|
|
|
|
|
|
2018-08-02 22:53:00 +00:00
|
|
|
|
type Lang = String;
|
|
|
|
|
|
|
|
|
|
/// Sent by the server on SASL failure.
|
|
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
|
pub struct Failure {
|
|
|
|
|
/// One of the allowed defined-conditions for SASL.
|
|
|
|
|
pub defined_condition: DefinedCondition,
|
|
|
|
|
|
|
|
|
|
/// A human-readable explanation for the failure.
|
|
|
|
|
pub texts: BTreeMap<Lang, String>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl TryFrom<Element> for Failure {
|
|
|
|
|
type Err = Error;
|
|
|
|
|
|
|
|
|
|
fn try_from(root: Element) -> Result<Failure, Error> {
|
|
|
|
|
check_self!(root, "failure", SASL);
|
|
|
|
|
check_no_attributes!(root, "failure");
|
|
|
|
|
|
|
|
|
|
let mut defined_condition = None;
|
|
|
|
|
let mut texts = BTreeMap::new();
|
|
|
|
|
|
|
|
|
|
for child in root.children() {
|
|
|
|
|
if child.is("text", ns::SASL) {
|
|
|
|
|
check_no_unknown_attributes!(child, "text", ["xml:lang"]);
|
|
|
|
|
check_no_children!(child, "text");
|
|
|
|
|
let lang = get_attr!(child, "xml:lang", default);
|
|
|
|
|
if texts.insert(lang, child.text()).is_some() {
|
2018-12-18 14:32:05 +00:00
|
|
|
|
return Err(Error::ParseError(
|
|
|
|
|
"Text element present twice for the same xml:lang in failure element.",
|
|
|
|
|
));
|
2018-08-02 22:53:00 +00:00
|
|
|
|
}
|
|
|
|
|
} else if child.has_ns(ns::SASL) {
|
|
|
|
|
if defined_condition.is_some() {
|
2018-12-18 14:32:05 +00:00
|
|
|
|
return Err(Error::ParseError(
|
|
|
|
|
"Failure must not have more than one defined-condition.",
|
|
|
|
|
));
|
2018-08-02 22:53:00 +00:00
|
|
|
|
}
|
|
|
|
|
check_no_attributes!(child, "defined-condition");
|
|
|
|
|
check_no_children!(child, "defined-condition");
|
|
|
|
|
let condition = match DefinedCondition::try_from(child.clone()) {
|
|
|
|
|
Ok(condition) => condition,
|
|
|
|
|
// TODO: do we really want to eat this error?
|
|
|
|
|
Err(_) => DefinedCondition::NotAuthorized,
|
|
|
|
|
};
|
|
|
|
|
defined_condition = Some(condition);
|
|
|
|
|
} else {
|
|
|
|
|
return Err(Error::ParseError("Unknown element in Failure."));
|
|
|
|
|
}
|
|
|
|
|
}
|
2018-12-18 14:32:05 +00:00
|
|
|
|
let defined_condition =
|
|
|
|
|
defined_condition.ok_or(Error::ParseError("Failure must have a defined-condition."))?;
|
2018-08-02 22:53:00 +00:00
|
|
|
|
|
|
|
|
|
Ok(Failure {
|
|
|
|
|
defined_condition: defined_condition,
|
|
|
|
|
texts: texts,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl From<Failure> for Element {
|
|
|
|
|
fn from(failure: Failure) -> Element {
|
|
|
|
|
Element::builder("failure")
|
2018-12-18 14:32:05 +00:00
|
|
|
|
.ns(ns::SASL)
|
|
|
|
|
.append(failure.defined_condition)
|
|
|
|
|
.append(
|
|
|
|
|
failure
|
|
|
|
|
.texts
|
|
|
|
|
.into_iter()
|
|
|
|
|
.map(|(lang, text)| {
|
|
|
|
|
Element::builder("text")
|
|
|
|
|
.ns(ns::SASL)
|
|
|
|
|
.attr("xml:lang", lang)
|
|
|
|
|
.append(text)
|
|
|
|
|
.build()
|
|
|
|
|
})
|
|
|
|
|
.collect::<Vec<_>>(),
|
|
|
|
|
)
|
|
|
|
|
.build()
|
2018-08-02 22:53:00 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-02-20 16:01:12 +00:00
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::*;
|
2018-05-14 14:17:21 +00:00
|
|
|
|
use minidom::Element;
|
2018-12-18 14:32:05 +00:00
|
|
|
|
use try_from::TryFrom;
|
2018-02-20 16:01:12 +00:00
|
|
|
|
|
2018-10-28 12:10:48 +00:00
|
|
|
|
#[cfg(target_pointer_width = "32")]
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_size() {
|
|
|
|
|
assert_size!(Mechanism, 1);
|
|
|
|
|
assert_size!(Auth, 16);
|
|
|
|
|
assert_size!(Challenge, 12);
|
|
|
|
|
assert_size!(Response, 12);
|
|
|
|
|
assert_size!(Abort, 0);
|
|
|
|
|
assert_size!(Success, 12);
|
|
|
|
|
assert_size!(DefinedCondition, 1);
|
|
|
|
|
assert_size!(Failure, 16);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(target_pointer_width = "64")]
|
2018-10-26 12:26:16 +00:00
|
|
|
|
#[test]
|
|
|
|
|
fn test_size() {
|
|
|
|
|
assert_size!(Mechanism, 1);
|
|
|
|
|
assert_size!(Auth, 32);
|
|
|
|
|
assert_size!(Challenge, 24);
|
|
|
|
|
assert_size!(Response, 24);
|
|
|
|
|
assert_size!(Abort, 0);
|
|
|
|
|
assert_size!(Success, 24);
|
|
|
|
|
assert_size!(DefinedCondition, 1);
|
|
|
|
|
assert_size!(Failure, 32);
|
|
|
|
|
}
|
|
|
|
|
|
2018-02-20 16:01:12 +00:00
|
|
|
|
#[test]
|
|
|
|
|
fn test_simple() {
|
2018-12-18 14:32:05 +00:00
|
|
|
|
let elem: Element = "<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='PLAIN'/>"
|
|
|
|
|
.parse()
|
|
|
|
|
.unwrap();
|
2018-02-20 16:01:12 +00:00
|
|
|
|
let auth = Auth::try_from(elem).unwrap();
|
|
|
|
|
assert_eq!(auth.mechanism, Mechanism::Plain);
|
|
|
|
|
assert!(auth.data.is_empty());
|
|
|
|
|
}
|
2018-08-02 22:53:00 +00:00
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn section_6_5_1() {
|
2018-12-18 14:32:05 +00:00
|
|
|
|
let elem: Element =
|
|
|
|
|
"<failure xmlns='urn:ietf:params:xml:ns:xmpp-sasl'><aborted/></failure>"
|
|
|
|
|
.parse()
|
|
|
|
|
.unwrap();
|
2018-08-02 22:53:00 +00:00
|
|
|
|
let failure = Failure::try_from(elem).unwrap();
|
|
|
|
|
assert_eq!(failure.defined_condition, DefinedCondition::Aborted);
|
|
|
|
|
assert!(failure.texts.is_empty());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn section_6_5_2() {
|
|
|
|
|
let elem: Element = "<failure xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>
|
|
|
|
|
<account-disabled/>
|
|
|
|
|
<text xml:lang='en'>Call 212-555-1212 for assistance.</text>
|
2018-12-18 14:32:05 +00:00
|
|
|
|
</failure>"
|
|
|
|
|
.parse()
|
|
|
|
|
.unwrap();
|
2018-08-02 22:53:00 +00:00
|
|
|
|
let failure = Failure::try_from(elem).unwrap();
|
|
|
|
|
assert_eq!(failure.defined_condition, DefinedCondition::AccountDisabled);
|
2018-12-18 14:32:05 +00:00
|
|
|
|
assert_eq!(
|
|
|
|
|
failure.texts["en"],
|
|
|
|
|
String::from("Call 212-555-1212 for assistance.")
|
|
|
|
|
);
|
2018-08-02 22:53:00 +00:00
|
|
|
|
}
|
2018-12-20 19:58:13 +00:00
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn failure_with_non_prefixed_text_lang() {
|
|
|
|
|
let elem: Element = "<failure xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>
|
|
|
|
|
<not-authorized xmlns='urn:ietf:params:xml:ns:xmpp-sasl'/>
|
|
|
|
|
<text xmlns='urn:ietf:params:xml:ns:xmpp-sasl' lang='en'>Invalid username or password</text>
|
|
|
|
|
</failure>"
|
|
|
|
|
.parse()
|
|
|
|
|
.unwrap();
|
|
|
|
|
let failure = Failure::try_from(elem).unwrap();
|
|
|
|
|
assert_eq!(failure.defined_condition, DefinedCondition::NotAuthorized);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
failure.texts["en"],
|
|
|
|
|
String::from("Invalid username or password")
|
|
|
|
|
);
|
|
|
|
|
}
|
2018-02-20 16:01:12 +00:00
|
|
|
|
}
|