diff --git a/parsers/Cargo.toml b/parsers/Cargo.toml index 56b65be0..15fcbb60 100644 --- a/parsers/Cargo.toml +++ b/parsers/Cargo.toml @@ -24,7 +24,8 @@ chrono = { version = "0.4.5", default-features = false, features = ["std"] } # same repository dependencies jid = { version = "0.11", features = ["minidom"] } minidom = { version = "0.16" } -xso = { version = "0.1", features = ["macros", "minidom", "panicking-into-impl", "jid", "base64"] } +xso = { version = "0.1", features = ["macros", "minidom", "panicking-into-impl", "jid", "uuid", "base64"] } +uuid = { version = "1.9.1", features = ["v4"] } [features] # Build xmpp-parsers to make components instead of clients. diff --git a/parsers/ChangeLog b/parsers/ChangeLog index 31d4c8a1..13be68cd 100644 --- a/parsers/ChangeLog +++ b/parsers/ChangeLog @@ -17,6 +17,7 @@ XXXX-YY-ZZ RELEASER (!416) * New parsers/serialisers: - Stream Features (RFC 6120) (!400) + - Extensible SASL Profile (XEP-0388) - SASL Channel-Binding Type Capability (XEP-0440) Version 0.21.0: diff --git a/parsers/doap.xml b/parsers/doap.xml index f2876ff3..df1bded8 100644 --- a/parsers/doap.xml +++ b/parsers/doap.xml @@ -602,6 +602,14 @@ 0.21.0 + + + + complete + 1.0.1 + NEXT + + diff --git a/parsers/src/lib.rs b/parsers/src/lib.rs index 02428ed0..a5b148ca 100644 --- a/parsers/src/lib.rs +++ b/parsers/src/lib.rs @@ -260,6 +260,9 @@ pub mod legacy_omemo; /// XEP-0386: Bind 2 pub mod bind2; +/// XEP-0388: Extensible SASL Profile +pub mod sasl2; + /// XEP-0390: Entity Capabilities 2.0 pub mod ecaps2; diff --git a/parsers/src/ns.rs b/parsers/src/ns.rs index 7191bf7f..e87e4f04 100644 --- a/parsers/src/ns.rs +++ b/parsers/src/ns.rs @@ -278,6 +278,9 @@ pub const LEGACY_OMEMO_BUNDLES: &str = "eu.siacs.conversations.axolotl.bundles"; /// XEP-0386: Bind 2 pub const BIND2: &str = "urn:xmpp:bind:0"; +/// XEP-0388: Extensible SASL Profile +pub const SASL2: &str = "urn:xmpp:sasl:2"; + /// XEP-0390: Entity Capabilities 2.0 pub const ECAPS2: &str = "urn:xmpp:caps"; /// XEP-0390: Entity Capabilities 2.0 diff --git a/parsers/src/sasl2.rs b/parsers/src/sasl2.rs new file mode 100644 index 00000000..b9eb6a27 --- /dev/null +++ b/parsers/src/sasl2.rs @@ -0,0 +1,795 @@ +// Copyright (c) 2024 Emmanuel Gil Peyrot +// +// 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/. + +use xso::{text::Base64, AsXml, FromXml}; + +use crate::ns; +use jid::Jid; +use minidom::Element; + +/// Server advertisement for supported auth mechanisms +#[derive(FromXml, AsXml, PartialEq, Debug, Clone)] +#[xml(namespace = ns::SASL2, name = "authentication")] +pub struct Authentication { + /// Plaintext names of supported auth mechanisms + #[xml(extract(n = .., name = "mechanism", fields(text(type_ = String))))] + pub mechanisms: Vec, + + /// Additional auth information provided by server + #[xml(extract(default, name = "inline", fields(element(n = ..))))] + pub payloads: Vec, +} + +/// Client aborts the connection. +#[derive(FromXml, AsXml, PartialEq, Debug, Clone)] +#[xml(namespace = ns::SASL2, name = "abort")] +pub struct Abort { + /// Plaintext reason for aborting + #[xml(extract(default, fields(text(type_ = String))))] + pub text: Option, + + /// Extra untyped payloads + #[xml(element(n = ..))] + pub payloads: Vec, +} + +/// Optional client software information +#[derive(FromXml, AsXml, PartialEq, Debug, Clone)] +#[xml(namespace = ns::SASL2, name = "user-agent")] +pub struct UserAgent { + /// Random, unique identifier for the client + #[xml(attribute)] + pub id: uuid::Uuid, + + /// Name of the client software + #[xml(extract(default, fields(text(type_ = String))))] + pub software: Option, + + /// Name of the client device (eg. phone/laptop) + #[xml(extract(default, fields(text(type_ = String))))] + pub device: Option, +} + +/// Client authentication request +#[derive(FromXml, AsXml, PartialEq, Debug, Clone)] +#[xml(namespace = ns::SASL2, name = "authenticate")] +pub struct Authenticate { + /// Chosen SASL mechanism + #[xml(attribute)] + pub mechanism: String, + + /// SASL response + #[xml(extract(default, name = "initial-response", fields(text = Base64)))] + pub initial_response: Option>, + + /// Information about client software + #[xml(child)] + pub user_agent: UserAgent, + + /// Extra untyped payloads + #[xml(element(n = ..))] + pub payloads: Vec, +} + +/// SASL challenge +#[derive(FromXml, AsXml, PartialEq, Debug, Clone)] +#[xml(namespace = ns::SASL2, name = "challenge")] +pub struct Challenge { + /// SASL challenge data + #[xml(text = Base64)] + pub sasl_data: Vec, +} + +/// SASL response +#[derive(FromXml, AsXml, PartialEq, Debug, Clone)] +#[xml(namespace = ns::SASL2, name = "response")] +pub struct Response { + /// SASL challenge data + #[xml(text = Base64)] + pub sasl_data: Vec, +} + +/// Authentication was successful +#[derive(FromXml, AsXml, PartialEq, Debug, Clone)] +#[xml(namespace = ns::SASL2, name = "success")] +pub struct Success { + /// Additional SASL data + #[xml(extract(default, name = "additional-data", fields(text = Base64)))] + pub additional_data: Option>, + + /// Identity assigned by the server + #[xml(extract(name = "authorization-identifier", fields(text)))] + pub authorization_identifier: Jid, + + /// Extra untyped payloads + #[xml(element(n = ..))] + pub payloads: Vec, +} + +/// Authentication failed +#[derive(FromXml, AsXml, PartialEq, Debug, Clone)] +#[xml(namespace = ns::SASL2, name = "failure")] +pub struct Failure { + /// Plaintext reason for failure + #[xml(extract(default, fields(text(type_ = String))))] + pub text: Option, + + /// Extra untyped payloads + #[xml(element(n = ..))] + pub payloads: Vec, +} + +/// Authentication requires extra steps (eg. 2FA) +#[derive(FromXml, AsXml, PartialEq, Debug, Clone)] +#[xml(namespace = ns::SASL2, name = "continue")] +pub struct Continue { + /// Additional SASL data + #[xml(extract(name = "additional-data", fields(text = Base64)))] + pub additional_data: Vec, + + /// List of extra authentication steps. + /// + /// The client may choose any, but the server may respond with more Continue steps until all required + /// steps are fulfilled. + #[xml(extract(fields(extract(n = .., name = "task", fields(text(type_ = String))))))] + pub tasks: Vec, + + /// Plaintext reason for extra steps + #[xml(extract(default, fields(text(type_ = String))))] + pub text: Option, +} + +/// Client answers Continue extra step by selecting task. +#[derive(FromXml, AsXml, PartialEq, Debug, Clone)] +#[xml(namespace = ns::SASL2, name = "next")] +pub struct Next { + /// Task selected by client + #[xml(attribute)] + pub task: String, + + /// Extra untyped payloads + #[xml(element(n = ..))] + pub payloads: Vec, +} + +/// Client/Server data exchange about selected task. +#[derive(FromXml, AsXml, PartialEq, Debug, Clone)] +#[xml(namespace = ns::SASL2, name = "task-data")] +pub struct TaskData { + /// Extra untyped payloads + #[xml(element(n = ..))] + pub payloads: Vec, +} + +#[cfg(test)] +mod tests { + use super::*; + use base64::prelude::*; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(Authentication, 24); + assert_size!(Abort, 24); + assert_size!(UserAgent, 40); + assert_size!(Authenticate, 76); + assert_size!(Challenge, 12); + assert_size!(Response, 12); + assert_size!(Success, 40); + assert_size!(Failure, 24); + assert_size!(Continue, 24); + assert_size!(Next, 24); + assert_size!(TaskData, 12); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(Authentication, 48); + assert_size!(Abort, 48); + assert_size!(UserAgent, 64); + assert_size!(Authenticate, 136); + assert_size!(Challenge, 24); + assert_size!(Response, 24); + assert_size!(Success, 80); + assert_size!(Failure, 48); + assert_size!(Continue, 72); + assert_size!(Next, 48); + assert_size!(TaskData, 24); + } + + #[test] + fn test_simple() { + let elem: Element = "SCRAM-SHA-1" + .parse() + .unwrap(); + let auth = Authentication::try_from(elem).unwrap(); + assert_eq!(auth.mechanisms.len(), 1); + assert_eq!(auth.payloads.len(), 0); + + let elem: Element = "AAAA" + .parse() + .unwrap(); + let challenge = Challenge::try_from(elem).unwrap(); + assert_eq!(challenge.sasl_data, b"\0\0\0"); + + let elem: Element = "YWJj" + .parse() + .unwrap(); + let response = Response::try_from(elem).unwrap(); + assert_eq!(response.sasl_data, b"abc"); + } + + // XEP-0388 Example 2 + #[test] + fn test_auth() { + let elem: Element = r#" + SCRAM-SHA-1 + SCRAM-SHA-1-PLUS + + + + + "# + .parse() + .unwrap(); + + let auth = Authentication::try_from(elem).unwrap(); + + assert_eq!(auth.mechanisms.len(), 2); + let mut mech = auth.mechanisms.iter(); + assert_eq!(mech.next().unwrap(), "SCRAM-SHA-1"); + assert_eq!(mech.next().unwrap(), "SCRAM-SHA-1-PLUS"); + assert_eq!(mech.next(), None); + + assert_eq!(auth.payloads.len(), 2); + let mut payloads = auth.payloads.into_iter(); + let _sm = crate::sm::StreamManagement::try_from(payloads.next().unwrap()).unwrap(); + let _bind = crate::bind2::BindFeature::try_from(payloads.next().unwrap()).unwrap(); + } + + // XEP-0388 Example 3 + #[test] + fn test_authenticate() { + let elem: Element = r#" + cD10bHMtZXhwb3J0ZXIsLG49dXNlcixyPTEyQzRDRDVDLUUzOEUtNEE5OC04RjZELTE1QzM4RjUxQ0NDNg== + + AwesomeXMPP + Kiva's Phone + + "# + .parse() + .unwrap(); + + let auth = Authenticate::try_from(elem).unwrap(); + + assert_eq!(auth.mechanism, "SCRAM-SHA-1-PLUS"); + assert_eq!( + auth.initial_response.unwrap(), + BASE64_STANDARD.decode("cD10bHMtZXhwb3J0ZXIsLG49dXNlcixyPTEyQzRDRDVDLUUzOEUtNEE5OC04RjZELTE1QzM4RjUxQ0NDNg==").unwrap() + ); + + assert_eq!(auth.user_agent.software.as_ref().unwrap(), "AwesomeXMPP"); + assert_eq!(auth.user_agent.device.as_ref().unwrap(), "Kiva's Phone"); + } + + // XEP-0388 Example 4 + #[test] + fn test_authenticate_2() { + let elem: Element = r#" + SSBzaG91bGQgbWFrZSB0aGlzIGEgY29tcGV0aXRpb24= + + AwesomeXMPP + Kiva's Phone + + + "# + .parse() + .unwrap(); + + let auth = Authenticate::try_from(elem).unwrap(); + + assert_eq!(auth.mechanism, "BLURDYBLOOP"); + assert_eq!( + auth.initial_response.unwrap(), + BASE64_STANDARD + .decode("SSBzaG91bGQgbWFrZSB0aGlzIGEgY29tcGV0aXRpb24=") + .unwrap() + ); + + assert_eq!(auth.user_agent.software.as_ref().unwrap(), "AwesomeXMPP"); + assert_eq!(auth.user_agent.device.as_ref().unwrap(), "Kiva's Phone"); + + assert_eq!(auth.payloads.len(), 1); + let bind = auth.payloads.iter().next().unwrap(); + assert!(bind.is("bind", "urn:xmpp:bind:example")); + } + + // XEP-0388 Example 5 + #[test] + fn test_example_5() { + let elem: Element = "cj0xMkM0Q0Q1Qy1FMzhFLTRBOTgtOEY2RC0xNUMzOEY1MUNDQzZhMDkxMTdhNi1hYzUwLTRmMmYtOTNmMS05Mzc5OWMyYmRkZjYscz1RU1hDUitRNnNlazhiZjkyLGk9NDA5Ng==" + .parse() + .unwrap(); + let challenge = Challenge::try_from(elem).unwrap(); + assert_eq!( + challenge.sasl_data, + b"r=12C4CD5C-E38E-4A98-8F6D-15C38F51CCC6a09117a6-ac50-4f2f-93f1-93799c2bddf6,s=QSXCR+Q6sek8bf92,i=4096" + ); + + let elem: Element = "Yz1jRDEwYkhNdFpYaHdiM0owWlhJc0xNY29Rdk9kQkRlUGQ0T3N3bG1BV1YzZGcxYTFXaDF0WVBUQndWaWQxMFZVLHI9MTJDNENENUMtRTM4RS00QTk4LThGNkQtMTVDMzhGNTFDQ0M2YTA5MTE3YTYtYWM1MC00ZjJmLTkzZjEtOTM3OTljMmJkZGY2LHA9VUFwbzd4bzZQYTlKK1ZhZWpmei9kRzdCb21VPQ==" + .parse() + .unwrap(); + let response = Response::try_from(elem).unwrap(); + assert_eq!( + response.sasl_data, + b"c=cD10bHMtZXhwb3J0ZXIsLMcoQvOdBDePd4OswlmAWV3dg1a1Wh1tYPTBwVid10VU,r=12C4CD5C-E38E-4A98-8F6D-15C38F51CCC6a09117a6-ac50-4f2f-93f1-93799c2bddf6,p=UApo7xo6Pa9J+Vaejfz/dG7BomU=" + ); + } + + // XEP-0388 Example 7 and 8 + #[test] + fn test_example_7_8() { + let elem: Element = r#" + dj1tc1ZIcy9CeklPSERxWGVWSDdFbW1EdTlpZDg9 + user@example.org + "# + .parse() + .unwrap(); + + let success = Success::try_from(elem).unwrap(); + + assert_eq!( + success.additional_data.unwrap(), + BASE64_STANDARD + .decode("dj1tc1ZIcy9CeklPSERxWGVWSDdFbW1EdTlpZDg9") + .unwrap() + ); + + assert_eq!( + success.authorization_identifier, + Jid::new("user@example.org").unwrap() + ); + + let elem: Element = r#" + ip/AeIOfZXKBV+fW2smE0GUB3I//nnrrLCYkt0Vj + juliet@montague.example/Balcony/a987dsh9a87sdh + "# + .parse() + .unwrap(); + + let success = Success::try_from(elem).unwrap(); + + assert_eq!( + success.additional_data.unwrap(), + BASE64_STANDARD + .decode("ip/AeIOfZXKBV+fW2smE0GUB3I//nnrrLCYkt0Vj") + .unwrap() + ); + + assert_eq!( + success.authorization_identifier, + Jid::new("juliet@montague.example/Balcony/a987dsh9a87sdh").unwrap() + ); + } + + // XEP-0388 Example 9 + #[test] + fn example_success_stream_management() { + let elem: Element = r#" + SGFkIHlvdSBnb2luZywgdGhlcmUsIGRpZG4ndCBJPw== + juliet@montague.example + + "# + .parse() + .unwrap(); + + let success = Success::try_from(elem).unwrap(); + + assert_eq!( + success.additional_data.unwrap(), + BASE64_STANDARD + .decode("SGFkIHlvdSBnb2luZywgdGhlcmUsIGRpZG4ndCBJPw==") + .unwrap() + ); + + assert_eq!( + success.authorization_identifier, + Jid::new("juliet@montague.example").unwrap() + ); + + assert_eq!(success.payloads.len(), 1); + let resumed = + crate::sm::Resumed::try_from(success.payloads.into_iter().next().unwrap()).unwrap(); + assert_eq!(resumed.h, 345); + assert_eq!(resumed.previd, crate::sm::StreamId(String::from("124"))); + } + + // XEP-0388 Example 10 + #[test] + fn example_failure() { + let elem: Element = r#" + + + This is a terrible example. +"# + .parse() + .unwrap(); + + let failure = Failure::try_from(elem).unwrap(); + + assert_eq!(failure.text.unwrap(), "This is a terrible example."); + + assert_eq!(failure.payloads.len(), 2); + + let mut payloads = failure.payloads.into_iter(); + + let condition = crate::sasl::DefinedCondition::try_from(payloads.next().unwrap()).unwrap(); + assert_eq!(condition, crate::sasl::DefinedCondition::Aborted); + + assert!(payloads + .next() + .unwrap() + .is("optional-application-specific", "urn:something:else")); + } + + #[test] + fn example_failure_no_text() { + let elem: Element = r#""# + .parse() + .unwrap(); + + let failure = Failure::try_from(elem).unwrap(); + + assert_eq!(failure.text, None); + + assert_eq!(failure.payloads.len(), 1); + + let mut payloads = failure.payloads.into_iter(); + + let condition = crate::sasl::DefinedCondition::try_from(payloads.next().unwrap()).unwrap(); + assert_eq!(condition, crate::sasl::DefinedCondition::Aborted); + } + + // XEP-0388 Example 11 + #[test] + fn example_11() { + let elem: Element = r#" + SSdtIGJvcmVkIG5vdy4= + + HOTP-EXAMPLE + TOTP-EXAMPLE + + This account requires 2FA +"# + .parse() + .unwrap(); + + let cont = Continue::try_from(elem).unwrap(); + + assert_eq!( + cont.additional_data, + BASE64_STANDARD.decode("SSdtIGJvcmVkIG5vdy4=").unwrap() + ); + + assert_eq!(cont.text.as_deref(), Some("This account requires 2FA")); + + assert_eq!(cont.tasks.len(), 2); + let mut tasks = cont.tasks.into_iter(); + + assert_eq!(tasks.next().unwrap(), "HOTP-EXAMPLE"); + + assert_eq!(tasks.next().unwrap(), "TOTP-EXAMPLE"); + } + + // XEP-0388 Example 12 + #[test] + fn test_fictional_totp() { + let elem: Element = r#" + SSd2ZSBydW4gb3V0IG9mIGlkZWFzIGhlcmUu +"# + .parse() + .unwrap(); + + let next = Next::try_from(elem).unwrap(); + assert_eq!(next.task, "TOTP-EXAMPLE"); + + let payload = next.payloads.into_iter().next().unwrap(); + assert!(payload.is("totp", "urn:totp:example")); + assert_eq!(&payload.text(), "SSd2ZSBydW4gb3V0IG9mIGlkZWFzIGhlcmUu"); + + let elem: Element = r#" + 94d27acffa2e99a42ba7786162a9e73e7ab17b9d +"# + .parse() + .unwrap(); + + let task_data = TaskData::try_from(elem).unwrap(); + let payload = task_data.payloads.into_iter().next().unwrap(); + assert!(payload.is("totp", "urn:totp:example")); + assert_eq!(&payload.text(), "94d27acffa2e99a42ba7786162a9e73e7ab17b9d"); + + let elem: Element = r#" + OTRkMjdhY2ZmYTJlOTlhNDJiYTc3ODYxNjJhOWU3M2U3YWIxN2I5ZAo= +"# + .parse() + .unwrap(); + + let task_data = TaskData::try_from(elem).unwrap(); + let payload = task_data.payloads.into_iter().next().unwrap(); + assert!(payload.is("totp", "urn:totp:example")); + assert_eq!( + &payload.text(), + "OTRkMjdhY2ZmYTJlOTlhNDJiYTc3ODYxNjJhOWU3M2U3YWIxN2I5ZAo=" + ); + + let elem: Element = r#" + SGFkIHlvdSBnb2luZywgdGhlcmUsIGRpZG4ndCBJPw== + juliet@montague.example +"# + .parse() + .unwrap(); + + let success = Success::try_from(elem).unwrap(); + assert_eq!(success.additional_data, None); + + let payload = success.payloads.into_iter().next().unwrap(); + assert!(payload.is("totp", "urn:totp:example")); + assert_eq!( + &payload.text(), + "SGFkIHlvdSBnb2luZywgdGhlcmUsIGRpZG4ndCBJPw==" + ); + + assert_eq!( + success.authorization_identifier, + Jid::new("juliet@montague.example").unwrap(), + ) + } + + /// XEP-0388 Example 13 + #[test] + fn example_13() { + let elem: Element = r#" + AGFsaWNlQGV4YW1wbGUub3JnCjM0NQ== + + AwesomeXMPP + Kiva's Phone + +"# + .parse() + .unwrap(); + + let auth = Authenticate::try_from(elem).unwrap(); + + assert_eq!(auth.mechanism, "PLAIN"); + assert_eq!( + auth.initial_response.unwrap(), + BASE64_STANDARD + .decode("AGFsaWNlQGV4YW1wbGUub3JnCjM0NQ==") + .unwrap() + ); + + assert_eq!(auth.payloads.len(), 0); + + let user_agent = auth.user_agent; + assert_eq!( + user_agent.id, + "d4565fa7-4d72-4749-b3d3-740edbf87770".try_into().unwrap() + ); + assert_eq!(user_agent.software.as_deref(), Some("AwesomeXMPP")); + assert_eq!(user_agent.device.as_deref(), Some("Kiva's Phone")); + + let elem: Element = r#" + alice@example.org +"# + .parse() + .unwrap(); + + let success = Success::try_from(elem).unwrap(); + assert_eq!( + success.authorization_identifier, + Jid::new("alice@example.org").unwrap() + ); + assert_eq!(success.additional_data, None); + assert_eq!(success.payloads.len(), 0); + } + + // XEP-0388 Example 14 + #[test] + fn example_14() { + let elem: Element = r#" + + AwesomeXMPP + Kiva's Phone + +"# + .parse() + .unwrap(); + + let auth = Authenticate::try_from(elem).unwrap(); + + assert_eq!(auth.mechanism, "CRAM-MD5"); + assert_eq!(auth.initial_response, None); + assert_eq!(auth.payloads.len(), 0); + + let user_agent = auth.user_agent; + assert_eq!( + user_agent.id, + "d4565fa7-4d72-4749-b3d3-740edbf87770".try_into().unwrap() + ); + assert_eq!(user_agent.software.as_deref(), Some("AwesomeXMPP")); + assert_eq!(user_agent.device.as_deref(), Some("Kiva's Phone")); + + let elem: Element = r#"PDE4OTYuNjk3MTcwOTUyQHBvc3RvZmZpY2UucmVzdG9uLm1jaS5uZXQ+"# + .parse() + .unwrap(); + + let challenge = Challenge::try_from(elem).unwrap(); + assert_eq!( + challenge.sasl_data, + BASE64_STANDARD + .decode("PDE4OTYuNjk3MTcwOTUyQHBvc3RvZmZpY2UucmVzdG9uLm1jaS5uZXQ+") + .unwrap() + ); + + let elem: Element = r#"dGltIGI5MTNhNjAyYzdlZGE3YTQ5NWI0ZTZlNzMzNGQzODkw"# + .parse() + .unwrap(); + + let response = Response::try_from(elem).unwrap(); + assert_eq!( + response.sasl_data, + BASE64_STANDARD + .decode("dGltIGI5MTNhNjAyYzdlZGE3YTQ5NWI0ZTZlNzMzNGQzODkw") + .unwrap() + ); + + let elem: Element = r#" + tim@example.org + + "# + .parse() + .unwrap(); + + let success = Success::try_from(elem).unwrap(); + assert_eq!( + success.authorization_identifier, + Jid::new("tim@example.org").unwrap() + ); + assert_eq!(success.additional_data, None); + assert_eq!(success.payloads.len(), 0); + } + + // XEP-0388 Example 15 + #[test] + fn example_15() { + let elem: Element = r#" + SW5pdGlhbCBSZXNwb25zZQ== + + AwesomeXMPP + Kiva's Phone + + + this-one-please + +"# + .parse() + .unwrap(); + + let auth = Authenticate::try_from(elem).unwrap(); + assert_eq!(auth.mechanism, "BLURDYBLOOP"); + assert_eq!( + auth.initial_response, + Some(BASE64_STANDARD.decode("SW5pdGlhbCBSZXNwb25zZQ==").unwrap()) + ); + + assert_eq!( + auth.user_agent.id, + "d4565fa7-4d72-4749-b3d3-740edbf87770".try_into().unwrap() + ); + assert_eq!(auth.user_agent.software.as_deref(), Some("AwesomeXMPP")); + assert_eq!(auth.user_agent.device.as_deref(), Some("Kiva's Phone")); + + assert_eq!(auth.payloads.len(), 1); + let bind = auth.payloads.into_iter().next().unwrap(); + assert!(bind.is("megabind", "urn:example:megabind")); + + let mut bind_payloads = bind.children(); + let resource = bind_payloads.next().unwrap(); + assert_eq!(resource.name(), "resource"); + assert_eq!(&resource.text(), "this-one-please"); + assert_eq!(bind_payloads.next(), None); + + let elem: Element = r#"PDE4OTYuNjk3MTcwOTUyQHBvc3RvZmZpY2UucmVzdG9uLm1jaS5uZXQ+"# + .parse() + .unwrap(); + let challenge = Challenge::try_from(elem).unwrap(); + assert_eq!( + challenge.sasl_data, + BASE64_STANDARD + .decode("PDE4OTYuNjk3MTcwOTUyQHBvc3RvZmZpY2UucmVzdG9uLm1jaS5uZXQ+") + .unwrap() + ); + + let elem: Element = r#"dGltIGI5MTNhNjAyYzdlZGE3YTQ5NWI0ZTZlNzMzNGQzODkw"# + .parse() + .unwrap(); + let response = Response::try_from(elem).unwrap(); + assert_eq!(response.sasl_data, b"tim b913a602c7eda7a495b4e6e7334d3890"); + + let elem: Element = r#" + QWRkaXRpb25hbCBEYXRh + + UNREALISTIC-2FA + +"# + .parse() + .unwrap(); + let cont = Continue::try_from(elem).unwrap(); + assert_eq!( + cont.additional_data, + BASE64_STANDARD.decode("QWRkaXRpb25hbCBEYXRh").unwrap() + ); + assert_eq!(cont.tasks.len(), 1); + assert_eq!(cont.tasks.into_iter().next().unwrap(), "UNREALISTIC-2FA"); + + let elem: Element = r#" + VW5yZWFsaXN0aWMgMkZBIElS +"# + .parse() + .unwrap(); + let next = Next::try_from(elem).unwrap(); + assert_eq!(next.payloads.len(), 1); + let params = next.payloads.into_iter().next().unwrap(); + assert!(params.is("parameters", "urn:example:unrealistic2fa")); + assert_eq!(¶ms.text(), "VW5yZWFsaXN0aWMgMkZBIElS"); + + let elem: Element = r#" + PDE4OTYuNjk3MTcwOTUyQHBvc3RvZmZpY2UucmVzdG9uLm1jaS5uZXQ+ +"# + .parse() + .unwrap(); + let task_data = TaskData::try_from(elem).unwrap(); + assert_eq!(task_data.payloads.len(), 1); + let question = task_data.payloads.into_iter().next().unwrap(); + assert!(question.is("question", "urn:example:unrealistic2fa")); + assert_eq!( + &question.text(), + "PDE4OTYuNjk3MTcwOTUyQHBvc3RvZmZpY2UucmVzdG9uLm1jaS5uZXQ+" + ); + + let elem: Element = r#" + dGltIGI5MTNhNjAyYzdlZGE3YTQ5NWI0ZTZlNzMzNGQzODkw +"# + .parse() + .unwrap(); + let task_data = TaskData::try_from(elem).unwrap(); + assert_eq!(task_data.payloads.len(), 1); + let response = task_data.payloads.into_iter().next().unwrap(); + assert!(response.is("response", "urn:example:unrealistic2fa")); + assert_eq!( + &response.text(), + "dGltIGI5MTNhNjAyYzdlZGE3YTQ5NWI0ZTZlNzMzNGQzODkw" + ); + + let elem: Element = r#" + VW5yZWFsaXN0aWMgMkZBIG11dHVhbCBhdXRoIGRhdGE= + alice@example.org/this-one-please +"# + .parse() + .unwrap(); + let success = Success::try_from(elem).unwrap(); + assert_eq!( + success.authorization_identifier, + Jid::new("alice@example.org/this-one-please").unwrap() + ); + + assert_eq!(success.payloads.len(), 1); + let res = success.payloads.into_iter().next().unwrap(); + assert!(res.is("result", "urn:example:unrealistic2fa")); + assert_eq!(&res.text(), "VW5yZWFsaXN0aWMgMkZBIG11dHVhbCBhdXRoIGRhdGE="); + } +}