initial work towards server-side support
This commit is contained in:
parent
2d8fffdbfc
commit
4b9f2376af
10 changed files with 759 additions and 260 deletions
|
@ -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() {
|
29
sasl/src/client/mod.rs
Normal file
29
sasl/src/client/mod.rs
Normal file
|
@ -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;
|
201
sasl/src/common/mod.rs
Normal file
201
sasl/src/common/mod.rs
Normal file
|
@ -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",
|
||||
}
|
||||
}
|
||||
}
|
155
sasl/src/common/scram.rs
Normal file
155
sasl/src/common/scram.rs
Normal file
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
215
sasl/src/lib.rs
215
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<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 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<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,,",
|
||||
}
|
||||
}
|
||||
|
||||
/// 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;
|
||||
|
|
13
sasl/src/server/mechanisms/plain.rs
Normal file
13
sasl/src/server/mechanisms/plain.rs
Normal file
|
@ -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> {
|
||||
}
|
||||
}
|
239
sasl/src/server/mod.rs
Normal file
239
sasl/src/server/mod.rs
Normal file
|
@ -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;
|
||||
}
|
Loading…
Reference in a new issue