Detailed changes
@@ -1,8 +1,7 @@
//! Provides the SASL "ANONYMOUS" mechanism.
-use Credentials;
-use Mechanism;
-use Secret;
+use client::Mechanism;
+use common::{Credentials, Secret};
/// A struct for the SASL ANONYMOUS mechanism.
pub struct Anonymous;
@@ -6,4 +6,4 @@ mod scram;
pub use self::anonymous::Anonymous;
pub use self::plain::Plain;
-pub use self::scram::{Scram, ScramProvider, Sha1, Sha256};
+pub use self::scram::Scram;
@@ -1,8 +1,7 @@
//! Provides the SASL "PLAIN" mechanism.
-use Credentials;
-use Mechanism;
-use Secret;
+use client::Mechanism;
+use common::{Credentials, Identity, Password, Secret};
/// A struct for the SASL PLAIN mechanism.
pub struct Plain {
@@ -29,14 +28,14 @@ impl Mechanism for Plain {
}
fn from_credentials(credentials: Credentials) -> Result<Plain, String> {
- if let Secret::Password(password) = credentials.secret {
- if let Some(username) = credentials.username {
+ if let Secret::Password(Password::Plain(password)) = credentials.secret {
+ if let Identity::Username(username) = credentials.identity {
Ok(Plain::new(username, password))
} else {
Err("PLAIN requires a username".to_owned())
}
} else {
- Err("PLAIN requires a password".to_owned())
+ Err("PLAIN requires a plaintext password".to_owned())
}
}
@@ -2,140 +2,14 @@
use base64;
-use ChannelBinding;
-use Credentials;
-use Mechanism;
-use Secret;
+use client::Mechanism;
+use common::scram::{generate_nonce, ScramProvider};
+use common::{parse_frame, xor, ChannelBinding, Credentials, Identity, Password, Secret};
use error::Error;
-use openssl::error::ErrorStack;
-use openssl::hash::hash;
-use openssl::hash::MessageDigest;
-use openssl::pkcs5::pbkdf2_hmac;
-use openssl::pkey::PKey;
-use openssl::rand::rand_bytes;
-use openssl::sign::Signer;
-
use std::marker::PhantomData;
-use std::collections::HashMap;
-
-use std::string::FromUtf8Error;
-
-#[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]
- );
-}
-
-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
-}
-
-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)
-}
-
-fn generate_nonce() -> Result<String, ErrorStack> {
- let mut data = vec![0; 32];
- rand_bytes(&mut data)?;
- Ok(base64::encode(&data))
-}
-
-/// A trait which defines the needed methods for SCRAM.
-pub trait ScramProvider {
- /// 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]) -> Vec<u8>;
-
- /// A function which does PBKDF2 key derivation using the hash function.
- fn derive(data: &[u8], salt: &[u8], iterations: usize) -> Vec<u8>;
-}
-
-/// A `ScramProvider` which provides SCRAM-SHA-1 and SCRAM-SHA-1-PLUS
-pub struct Sha1;
-
-impl ScramProvider for Sha1 {
- // TODO: look at all these unwraps
- fn name() -> &'static str {
- "SHA-1"
- }
-
- fn hash(data: &[u8]) -> Vec<u8> {
- hash(MessageDigest::sha1(), data).unwrap()
- }
-
- fn hmac(data: &[u8], key: &[u8]) -> Vec<u8> {
- let pkey = PKey::hmac(key).unwrap();
- let mut signer = Signer::new(MessageDigest::sha1(), &pkey).unwrap();
- signer.update(data).unwrap();
- signer.finish().unwrap()
- }
-
- fn derive(data: &[u8], salt: &[u8], iterations: usize) -> Vec<u8> {
- let mut result = vec![0; 20];
- pbkdf2_hmac(data, salt, iterations, MessageDigest::sha1(), &mut result).unwrap();
- result
- }
-}
-
-/// A `ScramProvider` which provides SCRAM-SHA-256 and SCRAM-SHA-256-PLUS
-pub struct Sha256;
-
-impl ScramProvider for Sha256 {
- // TODO: look at all these unwraps
- fn name() -> &'static str {
- "SHA-256"
- }
-
- fn hash(data: &[u8]) -> Vec<u8> {
- hash(MessageDigest::sha256(), data).unwrap()
- }
-
- fn hmac(data: &[u8], key: &[u8]) -> Vec<u8> {
- let pkey = PKey::hmac(key).unwrap();
- let mut signer = Signer::new(MessageDigest::sha256(), &pkey).unwrap();
- signer.update(data).unwrap();
- signer.finish().unwrap()
- }
-
- fn derive(data: &[u8], salt: &[u8], iterations: usize) -> Vec<u8> {
- let mut result = vec![0; 32];
- pbkdf2_hmac(data, salt, iterations, MessageDigest::sha256(), &mut result).unwrap();
- result
- }
-}
-
enum ScramState {
Init,
SentInitialMessage {
@@ -151,7 +25,7 @@ enum ScramState {
pub struct Scram<S: ScramProvider> {
name: String,
username: String,
- password: String,
+ password: Password,
client_nonce: String,
state: ScramState,
channel_binding: ChannelBinding,
@@ -164,7 +38,7 @@ impl<S: ScramProvider> Scram<S> {
///
/// 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>>(
+ pub fn new<N: Into<String>, P: Into<Password>>(
username: N,
password: P,
channel_binding: ChannelBinding,
@@ -183,7 +57,7 @@ impl<S: ScramProvider> Scram<S> {
// Used for testing.
#[doc(hidden)]
#[cfg(test)]
- pub fn new_with_nonce<N: Into<String>, P: Into<String>>(
+ pub fn new_with_nonce<N: Into<String>, P: Into<Password>>(
username: N,
password: P,
nonce: String,
@@ -208,7 +82,7 @@ impl<S: ScramProvider> Mechanism for Scram<S> {
fn from_credentials(credentials: Credentials) -> Result<Scram<S>, String> {
if let Secret::Password(password) = credentials.secret {
- if let Some(username) = credentials.username {
+ if let Identity::Username(username) = credentials.identity {
Scram::new(username, password, credentials.channel_binding)
.map_err(|_| "can't generate nonce".to_owned())
} else {
@@ -262,7 +136,7 @@ impl<S: ScramProvider> Mechanism for Scram<S> {
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.as_bytes(), &salt, iterations);
+ 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();
@@ -271,6 +145,7 @@ impl<S: ScramProvider> Mechanism for Scram<S> {
auth_message.extend(challenge);
auth_message.push(b',');
auth_message.extend(&client_final_message_bare);
+ println!("_ {}", String::from_utf8_lossy(&auth_message));
let stored_key = S::hash(&client_key);
let client_signature = S::hmac(&auth_message, &stored_key);
let client_proof = xor(&client_key, &client_signature);
@@ -315,9 +190,9 @@ impl<S: ScramProvider> Mechanism for Scram<S> {
#[cfg(test)]
mod tests {
- use Mechanism;
-
- use super::*;
+ use client::mechanisms::Scram;
+ use client::Mechanism;
+ use common::scram::{Sha1, Sha256};
#[test]
fn scram_sha1_works() {
@@ -0,0 +1,29 @@
+use common::Credentials;
+
+/// 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, String>
+ where
+ Self: Sized;
+
+ /// Provides initial payload of the SASL mechanism.
+ fn initial(&mut self) -> Result<Vec<u8>, String> {
+ Ok(Vec::new())
+ }
+
+ /// Creates a response to the SASL challenge.
+ fn response(&mut self, _challenge: &[u8]) -> Result<Vec<u8>, String> {
+ Ok(Vec::new())
+ }
+
+ /// Verifies the server success response, if there is one.
+ fn success(&mut self, _data: &[u8]) -> Result<(), String> {
+ Ok(())
+ }
+}
+
+pub mod mechanisms;
@@ -0,0 +1,201 @@
+use std::collections::HashMap;
+
+use std::convert::From;
+
+use std::string::FromUtf8Error;
+
+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: usize,
+ 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: usize,
+ data: Vec<u8>,
+ },
+}
+
+impl From<String> for Password {
+ fn from(s: String) -> Password {
+ Password::Plain(s)
+ }
+}
+
+impl<'a> From<&'a str> for Password {
+ fn from(s: &'a str) -> Password {
+ Password::Plain(s.to_owned())
+ }
+}
+
+#[cfg(test)]
+#[test]
+fn xor_works() {
+ assert_eq!(
+ xor(
+ &[135, 94, 53, 134, 73, 233, 140, 221, 150, 12, 96, 111, 54, 66, 11, 76],
+ &[163, 9, 122, 180, 107, 44, 22, 252, 248, 134, 112, 82, 84, 122, 56, 209]
+ ),
+ &[36, 87, 79, 50, 34, 197, 154, 33, 110, 138, 16, 61, 98, 56, 51, 157]
+ );
+}
+
+#[doc(hidden)]
+pub fn xor(a: &[u8], b: &[u8]) -> Vec<u8> {
+ assert_eq!(a.len(), b.len());
+ let mut ret = Vec::with_capacity(a.len());
+ for (a, b) in a.into_iter().zip(b) {
+ ret.push(a ^ b);
+ }
+ ret
+}
+
+#[doc(hidden)]
+pub fn parse_frame(frame: &[u8]) -> Result<HashMap<String, String>, FromUtf8Error> {
+ let inner = String::from_utf8(frame.to_owned())?;
+ let mut ret = HashMap::new();
+ for s in inner.split(',') {
+ let mut tmp = s.splitn(2, '=');
+ let key = tmp.next();
+ let val = tmp.next();
+ match (key, val) {
+ (Some(k), Some(v)) => {
+ ret.insert(k.to_owned(), v.to_owned());
+ }
+ _ => (),
+ }
+ }
+ Ok(ret)
+}
+
+/// Channel binding configuration.
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub enum ChannelBinding {
+ /// No channel binding data.
+ None,
+ /// Advertise that the client does not think the server supports channel binding.
+ Unsupported,
+ /// p=tls-unique channel binding data.
+ TlsUnique(Vec<u8>),
+}
+
+impl ChannelBinding {
+ /// Return the gs2 header for this channel binding mechanism.
+ pub fn header(&self) -> &[u8] {
+ match *self {
+ ChannelBinding::None => b"n,,",
+ ChannelBinding::Unsupported => b"y,,",
+ ChannelBinding::TlsUnique(_) => b"p=tls-unique,,",
+ }
+ }
+
+ /// Return the channel binding data for this channel binding mechanism.
+ pub fn data(&self) -> &[u8] {
+ match *self {
+ ChannelBinding::None => &[],
+ ChannelBinding::Unsupported => &[],
+ ChannelBinding::TlsUnique(ref data) => data,
+ }
+ }
+
+ /// Checks whether this channel binding mechanism is supported.
+ pub fn supports(&self, mechanism: &str) -> bool {
+ match *self {
+ ChannelBinding::None => false,
+ ChannelBinding::Unsupported => false,
+ ChannelBinding::TlsUnique(_) => mechanism == "tls-unique",
+ }
+ }
+}
@@ -0,0 +1,155 @@
+use openssl::error::ErrorStack;
+use openssl::hash::hash;
+use openssl::hash::MessageDigest;
+use openssl::pkcs5::pbkdf2_hmac;
+use openssl::pkey::PKey;
+use openssl::rand::rand_bytes;
+use openssl::sign::Signer;
+
+use common::Password;
+
+use base64;
+
+/// Generate a nonce for SCRAM authentication.
+pub fn generate_nonce() -> Result<String, ErrorStack> {
+ let mut data = vec![0; 32];
+ rand_bytes(&mut data)?;
+ Ok(base64::encode(&data))
+}
+
+/// A trait which defines the needed methods for SCRAM.
+pub trait ScramProvider {
+ /// 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]) -> Vec<u8>;
+
+ /// A function which does PBKDF2 key derivation using the hash function.
+ fn derive(data: &Password, salt: &[u8], iterations: usize) -> Result<Vec<u8>, String>;
+}
+
+/// A `ScramProvider` which provides SCRAM-SHA-1 and SCRAM-SHA-1-PLUS
+pub struct Sha1;
+
+impl ScramProvider for Sha1 {
+ // TODO: look at all these unwraps
+ fn name() -> &'static str {
+ "SHA-1"
+ }
+
+ fn hash(data: &[u8]) -> Vec<u8> {
+ hash(MessageDigest::sha1(), data).unwrap()
+ }
+
+ fn hmac(data: &[u8], key: &[u8]) -> Vec<u8> {
+ let pkey = PKey::hmac(key).unwrap();
+ let mut signer = Signer::new(MessageDigest::sha1(), &pkey).unwrap();
+ signer.update(data).unwrap();
+ signer.finish().unwrap()
+ }
+
+ fn derive(password: &Password, salt: &[u8], iterations: usize) -> Result<Vec<u8>, String> {
+ match *password {
+ Password::Plain(ref plain) => {
+ let mut result = vec![0; 20];
+ pbkdf2_hmac(
+ plain.as_bytes(),
+ salt,
+ iterations,
+ MessageDigest::sha1(),
+ &mut result,
+ )
+ .unwrap();
+ Ok(result)
+ }
+ Password::Pbkdf2 {
+ ref method,
+ salt: ref my_salt,
+ iterations: my_iterations,
+ ref data,
+ } => {
+ if method != Self::name() {
+ Err(format!(
+ "incompatible hashing method, {} is not {}",
+ method,
+ Self::name()
+ ))
+ } else if my_salt == &salt {
+ Err(format!("incorrect salt"))
+ } else if my_iterations == iterations {
+ Err(format!(
+ "incompatible iteration count, {} is not {}",
+ 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 {
+ // TODO: look at all these unwraps
+ fn name() -> &'static str {
+ "SHA-256"
+ }
+
+ fn hash(data: &[u8]) -> Vec<u8> {
+ hash(MessageDigest::sha256(), data).unwrap()
+ }
+
+ fn hmac(data: &[u8], key: &[u8]) -> Vec<u8> {
+ let pkey = PKey::hmac(key).unwrap();
+ let mut signer = Signer::new(MessageDigest::sha256(), &pkey).unwrap();
+ signer.update(data).unwrap();
+ signer.finish().unwrap()
+ }
+
+ fn derive(password: &Password, salt: &[u8], iterations: usize) -> Result<Vec<u8>, String> {
+ match *password {
+ Password::Plain(ref plain) => {
+ let mut result = vec![0; 32];
+ pbkdf2_hmac(
+ plain.as_bytes(),
+ salt,
+ iterations,
+ MessageDigest::sha256(),
+ &mut result,
+ )
+ .unwrap();
+ Ok(result)
+ }
+ Password::Pbkdf2 {
+ ref method,
+ salt: ref my_salt,
+ iterations: my_iterations,
+ ref data,
+ } => {
+ if method != Self::name() {
+ Err(format!(
+ "incompatible hashing method, {} is not {}",
+ method,
+ Self::name()
+ ))
+ } else if my_salt == &salt {
+ Err(format!("incorrect salt"))
+ } else if my_iterations == iterations {
+ Err(format!(
+ "incompatible iteration count, {} is not {}",
+ my_iterations, iterations
+ ))
+ } else {
+ Ok(data.to_vec())
+ }
+ }
+ }
+ }
+}
@@ -1,12 +1,15 @@
-#![deny(missing_docs)]
+//#![deny(missing_docs)]
//! This crate provides a framework for SASL authentication and a few authentication mechanisms.
//!
//! # Examples
//!
+//! ## Simple client-sided usage
+//!
//! ```rust
-//! use sasl::{Credentials, Mechanism, Error};
-//! use sasl::mechanisms::Plain;
+//! use sasl::client::Mechanism;
+//! use sasl::common::Credentials;
+//! use sasl::client::mechanisms::Plain;
//!
//! let creds = Credentials::default()
//! .with_username("user")
@@ -19,7 +22,98 @@
//! assert_eq!(initial_data, b"\0user\0pencil");
//! ```
//!
-//! You may look at the tests of `mechanisms/scram.rs` for examples of more advanced usage.
+//! ## More complex usage
+//!
+//! ```rust
+//! use sasl::server::{Validator, Mechanism as ServerMechanism, Response};
+//! use sasl::server::mechanisms::{Plain as ServerPlain, Scram as ServerScram};
+//! use sasl::client::Mechanism as ClientMechanism;
+//! use sasl::client::mechanisms::{Plain as ClientPlain, Scram as ClientScram};
+//! use sasl::common::{Identity, Credentials, Secret, Password, ChannelBinding};
+//! use sasl::common::scram::{ScramProvider, Sha1, Sha256};
+//!
+//! const USERNAME: &'static str = "user";
+//! const PASSWORD: &'static str = "pencil";
+//! const SALT: [u8; 8] = [35, 71, 92, 105, 212, 219, 114, 93];
+//! const ITERATIONS: usize = 4096;
+//!
+//! struct MyValidator;
+//!
+//! impl Validator for MyValidator {
+//! fn validate_credentials(&self, creds: &Credentials) -> Result<Identity, String> {
+//! if creds.identity != Identity::Username(USERNAME.to_owned()) {
+//! Err("authentication failure".to_owned())
+//! }
+//! else if creds.secret != Secret::password_plain(PASSWORD) {
+//! Err("authentication failure".to_owned())
+//! }
+//! else {
+//! Ok(creds.identity.clone())
+//! }
+//! }
+//!
+//! fn request_pbkdf2<S: ScramProvider>(&self) -> Result<(Vec<u8>, usize, Vec<u8>), String> {
+//! Ok( ( SALT.to_vec()
+//! , ITERATIONS
+//! , S::derive(&Password::Plain(PASSWORD.to_owned()), &SALT, ITERATIONS)? ) )
+//! }
+//! }
+//!
+//! 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("authentication failure".to_owned()));
+//!
+//! let creds = Credentials::default()
+//! .with_username(USERNAME)
+//! .with_password(PASSWORD);
+//!
+//! fn finish<CM, SM, V>(cm: &mut CM, sm: &mut SM) -> Result<Identity, String>
+//! where CM: ClientMechanism,
+//! SM: ServerMechanism<V>,
+//! V: Validator {
+//! 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!();
+//! }
+//! }
+//!
+//! 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
//!
@@ -34,113 +128,8 @@ extern crate openssl;
mod error;
-pub use error::Error;
-
-/// A struct containing SASL credentials.
-#[derive(Clone, Debug)]
-pub struct Credentials {
- /// The requested username.
- pub username: Option<String>,
- /// 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 {
- username: None,
- secret: Secret::None,
- channel_binding: ChannelBinding::None,
- }
- }
-}
-
-impl Credentials {
- /// Creates a new Credentials with the specified username.
- pub fn with_username<N: Into<String>>(mut self, username: N) -> Credentials {
- self.username = Some(username.into());
- self
- }
-
- /// Creates a new Credentials with the specified password.
- pub fn with_password<P: Into<String>>(mut self, password: P) -> Credentials {
- self.secret = Secret::Password(password.into());
- 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
- }
-}
-
-/// 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,,",
- }
- }
+pub mod client;
+pub mod common;
+pub mod server;
- /// 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,
- }
- }
-}
-
-/// Represents a SASL secret, like a password.
-#[derive(Clone, Debug, PartialEq, Eq)]
-pub enum Secret {
- /// No extra data needed.
- None,
- /// Password required.
- Password(String),
-}
-
-/// 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, String>
- where
- Self: Sized;
-
- /// Provides initial payload of the SASL mechanism.
- fn initial(&mut self) -> Result<Vec<u8>, String> {
- Ok(Vec::new())
- }
-
- /// Creates a response to the SASL challenge.
- fn response(&mut self, _challenge: &[u8]) -> Result<Vec<u8>, String> {
- Ok(Vec::new())
- }
-
- /// Verifies the server success response, if there is one.
- fn success(&mut self, _data: &[u8]) -> Result<(), String> {
- Ok(())
- }
-}
-
-pub mod mechanisms;
+pub use error::Error;
@@ -0,0 +1,13 @@
+use server::Mechanism;
+use common::{Secret, Credentials, Password};
+
+pub struct Plain {
+ password: String,
+}
+
+impl<V: Validator> Mechanism<V> for Plain {
+ fn name(&self) -> &str { "PLAIN" }
+
+ fn from_initial_message(validator: &V, msg: &[u8]) -> Result<(Self, String), String> {
+ }
+}
@@ -0,0 +1,239 @@
+use common::scram::ScramProvider;
+use common::{Credentials, Identity};
+
+pub trait Validator {
+ fn validate_credentials(&self, credentials: &Credentials) -> Result<Identity, String>;
+ fn request_pbkdf2<S: ScramProvider>(&self) -> Result<(Vec<u8>, usize, Vec<u8>), String>;
+}
+
+pub trait Mechanism<V: Validator> {
+ fn name(&self) -> &str;
+ fn respond(&mut self, payload: &[u8]) -> Result<Response, String>;
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum Response {
+ Success(Identity, Vec<u8>),
+ Proceed(Vec<u8>),
+}
+
+pub mod mechanisms {
+ mod plain {
+ use common::{ChannelBinding, Credentials, Identity, Secret};
+ use server::{Mechanism, Response, Validator};
+
+ pub struct Plain<V: Validator> {
+ validator: V,
+ }
+
+ impl<V: Validator> Plain<V> {
+ pub fn new(validator: V) -> Plain<V> {
+ Plain {
+ validator: validator,
+ }
+ }
+ }
+
+ impl<V: Validator> Mechanism<V> for Plain<V> {
+ fn name(&self) -> &str {
+ "PLAIN"
+ }
+
+ fn respond(&mut self, payload: &[u8]) -> Result<Response, String> {
+ let mut sp = payload.split(|&b| b == 0);
+ sp.next();
+ let username = sp
+ .next()
+ .ok_or_else(|| "no username specified".to_owned())?;
+ let username =
+ String::from_utf8(username.to_vec()).map_err(|_| "error decoding username")?;
+ let password = sp
+ .next()
+ .ok_or_else(|| "no password specified".to_owned())?;
+ let password =
+ String::from_utf8(password.to_vec()).map_err(|_| "error decoding password")?;
+ let creds = Credentials {
+ identity: Identity::Username(username),
+ secret: Secret::password_plain(password),
+ channel_binding: ChannelBinding::None,
+ };
+ let ret = self.validator.validate_credentials(&creds)?;
+ Ok(Response::Success(ret, Vec::new()))
+ }
+ }
+ }
+
+ mod scram {
+ use std::marker::PhantomData;
+
+ use base64;
+
+ use common::scram::{generate_nonce, ScramProvider};
+ use common::{parse_frame, xor, ChannelBinding, Credentials, Identity, Secret};
+ use server::{Mechanism, Response, Validator};
+
+ enum ScramState {
+ Init,
+ SentChallenge {
+ initial_client_message: Vec<u8>,
+ initial_server_message: Vec<u8>,
+ gs2_header: Vec<u8>,
+ server_nonce: String,
+ username: String,
+ salted_password: Vec<u8>,
+ },
+ Done,
+ }
+
+ pub struct Scram<S: ScramProvider, V: Validator> {
+ name: String,
+ state: ScramState,
+ channel_binding: ChannelBinding,
+ validator: V,
+ _marker: PhantomData<S>,
+ }
+
+ impl<S: ScramProvider, V: Validator> Scram<S, V> {
+ pub fn new(validator: V, channel_binding: ChannelBinding) -> Scram<S, V> {
+ Scram {
+ name: format!("SCRAM-{}", S::name()),
+ state: ScramState::Init,
+ channel_binding: channel_binding,
+ validator: validator,
+ _marker: PhantomData,
+ }
+ }
+ }
+
+ impl<S: ScramProvider, V: Validator> Mechanism<V> for Scram<S, V> {
+ fn name(&self) -> &str {
+ &self.name
+ }
+
+ fn respond(&mut self, payload: &[u8]) -> Result<Response, String> {
+ 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("failed to decode message".to_owned());
+ }
+ 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("channel binding not supported".to_owned());
+ }
+ }
+ ref other => {
+ // Supported.
+ if gs2_header[0] == 0x79 {
+ // ord("y")
+ return Err("channel binding is supported".to_owned());
+ } else if !other.supports("tls-unique") {
+ // TODO: grab the data
+ return Err("channel binding mechanism incorrect".to_owned());
+ }
+ }
+ }
+ let frame = parse_frame(&rest)
+ .map_err(|_| "can't decode initial message".to_owned())?;
+ let username = frame.get("n").ok_or_else(|| "no username".to_owned())?;
+ let client_nonce = frame.get("r").ok_or_else(|| "no nonce".to_owned())?;
+ let mut server_nonce = String::new();
+ server_nonce += client_nonce;
+ server_nonce +=
+ &generate_nonce().map_err(|_| "failed to generate nonce".to_owned())?;
+ let (salt, iterations, data) = self.validator.request_pbkdf2::<S>()?;
+ let mut buf = Vec::new();
+ buf.extend(b"r=");
+ buf.extend(server_nonce.bytes());
+ buf.extend(b",s=");
+ buf.extend(base64::encode(&salt).bytes());
+ buf.extend(b",i=");
+ buf.extend(iterations.to_string().bytes());
+ ret = Response::Proceed(buf.clone());
+ next_state = ScramState::SentChallenge {
+ server_nonce: server_nonce,
+ username: username.to_owned(),
+ salted_password: data,
+ initial_client_message: rest,
+ initial_server_message: buf,
+ gs2_header: gs2_header,
+ };
+ }
+ ScramState::SentChallenge {
+ server_nonce: ref server_nonce,
+ username: ref username,
+ salted_password: ref salted_password,
+ gs2_header: ref gs2_header,
+ initial_client_message: ref initial_client_message,
+ initial_server_message: ref initial_server_message,
+ } => {
+ let frame =
+ parse_frame(payload).map_err(|_| "can't decode response".to_owned())?;
+ 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 stored_key = S::hash(&client_key);
+ 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(|| "no proof".to_owned())?;
+ let sent_proof = base64::decode(sent_proof)
+ .map_err(|_| "can't decode proof".to_owned())?;
+ if client_proof != sent_proof {
+ return Err("authentication failed".to_owned());
+ }
+ 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::Username(username.to_owned()), buf);
+ next_state = ScramState::Done;
+ }
+ ScramState::Done => {
+ return Err("sasl session is already over".to_owned());
+ }
+ }
+ self.state = next_state;
+ Ok(ret)
+ }
+ }
+ }
+
+ pub use self::plain::Plain;
+ pub use self::scram::Scram;
+}