├── .gitignore ├── .travis.yml ├── Cargo.toml ├── LICENSE ├── RELEASES.md ├── src ├── error.rs ├── utils.rs ├── lib.rs ├── client.rs └── server.rs ├── README.md └── tests └── client_server.rs /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock 7 | Cargo.lock 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | cache: cargo 3 | 4 | rust: 5 | - stable 6 | - beta 7 | - nightly 8 | 9 | jobs: 10 | include: 11 | - rust: nightly 12 | name: "Lint with Clippy" 13 | install: rustup component add clippy 14 | script: cargo clippy --verbose --all-targets -- -D warnings 15 | 16 | install: skip 17 | script: 18 | - cargo test --verbose 19 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["Thomas Bahn "] 3 | description = "A SCRAM provider library." 4 | documentation = "https://docs.rs/scram" 5 | keywords = [ "scram", "authentication"] 6 | license = "MIT" 7 | name = "scram" 8 | readme = "README.md" 9 | repository = "https://github.com/tomprogrammer/scram" 10 | version = "0.6.0" 11 | 12 | [dependencies] 13 | base64 = "0.13.0" 14 | rand = "0.8.0" 15 | ring = "0.16.9" 16 | 17 | [badges] 18 | maintenance = { status = "actively-developed" } 19 | travis-ci = { repository = "https://github.com/tomprogrammer/scram", branch = "master" } 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Thomas Bahn 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /RELEASES.md: -------------------------------------------------------------------------------- 1 | Version 0.6.0 (2021-02-21) 2 | ========================== 3 | 4 | * Updated external dependeny `rand` requires a minor bump. 5 | * Yanked 0.5.1 6 | 7 | Version 0.5.1 (2021-02-21) 8 | ========================== 9 | 10 | * Update dependencies `base64` and `rand`. 11 | * Use documentation intra links. 12 | 13 | Version 0.5.0 (2020-02-03) 14 | ========================== 15 | 16 | * Update dependencies `ring`, `base64` and `rand`. 17 | * `utils::hash_password` takes `NonZeroU32` instead of `u16` for the number of iterations. 18 | * `ScramClient::new` and `ServerFirst::server_first` don't return a `Result` anymore. 19 | 20 | Version 0.4.0 (2018-08-05) 21 | ========================== 22 | 23 | * Raise *minimum rustc version* to `1.25.0`. 24 | * Update `ring` to version `0.13`. 25 | 26 | Version 0.3.0 (2018-05-23) 27 | ========================== 28 | 29 | * Raise *minimum rustc version* to `1.22.0`. 30 | * Update `rand` to version `0.5`. 31 | * Refactor code to use new language features available with `1.22.0`. 32 | 33 | Version 0.2.4 (2018-05-02) 34 | ========================== 35 | 36 | * Downgrade `ring` to version `0.12.1` as per recommendation from its author. 37 | 38 | Version 0.2.3 (2018-01-09) 39 | ========================== 40 | 41 | * Update `ring` to version `0.13.0-alpha` since previous versions fail to build. 42 | * Update `rand` and `base64` to their latest version. 43 | 44 | Version 0.2.1 (2017-07-19) 45 | ========================== 46 | Update `ring` to version `0.11` and `base64` to `0.6`. 47 | 48 | Version 0.2.0 (2017-05-10) 49 | ========================== 50 | 51 | * *New feature:* A SCRAM server implementation contributed by dyule. Thanks! 52 | * Rename `client::ClientFirst` to `client::ScramClient`. The former is deprecated, but keeps working 53 | in the v0.2 series. 54 | * Reexport `ScramClient`, `ScramServer` and other often used structs at crate root. 55 | * Update `ring` to version `0.9.4`. 56 | * Replace dependency on `data_encoding` by the `base64` crate. 57 | 58 | Version 0.1.1 (2017-01-30) 59 | ========================== 60 | Update `ring` to version `0.6`. 61 | 62 | Version 0.1.0 (2016-08-24) 63 | ========================== 64 | Version numbers like `0.0.x` are non-compatible, using `0.1.0` allows to push minior updates. 65 | 66 | Version 0.0.2 (2016-08-24) 67 | ========================== 68 | Update `ring` to version `0.3.0`. 69 | 70 | Version 0.0.1 (2016-08-20) 71 | ========================== 72 | First release providing a SCRAM-SHA-256 client implementation without support for channel-binding. 73 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use std::{error, fmt}; 2 | 3 | /// The SCRAM mechanism error cases. 4 | #[derive(Debug, PartialEq)] 5 | pub enum Error { 6 | /// A message wasn't formatted as required. `Kind` contains further information. 7 | /// 8 | /// RFC5803 section 7 describes the format of the exchanged messages. 9 | Protocol(Kind), 10 | /// The server required a mandatory extension to be present that this client doesn't support. 11 | UnsupportedExtension, 12 | /// The server couldn't be validated. This usually means that the server didn't posess a stored 13 | /// key to verify the credentials. 14 | InvalidServer, 15 | /// The server rejected the authentication request. `String` contains a message from the server. 16 | Authentication(String), 17 | /// The username supplied was not valid 18 | InvalidUser(String), 19 | } 20 | 21 | /// The kinds of protocol errors. 22 | #[derive(Debug, PartialEq)] 23 | pub enum Kind { 24 | /// The server responded with a nonce that doesn't start with our nonce. 25 | InvalidNonce, 26 | /// The content of the field `Field` is invalid. 27 | InvalidField(Field), 28 | /// The field `Field` was expected but not found. 29 | ExpectedField(Field), 30 | } 31 | 32 | /// The fields used in the exchanged messages. 33 | #[derive(Debug, PartialEq)] 34 | pub enum Field { 35 | /// Nonce 36 | Nonce, 37 | /// Salt 38 | Salt, 39 | /// Iterations 40 | Iterations, 41 | /// Verify or Error 42 | VerifyOrError, 43 | /// Channel Binding 44 | ChannelBinding, 45 | /// Authorization ID 46 | Authzid, 47 | /// Authcid 48 | Authcid, 49 | /// GS2Header 50 | GS2Header, 51 | /// Client Proof 52 | Proof, 53 | } 54 | 55 | impl fmt::Display for Error { 56 | fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { 57 | use self::Error::*; 58 | use self::Kind::*; 59 | match *self { 60 | Protocol(InvalidNonce) => write!(fmt, "Invalid nonce"), 61 | Protocol(InvalidField(ref field)) => write!(fmt, "Invalid field {:?}", field), 62 | Protocol(ExpectedField(ref field)) => write!(fmt, "Expected field {:?}", field), 63 | UnsupportedExtension => write!(fmt, "Unsupported extension"), 64 | InvalidServer => write!(fmt, "Server failed validation"), 65 | InvalidUser(ref username) => write!(fmt, "Invalid user: '{}'", username), 66 | Authentication(ref msg) => write!(fmt, "authentication error {}", msg), 67 | } 68 | } 69 | } 70 | 71 | impl error::Error for Error { 72 | fn description(&self) -> &str { 73 | use self::Error::*; 74 | use self::Kind::*; 75 | match *self { 76 | Protocol(InvalidNonce) => "Invalid nonce", 77 | Protocol(InvalidField(_)) => "Invalid field", 78 | Protocol(ExpectedField(_)) => "Expected field", 79 | UnsupportedExtension => "Unsupported extension", 80 | InvalidServer => "Server failed validation", 81 | InvalidUser(_) => "Invalid user", 82 | Authentication(_) => "Unspecified error", 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use base64; 2 | use ring::digest::{self, digest, SHA256_OUTPUT_LEN}; 3 | use ring::hmac::{self, Context, Key, HMAC_SHA256}; 4 | use ring::pbkdf2::{self, PBKDF2_HMAC_SHA256 as SHA256}; 5 | use std::num::NonZeroU32; 6 | 7 | /// Parses a part of a SCRAM message, after it has been split on commas. 8 | /// Checks to make sure there's a key, and then verifies its the right key. 9 | /// Returns everything after the first '='. 10 | /// Returns a `ExpectedField` error when one of the above conditions fails. 11 | macro_rules! parse_part { 12 | ($iter:expr, $field:ident, $key:expr) => { 13 | if let Some(part) = $iter.next() { 14 | if part.len() < 2 { 15 | return Err(Error::Protocol(Kind::ExpectedField(Field::$field))); 16 | } else if &part.as_bytes()[..2] == $key { 17 | &part[2..] 18 | } else { 19 | return Err(Error::Protocol(Kind::ExpectedField(Field::$field))); 20 | } 21 | } else { 22 | return Err(Error::Protocol(Kind::ExpectedField(Field::$field))); 23 | } 24 | }; 25 | } 26 | 27 | /// Hashes a password with SHA-256 with the given salt and number of iterations. This should 28 | /// be used by [`AuthenticationProvider`](crate::server::AuthenticationProvider) implementors to 29 | /// hash any passwords prior to being saved. 30 | pub fn hash_password( 31 | password: &str, 32 | iterations: NonZeroU32, 33 | salt: &[u8], 34 | ) -> [u8; SHA256_OUTPUT_LEN] { 35 | let mut salted_password = [0u8; SHA256_OUTPUT_LEN]; 36 | pbkdf2::derive( 37 | SHA256, 38 | iterations, 39 | salt, 40 | password.as_bytes(), 41 | &mut salted_password, 42 | ); 43 | salted_password 44 | } 45 | 46 | /// Finds the client proof and server signature based on the shared hashed key. 47 | pub fn find_proofs( 48 | gs2header: &str, 49 | client_first_bare: &str, 50 | server_first: &str, 51 | salted_password: &[u8], 52 | nonce: &str, 53 | ) -> ([u8; SHA256_OUTPUT_LEN], hmac::Tag) { 54 | fn sign_slice(key: &Key, slice: &[&[u8]]) -> hmac::Tag { 55 | let mut signature_context = Context::with_key(key); 56 | for item in slice { 57 | signature_context.update(item); 58 | } 59 | signature_context.sign() 60 | } 61 | 62 | let client_final_without_proof = 63 | format!("c={},r={}", base64::encode(gs2header.as_bytes()), nonce); 64 | let auth_message = [ 65 | client_first_bare.as_bytes(), 66 | b",", 67 | server_first.as_bytes(), 68 | b",", 69 | client_final_without_proof.as_bytes(), 70 | ]; 71 | 72 | let salted_password_signing_key = Key::new(HMAC_SHA256, salted_password); 73 | let client_key = hmac::sign(&salted_password_signing_key, b"Client Key"); 74 | let server_key = hmac::sign(&salted_password_signing_key, b"Server Key"); 75 | let stored_key = digest(&digest::SHA256, client_key.as_ref()); 76 | let stored_key_signing_key = Key::new(HMAC_SHA256, stored_key.as_ref()); 77 | let client_signature = sign_slice(&stored_key_signing_key, &auth_message); 78 | let server_signature_signing_key = Key::new(HMAC_SHA256, server_key.as_ref()); 79 | let server_signature = sign_slice(&server_signature_signing_key, &auth_message); 80 | let mut client_proof = [0u8; SHA256_OUTPUT_LEN]; 81 | let xor_iter = client_key 82 | .as_ref() 83 | .iter() 84 | .zip(client_signature.as_ref()) 85 | .map(|(k, s)| k ^ s); 86 | for (p, x) in client_proof.iter_mut().zip(xor_iter) { 87 | *p = x 88 | } 89 | (client_proof, server_signature) 90 | } 91 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Salted Challenge Response Authentication Mechanism (SCRAM) 2 | 3 | This implementation provides a client and a server for the SCRAM-SHA-256 mechanism according to 4 | RFC5802 and RFC7677. It doesn't support channel-binding. 5 | 6 | [Read the documentation.](https://docs.rs/scram) 7 | 8 | # Limitations 9 | 10 | The mandatory SCRAM-SHA-1 authentication mechanism is currently not implemented. This is also true 11 | for the *-PLUS variants, because channel-binding is not supported by this library. If you like to 12 | contribute or maintain them I appreciate that. 13 | 14 | # Usage 15 | 16 | ## Client 17 | 18 | A typical usage scenario is shown below. For a detailed explanation of the methods please 19 | consider their documentation. In productive code you should replace the unwrapping by proper 20 | error handling. 21 | 22 | At first the user and the password must be supplied using either of the methods 23 | `ClientFirst::new` or `ClientFirst::with_rng`. These methods return a SCRAM 24 | state you can use to compute the first client message. 25 | 26 | The server and the client exchange four messages using the SCRAM mechanism. There is a rust type 27 | for each one of them. Calling the methods `client_first`, `handle_server_first`, `client_final` 28 | and `handle_server_final` on the different types advances the SCRAM handshake step by step. 29 | Computing client messages never fails but processing server messages can result in failure. 30 | 31 | ```rust 32 | use scram::ScramClient; 33 | 34 | // This function represents your I/O implementation. 35 | fn send_and_receive(message: &str) -> String { 36 | unimplemented!() 37 | } 38 | 39 | // Create a SCRAM state from the credentials. 40 | let scram = ScramClient::new("user", "password", None); 41 | 42 | // Get the client message and reassign the SCRAM state. 43 | let (scram, client_first) = scram.client_first(); 44 | 45 | // Send the client first message and receive the servers reply. 46 | let server_first = send_and_receive(&client_first); 47 | 48 | // Process the reply and again reassign the SCRAM state. You can add error handling to 49 | // abort the authentication attempt. 50 | let scram = scram.handle_server_first(&server_first).unwrap(); 51 | 52 | // Get the client final message and reassign the SCRAM state. 53 | let (scram, client_final) = scram.client_final(); 54 | 55 | // Send the client final message and receive the servers reply. 56 | let server_final = send_and_receive(&client_final); 57 | 58 | // Process the last message. Any error returned means that the authentication attempt 59 | // wasn't successful. 60 | let () = scram.handle_server_final(&server_final).unwrap(); 61 | ``` 62 | 63 | ## Server 64 | 65 | The server is created to respond to incoming challenges from a client. A typical usage pattern, 66 | with a default provider is shown below. In production, you would implement an AuthenticationProvider 67 | that could look up user credentials based on a username 68 | 69 | The server and the client exchange four messages using the SCRAM mechanism. There is a rust type for 70 | each one of them. Calling the methods `handle_client_first()`, `server_first()`, 71 | `handle_client_final()` and `server_final()` on the different types advances the SCRAM handshake 72 | step by step. Computing server messages never fails (unless the source of randomness for the nonce 73 | fails), but processing client messages can result in failure. 74 | 75 | The final step will not return an error if authentication failed, but will return an 76 | `AuthenticationStatus` which you can use to determine if authentication was successful or not. 77 | 78 | ```rust 79 | use scram::{ScramServer, AuthenticationStatus, AuthenticationProvider, PasswordInfo}; 80 | 81 | // Create a dummy authentication provider 82 | struct ExampleProvider; 83 | impl AuthenticationProvider for ExampleProvider { 84 | // Here you would look up password information for the the given username 85 | fn get_password_for(&self, username: &str) -> Option { 86 | unimplemented!() 87 | } 88 | 89 | } 90 | // These functions represent your I/O implementation. 91 | # #[allow(unused_variables)] 92 | fn receive() -> String { 93 | unimplemented!() 94 | } 95 | # #[allow(unused_variables)] 96 | fn send(message: &str) { 97 | unimplemented!() 98 | } 99 | 100 | // Create a new ScramServer using the example authenication provider 101 | let scram_server = ScramServer::new(ExampleProvider{}); 102 | 103 | // Receive a message from the client 104 | let client_first = receive(); 105 | 106 | // Create a SCRAM state from the client's first message 107 | let scram_server = scram_server.handle_client_first(&client_first).unwrap(); 108 | // Craft a response to the client's message and advance the SCRAM state 109 | // We could use our own source of randomness here, with `server_first_with_rng()` 110 | let (scram_server, server_first) = scram_server.server_first(); 111 | // Send our message to the client and read the response 112 | send(&server_first); 113 | let client_final = receive(); 114 | 115 | // Process the client's challenge and re-assign the SCRAM state. This could fail if the 116 | // message was poorly formatted 117 | let scram_server = scram_server.handle_client_final(&client_final).unwrap(); 118 | 119 | // Prepare the final message and get the authentication status 120 | let(status, server_final) = scram_server.server_final(); 121 | // Send our final message to the client 122 | send(&server_final); 123 | 124 | // Check if the client successfully authenticated 125 | assert_eq!(status, AuthenticationStatus::Authenticated); 126 | ``` 127 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # Salted Challenge Response Authentication Mechanism (SCRAM) 2 | //! 3 | //! This implementation currently provides a client and a server for the SCRAM-SHA-256 mechanism 4 | //! according to [RFC5802](https://tools.ietf.org/html/rfc5802) and 5 | //! [RFC7677](https://tools.ietf.org/html/rfc7677). It doesn't support channel-binding. 6 | //! 7 | //! # Usage 8 | //! 9 | //! ## Client 10 | //! A typical usage scenario is shown below. For a detailed explanation of the methods please 11 | //! consider their documentation. In productive code you should replace the unwrapping by proper 12 | //! error handling. 13 | //! 14 | //! At first the user and the password must be supplied using either of the methods 15 | //! [`ScramClient::new`](client::ScramClient::new) or 16 | //! [`ScramClient::with_rng`](client::ScramClient::with_rng). These methods return 17 | //! a SCRAM state you can use to compute the first client message. 18 | //! 19 | //! The server and the client exchange four messages using the SCRAM mechanism. There is a rust type 20 | //! for each one of them. Calling the methods 21 | //! [`client_first`](client::ScramClient::client_first), 22 | //! [`handle_server_first`](client::ServerFirst::handle_server_first), 23 | //! [`client_final`](client::ClientFinal::client_final) and 24 | //! [`handle_server_final`](client::ServerFinal::handle_server_final) on the 25 | //! different types advances the SCRAM handshake step by step. Computing client messages never fails 26 | //! but processing server messages can result in failure. 27 | //! 28 | //! ``` rust,no_run 29 | //! use scram::ScramClient; 30 | //! 31 | //! // This function represents your I/O implementation. 32 | //! # #[allow(unused_variables)] 33 | //! fn send_and_receive(message: &str) -> String { 34 | //! unimplemented!() 35 | //! } 36 | //! 37 | //! // Create a SCRAM state from the credentials. 38 | //! let scram = ScramClient::new("user", "password", None); 39 | //! 40 | //! // Get the client message and reassign the SCRAM state. 41 | //! let (scram, client_first) = scram.client_first(); 42 | //! 43 | //! // Send the client first message and receive the servers reply. 44 | //! let server_first = send_and_receive(&client_first); 45 | //! 46 | //! // Process the reply and again reassign the SCRAM state. You can add error handling to 47 | //! // abort the authentication attempt. 48 | //! let scram = scram.handle_server_first(&server_first).unwrap(); 49 | //! 50 | //! // Get the client final message and reassign the SCRAM state. 51 | //! let (scram, client_final) = scram.client_final(); 52 | //! 53 | //! // Send the client final message and receive the servers reply. 54 | //! let server_final = send_and_receive(&client_final); 55 | //! 56 | //! // Process the last message. Any error returned means that the authentication attempt 57 | //! // wasn't successful. 58 | //! let () = scram.handle_server_final(&server_final).unwrap(); 59 | //! ``` 60 | //! 61 | //! ## Server 62 | //! 63 | //! The server is created to respond to incoming challenges from a client. A typical usage pattern, 64 | //! with a default provider is shown below. In production, you would implement an 65 | //! [`AuthenticationProvider`] that could look up user credentials based on a username 66 | //! 67 | //! The server and the client exchange four messages using the SCRAM mechanism. There is a rust type 68 | //! for each one of them. Calling the methods 69 | //! [`handle_client_first`](server::ScramServer::handle_client_first), 70 | //! [`server_first`](server::ServerFirst::server_first), 71 | //! [`handle_client_final`](server::ClientFinal::handle_client_final) and 72 | //! [`server_final`](server::ServerFinal::server_final) on the different 73 | //! types advances the SCRAM handshake step by step. Computing server messages never fails (unless 74 | //! the source of randomness for the nonce fails), but processing client messages can result in 75 | //! failure. 76 | //! 77 | //! The final step will not return an error if authentication failed, but will return an 78 | //! [`AuthenticationStatus`] which you can use to determine 79 | //! if authentication was successful or not. 80 | //! 81 | //! ```rust,no_run 82 | //! use scram::{ScramServer, AuthenticationStatus, AuthenticationProvider, PasswordInfo}; 83 | //! 84 | //! // Create a dummy authentication provider 85 | //! struct ExampleProvider; 86 | //! impl AuthenticationProvider for ExampleProvider { 87 | //! // Here you would look up password information for the the given username 88 | //! fn get_password_for(&self, username: &str) -> Option { 89 | //! unimplemented!() 90 | //! } 91 | //! 92 | //! } 93 | //! // These functions represent your I/O implementation. 94 | //! # #[allow(unused_variables)] 95 | //! fn receive() -> String { 96 | //! unimplemented!() 97 | //! } 98 | //! # #[allow(unused_variables)] 99 | //! fn send(message: &str) { 100 | //! unimplemented!() 101 | //! } 102 | //! 103 | //! // Create a new ScramServer using the example authenication provider 104 | //! let scram_server = ScramServer::new(ExampleProvider{}); 105 | //! 106 | //! // Receive a message from the client 107 | //! let client_first = receive(); 108 | //! 109 | //! // Create a SCRAM state from the client's first message 110 | //! let scram_server = scram_server.handle_client_first(&client_first).unwrap(); 111 | //! // Craft a response to the client's message and advance the SCRAM state 112 | //! // We could use our own source of randomness here, with `server_first_with_rng()` 113 | //! let (scram_server, server_first) = scram_server.server_first(); 114 | //! // Send our message to the client and read the response 115 | //! send(&server_first); 116 | //! let client_final = receive(); 117 | //! 118 | //! // Process the client's challenge and re-assign the SCRAM state. This could fail if the 119 | //! // message was poorly formatted 120 | //! let scram_server = scram_server.handle_client_final(&client_final).unwrap(); 121 | //! 122 | //! // Prepare the final message and get the authentication status 123 | //! let(status, server_final) = scram_server.server_final(); 124 | //! // Send our final message to the client 125 | //! send(&server_final); 126 | //! 127 | //! // Check if the client successfully authenticated 128 | //! assert_eq!(status, AuthenticationStatus::Authenticated); 129 | //! ``` 130 | extern crate base64; 131 | extern crate rand; 132 | extern crate ring; 133 | 134 | /// The length of the client nonce in characters/bytes. 135 | const NONCE_LENGTH: usize = 24; 136 | 137 | #[macro_use] 138 | mod utils; 139 | pub mod client; 140 | mod error; 141 | pub mod server; 142 | 143 | pub use client::ScramClient; 144 | pub use error::{Error, Field, Kind}; 145 | pub use server::{AuthenticationProvider, AuthenticationStatus, PasswordInfo, ScramServer}; 146 | pub use utils::hash_password; 147 | -------------------------------------------------------------------------------- /tests/client_server.rs: -------------------------------------------------------------------------------- 1 | extern crate rand; 2 | extern crate ring; 3 | extern crate scram; 4 | 5 | use ring::digest::SHA256_OUTPUT_LEN; 6 | use scram::*; 7 | use std::num::NonZeroU32; 8 | 9 | struct TestProvider { 10 | user_password: [u8; SHA256_OUTPUT_LEN], 11 | admin_password: [u8; SHA256_OUTPUT_LEN], 12 | } 13 | 14 | impl TestProvider { 15 | pub fn new() -> Self { 16 | let pwd_iterations = NonZeroU32::new(4096).unwrap(); 17 | let user_password = hash_password("password", pwd_iterations, b"salt"); 18 | let adm_iterations = NonZeroU32::new(8192).unwrap(); 19 | let admin_password = hash_password("admin_password", adm_iterations, b"messy"); 20 | TestProvider { 21 | user_password: user_password, 22 | admin_password: admin_password, 23 | } 24 | } 25 | } 26 | 27 | impl server::AuthenticationProvider for TestProvider { 28 | fn get_password_for(&self, username: &str) -> Option { 29 | match username { 30 | "user" => Some(server::PasswordInfo::new( 31 | self.user_password.to_vec(), 32 | 4096, 33 | "salt".bytes().collect(), 34 | )), 35 | "admin" => Some(server::PasswordInfo::new( 36 | self.admin_password.to_vec(), 37 | 8192, 38 | "messy".bytes().collect(), 39 | )), 40 | _ => None, 41 | } 42 | } 43 | 44 | fn authorize(&self, authcid: &str, authzid: &str) -> bool { 45 | authcid == authzid || authcid == "admin" && authzid == "user" 46 | } 47 | } 48 | 49 | #[test] 50 | fn test_simple_success() { 51 | let scram_client = ScramClient::new("user", "password", None); 52 | let scram_server = ScramServer::new(TestProvider::new()); 53 | 54 | let (scram_client, client_first) = scram_client.client_first(); 55 | 56 | let scram_server = scram_server.handle_client_first(&client_first).unwrap(); 57 | let (scram_server, server_first) = scram_server.server_first(); 58 | 59 | let scram_client = scram_client.handle_server_first(&server_first).unwrap(); 60 | let (scram_client, client_final) = scram_client.client_final(); 61 | 62 | let scram_server = scram_server.handle_client_final(&client_final).unwrap(); 63 | let (status, server_final) = scram_server.server_final(); 64 | 65 | scram_client.handle_server_final(&server_final).unwrap(); 66 | 67 | assert_eq!(status, AuthenticationStatus::Authenticated); 68 | } 69 | 70 | #[test] 71 | fn test_bad_password() { 72 | let scram_client = ScramClient::new("user", "badpassword", None); 73 | let scram_server = ScramServer::new(TestProvider::new()); 74 | 75 | let (scram_client, client_first) = scram_client.client_first(); 76 | 77 | let scram_server = scram_server.handle_client_first(&client_first).unwrap(); 78 | let (scram_server, server_first) = scram_server.server_first(); 79 | 80 | let scram_client = scram_client.handle_server_first(&server_first).unwrap(); 81 | let (scram_client, client_final) = scram_client.client_final(); 82 | 83 | let scram_server = scram_server.handle_client_final(&client_final).unwrap(); 84 | let (status, server_final) = scram_server.server_final(); 85 | 86 | assert_eq!(status, AuthenticationStatus::NotAuthenticated); 87 | assert!(scram_client.handle_server_final(&server_final).is_err()); 88 | } 89 | 90 | #[test] 91 | fn test_authorize_different() { 92 | let scram_client = ScramClient::new("admin", "admin_password", Some("user")); 93 | let scram_server = ScramServer::new(TestProvider::new()); 94 | 95 | let (scram_client, client_first) = scram_client.client_first(); 96 | 97 | let scram_server = scram_server.handle_client_first(&client_first).unwrap(); 98 | let (scram_server, server_first) = scram_server.server_first(); 99 | 100 | let scram_client = scram_client.handle_server_first(&server_first).unwrap(); 101 | let (scram_client, client_final) = scram_client.client_final(); 102 | 103 | let scram_server = scram_server.handle_client_final(&client_final).unwrap(); 104 | let (status, server_final) = scram_server.server_final(); 105 | 106 | scram_client.handle_server_final(&server_final).unwrap(); 107 | 108 | assert_eq!(status, AuthenticationStatus::Authenticated); 109 | } 110 | 111 | #[test] 112 | fn test_authorize_fail() { 113 | let scram_client = ScramClient::new("user", "password", Some("admin")); 114 | let scram_server = ScramServer::new(TestProvider::new()); 115 | 116 | let (scram_client, client_first) = scram_client.client_first(); 117 | 118 | let scram_server = scram_server.handle_client_first(&client_first).unwrap(); 119 | let (scram_server, server_first) = scram_server.server_first(); 120 | 121 | let scram_client = scram_client.handle_server_first(&server_first).unwrap(); 122 | let (scram_client, client_final) = scram_client.client_final(); 123 | 124 | let scram_server = scram_server.handle_client_final(&client_final).unwrap(); 125 | let (status, server_final) = scram_server.server_final(); 126 | 127 | assert_eq!(status, AuthenticationStatus::NotAuthorized); 128 | assert!(scram_client.handle_server_final(&server_final).is_err()); 129 | } 130 | 131 | #[test] 132 | fn test_authorize_non_existent() { 133 | let scram_client = ScramClient::new("admin", "admin_password", Some("nonexistent")); 134 | let scram_server = ScramServer::new(TestProvider::new()); 135 | 136 | let (scram_client, client_first) = scram_client.client_first(); 137 | 138 | let scram_server = scram_server.handle_client_first(&client_first).unwrap(); 139 | let (scram_server, server_first) = scram_server.server_first(); 140 | 141 | let scram_client = scram_client.handle_server_first(&server_first).unwrap(); 142 | let (scram_client, client_final) = scram_client.client_final(); 143 | 144 | let scram_server = scram_server.handle_client_final(&client_final).unwrap(); 145 | let (status, server_final) = scram_server.server_final(); 146 | 147 | assert_eq!(status, AuthenticationStatus::NotAuthorized); 148 | assert!(scram_client.handle_server_final(&server_final).is_err()); 149 | } 150 | 151 | #[test] 152 | fn test_invalid_user() { 153 | let scram_client = ScramClient::new("nobody", "password", None); 154 | let scram_server = ScramServer::new(TestProvider::new()); 155 | 156 | let (_, client_first) = scram_client.client_first(); 157 | 158 | assert!(scram_server.handle_client_first(&client_first).is_err()) 159 | } 160 | 161 | #[test] 162 | fn test_empty_username() { 163 | let scram_client = ScramClient::new("", "password", None); 164 | let scram_server = ScramServer::new(TestProvider::new()); 165 | 166 | let (_, client_first) = scram_client.client_first(); 167 | 168 | assert!(scram_server.handle_client_first(&client_first).is_err()) 169 | } 170 | 171 | #[test] 172 | fn test_empty_password() { 173 | let scram_client = ScramClient::new("user", "", None); 174 | let scram_server = ScramServer::new(TestProvider::new()); 175 | 176 | let (scram_client, client_first) = scram_client.client_first(); 177 | 178 | let scram_server = scram_server.handle_client_first(&client_first).unwrap(); 179 | let (scram_server, server_first) = scram_server.server_first(); 180 | 181 | let scram_client = scram_client.handle_server_first(&server_first).unwrap(); 182 | let (scram_client, client_final) = scram_client.client_final(); 183 | 184 | let scram_server = scram_server.handle_client_final(&client_final).unwrap(); 185 | let (status, server_final) = scram_server.server_final(); 186 | 187 | assert_eq!(status, AuthenticationStatus::NotAuthenticated); 188 | assert!(scram_client.handle_server_final(&server_final).is_err()); 189 | } 190 | -------------------------------------------------------------------------------- /src/client.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | use std::num::NonZeroU32; 3 | 4 | use base64; 5 | use rand::distributions::{Distribution, Uniform}; 6 | use rand::{rngs::OsRng, Rng}; 7 | use ring::digest::SHA256_OUTPUT_LEN; 8 | use ring::hmac; 9 | 10 | use error::{Error, Field, Kind}; 11 | use utils::{find_proofs, hash_password}; 12 | use NONCE_LENGTH; 13 | 14 | #[deprecated( 15 | since = "0.2.0", 16 | note = "Please use `ScramClient` instead. (exported at crate root)" 17 | )] 18 | pub type ClientFirst<'a> = ScramClient<'a>; 19 | 20 | /// Parses a `server_first_message` returning a (none, salt, iterations) tuple if successful. 21 | fn parse_server_first(data: &str) -> Result<(&str, Vec, NonZeroU32), Error> { 22 | if data.len() < 2 { 23 | return Err(Error::Protocol(Kind::ExpectedField(Field::Nonce))); 24 | } 25 | let mut parts = data.split(',').peekable(); 26 | match parts.peek() { 27 | Some(part) if &part.as_bytes()[..2] == b"m=" => { 28 | return Err(Error::UnsupportedExtension); 29 | } 30 | Some(_) => {} 31 | None => { 32 | return Err(Error::Protocol(Kind::ExpectedField(Field::Nonce))); 33 | } 34 | } 35 | let nonce = match parts.next() { 36 | Some(part) if &part.as_bytes()[..2] == b"r=" => &part[2..], 37 | _ => { 38 | return Err(Error::Protocol(Kind::ExpectedField(Field::Nonce))); 39 | } 40 | }; 41 | let salt = match parts.next() { 42 | Some(part) if &part.as_bytes()[..2] == b"s=" => base64::decode(part[2..].as_bytes()) 43 | .map_err(|_| Error::Protocol(Kind::InvalidField(Field::Salt)))?, 44 | _ => { 45 | return Err(Error::Protocol(Kind::ExpectedField(Field::Salt))); 46 | } 47 | }; 48 | let iterations = match parts.next() { 49 | Some(part) if &part.as_bytes()[..2] == b"i=" => part[2..] 50 | .parse() 51 | .map_err(|_| Error::Protocol(Kind::InvalidField(Field::Iterations)))?, 52 | _ => { 53 | return Err(Error::Protocol(Kind::ExpectedField(Field::Iterations))); 54 | } 55 | }; 56 | Ok((nonce, salt, iterations)) 57 | } 58 | 59 | fn parse_server_final(data: &str) -> Result, Error> { 60 | if data.len() < 2 { 61 | return Err(Error::Protocol(Kind::ExpectedField(Field::VerifyOrError))); 62 | } 63 | match &data[..2] { 64 | "v=" => base64::decode(&data.as_bytes()[2..]) 65 | .map_err(|_| Error::Protocol(Kind::InvalidField(Field::VerifyOrError))), 66 | "e=" => Err(Error::Authentication(data[2..].to_string())), 67 | _ => Err(Error::Protocol(Kind::ExpectedField(Field::VerifyOrError))), 68 | } 69 | } 70 | 71 | /// The initial state of the SCRAM mechanism. It's the entry point for a SCRAM handshake. 72 | #[derive(Debug)] 73 | pub struct ScramClient<'a> { 74 | gs2header: Cow<'static, str>, 75 | password: &'a str, 76 | nonce: String, 77 | authcid: &'a str, 78 | } 79 | 80 | impl<'a> ScramClient<'a> { 81 | /// Constructs an initial state for the SCRAM mechanism using the provided credentials. 82 | /// 83 | /// # Arguments 84 | /// 85 | /// * authcid - An username used for authentication. 86 | /// * password - A password used to prove that the user is authentic. 87 | /// * authzid - An username used for authorization. This can be used to impersonate as `authzid` 88 | /// using the credentials of `authcid`. If `authzid` is `None` the authorized username will be 89 | /// the same as the authenticated username. 90 | /// 91 | /// # Return value 92 | /// 93 | /// An I/O error is returned if the internal random number generator couldn't be constructed. 94 | pub fn new(authcid: &'a str, password: &'a str, authzid: Option<&'a str>) -> Self { 95 | Self::with_rng(authcid, password, authzid, &mut OsRng) 96 | } 97 | 98 | /// Constructs an initial state for the SCRAM mechanism using the provided credentials and a 99 | /// custom random number generator. 100 | /// 101 | /// # Arguments 102 | /// 103 | /// * authcid - An username used for authentication. 104 | /// * password - A password used to prove that the user is authentic. 105 | /// * authzid - An username used for authorization. This can be used to impersonate as `authzid` 106 | /// using the credentials of `authcid`. If `authzid` is `None` the authorized username will be 107 | /// the same as the authenticated username. 108 | /// * rng: A random number generator used to generate random nonces. Please only use a 109 | /// cryptographically secure random number generator! 110 | pub fn with_rng( 111 | authcid: &'a str, 112 | password: &'a str, 113 | authzid: Option<&'a str>, 114 | rng: &mut R, 115 | ) -> Self { 116 | let gs2header: Cow<'static, str> = match authzid { 117 | Some(authzid) => format!("n,a={},", authzid).into(), 118 | None => "n,,".into(), 119 | }; 120 | let nonce: String = Uniform::from(33..125) 121 | .sample_iter(rng) 122 | .map(|x: u8| if x > 43 { (x + 1) as char } else { x as char }) 123 | .take(NONCE_LENGTH) 124 | .collect(); 125 | ScramClient { 126 | gs2header, 127 | password, 128 | authcid, 129 | nonce, 130 | } 131 | } 132 | 133 | /// Returns the next state and the first client message. 134 | /// 135 | /// Call the [`ServerFirst::handle_server_first`] method to continue the SCRAM handshake. 136 | pub fn client_first(self) -> (ServerFirst<'a>, String) { 137 | let escaped_authcid: Cow<'a, str> = 138 | if self.authcid.chars().any(|chr| chr == ',' || chr == '=') { 139 | self.authcid.into() 140 | } else { 141 | self.authcid.replace(',', "=2C").replace('=', "=3D").into() 142 | }; 143 | let client_first_bare = format!("n={},r={}", escaped_authcid, self.nonce); 144 | let client_first = format!("{}{}", self.gs2header, client_first_bare); 145 | let server_first = ServerFirst { 146 | gs2header: self.gs2header, 147 | password: self.password, 148 | client_nonce: self.nonce, 149 | client_first_bare, 150 | }; 151 | (server_first, client_first) 152 | } 153 | } 154 | 155 | /// The second state of the SCRAM mechanism after the first client message was computed. 156 | #[derive(Debug)] 157 | pub struct ServerFirst<'a> { 158 | gs2header: Cow<'static, str>, 159 | password: &'a str, 160 | client_nonce: String, 161 | client_first_bare: String, 162 | } 163 | 164 | impl<'a> ServerFirst<'a> { 165 | /// Processes the first answer from the server and returns the next state or an error. If an 166 | /// error is returned the SCRAM handshake is aborted. 167 | /// 168 | /// Call the [`ClientFinal::client_final`] method to continue the handshake. 169 | /// 170 | /// # Return value 171 | /// 172 | /// This method returns only a subset of the errors defined in [`Error`]: 173 | /// 174 | /// * Error::Protocol 175 | /// * Error::UnsupportedExtension 176 | pub fn handle_server_first(self, server_first: &str) -> Result { 177 | let (nonce, salt, iterations) = parse_server_first(server_first)?; 178 | if !nonce.starts_with(&self.client_nonce) { 179 | return Err(Error::Protocol(Kind::InvalidNonce)); 180 | } 181 | let salted_password = hash_password(self.password, iterations, &salt); 182 | let (client_proof, server_signature): ([u8; SHA256_OUTPUT_LEN], hmac::Tag) = find_proofs( 183 | &self.gs2header, 184 | &self.client_first_bare, 185 | &server_first, 186 | &salted_password, 187 | nonce, 188 | ); 189 | let client_final = format!( 190 | "c={},r={},p={}", 191 | base64::encode(self.gs2header.as_bytes()), 192 | nonce, 193 | base64::encode(&client_proof) 194 | ); 195 | Ok(ClientFinal { 196 | server_signature, 197 | client_final, 198 | }) 199 | } 200 | } 201 | 202 | /// The third state of the SCRAM mechanism after the first server message was successfully 203 | /// processed. 204 | #[derive(Debug)] 205 | pub struct ClientFinal { 206 | server_signature: hmac::Tag, 207 | client_final: String, 208 | } 209 | 210 | impl ClientFinal { 211 | /// Returns the next state and the final client message. 212 | /// 213 | /// Call the 214 | /// [`ServerFinal::handle_server_final`] method to continue the SCRAM handshake. 215 | #[inline] 216 | pub fn client_final(self) -> (ServerFinal, String) { 217 | let server_final = ServerFinal { 218 | server_signature: self.server_signature, 219 | }; 220 | (server_final, self.client_final) 221 | } 222 | } 223 | 224 | /// The final state of the SCRAM mechanism after the final client message was computed. 225 | #[derive(Debug)] 226 | pub struct ServerFinal { 227 | server_signature: hmac::Tag, 228 | } 229 | 230 | impl ServerFinal { 231 | /// Processes the final answer from the server and returns the authentication result. 232 | /// 233 | /// # Return value 234 | /// 235 | /// * A value of `Ok(())` signals a successful authentication attempt. 236 | /// * A value of `Err(Error::Protocol(_)` or `Err(Error::UnsupportedExtension)` means that the 237 | /// authentication request failed. 238 | /// * A value of `Err(Error::InvalidServer)` or `Err(Error::Authentication(_))` means that the 239 | /// authentication request was rejected. 240 | /// 241 | /// Detailed semantics are documented in the [`Error`] type. 242 | pub fn handle_server_final(self, server_final: &str) -> Result<(), Error> { 243 | if self.server_signature.as_ref() == &*parse_server_final(server_final)? { 244 | Ok(()) 245 | } else { 246 | Err(Error::InvalidServer) 247 | } 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /src/server.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | use base64; 4 | use rand::distributions::{Distribution, Uniform}; 5 | use rand::{rngs::OsRng, Rng}; 6 | use ring::digest::SHA256_OUTPUT_LEN; 7 | use ring::hmac; 8 | 9 | use error::{Error, Field, Kind}; 10 | use utils::find_proofs; 11 | use NONCE_LENGTH; 12 | 13 | /// Responds to client authentication challenges. It's the entrypoint for the SCRAM server side 14 | /// implementation. 15 | pub struct ScramServer { 16 | /// The ['AuthenticationProvider'] that will find passwords and check authorization. 17 | provider: P, 18 | } 19 | 20 | /// Contains information about stored passwords. In particular, it stores the password that has been 21 | /// salted and hashed, the salt that was used, and the number of iterations of the hashing algorithm 22 | pub struct PasswordInfo { 23 | hashed_password: Vec, 24 | salt: Vec, 25 | iterations: u16, 26 | } 27 | 28 | /// The status of authentication after the final client message has been received by the server. 29 | #[derive(Clone, Copy, PartialEq, Debug)] 30 | pub enum AuthenticationStatus { 31 | /// The client has correctly authenticated, and has been authorized. 32 | Authenticated, 33 | /// The client was not correctly authenticated, meaning they supplied an incorrect password. 34 | NotAuthenticated, 35 | /// The client authenticated correctly, but was not authorized for the alternate user they 36 | /// requested. 37 | NotAuthorized, 38 | } 39 | 40 | impl PasswordInfo { 41 | /// Create a new `PasswordInfo` from the given information. The password is assumed to have 42 | /// already been hashed using the given salt and iterations. 43 | pub fn new(hashed_password: Vec, iterations: u16, salt: Vec) -> Self { 44 | PasswordInfo { 45 | hashed_password, 46 | iterations, 47 | salt, 48 | } 49 | } 50 | } 51 | 52 | /// An `AuthenticationProvider` looks up password information for a given user, and also checks if a 53 | /// user is authorized to act on another user's behalf. The authorization component is optional, and 54 | /// if not implemented will simply allow users to act on their own behalf, and no one else's. 55 | /// 56 | /// To ensure the password is hashed correctly, cleartext passwords can be hased using the 57 | /// [`hash_password`](crate::utils::hash_password) function provided in the crate root. 58 | pub trait AuthenticationProvider { 59 | /// Gets the [`PasswordInfo`] for the given user. 60 | fn get_password_for(&self, username: &str) -> Option; 61 | 62 | /// Checks to see if the user given by `authcid` is authorized to act as the user given by 63 | /// `authzid.` Implementors do not need to implement this method. The default implementation 64 | /// just checks if the two are equal 65 | fn authorize(&self, authcid: &str, authzid: &str) -> bool { 66 | authcid == authzid 67 | } 68 | } 69 | 70 | /// Parses a client's first message by splitting it on commas and analyzing each part. Gives an 71 | /// error if the data was malformed in any way 72 | fn parse_client_first(data: &str) -> Result<(&str, Option<&str>, &str), Error> { 73 | let mut parts = data.split(','); 74 | 75 | // Channel binding 76 | if let Some(part) = parts.next() { 77 | if let Some(cb) = part.chars().next() { 78 | if cb == 'p' { 79 | return Err(Error::UnsupportedExtension); 80 | } 81 | if cb != 'n' && cb != 'y' || part.len() > 1 { 82 | return Err(Error::Protocol(Kind::InvalidField(Field::ChannelBinding))); 83 | } 84 | } else { 85 | return Err(Error::Protocol(Kind::ExpectedField(Field::ChannelBinding))); 86 | } 87 | } else { 88 | return Err(Error::Protocol(Kind::ExpectedField(Field::ChannelBinding))); 89 | } 90 | 91 | // Authzid 92 | let authzid = if let Some(part) = parts.next() { 93 | if part.is_empty() { 94 | None 95 | } else if part.len() < 2 || &part.as_bytes()[..2] != b"a=" { 96 | return Err(Error::Protocol(Kind::ExpectedField(Field::Authzid))); 97 | } else { 98 | Some(&part[2..]) 99 | } 100 | } else { 101 | return Err(Error::Protocol(Kind::ExpectedField(Field::Authzid))); 102 | }; 103 | 104 | // Authcid 105 | let authcid = parse_part!(parts, Authcid, b"n="); 106 | 107 | // Nonce 108 | let nonce = match parts.next() { 109 | Some(part) if &part.as_bytes()[..2] == b"r=" => &part[2..], 110 | _ => { 111 | return Err(Error::Protocol(Kind::ExpectedField(Field::Nonce))); 112 | } 113 | }; 114 | Ok((authcid, authzid, nonce)) 115 | } 116 | 117 | /// Parses the client's final message. Gives an error if the data was malformed. 118 | fn parse_client_final(data: &str) -> Result<(&str, &str, &str), Error> { 119 | // 6 is the length of the required parts of the message 120 | let mut parts = data.split(','); 121 | let gs2header = parse_part!(parts, GS2Header, b"c="); 122 | let nonce = parse_part!(parts, Nonce, b"r="); 123 | let proof = parse_part!(parts, Proof, b"p="); 124 | Ok((gs2header, nonce, proof)) 125 | } 126 | 127 | impl ScramServer

