Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implemented a SCRAM server, with testing #3

Merged
merged 1 commit into from
Mar 14, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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