Merge remote-tracking branch 'foo/master' into sasl-foo
This commit is contained in:
commit
b81c7e23df
20 changed files with 2040 additions and 0 deletions
2
sasl/.gitignore
vendored
Normal file
2
sasl/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
target
|
||||
Cargo.lock
|
||||
12
sasl/CHANGELOG.md
Normal file
12
sasl/CHANGELOG.md
Normal file
|
|
@ -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.
|
||||
28
sasl/Cargo.toml
Normal file
28
sasl/Cargo.toml
Normal file
|
|
@ -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 }
|
||||
373
sasl/LICENSE
Normal file
373
sasl/LICENSE
Normal file
|
|
@ -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.
|
||||
26
sasl/README.md
Normal file
26
sasl/README.md
Normal file
|
|
@ -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/.
|
||||
31
sasl/src/client/mechanisms/anonymous.rs
Normal file
31
sasl/src/client/mechanisms/anonymous.rs
Normal file
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
13
sasl/src/client/mechanisms/mod.rs
Normal file
13
sasl/src/client/mechanisms/mod.rs
Normal file
|
|
@ -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;
|
||||
50
sasl/src/client/mechanisms/plain.rs
Normal file
50
sasl/src/client/mechanisms/plain.rs
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
246
sasl/src/client/mechanisms/scram.rs
Normal file
246
sasl/src/client/mechanisms/scram.rs
Normal file
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
115
sasl/src/client/mod.rs
Normal file
115
sasl/src/client/mod.rs
Normal file
|
|
@ -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;
|
||||
202
sasl/src/common/mod.rs
Normal file
202
sasl/src/common/mod.rs
Normal file
|
|
@ -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",
|
||||
}
|
||||
}
|
||||
}
|
||||
179
sasl/src/common/scram.rs
Normal file
179
sasl/src/common/scram.rs
Normal file
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
19
sasl/src/error.rs
Normal file
19
sasl/src/error.rs
Normal file
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
193
sasl/src/lib.rs
Normal file
193
sasl/src/lib.rs
Normal file
|
|
@ -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;
|
||||
89
sasl/src/secret.rs
Normal file
89
sasl/src/secret.rs
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
29
sasl/src/server/mechanisms/anonymous.rs
Normal file
29
sasl/src/server/mechanisms/anonymous.rs
Normal file
|
|
@ -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()))
|
||||
}
|
||||
}
|
||||
11
sasl/src/server/mechanisms/mod.rs
Normal file
11
sasl/src/server/mechanisms/mod.rs
Normal file
|
|
@ -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;
|
||||
39
sasl/src/server/mechanisms/plain.rs
Normal file
39
sasl/src/server/mechanisms/plain.rs
Normal file
|
|
@ -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()))
|
||||
}
|
||||
}
|
||||
185
sasl/src/server/mechanisms/scram.rs
Normal file
185
sasl/src/server/mechanisms/scram.rs
Normal file
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
198
sasl/src/server/mod.rs
Normal file
198
sasl/src/server/mod.rs
Normal file
|
|
@ -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;
|
||||
Loading…
Reference in a new issue