{ 128 | /// Creates a new `ScramServer` using the given authentication provider. 129 | pub fn new(provider: P) -> Self { 130 | ScramServer { provider } 131 | } 132 | 133 | /// Handle a challenge message sent by the client to the server. If the message is well formed, 134 | /// and the requested user exists, then this will progress to the next stage of the 135 | /// authentication process, [`ServerFirst`]. Otherwise, it will return an error. 136 | pub fn handle_client_first<'a>( 137 | &'a self, 138 | client_first: &'a str, 139 | ) -> Result, Error> { 140 | let (authcid, authzid, client_nonce) = parse_client_first(client_first)?; 141 | let password_info = self 142 | .provider 143 | .get_password_for(authcid) 144 | .ok_or_else(|| Error::InvalidUser(authcid.to_string()))?; 145 | Ok(ServerFirst { 146 | client_nonce, 147 | authcid, 148 | authzid, 149 | provider: &self.provider, 150 | password_info, 151 | }) 152 | } 153 | } 154 | 155 | /// Represents the first stage in the authentication process, after the client has submitted their 156 | /// first message. This struct is responsible for responding to the message 157 | pub struct ServerFirst<'a, P: 'a + AuthenticationProvider> { 158 | client_nonce: &'a str, 159 | authcid: &'a str, 160 | authzid: Option<&'a str>, 161 | provider: &'a P, 162 | password_info: PasswordInfo, 163 | } 164 | 165 | impl<'a, P: AuthenticationProvider> ServerFirst<'a, P> { 166 | /// Creates the server's first message in response to the client's first message. By default, 167 | /// this method uses [`OsRng`] as its source of randomness for the nonce. To specify the 168 | /// randomness source, use [`server_first_with_rng`](Self::server_first_with_rng). This method 169 | /// will return an error when it cannot initialize the OS's randomness source. See the 170 | /// documentation on `OsRng` for more information. 171 | pub fn server_first(self) -> (ClientFinal<'a, P>, String) { 172 | self.server_first_with_rng(&mut OsRng) 173 | } 174 | 175 | /// Creates the server's first message in response to the client's first message, with the 176 | /// given source of randomness used for the server's nonce. The randomness is assigned here 177 | /// instead of universally in [`ScramServer`] for increased flexibility, and also to keep 178 | /// `ScramServer` immutable. 179 | pub fn server_first_with_rng(self, rng: &mut R) -> (ClientFinal<'a, P>, String) { 180 | let mut nonce = String::with_capacity(self.client_nonce.len() + NONCE_LENGTH); 181 | nonce.push_str(self.client_nonce); 182 | nonce.extend( 183 | Uniform::from(33..125) 184 | .sample_iter(rng) 185 | .map(|x: u8| if x > 43 { (x + 1) as char } else { x as char }) 186 | .take(NONCE_LENGTH), 187 | ); 188 | 189 | let gs2header: Cow<'static, str> = match self.authzid { 190 | Some(authzid) => format!("n,a={},", authzid).into(), 191 | None => "n,,".into(), 192 | }; 193 | let client_first_bare: Cow<'static, str> = 194 | format!("n={},r={}", self.authcid, self.client_nonce).into(); 195 | let server_first: Cow<'static, str> = format!( 196 | "r={},s={},i={}", 197 | nonce, 198 | base64::encode(self.password_info.salt.as_slice()), 199 | self.password_info.iterations 200 | ) 201 | .into(); 202 | ( 203 | ClientFinal { 204 | hashed_password: self.password_info.hashed_password, 205 | nonce, 206 | gs2header, 207 | client_first_bare, 208 | server_first: server_first.clone(), 209 | authcid: self.authcid, 210 | authzid: self.authzid, 211 | provider: self.provider, 212 | }, 213 | server_first.into_owned(), 214 | ) 215 | } 216 | } 217 | 218 | /// Represents the stage after the server has generated its first response to the client. This 219 | /// struct is responsible for handling the client's final message. 220 | pub struct ClientFinal<'a, P: 'a + AuthenticationProvider> { 221 | hashed_password: Vec, 222 | nonce: String, 223 | gs2header: Cow<'static, str>, 224 | client_first_bare: Cow<'static, str>, 225 | server_first: Cow<'static, str>, 226 | authcid: &'a str, 227 | authzid: Option<&'a str>, 228 | provider: &'a P, 229 | } 230 | 231 | impl<'a, P: AuthenticationProvider> ClientFinal<'a, P> { 232 | /// Handle the final client message. If the message is not well formed, or the authorization 233 | /// header is invalid, then this will return an error. In all other cases (including when 234 | /// authentication or authorization has failed), this will return `Ok` along with a message to 235 | /// send the client. In cases where authentication or authorization has failed, the message will 236 | /// contain error information for the client. To check if authentication and authorization have 237 | /// succeeded, use [`server_final`](ServerFinal::server_final) on the return value. 238 | pub fn handle_client_final(self, client_final: &str) -> Result { 239 | let (gs2header_enc, nonce, proof) = parse_client_final(client_final)?; 240 | if !self.verify_header(gs2header_enc) { 241 | return Err(Error::Protocol(Kind::InvalidField(Field::GS2Header))); 242 | } 243 | if !self.verify_nonce(nonce) { 244 | return Err(Error::Protocol(Kind::InvalidField(Field::Nonce))); 245 | } 246 | if let Some(signature) = self.verify_proof(proof)? { 247 | if let Some(authzid) = self.authzid { 248 | if self.provider.authorize(self.authcid, authzid) { 249 | Ok(ServerFinal { 250 | status: AuthenticationStatus::Authenticated, 251 | signature, 252 | }) 253 | } else { 254 | Ok(ServerFinal { 255 | status: AuthenticationStatus::NotAuthorized, 256 | signature: format!( 257 | "e=User '{}' not authorized to act as '{}'", 258 | self.authcid, authzid 259 | ), 260 | }) 261 | } 262 | } else { 263 | Ok(ServerFinal { 264 | status: AuthenticationStatus::Authenticated, 265 | signature, 266 | }) 267 | } 268 | } else { 269 | Ok(ServerFinal { 270 | status: AuthenticationStatus::NotAuthenticated, 271 | signature: "e=Invalid Password".to_string(), 272 | }) 273 | } 274 | } 275 | 276 | /// Checks that the gs2header received from the client is the same as the one we've stored 277 | fn verify_header(&self, gs2header: &str) -> bool { 278 | let server_gs2header = base64::encode(self.gs2header.as_bytes()); 279 | server_gs2header == gs2header 280 | } 281 | 282 | /// Checks that the client has sent the same nonce 283 | fn verify_nonce(&self, nonce: &str) -> bool { 284 | nonce == self.nonce 285 | } 286 | 287 | /// Checks that the proof from the client matches our saved credentials 288 | fn verify_proof(&self, proof: &str) -> Result, Error> { 289 | let (client_proof, server_signature): ([u8; SHA256_OUTPUT_LEN], hmac::Tag) = find_proofs( 290 | &self.gs2header, 291 | &self.client_first_bare, 292 | &self.server_first, 293 | self.hashed_password.as_slice(), 294 | &self.nonce, 295 | ); 296 | let proof = if let Ok(proof) = base64::decode(proof.as_bytes()) { 297 | proof 298 | } else { 299 | return Err(Error::Protocol(Kind::InvalidField(Field::Proof))); 300 | }; 301 | if proof != client_proof { 302 | return Ok(None); 303 | } 304 | 305 | let server_signature_string = format!("v={}", base64::encode(server_signature.as_ref())); 306 | Ok(Some(server_signature_string)) 307 | } 308 | } 309 | 310 | /// Represents the final stage of authentication, after we have generated the final server message 311 | /// to send to the client 312 | pub struct ServerFinal { 313 | status: AuthenticationStatus, 314 | signature: String, 315 | } 316 | 317 | impl ServerFinal { 318 | /// Get the [`AuthenticationStatus`] of the exchange. This status can be successful, failed 319 | /// because of invalid authentication or failed because of invalid authorization. 320 | pub fn server_final(self) -> (AuthenticationStatus, String) { 321 | (self.status, self.signature) 322 | } 323 | } 324 | 325 | #[cfg(test)] 326 | mod tests { 327 | use super::super::{Error, Field, Kind}; 328 | use super::{parse_client_final, parse_client_first}; 329 | 330 | #[test] 331 | fn test_parse_client_first_success() { 332 | let (authcid, authzid, nonce) = parse_client_first("n,,n=user,r=abcdefghijk").unwrap(); 333 | assert_eq!(authcid, "user"); 334 | assert!(authzid.is_none()); 335 | assert_eq!(nonce, "abcdefghijk"); 336 | 337 | let (authcid, authzid, nonce) = 338 | parse_client_first("y,a=other user,n=user,r=abcdef=hijk").unwrap(); 339 | assert_eq!(authcid, "user"); 340 | assert_eq!(authzid, Some("other user")); 341 | assert_eq!(nonce, "abcdef=hijk"); 342 | 343 | let (authcid, authzid, nonce) = parse_client_first("n,,n=,r=").unwrap(); 344 | assert_eq!(authcid, ""); 345 | assert!(authzid.is_none()); 346 | assert_eq!(nonce, ""); 347 | } 348 | 349 | #[test] 350 | fn test_parse_client_first_missing_fields() { 351 | assert_eq!( 352 | parse_client_first("n,,n=user").unwrap_err(), 353 | Error::Protocol(Kind::ExpectedField(Field::Nonce)) 354 | ); 355 | assert_eq!( 356 | parse_client_first("n,,r=user").unwrap_err(), 357 | Error::Protocol(Kind::ExpectedField(Field::Authcid)) 358 | ); 359 | assert_eq!( 360 | parse_client_first("n,n=user,r=abc").unwrap_err(), 361 | Error::Protocol(Kind::ExpectedField(Field::Authzid)) 362 | ); 363 | assert_eq!( 364 | parse_client_first(",,n=user,r=abc").unwrap_err(), 365 | Error::Protocol(Kind::ExpectedField(Field::ChannelBinding)) 366 | ); 367 | assert_eq!( 368 | parse_client_first("").unwrap_err(), 369 | Error::Protocol(Kind::ExpectedField(Field::ChannelBinding)) 370 | ); 371 | assert_eq!( 372 | parse_client_first(",,,").unwrap_err(), 373 | Error::Protocol(Kind::ExpectedField(Field::ChannelBinding)) 374 | ); 375 | } 376 | #[test] 377 | fn test_parse_client_first_invalid_data() { 378 | assert_eq!( 379 | parse_client_first("a,,n=user,r=abc").unwrap_err(), 380 | Error::Protocol(Kind::InvalidField(Field::ChannelBinding)) 381 | ); 382 | assert_eq!( 383 | parse_client_first("p,,n=user,r=abc").unwrap_err(), 384 | Error::UnsupportedExtension 385 | ); 386 | assert_eq!( 387 | parse_client_first("nn,,n=user,r=abc").unwrap_err(), 388 | Error::Protocol(Kind::InvalidField(Field::ChannelBinding)) 389 | ); 390 | assert_eq!( 391 | parse_client_first("n,,n,r=abc").unwrap_err(), 392 | Error::Protocol(Kind::ExpectedField(Field::Authcid)) 393 | ); 394 | } 395 | 396 | #[test] 397 | fn test_parse_client_final_success() { 398 | let (gs2head, nonce, proof) = parse_client_final("c=abc,r=abcefg,p=783232").unwrap(); 399 | assert_eq!(gs2head, "abc"); 400 | assert_eq!(nonce, "abcefg"); 401 | assert_eq!(proof, "783232"); 402 | 403 | let (gs2head, nonce, proof) = parse_client_final("c=,r=,p=").unwrap(); 404 | assert_eq!(gs2head, ""); 405 | assert_eq!(nonce, ""); 406 | assert_eq!(proof, ""); 407 | } 408 | 409 | #[test] 410 | fn test_parse_client_final_missing_fields() { 411 | assert_eq!( 412 | parse_client_final("c=whatever,r=something").unwrap_err(), 413 | Error::Protocol(Kind::ExpectedField(Field::Proof)) 414 | ); 415 | assert_eq!( 416 | parse_client_final("c=whatever,p=words").unwrap_err(), 417 | Error::Protocol(Kind::ExpectedField(Field::Nonce)) 418 | ); 419 | assert_eq!( 420 | parse_client_final("c=whatever").unwrap_err(), 421 | Error::Protocol(Kind::ExpectedField(Field::Nonce)) 422 | ); 423 | assert_eq!( 424 | parse_client_final("c=").unwrap_err(), 425 | Error::Protocol(Kind::ExpectedField(Field::Nonce)) 426 | ); 427 | assert_eq!( 428 | parse_client_final("").unwrap_err(), 429 | Error::Protocol(Kind::ExpectedField(Field::GS2Header)) 430 | ); 431 | assert_eq!( 432 | parse_client_final("r=anonce").unwrap_err(), 433 | Error::Protocol(Kind::ExpectedField(Field::GS2Header)) 434 | ); 435 | } 436 | } 437 | --------------------------------------------------------------------------------