Merge remote-tracking branch 'foo/master' into sasl-foo

Maxime โ€œpepโ€ Buquet created

Change summary

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(+)

Detailed changes

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.

sasl/Cargo.toml ๐Ÿ”—

@@ -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 }

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.

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/.

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<Anonymous, MechanismError> {
+        if let Secret::None = credentials.secret {
+            Ok(Anonymous)
+        } else {
+            Err(MechanismError::AnonymousRequiresNoCredentials)
+        }
+    }
+}

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;

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<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
+    }
+}

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<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();
+    }
+}

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<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;

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<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",
+        }
+    }
+}

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<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())
+                }
+            }
+        }
+    }
+}

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<RngError> for Error {
+    fn from(err: RngError) -> Error {
+        Error::RngError(err)
+    }
+}

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<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;

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<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
+    }
+}

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<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()))
+    }
+}

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;

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<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()))
+    }
+}

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<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)
+    }
+}

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<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;