Skip to content

Commit

Permalink
Implemented a SCRAM server, with testing
Browse files Browse the repository at this point in the history
  • Loading branch information
danielyule authored and Thomas Bahn committed Mar 14, 2017
1 parent 08a72d7 commit 9e0f07d
Show file tree
Hide file tree
Showing 6 changed files with 775 additions and 56 deletions.
55 changes: 12 additions & 43 deletions src/client.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,16 @@
use std::borrow::Cow;
use std::io;

use data_encoding::base64;
use rand::distributions::IndependentSample;
use rand::distributions::range::Range;
use rand::os::OsRng;
use rand::Rng;
use ring::digest::{digest, SHA256, Digest};
use ring::hmac::{SigningKey, SigningContext, sign};
use ring::pbkdf2::{HMAC_SHA256, derive};
use error::{Error, Kind, Field};
use ring::digest::Digest;

/// The length of the client nonce in characters/bytes.
const NONCE_LENGTH: usize = 24;
/// The length of a SHA-256 hash in bytes.
const SHA256_LEN: usize = 32;
use utils::{hash_password, find_proofs};
use error::{Error, Kind, Field};
use ::{NONCE_LENGTH, SHA256_LEN};

/// Parses a `server_first_message` returning a (none, salt, iterations) tuple if successful.
fn parse_server_first(data: &str) -> Result<(&str, Vec<u8>, u16), Error> {
Expand Down Expand Up @@ -174,48 +171,20 @@ impl<'a> ServerFirst<'a> {
/// * Error::Protocol
/// * Error::UnsupportedExtension
pub fn handle_server_first(self, server_first: &str) -> Result<ClientFinal, Error> {
fn sign_slice(key: &SigningKey, slice: &[&[u8]]) -> Digest {
let mut signature_context = SigningContext::with_key(key);
for item in slice {
signature_context.update(item);
}
signature_context.sign()
}

let (nonce, salt, iterations) = try!(parse_server_first(server_first));
if !nonce.starts_with(&self.client_nonce) {
return Err(Error::Protocol(Kind::InvalidNonce));
}

let client_final_without_proof = format!("c={},r={}",
base64::encode(self.gs2header.as_bytes()),
nonce);
let auth_message = [self.client_first_bare.as_bytes(),
b",",
server_first.as_bytes(),
b",",
client_final_without_proof.as_bytes()];
let salted_password = hash_password(self.password, iterations, &salt);

let mut salted_password = [0u8; SHA256_LEN];
derive(&HMAC_SHA256,
iterations as usize,
&salt,
self.password.as_bytes(),
&mut salted_password);
let salted_password_signing_key = SigningKey::new(&SHA256, &salted_password);
let client_key = sign(&salted_password_signing_key, b"Client Key");
let server_key = sign(&salted_password_signing_key, b"Server Key");
let stored_key = digest(&SHA256, client_key.as_ref());
let stored_key_signing_key = SigningKey::new(&SHA256, stored_key.as_ref());
let client_signature = sign_slice(&stored_key_signing_key, &auth_message);
let server_signature_signing_key = SigningKey::new(&SHA256, server_key.as_ref());
let server_signature = sign_slice(&server_signature_signing_key, &auth_message);
let mut client_proof = [0u8; SHA256_LEN];
let xor_iter =
client_key.as_ref().iter().zip(client_signature.as_ref()).map(|(k, s)| k ^ s);
for (p, x) in client_proof.iter_mut().zip(xor_iter) {
*p = x
}
let (client_proof, server_signature): ([u8; SHA256_LEN], Digest) =
find_proofs(&self.gs2header,
&self.client_first_bare.into(),
&server_first.into(),
&salted_password,
nonce);

let client_final = format!("c={},r={},p={}",
base64::encode(self.gs2header.as_bytes()),
Expand Down
20 changes: 17 additions & 3 deletions src/error.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::{error, fmt};

/// The SCRAM mechanism error cases.
#[derive(Debug)]
#[derive(Debug, PartialEq)]
pub enum Error {
/// A message wasn't formatted as required. `Kind` contains further information.
///
Expand All @@ -14,10 +14,12 @@ pub enum Error {
InvalidServer,
/// The server rejected the authentication request. `String` contains a message from the server.
Authentication(String),
/// The username supplied was not valid
InvalidUser(String),
}

/// The kinds of protocol errors.
#[derive(Debug)]
#[derive(Debug, PartialEq)]
pub enum Kind {
/// The server responded with a nonce that doesn't start with our nonce.
InvalidNonce,
Expand All @@ -28,7 +30,7 @@ pub enum Kind {
}

/// The fields used in the exchanged messages.
#[derive(Debug)]
#[derive(Debug, PartialEq)]
pub enum Field {
/// Nonce
Nonce,
Expand All @@ -38,6 +40,16 @@ pub enum Field {
Iterations,
/// Verify or Error
VerifyOrError,
/// Channel Binding
ChannelBinding,
/// Authtorization ID
Authzid,
/// Authcid
Authcid,
/// GS2Header
GS2Header,
/// Client Proof
Proof
}

impl fmt::Display for Error {
Expand All @@ -50,6 +62,7 @@ impl fmt::Display for Error {
Protocol(ExpectedField(ref field)) => write!(fmt, "Expected field {:?}", field),
UnsupportedExtension => write!(fmt, "Unsupported extension"),
InvalidServer => write!(fmt, "Server failed validation"),
InvalidUser(ref username) => write!(fmt, "Invalid user: '{}'", username),
Authentication(ref msg) => write!(fmt, "authentication error {}", msg),
}
}
Expand All @@ -65,6 +78,7 @@ impl error::Error for Error {
Protocol(ExpectedField(_)) => "Expected field",
UnsupportedExtension => "Unsupported extension",
InvalidServer => "Server failed validation",
InvalidUser(_) => "Invalid user",
Authentication(_) => "Unspecified error",
}
}
Expand Down
98 changes: 88 additions & 10 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,27 @@
//!
//! # Usage
//!
//! ## Client
//! A typical usage scenario is shown below. For a detailed explanation of the methods please
//! consider their documentation. In productive code you should replace the unwrapping by proper
//! error handling.
//!
//! At first the user and the password must be supplied using either of the methods
//! [`ClientFirst::new`](struct.ClientFirst.html#method.new) or
//! [`ClientFirst::with_rng`](struct.ClientFirst.html#method.with_rng). These methods return a SCRAM
//! [`ClientFirst::new`](client/struct.ClientFirst.html#method.new) or
//! [`ClientFirst::with_rng`](client/struct.ClientFirst.html#method.with_rng). These methods return a SCRAM
//! state you can use to compute the first client message.
//!
//! The server and the client exchange four messages using the SCRAM mechanism. There is a rust type
//! for each one of them. Calling the methods
//! [`client_first`](struct.ClientFirst.html#method.client_first),
//! [`handle_server_first`](struct.ServerFirst.html#method.handle_server_first),
//! [`client_final`](struct.ClientFinal.html#method.client_final) and
//! [`handle_server_final`](struct.ServerFinal.html#method.handle_server_final) on the different
//! [`client_first`](client/struct.ClientFirst.html#method.client_first),
//! [`handle_server_first`](client/struct.ServerFirst.html#method.handle_server_first),
//! [`client_final`](client/struct.ClientFinal.html#method.client_final) and
//! [`handle_server_final`](client/struct.ServerFinal.html#method.handle_server_final) on the different
//! types advances the SCRAM handshake step by step. Computing client messages never fails but
//! processing server messages can result in failure.
//!
//! ``` rust,no_run
//! use scram::ClientFirst;
//! use scram::client::ClientFirst;
//!
//! // This function represents your I/O implementation.
//! # #[allow(unused_variables)]
Expand Down Expand Up @@ -55,13 +56,90 @@
//! // wasn't successful.
//! let () = scram.handle_server_final(&server_final).unwrap();
//! ```

//!
//! ## Server
//!
//! The server is created to respond to incoming challenges from a client. A typical usage pattern,
//! with a default provider is shown below. In production, you would implement an AuthenticationProvider
//! that could look up user credentials based on a username
//!
//! The server and the client exchange four messages using the SCRAM mechanism. There is a rust type
//! for each one of them. Calling the methods
//! [`handle_client_first`](server/struct.ScramServer.html#method.handle_client_first),
//! [`server_first`](server/struct.ServerFirst.html#method.server_first),
//! [`handle_client_final`](server/struct.ClientFinal.html#method.handle_client_final) and
//! [`server_final`](server/struct.ServerFinal.html#method.server_final) on the different
//! types advances the SCRAM handshake step by step. Computing server messages never fails (unless
//! the source of randomness for the nonce fails), but processing client messages can result in
//! failure.
//!
//! The final step will not return an error if authentication failed, but will return an
//! [`AuthenticationStatus`](server/enum.AuthenticationStatus/html) which you can use to determine
//! if authentication was successful or not.
//!
//! ```rust,no_run
//! use scram::server::{ScramServer, AuthenticationStatus, AuthenticationProvider, PasswordInfo};
//!
//! // Create a dummy authentication provider
//! struct ExampleProvider;
//! impl AuthenticationProvider for ExampleProvider {
//! // Here you would look up password information for the the given username
//! fn get_password_for(&self, username: &str) -> Option<PasswordInfo> {
//! unimplemented!()
//! }
//!
//! }
//! // These functions represent your I/O implementation.
//! # #[allow(unused_variables)]
//! fn receive() -> String {
//! unimplemented!()
//! }
//! # #[allow(unused_variables)]
//! fn send(message: &str) {
//! unimplemented!()
//! }
//!
//! // Create a new ScramServer using the example authenication provider
//! let scram_server = ScramServer::new(ExampleProvider{});
//!
//! // Receive a message from the client
//! let client_first = receive();
//!
//! // Create a SCRAM state from the client's first message
//! let scram_server = scram_server.handle_client_first(&client_first).unwrap();
//! // Craft a response to the client's message and advance the SCRAM state
//! // We could use our own source of randomness here, with `server_first_with_rng()`
//! let (scram_server, server_first) = scram_server.server_first().unwrap();
//! // Send our message to the client and read the response
//! send(&server_first);
//! let client_final = receive();
//!
//! // Process the client's challenge and re-assign the SCRAM state. This could fail if the
//! // message was poorly formatted
//! let scram_server = scram_server.handle_client_final(&client_final).unwrap();
//!
//! // Prepare the final message and get the authentication status
//! let(status, server_final) = scram_server.server_final();
//! // Send our final message to the client
//! send(&server_final);
//!
//! // Check if the client successfully authenticated
//! assert_eq!(status, AuthenticationStatus::Authenticated);
//! ```
extern crate data_encoding;
extern crate rand;
extern crate ring;

/// The length of the client nonce in characters/bytes.
const NONCE_LENGTH: usize = 24;
/// The length of a SHA-256 hash in bytes.
const SHA256_LEN: usize = 32;

#[macro_use]
mod utils;
mod error;
mod client;
pub mod client;
pub mod server;

pub use error::{Error, Kind, Field};
pub use client::{ClientFirst, ServerFirst, ClientFinal, ServerFinal};
pub use utils::hash_password;
Loading

0 comments on commit 9e0f07d

Please sign in to comment.