diff --git a/sasl/.gitignore b/sasl/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..a9d37c560c6ab8d4afbf47eda643e8c42e857716 --- /dev/null +++ b/sasl/.gitignore @@ -0,0 +1,2 @@ +target +Cargo.lock diff --git a/sasl/CHANGELOG.md b/sasl/CHANGELOG.md new file mode 100644 index 0000000000000000000000000000000000000000..b51524494265ac043bf4170c943b3404d56671aa --- /dev/null +++ b/sasl/CHANGELOG.md @@ -0,0 +1,12 @@ +Version 0.5.0, released 2021-01-12: + * Important changes + - Relicensed to MPL-2.0 from LGPL-3.0-or-later. + - Made all of the errors into enums, instead of strings. + * Small changes + - Replaced rand\_os with getrandom. + - Bumped all dependencies. + +Version 0.4.2, released 2018-05-19: + * Small changes + - Marc-Antoine Perennou updated the openssl and base64 dependencies to 0.10.4 and 0.9.0 respectively. + - lumi updated them further to 0.10.7 and 0.9.1 respectively. diff --git a/sasl/Cargo.toml b/sasl/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..098fe144f22a54ae2078c7a0d601b929cd6a29d9 --- /dev/null +++ b/sasl/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "sasl" +version = "0.5.0" +authors = ["lumi "] +description = "A crate for SASL authentication. Currently only does the client side." +homepage = "https://gitlab.com/xmpp-rs/sasl-rs" +repository = "https://gitlab.com/xmpp-rs/sasl-rs" +documentation = "https://docs.rs/sasl" +readme = "README.md" +keywords = ["sasl", "authentication"] +license = "MPL-2.0" +edition = "2018" + +[badges] +gitlab = { repository = "xmpp-rs/sasl-rs" } + +[features] +default = ["scram", "anonymous"] +scram = ["base64", "getrandom", "sha-1", "sha2", "hmac", "pbkdf2"] +anonymous = ["getrandom"] + +[dependencies] +base64 = { version = "0.20", optional = true } +getrandom = { version = "0.2", optional = true } +sha-1 = { version = "0.10", optional = true } +sha2 = { version = "0.10", optional = true } +hmac = { version = "0.12", optional = true } +pbkdf2 = { version = "0.11", default-features = false, optional = true } diff --git a/sasl/LICENSE b/sasl/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..a612ad9813b006ce81d1ee438dd784da99a54007 --- /dev/null +++ b/sasl/LICENSE @@ -0,0 +1,373 @@ +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + 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/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. diff --git a/sasl/README.md b/sasl/README.md new file mode 100644 index 0000000000000000000000000000000000000000..5972c5915932aacc3e975bf3fabc8464d57bb965 --- /dev/null +++ b/sasl/README.md @@ -0,0 +1,26 @@ +sasl-rs +======= + +What's this? +------------ + +A crate which handles SASL authentication. Still unstable until 1.0.0. + +Can I see an example? +--------------------- + +Look at the documentation [here](https://docs.rs/sasl). + +What license is it under? +------------------------- + +MPL-2.0. See `LICENSE`. + +License yadda yadda. +-------------------- + + Copyright 2017, sasl-rs contributors. + + 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 https://mozilla.org/MPL/2.0/. diff --git a/sasl/src/client/mechanisms/anonymous.rs b/sasl/src/client/mechanisms/anonymous.rs new file mode 100644 index 0000000000000000000000000000000000000000..96b236a6b0c7829a1269f77c2e8434d4521eaa70 --- /dev/null +++ b/sasl/src/client/mechanisms/anonymous.rs @@ -0,0 +1,31 @@ +//! Provides the SASL "ANONYMOUS" mechanism. + +use crate::client::{Mechanism, MechanismError}; +use crate::common::{Credentials, Secret}; + +/// A struct for the SASL ANONYMOUS mechanism. +pub struct Anonymous; + +impl Anonymous { + /// Constructs a new struct for authenticating using the SASL ANONYMOUS mechanism. + /// + /// It is recommended that instead you use a `Credentials` struct and turn it into the + /// requested mechanism using `from_credentials`. + pub fn new() -> Anonymous { + Anonymous + } +} + +impl Mechanism for Anonymous { + fn name(&self) -> &str { + "ANONYMOUS" + } + + fn from_credentials(credentials: Credentials) -> Result { + if let Secret::None = credentials.secret { + Ok(Anonymous) + } else { + Err(MechanismError::AnonymousRequiresNoCredentials) + } + } +} diff --git a/sasl/src/client/mechanisms/mod.rs b/sasl/src/client/mechanisms/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..bde2fcbf0fd6df9d727478dc2e3afae2143f0b6b --- /dev/null +++ b/sasl/src/client/mechanisms/mod.rs @@ -0,0 +1,13 @@ +//! Provides a few SASL mechanisms. + +mod anonymous; +mod plain; + +#[cfg(feature = "scram")] +mod scram; + +pub use self::anonymous::Anonymous; +pub use self::plain::Plain; + +#[cfg(feature = "scram")] +pub use self::scram::Scram; diff --git a/sasl/src/client/mechanisms/plain.rs b/sasl/src/client/mechanisms/plain.rs new file mode 100644 index 0000000000000000000000000000000000000000..bc08fd85b711a5b8d035b98764e00573601750b8 --- /dev/null +++ b/sasl/src/client/mechanisms/plain.rs @@ -0,0 +1,50 @@ +//! Provides the SASL "PLAIN" mechanism. + +use crate::client::{Mechanism, MechanismError}; +use crate::common::{Credentials, Identity, Password, Secret}; + +/// A struct for the SASL PLAIN mechanism. +pub struct Plain { + username: String, + password: String, +} + +impl Plain { + /// Constructs a new struct for authenticating using the SASL PLAIN mechanism. + /// + /// It is recommended that instead you use a `Credentials` struct and turn it into the + /// requested mechanism using `from_credentials`. + pub fn new, P: Into>(username: N, password: P) -> Plain { + Plain { + username: username.into(), + password: password.into(), + } + } +} + +impl Mechanism for Plain { + fn name(&self) -> &str { + "PLAIN" + } + + fn from_credentials(credentials: Credentials) -> Result { + if let Secret::Password(Password::Plain(password)) = credentials.secret { + if let Identity::Username(username) = credentials.identity { + Ok(Plain::new(username, password)) + } else { + Err(MechanismError::PlainRequiresUsername) + } + } else { + Err(MechanismError::PlainRequiresPlaintextPassword) + } + } + + fn initial(&mut self) -> Vec { + let mut auth = Vec::new(); + auth.push(0); + auth.extend(self.username.bytes()); + auth.push(0); + auth.extend(self.password.bytes()); + auth + } +} diff --git a/sasl/src/client/mechanisms/scram.rs b/sasl/src/client/mechanisms/scram.rs new file mode 100644 index 0000000000000000000000000000000000000000..10f828d8bc867a7ddcdb9b16bd6b6ae8f9adda10 --- /dev/null +++ b/sasl/src/client/mechanisms/scram.rs @@ -0,0 +1,246 @@ +//! Provides the SASL "SCRAM-*" mechanisms and a way to implement more. + +use base64; + +use crate::client::{Mechanism, MechanismError}; +use crate::common::scram::{generate_nonce, ScramProvider}; +use crate::common::{parse_frame, xor, ChannelBinding, Credentials, Identity, Password, Secret}; + +use crate::error::Error; + +use std::marker::PhantomData; + +enum ScramState { + Init, + SentInitialMessage { + initial_message: Vec, + gs2_header: Vec, + }, + GotServerData { + server_signature: Vec, + }, +} + +/// A struct for the SASL SCRAM-* and SCRAM-*-PLUS mechanisms. +pub struct Scram { + name: String, + username: String, + password: Password, + client_nonce: String, + state: ScramState, + channel_binding: ChannelBinding, + _marker: PhantomData, +} + +impl Scram { + /// Constructs a new struct for authenticating using the SASL SCRAM-* and SCRAM-*-PLUS + /// mechanisms, depending on the passed channel binding. + /// + /// It is recommended that instead you use a `Credentials` struct and turn it into the + /// requested mechanism using `from_credentials`. + pub fn new, P: Into>( + username: N, + password: P, + channel_binding: ChannelBinding, + ) -> Result, Error> { + Ok(Scram { + name: format!("SCRAM-{}", S::name()), + username: username.into(), + password: password.into(), + client_nonce: generate_nonce()?, + state: ScramState::Init, + channel_binding: channel_binding, + _marker: PhantomData, + }) + } + + // Used for testing. + #[doc(hidden)] + #[cfg(test)] + pub fn new_with_nonce, P: Into>( + username: N, + password: P, + nonce: String, + ) -> Scram { + Scram { + name: format!("SCRAM-{}", S::name()), + username: username.into(), + password: password.into(), + client_nonce: nonce, + state: ScramState::Init, + channel_binding: ChannelBinding::None, + _marker: PhantomData, + } + } +} + +impl Mechanism for Scram { + fn name(&self) -> &str { + // TODO: this is quite the workaround… + &self.name + } + + fn from_credentials(credentials: Credentials) -> Result, MechanismError> { + if let Secret::Password(password) = credentials.secret { + if let Identity::Username(username) = credentials.identity { + Scram::new(username, password, credentials.channel_binding) + .map_err(|_| MechanismError::CannotGenerateNonce) + } else { + Err(MechanismError::ScramRequiresUsername) + } + } else { + Err(MechanismError::ScramRequiresPassword) + } + } + + fn initial(&mut self) -> Vec { + let mut gs2_header = Vec::new(); + gs2_header.extend(self.channel_binding.header()); + let mut bare = Vec::new(); + bare.extend(b"n="); + bare.extend(self.username.bytes()); + bare.extend(b",r="); + bare.extend(self.client_nonce.bytes()); + let mut data = Vec::new(); + data.extend(&gs2_header); + data.extend(bare.clone()); + self.state = ScramState::SentInitialMessage { + initial_message: bare, + gs2_header: gs2_header, + }; + data + } + + fn response(&mut self, challenge: &[u8]) -> Result, MechanismError> { + let next_state; + let ret; + match self.state { + ScramState::SentInitialMessage { + ref initial_message, + ref gs2_header, + } => { + let frame = + parse_frame(challenge).map_err(|_| MechanismError::CannotDecodeChallenge)?; + let server_nonce = frame.get("r"); + let salt = frame.get("s").and_then(|v| base64::decode(v).ok()); + let iterations = frame.get("i").and_then(|v| v.parse().ok()); + let server_nonce = server_nonce.ok_or_else(|| MechanismError::NoServerNonce)?; + let salt = salt.ok_or_else(|| MechanismError::NoServerSalt)?; + let iterations = iterations.ok_or_else(|| MechanismError::NoServerIterations)?; + // TODO: SASLprep + let mut client_final_message_bare = Vec::new(); + client_final_message_bare.extend(b"c="); + let mut cb_data: Vec = Vec::new(); + cb_data.extend(gs2_header); + cb_data.extend(self.channel_binding.data()); + client_final_message_bare.extend(base64::encode(&cb_data).bytes()); + client_final_message_bare.extend(b",r="); + client_final_message_bare.extend(server_nonce.bytes()); + let salted_password = S::derive(&self.password, &salt, iterations)?; + let client_key = S::hmac(b"Client Key", &salted_password)?; + let server_key = S::hmac(b"Server Key", &salted_password)?; + let mut auth_message = Vec::new(); + auth_message.extend(initial_message); + auth_message.push(b','); + auth_message.extend(challenge); + auth_message.push(b','); + auth_message.extend(&client_final_message_bare); + let stored_key = S::hash(&client_key); + let client_signature = S::hmac(&auth_message, &stored_key)?; + let client_proof = xor(&client_key, &client_signature); + let server_signature = S::hmac(&auth_message, &server_key)?; + let mut client_final_message = Vec::new(); + client_final_message.extend(&client_final_message_bare); + client_final_message.extend(b",p="); + client_final_message.extend(base64::encode(&client_proof).bytes()); + next_state = ScramState::GotServerData { + server_signature: server_signature, + }; + ret = client_final_message; + } + _ => { + return Err(MechanismError::InvalidState); + } + } + self.state = next_state; + Ok(ret) + } + + fn success(&mut self, data: &[u8]) -> Result<(), MechanismError> { + let frame = parse_frame(data).map_err(|_| MechanismError::CannotDecodeSuccessResponse)?; + match self.state { + ScramState::GotServerData { + ref server_signature, + } => { + if let Some(sig) = frame.get("v").and_then(|v| base64::decode(&v).ok()) { + if sig == *server_signature { + Ok(()) + } else { + Err(MechanismError::InvalidSignatureInSuccessResponse) + } + } else { + Err(MechanismError::NoSignatureInSuccessResponse) + } + } + _ => Err(MechanismError::InvalidState), + } + } +} + +#[cfg(test)] +mod tests { + use crate::client::mechanisms::Scram; + use crate::client::Mechanism; + use crate::common::scram::{Sha1, Sha256}; + + #[test] + fn scram_sha1_works() { + // Source: https://wiki.xmpp.org/web/SASLandSCRAM-SHA-1 + let username = "user"; + let password = "pencil"; + let client_nonce = "fyko+d2lbbFgONRv9qkxdawL"; + let client_init = b"n,,n=user,r=fyko+d2lbbFgONRv9qkxdawL"; + let server_init = b"r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,s=QSXCR+Q6sek8bf92,i=4096"; + let client_final = + b"c=biws,r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,p=v0X8v3Bz2T0CJGbJQyF0X+HI4Ts="; + let server_final = b"v=rmF9pqV8S7suAoZWja4dJRkFsKQ="; + let mut mechanism = + Scram::::new_with_nonce(username, password, client_nonce.to_owned()); + let init = mechanism.initial(); + assert_eq!( + String::from_utf8(init.clone()).unwrap(), + String::from_utf8(client_init[..].to_owned()).unwrap() + ); // depends on ordering… + let resp = mechanism.response(&server_init[..]).unwrap(); + assert_eq!( + String::from_utf8(resp.clone()).unwrap(), + String::from_utf8(client_final[..].to_owned()).unwrap() + ); // again, depends on ordering… + mechanism.success(&server_final[..]).unwrap(); + } + + #[test] + fn scram_sha256_works() { + // Source: RFC 7677 + let username = "user"; + let password = "pencil"; + let client_nonce = "rOprNGfwEbeRWgbNEkqO"; + let client_init = b"n,,n=user,r=rOprNGfwEbeRWgbNEkqO"; + let server_init = b"r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0,s=W22ZaJ0SNY7soEsUEjb6gQ==,i=4096"; + let client_final = b"c=biws,r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0,p=dHzbZapWIk4jUhN+Ute9ytag9zjfMHgsqmmiz7AndVQ="; + let server_final = b"v=6rriTRBi23WpRR/wtup+mMhUZUn/dB5nLTJRsjl95G4="; + let mut mechanism = + Scram::::new_with_nonce(username, password, client_nonce.to_owned()); + let init = mechanism.initial(); + assert_eq!( + String::from_utf8(init.clone()).unwrap(), + String::from_utf8(client_init[..].to_owned()).unwrap() + ); // depends on ordering… + let resp = mechanism.response(&server_init[..]).unwrap(); + assert_eq!( + String::from_utf8(resp.clone()).unwrap(), + String::from_utf8(client_final[..].to_owned()).unwrap() + ); // again, depends on ordering… + mechanism.success(&server_final[..]).unwrap(); + } +} diff --git a/sasl/src/client/mod.rs b/sasl/src/client/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..8acca9ffbc200c86e19d89c60fb1fd3a2876ed74 --- /dev/null +++ b/sasl/src/client/mod.rs @@ -0,0 +1,115 @@ +use std::fmt; + +use crate::common::Credentials; + +#[cfg(feature = "scram")] +use crate::common::scram::DeriveError; +#[cfg(feature = "scram")] +use hmac::digest::InvalidLength; + +#[derive(Debug, PartialEq)] +pub enum MechanismError { + AnonymousRequiresNoCredentials, + + PlainRequiresUsername, + PlainRequiresPlaintextPassword, + + CannotGenerateNonce, + ScramRequiresUsername, + ScramRequiresPassword, + + CannotDecodeChallenge, + NoServerNonce, + NoServerSalt, + NoServerIterations, + #[cfg(feature = "scram")] + DeriveError(DeriveError), + #[cfg(feature = "scram")] + InvalidKeyLength(InvalidLength), + InvalidState, + + CannotDecodeSuccessResponse, + InvalidSignatureInSuccessResponse, + NoSignatureInSuccessResponse, +} + +#[cfg(feature = "scram")] +impl From for MechanismError { + fn from(err: DeriveError) -> MechanismError { + MechanismError::DeriveError(err) + } +} + +#[cfg(feature = "scram")] +impl From for MechanismError { + fn from(err: InvalidLength) -> MechanismError { + MechanismError::InvalidKeyLength(err) + } +} + +impl fmt::Display for MechanismError { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + write!( + fmt, + "{}", + match self { + MechanismError::AnonymousRequiresNoCredentials => + "ANONYMOUS mechanism requires no credentials", + + MechanismError::PlainRequiresUsername => "PLAIN requires a username", + MechanismError::PlainRequiresPlaintextPassword => + "PLAIN requires a plaintext password", + + MechanismError::CannotGenerateNonce => "can't generate nonce", + MechanismError::ScramRequiresUsername => "SCRAM requires a username", + MechanismError::ScramRequiresPassword => "SCRAM requires a password", + + MechanismError::CannotDecodeChallenge => "can't decode challenge", + MechanismError::NoServerNonce => "no server nonce", + MechanismError::NoServerSalt => "no server salt", + MechanismError::NoServerIterations => "no server iterations", + #[cfg(feature = "scram")] + MechanismError::DeriveError(err) => return write!(fmt, "derive error: {}", err), + #[cfg(feature = "scram")] + MechanismError::InvalidKeyLength(err) => + return write!(fmt, "invalid key length: {}", err), + MechanismError::InvalidState => "not in the right state to receive this response", + + MechanismError::CannotDecodeSuccessResponse => "can't decode success response", + MechanismError::InvalidSignatureInSuccessResponse => + "invalid signature in success response", + MechanismError::NoSignatureInSuccessResponse => "no signature in success response", + } + ) + } +} + +impl std::error::Error for MechanismError {} + +/// A trait which defines SASL mechanisms. +pub trait Mechanism { + /// The name of the mechanism. + fn name(&self) -> &str; + + /// Creates this mechanism from `Credentials`. + fn from_credentials(credentials: Credentials) -> Result + where + Self: Sized; + + /// Provides initial payload of the SASL mechanism. + fn initial(&mut self) -> Vec { + Vec::new() + } + + /// Creates a response to the SASL challenge. + fn response(&mut self, _challenge: &[u8]) -> Result, MechanismError> { + Ok(Vec::new()) + } + + /// Verifies the server success response, if there is one. + fn success(&mut self, _data: &[u8]) -> Result<(), MechanismError> { + Ok(()) + } +} + +pub mod mechanisms; diff --git a/sasl/src/common/mod.rs b/sasl/src/common/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..a0922f1163debd3bd030386a9bda669b9b1f21c3 --- /dev/null +++ b/sasl/src/common/mod.rs @@ -0,0 +1,202 @@ +use std::collections::HashMap; + +use std::convert::From; + +use std::string::FromUtf8Error; + +#[cfg(feature = "scram")] +pub mod scram; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Identity { + None, + Username(String), +} + +impl From for Identity { + fn from(s: String) -> Identity { + Identity::Username(s) + } +} + +impl<'a> From<&'a str> for Identity { + fn from(s: &'a str) -> Identity { + Identity::Username(s.to_owned()) + } +} + +/// A struct containing SASL credentials. +#[derive(Clone, Debug)] +pub struct Credentials { + /// The requested identity. + pub identity: Identity, + /// The secret used to authenticate. + pub secret: Secret, + /// Channel binding data, for *-PLUS mechanisms. + pub channel_binding: ChannelBinding, +} + +impl Default for Credentials { + fn default() -> Credentials { + Credentials { + identity: Identity::None, + secret: Secret::None, + channel_binding: ChannelBinding::Unsupported, + } + } +} + +impl Credentials { + /// Creates a new Credentials with the specified username. + pub fn with_username>(mut self, username: N) -> Credentials { + self.identity = Identity::Username(username.into()); + self + } + + /// Creates a new Credentials with the specified plaintext password. + pub fn with_password>(mut self, password: P) -> Credentials { + self.secret = Secret::password_plain(password); + self + } + + /// Creates a new Credentials with the specified chanel binding. + pub fn with_channel_binding(mut self, channel_binding: ChannelBinding) -> Credentials { + self.channel_binding = channel_binding; + self + } +} + +/// Represents a SASL secret, like a password. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Secret { + /// No extra data needed. + None, + /// Password required. + Password(Password), +} + +impl Secret { + pub fn password_plain>(password: S) -> Secret { + Secret::Password(Password::Plain(password.into())) + } + + pub fn password_pbkdf2>( + method: S, + salt: Vec, + iterations: u32, + data: Vec, + ) -> Secret { + Secret::Password(Password::Pbkdf2 { + method: method.into(), + salt: salt, + iterations: iterations, + data: data, + }) + } +} + +/// Represents a password. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Password { + /// A plaintext password. + Plain(String), + /// A password digest derived using PBKDF2. + Pbkdf2 { + method: String, + salt: Vec, + iterations: u32, + data: Vec, + }, +} + +impl From for Password { + fn from(s: String) -> Password { + Password::Plain(s) + } +} + +impl<'a> From<&'a str> for Password { + fn from(s: &'a str) -> Password { + Password::Plain(s.to_owned()) + } +} + +#[cfg(test)] +#[test] +fn xor_works() { + assert_eq!( + xor( + &[135, 94, 53, 134, 73, 233, 140, 221, 150, 12, 96, 111, 54, 66, 11, 76], + &[163, 9, 122, 180, 107, 44, 22, 252, 248, 134, 112, 82, 84, 122, 56, 209] + ), + &[36, 87, 79, 50, 34, 197, 154, 33, 110, 138, 16, 61, 98, 56, 51, 157] + ); +} + +#[doc(hidden)] +pub fn xor(a: &[u8], b: &[u8]) -> Vec { + assert_eq!(a.len(), b.len()); + let mut ret = Vec::with_capacity(a.len()); + for (a, b) in a.into_iter().zip(b) { + ret.push(a ^ b); + } + ret +} + +#[doc(hidden)] +pub fn parse_frame(frame: &[u8]) -> Result, FromUtf8Error> { + let inner = String::from_utf8(frame.to_owned())?; + let mut ret = HashMap::new(); + for s in inner.split(',') { + let mut tmp = s.splitn(2, '='); + let key = tmp.next(); + let val = tmp.next(); + match (key, val) { + (Some(k), Some(v)) => { + ret.insert(k.to_owned(), v.to_owned()); + } + _ => (), + } + } + Ok(ret) +} + +/// Channel binding configuration. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum ChannelBinding { + /// No channel binding data. + None, + /// Advertise that the client does not think the server supports channel binding. + Unsupported, + /// p=tls-unique channel binding data. + TlsUnique(Vec), +} + +impl ChannelBinding { + /// Return the gs2 header for this channel binding mechanism. + pub fn header(&self) -> &[u8] { + match *self { + ChannelBinding::None => b"n,,", + ChannelBinding::Unsupported => b"y,,", + ChannelBinding::TlsUnique(_) => b"p=tls-unique,,", + } + } + + /// Return the channel binding data for this channel binding mechanism. + pub fn data(&self) -> &[u8] { + match *self { + ChannelBinding::None => &[], + ChannelBinding::Unsupported => &[], + ChannelBinding::TlsUnique(ref data) => data, + } + } + + /// Checks whether this channel binding mechanism is supported. + pub fn supports(&self, mechanism: &str) -> bool { + match *self { + ChannelBinding::None => false, + ChannelBinding::Unsupported => false, + ChannelBinding::TlsUnique(_) => mechanism == "tls-unique", + } + } +} diff --git a/sasl/src/common/scram.rs b/sasl/src/common/scram.rs new file mode 100644 index 0000000000000000000000000000000000000000..b39f29143420be9ee07479b3f9417311333c8726 --- /dev/null +++ b/sasl/src/common/scram.rs @@ -0,0 +1,179 @@ +use getrandom::{getrandom, Error as RngError}; +use hmac::{digest::InvalidLength, Hmac, Mac}; +use pbkdf2::pbkdf2; +use sha1::{Digest, Sha1 as Sha1_hash}; +use sha2::Sha256 as Sha256_hash; + +use crate::common::Password; + +use crate::secret; + +use base64; + +/// Generate a nonce for SCRAM authentication. +pub fn generate_nonce() -> Result { + let mut data = [0u8; 32]; + getrandom(&mut data)?; + Ok(base64::encode(&data)) +} + +#[derive(Debug, PartialEq)] +pub enum DeriveError { + IncompatibleHashingMethod(String, String), + IncorrectSalt, + IncompatibleIterationCount(u32, u32), +} + +impl std::fmt::Display for DeriveError { + fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + DeriveError::IncompatibleHashingMethod(one, two) => { + write!(fmt, "incompatible hashing method, {} is not {}", one, two) + } + DeriveError::IncorrectSalt => write!(fmt, "incorrect salt"), + DeriveError::IncompatibleIterationCount(one, two) => { + write!(fmt, "incompatible iteration count, {} is not {}", one, two) + } + } + } +} + +impl std::error::Error for DeriveError {} + +/// A trait which defines the needed methods for SCRAM. +pub trait ScramProvider { + /// The kind of secret this `ScramProvider` requires. + type Secret: secret::Secret; + + /// The name of the hash function. + fn name() -> &'static str; + + /// A function which hashes the data using the hash function. + fn hash(data: &[u8]) -> Vec; + + /// A function which performs an HMAC using the hash function. + fn hmac(data: &[u8], key: &[u8]) -> Result, InvalidLength>; + + /// A function which does PBKDF2 key derivation using the hash function. + fn derive(data: &Password, salt: &[u8], iterations: u32) -> Result, DeriveError>; +} + +/// A `ScramProvider` which provides SCRAM-SHA-1 and SCRAM-SHA-1-PLUS +pub struct Sha1; + +impl ScramProvider for Sha1 { + type Secret = secret::Pbkdf2Sha1; + + fn name() -> &'static str { + "SHA-1" + } + + fn hash(data: &[u8]) -> Vec { + let hash = Sha1_hash::digest(data); + let mut vec = Vec::with_capacity(Sha1_hash::output_size()); + vec.extend_from_slice(hash.as_slice()); + vec + } + + fn hmac(data: &[u8], key: &[u8]) -> Result, InvalidLength> { + type HmacSha1 = Hmac; + let mut mac = HmacSha1::new_from_slice(key)?; + mac.update(data); + let result = mac.finalize(); + let mut vec = Vec::with_capacity(Sha1_hash::output_size()); + vec.extend_from_slice(result.into_bytes().as_slice()); + Ok(vec) + } + + fn derive(password: &Password, salt: &[u8], iterations: u32) -> Result, DeriveError> { + match *password { + Password::Plain(ref plain) => { + let mut result = vec![0; 20]; + pbkdf2::>(plain.as_bytes(), salt, iterations, &mut result); + Ok(result) + } + Password::Pbkdf2 { + ref method, + salt: ref my_salt, + iterations: my_iterations, + ref data, + } => { + if method != Self::name() { + Err(DeriveError::IncompatibleHashingMethod( + method.to_string(), + Self::name().to_string(), + )) + } else if my_salt == &salt { + Err(DeriveError::IncorrectSalt) + } else if my_iterations == iterations { + Err(DeriveError::IncompatibleIterationCount( + my_iterations, + iterations, + )) + } else { + Ok(data.to_vec()) + } + } + } + } +} + +/// A `ScramProvider` which provides SCRAM-SHA-256 and SCRAM-SHA-256-PLUS +pub struct Sha256; + +impl ScramProvider for Sha256 { + type Secret = secret::Pbkdf2Sha256; + + fn name() -> &'static str { + "SHA-256" + } + + fn hash(data: &[u8]) -> Vec { + let hash = Sha256_hash::digest(data); + let mut vec = Vec::with_capacity(Sha256_hash::output_size()); + vec.extend_from_slice(hash.as_slice()); + vec + } + + fn hmac(data: &[u8], key: &[u8]) -> Result, InvalidLength> { + type HmacSha256 = Hmac; + let mut mac = HmacSha256::new_from_slice(key)?; + mac.update(data); + let result = mac.finalize(); + let mut vec = Vec::with_capacity(Sha256_hash::output_size()); + vec.extend_from_slice(result.into_bytes().as_slice()); + Ok(vec) + } + + fn derive(password: &Password, salt: &[u8], iterations: u32) -> Result, DeriveError> { + match *password { + Password::Plain(ref plain) => { + let mut result = vec![0; 32]; + pbkdf2::>(plain.as_bytes(), salt, iterations, &mut result); + Ok(result) + } + Password::Pbkdf2 { + ref method, + salt: ref my_salt, + iterations: my_iterations, + ref data, + } => { + if method != Self::name() { + Err(DeriveError::IncompatibleHashingMethod( + method.to_string(), + Self::name().to_string(), + )) + } else if my_salt == &salt { + Err(DeriveError::IncorrectSalt) + } else if my_iterations == iterations { + Err(DeriveError::IncompatibleIterationCount( + my_iterations, + iterations, + )) + } else { + Ok(data.to_vec()) + } + } + } + } +} diff --git a/sasl/src/error.rs b/sasl/src/error.rs new file mode 100644 index 0000000000000000000000000000000000000000..eb8859092c993f0b266268f82786879d1911ee59 --- /dev/null +++ b/sasl/src/error.rs @@ -0,0 +1,19 @@ +#[cfg(feature = "scram")] +use getrandom::Error as RngError; + +/// A wrapper enum for things that could go wrong in this crate. +#[derive(Debug)] +pub enum Error { + #[cfg(feature = "scram")] + /// An error while initializing the Rng. + RngError(RngError), + /// An error in a SASL mechanism. + SaslError(String), +} + +#[cfg(feature = "scram")] +impl From for Error { + fn from(err: RngError) -> Error { + Error::RngError(err) + } +} diff --git a/sasl/src/lib.rs b/sasl/src/lib.rs new file mode 100644 index 0000000000000000000000000000000000000000..f85571a78b629138c2b9a6d0b82b35831a39cdd0 --- /dev/null +++ b/sasl/src/lib.rs @@ -0,0 +1,193 @@ +//#![deny(missing_docs)] + +//! This crate provides a framework for SASL authentication and a few authentication mechanisms. +//! +//! # Examples +//! +//! ## Simple client-sided usage +//! +//! ```rust +//! use sasl::client::Mechanism; +//! use sasl::common::Credentials; +//! use sasl::client::mechanisms::Plain; +//! +//! let creds = Credentials::default() +//! .with_username("user") +//! .with_password("pencil"); +//! +//! let mut mechanism = Plain::from_credentials(creds).unwrap(); +//! +//! let initial_data = mechanism.initial(); +//! +//! assert_eq!(initial_data, b"\0user\0pencil"); +//! ``` +//! +//! ## More complex usage +//! +//! ```rust,ignore +//! #[macro_use] extern crate sasl; +//! +//! use sasl::server::{Validator, Provider, Mechanism as ServerMechanism, Response}; +//! use sasl::server::{ValidatorError, ProviderError, MechanismError as ServerMechanismError}; +//! use sasl::server::mechanisms::{Plain as ServerPlain, Scram as ServerScram}; +//! use sasl::client::{Mechanism as ClientMechanism, MechanismError as ClientMechanismError}; +//! use sasl::client::mechanisms::{Plain as ClientPlain, Scram as ClientScram}; +//! use sasl::common::{Identity, Credentials, Password, ChannelBinding}; +//! use sasl::common::scram::{ScramProvider, Sha1, Sha256}; +//! use sasl::secret; +//! +//! const USERNAME: &'static str = "user"; +//! const PASSWORD: &'static str = "pencil"; +//! const SALT: [u8; 8] = [35, 71, 92, 105, 212, 219, 114, 93]; +//! const ITERATIONS: u32 = 4096; +//! +//! struct MyValidator; +//! +//! impl Validator for MyValidator { +//! fn validate(&self, identity: &Identity, value: &secret::Plain) -> Result<(), ValidatorError> { +//! let &secret::Plain(ref password) = value; +//! if identity != &Identity::Username(USERNAME.to_owned()) { +//! Err(ValidatorError::AuthenticationFailed) +//! } +//! else if password != PASSWORD { +//! Err(ValidatorError::AuthenticationFailed) +//! } +//! else { +//! Ok(()) +//! } +//! } +//! } +//! +//! impl Provider for MyValidator { +//! fn provide(&self, identity: &Identity) -> Result { +//! if identity != &Identity::Username(USERNAME.to_owned()) { +//! Err(ProviderError::AuthenticationFailed) +//! } +//! else { +//! let digest = sasl::common::scram::Sha1::derive +//! ( &Password::Plain((PASSWORD.to_owned())) +//! , &SALT[..] +//! , ITERATIONS )?; +//! Ok(secret::Pbkdf2Sha1 { +//! salt: SALT.to_vec(), +//! iterations: ITERATIONS, +//! digest: digest, +//! }) +//! } +//! } +//! } +//! +//! impl_validator_using_provider!(MyValidator, secret::Pbkdf2Sha1); +//! +//! impl Provider for MyValidator { +//! fn provide(&self, identity: &Identity) -> Result { +//! if identity != &Identity::Username(USERNAME.to_owned()) { +//! Err(ProviderError::AuthenticationFailed) +//! } +//! else { +//! let digest = sasl::common::scram::Sha256::derive +//! ( &Password::Plain((PASSWORD.to_owned())) +//! , &SALT[..] +//! , ITERATIONS )?; +//! Ok(secret::Pbkdf2Sha256 { +//! salt: SALT.to_vec(), +//! iterations: ITERATIONS, +//! digest: digest, +//! }) +//! } +//! } +//! } +//! +//! impl_validator_using_provider!(MyValidator, secret::Pbkdf2Sha256); +//! +//! #[derive(Debug, PartialEq)] +//! enum MechanismError { +//! Client(ClientMechanismError), +//! Server(ServerMechanismError), +//! } +//! +//! impl From for MechanismError { +//! fn from(err: ClientMechanismError) -> MechanismError { +//! MechanismError::Client(err) +//! } +//! } +//! +//! impl From for MechanismError { +//! fn from(err: ServerMechanismError) -> MechanismError { +//! MechanismError::Server(err) +//! } +//! } +//! +//! fn finish(cm: &mut CM, sm: &mut SM) -> Result +//! where CM: ClientMechanism, +//! SM: ServerMechanism { +//! let init = cm.initial(); +//! println!("C: {}", String::from_utf8_lossy(&init)); +//! let mut resp = sm.respond(&init)?; +//! loop { +//! let msg; +//! match resp { +//! Response::Proceed(ref data) => { +//! println!("S: {}", String::from_utf8_lossy(&data)); +//! msg = cm.response(data)?; +//! println!("C: {}", String::from_utf8_lossy(&msg)); +//! }, +//! _ => break, +//! } +//! resp = sm.respond(&msg)?; +//! } +//! if let Response::Success(ret, fin) = resp { +//! println!("S: {}", String::from_utf8_lossy(&fin)); +//! cm.success(&fin)?; +//! Ok(ret) +//! } +//! else { +//! unreachable!(); +//! } +//! } +//! +//! fn main() { +//! let mut mech = ServerPlain::new(MyValidator); +//! let expected_response = Response::Success(Identity::Username("user".to_owned()), Vec::new()); +//! assert_eq!(mech.respond(b"\0user\0pencil"), Ok(expected_response)); +//! +//! let mut mech = ServerPlain::new(MyValidator); +//! assert_eq!(mech.respond(b"\0user\0marker"), Err(ServerMechanismError::ValidatorError(ValidatorError::AuthenticationFailed))); +//! +//! let creds = Credentials::default() +//! .with_username(USERNAME) +//! .with_password(PASSWORD); +//! let mut client_mech = ClientPlain::from_credentials(creds.clone()).unwrap(); +//! let mut server_mech = ServerPlain::new(MyValidator); +//! +//! assert_eq!(finish(&mut client_mech, &mut server_mech), Ok(Identity::Username(USERNAME.to_owned()))); +//! +//! let mut client_mech = ClientScram::::from_credentials(creds.clone()).unwrap(); +//! let mut server_mech = ServerScram::::new(MyValidator, ChannelBinding::Unsupported); +//! +//! assert_eq!(finish(&mut client_mech, &mut server_mech), Ok(Identity::Username(USERNAME.to_owned()))); +//! +//! let mut client_mech = ClientScram::::from_credentials(creds.clone()).unwrap(); +//! let mut server_mech = ServerScram::::new(MyValidator, ChannelBinding::Unsupported); +//! +//! assert_eq!(finish(&mut client_mech, &mut server_mech), Ok(Identity::Username(USERNAME.to_owned()))); +//! } +//! ``` +//! +//! # Usage +//! +//! You can use this in your crate by adding this under `dependencies` in your `Cargo.toml`: +//! +//! ```toml,ignore +//! sasl = "*" +//! ``` + +mod error; + +pub mod client; +#[macro_use] +pub mod server; +pub mod common; +pub mod secret; + +pub use crate::error::Error; diff --git a/sasl/src/secret.rs b/sasl/src/secret.rs new file mode 100644 index 0000000000000000000000000000000000000000..bfd5b4e54af4caf3721d518136e3b0046d82d736 --- /dev/null +++ b/sasl/src/secret.rs @@ -0,0 +1,89 @@ +#[cfg(feature = "scram")] +use crate::common::scram::DeriveError; + +pub trait Secret {} + +pub trait Pbkdf2Secret { + fn salt(&self) -> &[u8]; + fn iterations(&self) -> u32; + fn digest(&self) -> &[u8]; +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Plain(pub String); + +impl Secret for Plain {} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Pbkdf2Sha1 { + pub salt: Vec, + pub iterations: u32, + pub digest: Vec, +} + +impl Pbkdf2Sha1 { + #[cfg(feature = "scram")] + pub fn derive(password: &str, salt: &[u8], iterations: u32) -> Result { + use crate::common::scram::{ScramProvider, Sha1}; + use crate::common::Password; + let digest = Sha1::derive(&Password::Plain(password.to_owned()), salt, iterations)?; + Ok(Pbkdf2Sha1 { + salt: salt.to_vec(), + iterations: iterations, + digest: digest, + }) + } +} + +impl Secret for Pbkdf2Sha1 {} + +impl Pbkdf2Secret for Pbkdf2Sha1 { + fn salt(&self) -> &[u8] { + &self.salt + } + fn iterations(&self) -> u32 { + self.iterations + } + fn digest(&self) -> &[u8] { + &self.digest + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Pbkdf2Sha256 { + pub salt: Vec, + pub iterations: u32, + pub digest: Vec, +} + +impl Pbkdf2Sha256 { + #[cfg(feature = "scram")] + pub fn derive( + password: &str, + salt: &[u8], + iterations: u32, + ) -> Result { + use crate::common::scram::{ScramProvider, Sha256}; + use crate::common::Password; + let digest = Sha256::derive(&Password::Plain(password.to_owned()), salt, iterations)?; + Ok(Pbkdf2Sha256 { + salt: salt.to_vec(), + iterations: iterations, + digest: digest, + }) + } +} + +impl Secret for Pbkdf2Sha256 {} + +impl Pbkdf2Secret for Pbkdf2Sha256 { + fn salt(&self) -> &[u8] { + &self.salt + } + fn iterations(&self) -> u32 { + self.iterations + } + fn digest(&self) -> &[u8] { + &self.digest + } +} diff --git a/sasl/src/server/mechanisms/anonymous.rs b/sasl/src/server/mechanisms/anonymous.rs new file mode 100644 index 0000000000000000000000000000000000000000..5a5381e7424a96cb2d457dc38deabf3dcd9a959a --- /dev/null +++ b/sasl/src/server/mechanisms/anonymous.rs @@ -0,0 +1,29 @@ +use crate::common::Identity; +use crate::server::{Mechanism, MechanismError, Response}; + +use getrandom::getrandom; + +pub struct Anonymous; + +impl Anonymous { + pub fn new() -> Anonymous { + Anonymous + } +} + +impl Mechanism for Anonymous { + fn name(&self) -> &str { + "ANONYMOUS" + } + + fn respond(&mut self, payload: &[u8]) -> Result { + if !payload.is_empty() { + return Err(MechanismError::FailedToDecodeMessage); + } + let mut rand = [0u8; 16]; + getrandom(&mut rand)?; + let username = format!("{:02x?}", rand); + let ident = Identity::Username(username); + Ok(Response::Success(ident, Vec::new())) + } +} diff --git a/sasl/src/server/mechanisms/mod.rs b/sasl/src/server/mechanisms/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..1989a580c75f1b57842a53cb1109ce5276546f9f --- /dev/null +++ b/sasl/src/server/mechanisms/mod.rs @@ -0,0 +1,11 @@ +#[cfg(feature = "anonymous")] +mod anonymous; +mod plain; +#[cfg(feature = "scram")] +mod scram; + +#[cfg(feature = "anonymous")] +pub use self::anonymous::Anonymous; +pub use self::plain::Plain; +#[cfg(feature = "scram")] +pub use self::scram::Scram; diff --git a/sasl/src/server/mechanisms/plain.rs b/sasl/src/server/mechanisms/plain.rs new file mode 100644 index 0000000000000000000000000000000000000000..79d090f66eede2a6bc03d688da283a870a59f858 --- /dev/null +++ b/sasl/src/server/mechanisms/plain.rs @@ -0,0 +1,39 @@ +use crate::common::Identity; +use crate::secret; +use crate::server::{Mechanism, MechanismError, Response, Validator}; + +pub struct Plain> { + validator: V, +} + +impl> Plain { + pub fn new(validator: V) -> Plain { + Plain { + validator: validator, + } + } +} + +impl> Mechanism for Plain { + fn name(&self) -> &str { + "PLAIN" + } + + fn respond(&mut self, payload: &[u8]) -> Result { + let mut sp = payload.split(|&b| b == 0); + sp.next(); + let username = sp + .next() + .ok_or_else(|| MechanismError::NoUsernameSpecified)?; + let username = String::from_utf8(username.to_vec()) + .map_err(|_| MechanismError::ErrorDecodingUsername)?; + let password = sp + .next() + .ok_or_else(|| MechanismError::NoPasswordSpecified)?; + let password = String::from_utf8(password.to_vec()) + .map_err(|_| MechanismError::ErrorDecodingPassword)?; + let ident = Identity::Username(username); + self.validator.validate(&ident, &secret::Plain(password))?; + Ok(Response::Success(ident, Vec::new())) + } +} diff --git a/sasl/src/server/mechanisms/scram.rs b/sasl/src/server/mechanisms/scram.rs new file mode 100644 index 0000000000000000000000000000000000000000..41d7ac7ffa1c8597820d6d3f96df8312ebde663b --- /dev/null +++ b/sasl/src/server/mechanisms/scram.rs @@ -0,0 +1,185 @@ +use std::marker::PhantomData; + +use base64; + +use crate::common::scram::{generate_nonce, ScramProvider}; +use crate::common::{parse_frame, xor, ChannelBinding, Identity}; +use crate::secret; +use crate::secret::Pbkdf2Secret; +use crate::server::{Mechanism, MechanismError, Provider, Response}; + +enum ScramState { + Init, + SentChallenge { + initial_client_message: Vec, + initial_server_message: Vec, + gs2_header: Vec, + server_nonce: String, + identity: Identity, + salted_password: Vec, + }, + Done, +} + +pub struct Scram +where + S: ScramProvider, + P: Provider, + S::Secret: secret::Pbkdf2Secret, +{ + name: String, + state: ScramState, + channel_binding: ChannelBinding, + provider: P, + _marker: PhantomData, +} + +impl Scram +where + S: ScramProvider, + P: Provider, + S::Secret: secret::Pbkdf2Secret, +{ + pub fn new(provider: P, channel_binding: ChannelBinding) -> Scram { + Scram { + name: format!("SCRAM-{}", S::name()), + state: ScramState::Init, + channel_binding: channel_binding, + provider: provider, + _marker: PhantomData, + } + } +} + +impl Mechanism for Scram +where + S: ScramProvider, + P: Provider, + S::Secret: secret::Pbkdf2Secret, +{ + fn name(&self) -> &str { + &self.name + } + + fn respond(&mut self, payload: &[u8]) -> Result { + let next_state; + let ret; + match self.state { + ScramState::Init => { + // TODO: really ugly, mostly because parse_frame takes a &[u8] and i don't + // want to double validate utf-8 + // + // NEED TO CHANGE THIS THOUGH. IT'S AWFUL. + let mut commas = 0; + let mut idx = 0; + for &b in payload { + idx += 1; + if b == 0x2C { + commas += 1; + if commas >= 2 { + break; + } + } + } + if commas < 2 { + return Err(MechanismError::FailedToDecodeMessage); + } + let gs2_header = payload[..idx].to_vec(); + let rest = payload[idx..].to_vec(); + // TODO: process gs2 header properly, not this ugly stuff + match self.channel_binding { + ChannelBinding::None | ChannelBinding::Unsupported => { + // Not supported. + if gs2_header[0] != 0x79 { + // ord("y") + return Err(MechanismError::ChannelBindingNotSupported); + } + } + ref other => { + // Supported. + if gs2_header[0] == 0x79 { + // ord("y") + return Err(MechanismError::ChannelBindingIsSupported); + } else if !other.supports("tls-unique") { + // TODO: grab the data + return Err(MechanismError::ChannelBindingMechanismIncorrect); + } + } + } + let frame = + parse_frame(&rest).map_err(|_| MechanismError::CannotDecodeInitialMessage)?; + let username = frame.get("n").ok_or_else(|| MechanismError::NoUsername)?; + let identity = Identity::Username(username.to_owned()); + let client_nonce = frame.get("r").ok_or_else(|| MechanismError::NoNonce)?; + let mut server_nonce = String::new(); + server_nonce += client_nonce; + server_nonce += + &generate_nonce().map_err(|_| MechanismError::FailedToGenerateNonce)?; + let pbkdf2 = self.provider.provide(&identity)?; + let mut buf = Vec::new(); + buf.extend(b"r="); + buf.extend(server_nonce.bytes()); + buf.extend(b",s="); + buf.extend(base64::encode(pbkdf2.salt()).bytes()); + buf.extend(b",i="); + buf.extend(pbkdf2.iterations().to_string().bytes()); + ret = Response::Proceed(buf.clone()); + next_state = ScramState::SentChallenge { + server_nonce: server_nonce, + identity: identity, + salted_password: pbkdf2.digest().to_vec(), + initial_client_message: rest, + initial_server_message: buf, + gs2_header: gs2_header, + }; + } + ScramState::SentChallenge { + ref server_nonce, + ref identity, + ref salted_password, + ref gs2_header, + ref initial_client_message, + ref initial_server_message, + } => { + let frame = + parse_frame(payload).map_err(|_| MechanismError::CannotDecodeResponse)?; + let mut cb_data: Vec = Vec::new(); + cb_data.extend(gs2_header); + cb_data.extend(self.channel_binding.data()); + let mut client_final_message_bare = Vec::new(); + client_final_message_bare.extend(b"c="); + client_final_message_bare.extend(base64::encode(&cb_data).bytes()); + client_final_message_bare.extend(b",r="); + client_final_message_bare.extend(server_nonce.bytes()); + let client_key = S::hmac(b"Client Key", &salted_password)?; + let server_key = S::hmac(b"Server Key", &salted_password)?; + let mut auth_message = Vec::new(); + auth_message.extend(initial_client_message); + auth_message.extend(b","); + auth_message.extend(initial_server_message); + auth_message.extend(b","); + auth_message.extend(client_final_message_bare.clone()); + let stored_key = S::hash(&client_key); + let client_signature = S::hmac(&auth_message, &stored_key)?; + let client_proof = xor(&client_key, &client_signature); + let sent_proof = frame.get("p").ok_or_else(|| MechanismError::NoProof)?; + let sent_proof = + base64::decode(sent_proof).map_err(|_| MechanismError::CannotDecodeProof)?; + if client_proof != sent_proof { + return Err(MechanismError::AuthenticationFailed); + } + let server_signature = S::hmac(&auth_message, &server_key)?; + let mut buf = Vec::new(); + buf.extend(b"v="); + buf.extend(base64::encode(&server_signature).bytes()); + ret = Response::Success(identity.clone(), buf); + next_state = ScramState::Done; + } + ScramState::Done => { + return Err(MechanismError::SaslSessionAlreadyOver); + } + } + self.state = next_state; + Ok(ret) + } +} diff --git a/sasl/src/server/mod.rs b/sasl/src/server/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..897b000d88eb990676e8b532d97d96184432e79b --- /dev/null +++ b/sasl/src/server/mod.rs @@ -0,0 +1,198 @@ +use crate::common::Identity; +use crate::secret::Secret; +use std::fmt; + +#[cfg(feature = "scram")] +use crate::common::scram::DeriveError; + +#[macro_export] +macro_rules! impl_validator_using_provider { + ( $validator:ty, $secret:ty ) => { + impl $crate::server::Validator<$secret> for $validator { + fn validate( + &self, + identity: &$crate::common::Identity, + value: &$secret, + ) -> Result<(), $crate::server::ValidatorError> { + if &(self as &$crate::server::Provider<$secret>).provide(identity)? == value { + Ok(()) + } else { + Err($crate::server::ValidatorError::AuthenticationFailed) + } + } + } + }; +} + +pub trait Provider: Validator { + fn provide(&self, identity: &Identity) -> Result; +} + +pub trait Validator { + fn validate(&self, identity: &Identity, value: &S) -> Result<(), ValidatorError>; +} + +#[derive(Debug, PartialEq)] +pub enum ProviderError { + AuthenticationFailed, + #[cfg(feature = "scram")] + DeriveError(DeriveError), +} + +#[derive(Debug, PartialEq)] +pub enum ValidatorError { + AuthenticationFailed, + ProviderError(ProviderError), +} + +#[derive(Debug, PartialEq)] +pub enum MechanismError { + NoUsernameSpecified, + ErrorDecodingUsername, + NoPasswordSpecified, + ErrorDecodingPassword, + ValidatorError(ValidatorError), + + FailedToDecodeMessage, + ChannelBindingNotSupported, + ChannelBindingIsSupported, + ChannelBindingMechanismIncorrect, + CannotDecodeInitialMessage, + NoUsername, + NoNonce, + FailedToGenerateNonce, + ProviderError(ProviderError), + + CannotDecodeResponse, + #[cfg(feature = "scram")] + InvalidKeyLength(hmac::digest::InvalidLength), + #[cfg(any(feature = "scram", feature = "anonymous"))] + RandomFailure(getrandom::Error), + NoProof, + CannotDecodeProof, + AuthenticationFailed, + SaslSessionAlreadyOver, +} + +#[cfg(feature = "scram")] +impl From for ProviderError { + fn from(err: DeriveError) -> ProviderError { + ProviderError::DeriveError(err) + } +} + +impl From for ValidatorError { + fn from(err: ProviderError) -> ValidatorError { + ValidatorError::ProviderError(err) + } +} + +impl From for MechanismError { + fn from(err: ProviderError) -> MechanismError { + MechanismError::ProviderError(err) + } +} + +impl From for MechanismError { + fn from(err: ValidatorError) -> MechanismError { + MechanismError::ValidatorError(err) + } +} + +#[cfg(feature = "scram")] +impl From for MechanismError { + fn from(err: hmac::digest::InvalidLength) -> MechanismError { + MechanismError::InvalidKeyLength(err) + } +} + +#[cfg(any(feature = "scram", feature = "anonymous"))] +impl From for MechanismError { + fn from(err: getrandom::Error) -> MechanismError { + MechanismError::RandomFailure(err) + } +} + +impl fmt::Display for ProviderError { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + write!(fmt, "provider error") + } +} + +impl fmt::Display for ValidatorError { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + write!(fmt, "validator error") + } +} + +impl fmt::Display for MechanismError { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + match self { + MechanismError::NoUsernameSpecified => write!(fmt, "no username specified"), + MechanismError::ErrorDecodingUsername => write!(fmt, "error decoding username"), + MechanismError::NoPasswordSpecified => write!(fmt, "no password specified"), + MechanismError::ErrorDecodingPassword => write!(fmt, "error decoding password"), + MechanismError::ValidatorError(err) => write!(fmt, "validator error: {}", err), + + MechanismError::FailedToDecodeMessage => write!(fmt, "failed to decode message"), + MechanismError::ChannelBindingNotSupported => { + write!(fmt, "channel binding not supported") + } + MechanismError::ChannelBindingIsSupported => { + write!(fmt, "channel binding is supported") + } + MechanismError::ChannelBindingMechanismIncorrect => { + write!(fmt, "channel binding mechanism is incorrect") + } + MechanismError::CannotDecodeInitialMessage => { + write!(fmt, "can’t decode initial message") + } + MechanismError::NoUsername => write!(fmt, "no username"), + MechanismError::NoNonce => write!(fmt, "no nonce"), + MechanismError::FailedToGenerateNonce => write!(fmt, "failed to generate nonce"), + MechanismError::ProviderError(err) => write!(fmt, "provider error: {}", err), + + MechanismError::CannotDecodeResponse => write!(fmt, "can’t decode response"), + #[cfg(feature = "scram")] + MechanismError::InvalidKeyLength(err) => write!(fmt, "invalid key length: {}", err), + #[cfg(any(feature = "scram", feature = "anonymous"))] + MechanismError::RandomFailure(err) => { + write!(fmt, "failure to get random data: {}", err) + } + MechanismError::NoProof => write!(fmt, "no proof"), + MechanismError::CannotDecodeProof => write!(fmt, "can’t decode proof"), + MechanismError::AuthenticationFailed => write!(fmt, "authentication failed"), + MechanismError::SaslSessionAlreadyOver => write!(fmt, "SASL session already over"), + } + } +} + +impl Error for ProviderError {} + +impl Error for ValidatorError {} + +use std::error::Error; +impl Error for MechanismError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + match self { + MechanismError::ValidatorError(err) => Some(err), + MechanismError::ProviderError(err) => Some(err), + // TODO: figure out how to enable the std feature on this crate. + //MechanismError::InvalidKeyLength(err) => Some(err), + _ => None, + } + } +} + +pub trait Mechanism { + fn name(&self) -> &str; + fn respond(&mut self, payload: &[u8]) -> Result; +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Response { + Success(Identity, Vec), + Proceed(Vec), +} + +pub mod mechanisms;