sasl/.gitignore ๐
@@ -0,0 +1,2 @@
+target
+Cargo.lock
Maxime โpepโ Buquet created
sasl/.gitignore | 2
sasl/CHANGELOG.md | 12
sasl/Cargo.toml | 28 ++
sasl/LICENSE | 373 +++++++++++++++++++++++++++
sasl/README.md | 26 +
sasl/src/client/mechanisms/anonymous.rs | 31 ++
sasl/src/client/mechanisms/mod.rs | 13
sasl/src/client/mechanisms/plain.rs | 50 +++
sasl/src/client/mechanisms/scram.rs | 246 +++++++++++++++++
sasl/src/client/mod.rs | 115 ++++++++
sasl/src/common/mod.rs | 202 ++++++++++++++
sasl/src/common/scram.rs | 179 ++++++++++++
sasl/src/error.rs | 19 +
sasl/src/lib.rs | 193 +++++++++++++
sasl/src/secret.rs | 89 ++++++
sasl/src/server/mechanisms/anonymous.rs | 29 ++
sasl/src/server/mechanisms/mod.rs | 11
sasl/src/server/mechanisms/plain.rs | 39 ++
sasl/src/server/mechanisms/scram.rs | 185 +++++++++++++
sasl/src/server/mod.rs | 198 ++++++++++++++
20 files changed, 2,040 insertions(+)
@@ -0,0 +1,2 @@
+target
+Cargo.lock
@@ -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.
@@ -0,0 +1,28 @@
+[package]
+name = "sasl"
+version = "0.5.0"
+authors = ["lumi <lumi@pew.im>"]
+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 }
@@ -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.
@@ -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/.
@@ -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<Anonymous, MechanismError> {
+ if let Secret::None = credentials.secret {
+ Ok(Anonymous)
+ } else {
+ Err(MechanismError::AnonymousRequiresNoCredentials)
+ }
+ }
+}
@@ -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;
@@ -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<N: Into<String>, P: Into<String>>(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<Plain, MechanismError> {
+ 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<u8> {
+ let mut auth = Vec::new();
+ auth.push(0);
+ auth.extend(self.username.bytes());
+ auth.push(0);
+ auth.extend(self.password.bytes());
+ auth
+ }
+}
@@ -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<u8>,
+ gs2_header: Vec<u8>,
+ },
+ GotServerData {
+ server_signature: Vec<u8>,
+ },
+}
+
+/// A struct for the SASL SCRAM-* and SCRAM-*-PLUS mechanisms.
+pub struct Scram<S: ScramProvider> {
+ name: String,
+ username: String,
+ password: Password,
+ client_nonce: String,
+ state: ScramState,
+ channel_binding: ChannelBinding,
+ _marker: PhantomData<S>,
+}
+
+impl<S: ScramProvider> Scram<S> {
+ /// 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<N: Into<String>, P: Into<Password>>(
+ username: N,
+ password: P,
+ channel_binding: ChannelBinding,
+ ) -> Result<Scram<S>, 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<N: Into<String>, P: Into<Password>>(
+ username: N,
+ password: P,
+ nonce: String,
+ ) -> Scram<S> {
+ 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<S: ScramProvider> Mechanism for Scram<S> {
+ fn name(&self) -> &str {
+ // TODO: this is quite the workaroundโฆ
+ &self.name
+ }
+
+ fn from_credentials(credentials: Credentials) -> Result<Scram<S>, 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<u8> {
+ 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<Vec<u8>, 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<u8> = 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::<Sha1>::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::<Sha256>::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();
+ }
+}
@@ -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<DeriveError> for MechanismError {
+ fn from(err: DeriveError) -> MechanismError {
+ MechanismError::DeriveError(err)
+ }
+}
+
+#[cfg(feature = "scram")]
+impl From<InvalidLength> 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<Self, MechanismError>
+ where
+ Self: Sized;
+
+ /// Provides initial payload of the SASL mechanism.
+ fn initial(&mut self) -> Vec<u8> {
+ Vec::new()
+ }
+
+ /// Creates a response to the SASL challenge.
+ fn response(&mut self, _challenge: &[u8]) -> Result<Vec<u8>, 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;
@@ -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<String> 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<N: Into<String>>(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<P: Into<String>>(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<S: Into<String>>(password: S) -> Secret {
+ Secret::Password(Password::Plain(password.into()))
+ }
+
+ pub fn password_pbkdf2<S: Into<String>>(
+ method: S,
+ salt: Vec<u8>,
+ iterations: u32,
+ data: Vec<u8>,
+ ) -> 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<u8>,
+ iterations: u32,
+ data: Vec<u8>,
+ },
+}
+
+impl From<String> 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<u8> {
+ 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<HashMap<String, String>, 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<u8>),
+}
+
+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",
+ }
+ }
+}
@@ -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<String, RngError> {
+ 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<u8>;
+
+ /// A function which performs an HMAC using the hash function.
+ fn hmac(data: &[u8], key: &[u8]) -> Result<Vec<u8>, InvalidLength>;
+
+ /// A function which does PBKDF2 key derivation using the hash function.
+ fn derive(data: &Password, salt: &[u8], iterations: u32) -> Result<Vec<u8>, 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<u8> {
+ 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<Vec<u8>, InvalidLength> {
+ type HmacSha1 = Hmac<Sha1_hash>;
+ 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<Vec<u8>, DeriveError> {
+ match *password {
+ Password::Plain(ref plain) => {
+ let mut result = vec![0; 20];
+ pbkdf2::<Hmac<Sha1_hash>>(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<u8> {
+ 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<Vec<u8>, InvalidLength> {
+ type HmacSha256 = Hmac<Sha256_hash>;
+ 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<Vec<u8>, DeriveError> {
+ match *password {
+ Password::Plain(ref plain) => {
+ let mut result = vec![0; 32];
+ pbkdf2::<Hmac<Sha256_hash>>(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())
+ }
+ }
+ }
+ }
+}
@@ -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<RngError> for Error {
+ fn from(err: RngError) -> Error {
+ Error::RngError(err)
+ }
+}
@@ -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<secret::Plain> 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<secret::Pbkdf2Sha1> for MyValidator {
+//! fn provide(&self, identity: &Identity) -> Result<secret::Pbkdf2Sha1, ProviderError> {
+//! 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<secret::Pbkdf2Sha256> for MyValidator {
+//! fn provide(&self, identity: &Identity) -> Result<secret::Pbkdf2Sha256, ProviderError> {
+//! 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<ClientMechanismError> for MechanismError {
+//! fn from(err: ClientMechanismError) -> MechanismError {
+//! MechanismError::Client(err)
+//! }
+//! }
+//!
+//! impl From<ServerMechanismError> for MechanismError {
+//! fn from(err: ServerMechanismError) -> MechanismError {
+//! MechanismError::Server(err)
+//! }
+//! }
+//!
+//! fn finish<CM, SM>(cm: &mut CM, sm: &mut SM) -> Result<Identity, MechanismError>
+//! 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::<Sha1>::from_credentials(creds.clone()).unwrap();
+//! let mut server_mech = ServerScram::<Sha1, _>::new(MyValidator, ChannelBinding::Unsupported);
+//!
+//! assert_eq!(finish(&mut client_mech, &mut server_mech), Ok(Identity::Username(USERNAME.to_owned())));
+//!
+//! let mut client_mech = ClientScram::<Sha256>::from_credentials(creds.clone()).unwrap();
+//! let mut server_mech = ServerScram::<Sha256, _>::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;
@@ -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<u8>,
+ pub iterations: u32,
+ pub digest: Vec<u8>,
+}
+
+impl Pbkdf2Sha1 {
+ #[cfg(feature = "scram")]
+ pub fn derive(password: &str, salt: &[u8], iterations: u32) -> Result<Pbkdf2Sha1, DeriveError> {
+ 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<u8>,
+ pub iterations: u32,
+ pub digest: Vec<u8>,
+}
+
+impl Pbkdf2Sha256 {
+ #[cfg(feature = "scram")]
+ pub fn derive(
+ password: &str,
+ salt: &[u8],
+ iterations: u32,
+ ) -> Result<Pbkdf2Sha256, DeriveError> {
+ 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
+ }
+}
@@ -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<Response, MechanismError> {
+ 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()))
+ }
+}
@@ -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;
@@ -0,0 +1,39 @@
+use crate::common::Identity;
+use crate::secret;
+use crate::server::{Mechanism, MechanismError, Response, Validator};
+
+pub struct Plain<V: Validator<secret::Plain>> {
+ validator: V,
+}
+
+impl<V: Validator<secret::Plain>> Plain<V> {
+ pub fn new(validator: V) -> Plain<V> {
+ Plain {
+ validator: validator,
+ }
+ }
+}
+
+impl<V: Validator<secret::Plain>> Mechanism for Plain<V> {
+ fn name(&self) -> &str {
+ "PLAIN"
+ }
+
+ fn respond(&mut self, payload: &[u8]) -> Result<Response, MechanismError> {
+ 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()))
+ }
+}
@@ -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<u8>,
+ initial_server_message: Vec<u8>,
+ gs2_header: Vec<u8>,
+ server_nonce: String,
+ identity: Identity,
+ salted_password: Vec<u8>,
+ },
+ Done,
+}
+
+pub struct Scram<S, P>
+where
+ S: ScramProvider,
+ P: Provider<S::Secret>,
+ S::Secret: secret::Pbkdf2Secret,
+{
+ name: String,
+ state: ScramState,
+ channel_binding: ChannelBinding,
+ provider: P,
+ _marker: PhantomData<S>,
+}
+
+impl<S, P> Scram<S, P>
+where
+ S: ScramProvider,
+ P: Provider<S::Secret>,
+ S::Secret: secret::Pbkdf2Secret,
+{
+ pub fn new(provider: P, channel_binding: ChannelBinding) -> Scram<S, P> {
+ Scram {
+ name: format!("SCRAM-{}", S::name()),
+ state: ScramState::Init,
+ channel_binding: channel_binding,
+ provider: provider,
+ _marker: PhantomData,
+ }
+ }
+}
+
+impl<S, P> Mechanism for Scram<S, P>
+where
+ S: ScramProvider,
+ P: Provider<S::Secret>,
+ S::Secret: secret::Pbkdf2Secret,
+{
+ fn name(&self) -> &str {
+ &self.name
+ }
+
+ fn respond(&mut self, payload: &[u8]) -> Result<Response, MechanismError> {
+ 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<u8> = 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)
+ }
+}
@@ -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<S: Secret>: Validator<S> {
+ fn provide(&self, identity: &Identity) -> Result<S, ProviderError>;
+}
+
+pub trait Validator<S: Secret> {
+ 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<DeriveError> for ProviderError {
+ fn from(err: DeriveError) -> ProviderError {
+ ProviderError::DeriveError(err)
+ }
+}
+
+impl From<ProviderError> for ValidatorError {
+ fn from(err: ProviderError) -> ValidatorError {
+ ValidatorError::ProviderError(err)
+ }
+}
+
+impl From<ProviderError> for MechanismError {
+ fn from(err: ProviderError) -> MechanismError {
+ MechanismError::ProviderError(err)
+ }
+}
+
+impl From<ValidatorError> for MechanismError {
+ fn from(err: ValidatorError) -> MechanismError {
+ MechanismError::ValidatorError(err)
+ }
+}
+
+#[cfg(feature = "scram")]
+impl From<hmac::digest::InvalidLength> for MechanismError {
+ fn from(err: hmac::digest::InvalidLength) -> MechanismError {
+ MechanismError::InvalidKeyLength(err)
+ }
+}
+
+#[cfg(any(feature = "scram", feature = "anonymous"))]
+impl From<getrandom::Error> 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<Response, MechanismError>;
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum Response {
+ Success(Identity, Vec<u8>),
+ Proceed(Vec<u8>),
+}
+
+pub mod mechanisms;