From 4b9f2376af08a829922d16567bddef49b0487f6a Mon Sep 17 00:00:00 2001 From: lumi Date: Thu, 16 Mar 2017 20:04:22 +0100 Subject: [PATCH] initial work towards server-side support --- sasl/src/{ => client}/mechanisms/anonymous.rs | 5 +- sasl/src/{ => client}/mechanisms/mod.rs | 2 +- sasl/src/{ => client}/mechanisms/plain.rs | 11 +- sasl/src/{ => client}/mechanisms/scram.rs | 149 +---------- sasl/src/client/mod.rs | 29 +++ sasl/src/common/mod.rs | 201 +++++++++++++++ sasl/src/common/scram.rs | 155 ++++++++++++ sasl/src/lib.rs | 215 ++++++++-------- sasl/src/server/mechanisms/plain.rs | 13 + sasl/src/server/mod.rs | 239 ++++++++++++++++++ 10 files changed, 759 insertions(+), 260 deletions(-) rename sasl/src/{ => client}/mechanisms/anonymous.rs (93%) rename sasl/src/{ => client}/mechanisms/mod.rs (70%) rename sasl/src/{ => client}/mechanisms/plain.rs (79%) rename sasl/src/{ => client}/mechanisms/scram.rs (69%) create mode 100644 sasl/src/client/mod.rs create mode 100644 sasl/src/common/mod.rs create mode 100644 sasl/src/common/scram.rs create mode 100644 sasl/src/server/mechanisms/plain.rs create mode 100644 sasl/src/server/mod.rs diff --git a/sasl/src/mechanisms/anonymous.rs b/sasl/src/client/mechanisms/anonymous.rs similarity index 93% rename from sasl/src/mechanisms/anonymous.rs rename to sasl/src/client/mechanisms/anonymous.rs index cf1daf6a..d95245a8 100644 --- a/sasl/src/mechanisms/anonymous.rs +++ b/sasl/src/client/mechanisms/anonymous.rs @@ -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; diff --git a/sasl/src/mechanisms/mod.rs b/sasl/src/client/mechanisms/mod.rs similarity index 70% rename from sasl/src/mechanisms/mod.rs rename to sasl/src/client/mechanisms/mod.rs index 2ccd1d79..6dba5581 100644 --- a/sasl/src/mechanisms/mod.rs +++ b/sasl/src/client/mechanisms/mod.rs @@ -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; diff --git a/sasl/src/mechanisms/plain.rs b/sasl/src/client/mechanisms/plain.rs similarity index 79% rename from sasl/src/mechanisms/plain.rs rename to sasl/src/client/mechanisms/plain.rs index 58e0f241..786978da 100644 --- a/sasl/src/mechanisms/plain.rs +++ b/sasl/src/client/mechanisms/plain.rs @@ -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 { - 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()) } } diff --git a/sasl/src/mechanisms/scram.rs b/sasl/src/client/mechanisms/scram.rs similarity index 69% rename from sasl/src/mechanisms/scram.rs rename to sasl/src/client/mechanisms/scram.rs index 1a075e45..5f43a913 100644 --- a/sasl/src/mechanisms/scram.rs +++ b/sasl/src/client/mechanisms/scram.rs @@ -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 { - 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, 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 { - 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; - - /// A function which performs an HMAC using the hash function. - fn hmac(data: &[u8], key: &[u8]) -> Vec; - - /// A function which does PBKDF2 key derivation using the hash function. - fn derive(data: &[u8], salt: &[u8], iterations: usize) -> Vec; -} - -/// 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 { - hash(MessageDigest::sha1(), data).unwrap() - } - - fn hmac(data: &[u8], key: &[u8]) -> Vec { - 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 { - 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 { - hash(MessageDigest::sha256(), data).unwrap() - } - - fn hmac(data: &[u8], key: &[u8]) -> Vec { - 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 { - 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 { name: String, username: String, - password: String, + password: Password, client_nonce: String, state: ScramState, channel_binding: ChannelBinding, @@ -164,7 +38,7 @@ impl Scram { /// /// It is recommended that instead you use a `Credentials` struct and turn it into the /// requested mechanism using `from_credentials`. - pub fn new, P: Into>( + pub fn new, P: Into>( username: N, password: P, channel_binding: ChannelBinding, @@ -183,7 +57,7 @@ impl Scram { // Used for testing. #[doc(hidden)] #[cfg(test)] - pub fn new_with_nonce, P: Into>( + pub fn new_with_nonce, P: Into>( username: N, password: P, nonce: String, @@ -208,7 +82,7 @@ impl Mechanism for Scram { fn from_credentials(credentials: Credentials) -> Result, 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 Mechanism for Scram { 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 Mechanism for Scram { 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 Mechanism for Scram { #[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() { diff --git a/sasl/src/client/mod.rs b/sasl/src/client/mod.rs new file mode 100644 index 00000000..d8655d54 --- /dev/null +++ b/sasl/src/client/mod.rs @@ -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 + where + Self: Sized; + + /// Provides initial payload of the SASL mechanism. + fn initial(&mut self) -> Result, String> { + Ok(Vec::new()) + } + + /// Creates a response to the SASL challenge. + fn response(&mut self, _challenge: &[u8]) -> Result, 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; diff --git a/sasl/src/common/mod.rs b/sasl/src/common/mod.rs new file mode 100644 index 00000000..8e2016a7 --- /dev/null +++ b/sasl/src/common/mod.rs @@ -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 for Identity { + fn from(s: String) -> Identity { + Identity::Username(s) + } +} + +impl<'a> From<&'a str> for Identity { + fn from(s: &'a str) -> Identity { + Identity::Username(s.to_owned()) + } +} + +/// A struct containing SASL credentials. +#[derive(Clone, Debug)] +pub struct Credentials { + /// The requested identity. + pub identity: Identity, + /// The secret used to authenticate. + pub secret: Secret, + /// Channel binding data, for *-PLUS mechanisms. + pub channel_binding: ChannelBinding, +} + +impl Default for Credentials { + fn default() -> Credentials { + Credentials { + identity: Identity::None, + secret: Secret::None, + channel_binding: ChannelBinding::Unsupported, + } + } +} + +impl Credentials { + /// Creates a new Credentials with the specified username. + pub fn with_username>(mut self, username: N) -> Credentials { + self.identity = Identity::Username(username.into()); + self + } + + /// Creates a new Credentials with the specified plaintext password. + pub fn with_password>(mut self, password: P) -> Credentials { + self.secret = Secret::password_plain(password); + self + } + + /// Creates a new Credentials with the specified chanel binding. + pub fn with_channel_binding(mut self, channel_binding: ChannelBinding) -> Credentials { + self.channel_binding = channel_binding; + self + } +} + +/// Represents a SASL secret, like a password. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Secret { + /// No extra data needed. + None, + /// Password required. + Password(Password), +} + +impl Secret { + pub fn password_plain>(password: S) -> Secret { + Secret::Password(Password::Plain(password.into())) + } + + pub fn password_pbkdf2>( + method: S, + salt: Vec, + iterations: usize, + data: Vec, + ) -> Secret { + Secret::Password(Password::Pbkdf2 { + method: method.into(), + salt: salt, + iterations: iterations, + data: data, + }) + } +} + +/// Represents a password. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Password { + /// A plaintext password. + Plain(String), + /// A password digest derived using PBKDF2. + Pbkdf2 { + method: String, + salt: Vec, + iterations: usize, + data: Vec, + }, +} + +impl From for Password { + fn from(s: String) -> Password { + Password::Plain(s) + } +} + +impl<'a> From<&'a str> for Password { + fn from(s: &'a str) -> Password { + Password::Plain(s.to_owned()) + } +} + +#[cfg(test)] +#[test] +fn xor_works() { + assert_eq!( + xor( + &[135, 94, 53, 134, 73, 233, 140, 221, 150, 12, 96, 111, 54, 66, 11, 76], + &[163, 9, 122, 180, 107, 44, 22, 252, 248, 134, 112, 82, 84, 122, 56, 209] + ), + &[36, 87, 79, 50, 34, 197, 154, 33, 110, 138, 16, 61, 98, 56, 51, 157] + ); +} + +#[doc(hidden)] +pub fn xor(a: &[u8], b: &[u8]) -> Vec { + assert_eq!(a.len(), b.len()); + let mut ret = Vec::with_capacity(a.len()); + for (a, b) in a.into_iter().zip(b) { + ret.push(a ^ b); + } + ret +} + +#[doc(hidden)] +pub fn parse_frame(frame: &[u8]) -> Result, FromUtf8Error> { + let inner = String::from_utf8(frame.to_owned())?; + let mut ret = HashMap::new(); + for s in inner.split(',') { + let mut tmp = s.splitn(2, '='); + let key = tmp.next(); + let val = tmp.next(); + match (key, val) { + (Some(k), Some(v)) => { + ret.insert(k.to_owned(), v.to_owned()); + } + _ => (), + } + } + Ok(ret) +} + +/// Channel binding configuration. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum ChannelBinding { + /// No channel binding data. + None, + /// Advertise that the client does not think the server supports channel binding. + Unsupported, + /// p=tls-unique channel binding data. + TlsUnique(Vec), +} + +impl ChannelBinding { + /// Return the gs2 header for this channel binding mechanism. + pub fn header(&self) -> &[u8] { + match *self { + ChannelBinding::None => b"n,,", + ChannelBinding::Unsupported => b"y,,", + ChannelBinding::TlsUnique(_) => b"p=tls-unique,,", + } + } + + /// Return the channel binding data for this channel binding mechanism. + pub fn data(&self) -> &[u8] { + match *self { + ChannelBinding::None => &[], + ChannelBinding::Unsupported => &[], + ChannelBinding::TlsUnique(ref data) => data, + } + } + + /// Checks whether this channel binding mechanism is supported. + pub fn supports(&self, mechanism: &str) -> bool { + match *self { + ChannelBinding::None => false, + ChannelBinding::Unsupported => false, + ChannelBinding::TlsUnique(_) => mechanism == "tls-unique", + } + } +} diff --git a/sasl/src/common/scram.rs b/sasl/src/common/scram.rs new file mode 100644 index 00000000..76c90bf3 --- /dev/null +++ b/sasl/src/common/scram.rs @@ -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 { + 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; + + /// A function which performs an HMAC using the hash function. + fn hmac(data: &[u8], key: &[u8]) -> Vec; + + /// A function which does PBKDF2 key derivation using the hash function. + fn derive(data: &Password, salt: &[u8], iterations: usize) -> Result, 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 { + hash(MessageDigest::sha1(), data).unwrap() + } + + fn hmac(data: &[u8], key: &[u8]) -> Vec { + 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, 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 { + hash(MessageDigest::sha256(), data).unwrap() + } + + fn hmac(data: &[u8], key: &[u8]) -> Vec { + 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, 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()) + } + } + } + } +} diff --git a/sasl/src/lib.rs b/sasl/src/lib.rs index 441e3609..b05f6803 100644 --- a/sasl/src/lib.rs +++ b/sasl/src/lib.rs @@ -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 { +//! 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(&self) -> Result<(Vec, usize, Vec), 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: &mut CM, sm: &mut SM) -> Result +//! where CM: ClientMechanism, +//! SM: ServerMechanism, +//! 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::::from_credentials(creds.clone()).unwrap(); +//! let mut server_mech = ServerScram::::new(MyValidator, ChannelBinding::Unsupported); +//! +//! assert_eq!(finish(&mut client_mech, &mut server_mech), Ok(Identity::Username(USERNAME.to_owned()))); +//! +//! let mut client_mech = ClientScram::::from_credentials(creds.clone()).unwrap(); +//! let mut server_mech = ServerScram::::new(MyValidator, ChannelBinding::Unsupported); +//! +//! assert_eq!(finish(&mut client_mech, &mut server_mech), Ok(Identity::Username(USERNAME.to_owned()))); +//! ``` //! //! # Usage //! @@ -34,113 +128,8 @@ extern crate openssl; mod error; +pub mod client; +pub mod common; +pub mod server; + pub use error::Error; - -/// A struct containing SASL credentials. -#[derive(Clone, Debug)] -pub struct Credentials { - /// The requested username. - pub username: Option, - /// 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>(mut self, username: N) -> Credentials { - self.username = Some(username.into()); - self - } - - /// Creates a new Credentials with the specified password. - pub fn with_password>(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), -} - -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, - } - } -} - -/// 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 - where - Self: Sized; - - /// Provides initial payload of the SASL mechanism. - fn initial(&mut self) -> Result, String> { - Ok(Vec::new()) - } - - /// Creates a response to the SASL challenge. - fn response(&mut self, _challenge: &[u8]) -> Result, 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; diff --git a/sasl/src/server/mechanisms/plain.rs b/sasl/src/server/mechanisms/plain.rs new file mode 100644 index 00000000..1594256f --- /dev/null +++ b/sasl/src/server/mechanisms/plain.rs @@ -0,0 +1,13 @@ +use server::Mechanism; +use common::{Secret, Credentials, Password}; + +pub struct Plain { + password: String, +} + +impl Mechanism for Plain { + fn name(&self) -> &str { "PLAIN" } + + fn from_initial_message(validator: &V, msg: &[u8]) -> Result<(Self, String), String> { + } +} diff --git a/sasl/src/server/mod.rs b/sasl/src/server/mod.rs new file mode 100644 index 00000000..0d28d5e8 --- /dev/null +++ b/sasl/src/server/mod.rs @@ -0,0 +1,239 @@ +use common::scram::ScramProvider; +use common::{Credentials, Identity}; + +pub trait Validator { + fn validate_credentials(&self, credentials: &Credentials) -> Result; + fn request_pbkdf2(&self) -> Result<(Vec, usize, Vec), String>; +} + +pub trait Mechanism { + fn name(&self) -> &str; + fn respond(&mut self, payload: &[u8]) -> Result; +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Response { + Success(Identity, Vec), + Proceed(Vec), +} + +pub mod mechanisms { + mod plain { + use common::{ChannelBinding, Credentials, Identity, Secret}; + use server::{Mechanism, Response, Validator}; + + pub struct Plain { + validator: V, + } + + impl Plain { + pub fn new(validator: V) -> Plain { + Plain { + validator: validator, + } + } + } + + impl Mechanism for Plain { + fn name(&self) -> &str { + "PLAIN" + } + + fn respond(&mut self, payload: &[u8]) -> Result { + let mut sp = payload.split(|&b| b == 0); + sp.next(); + let username = sp + .next() + .ok_or_else(|| "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, + initial_server_message: Vec, + gs2_header: Vec, + server_nonce: String, + username: String, + salted_password: Vec, + }, + Done, + } + + pub struct Scram { + name: String, + state: ScramState, + channel_binding: ChannelBinding, + validator: V, + _marker: PhantomData, + } + + impl Scram { + pub fn new(validator: V, channel_binding: ChannelBinding) -> Scram { + Scram { + name: format!("SCRAM-{}", S::name()), + state: ScramState::Init, + channel_binding: channel_binding, + validator: validator, + _marker: PhantomData, + } + } + } + + impl Mechanism for Scram { + fn name(&self) -> &str { + &self.name + } + + fn respond(&mut self, payload: &[u8]) -> Result { + let next_state; + let ret; + match self.state { + ScramState::Init => { + // TODO: really ugly, mostly because parse_frame takes a &[u8] and i don't + // want to double validate utf-8 + // + // NEED TO CHANGE THIS THOUGH. IT'S AWFUL. + let mut commas = 0; + let mut idx = 0; + for &b in payload { + idx += 1; + if b == 0x2C { + commas += 1; + if commas >= 2 { + break; + } + } + } + if commas < 2 { + return Err("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::()?; + 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 = 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; +}