diff --git a/.github/workflows/sha-crypt.yml b/.github/workflows/sha-crypt.yml index a01cee3d..62cc45f2 100644 --- a/.github/workflows/sha-crypt.yml +++ b/.github/workflows/sha-crypt.yml @@ -35,12 +35,13 @@ jobs: toolchain: ${{ matrix.rust }} targets: ${{ matrix.target }} - run: cargo build --target ${{ matrix.target }} --no-default-features + - run: cargo build --target ${{ matrix.target }} --no-default-features --features simple minimal-versions: if: false # disabled while using pre-releases uses: RustCrypto/actions/.github/workflows/minimal-versions.yml@master with: - working-directory: ${{ github.workflow }} + working-directory: ${{ github.workflow }} test: runs-on: ubuntu-latest diff --git a/Cargo.lock b/Cargo.lock index dbd1bab5..c96b3dcd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -434,8 +434,8 @@ name = "sha-crypt" version = "0.6.0-pre.1" dependencies = [ "base64ct", - "getrandom", "mcf", + "password-hash", "sha2", "subtle", ] diff --git a/sha-crypt/Cargo.toml b/sha-crypt/Cargo.toml index 7b635403..623a7968 100644 --- a/sha-crypt/Cargo.toml +++ b/sha-crypt/Cargo.toml @@ -21,13 +21,14 @@ sha2 = { version = "0.11.0-rc.3", default-features = false } base64ct = { version = "1.8", default-features = false, features = ["alloc"] } # optional dependencies -getrandom = { version = "0.3", optional = true, default-features = false } mcf = { version = "0.6.0-rc.0", optional = true, default-features = false, features = ["alloc", "base64"] } +password-hash = { version = "0.6.0-rc.4", optional = true, default-features = false } subtle = { version = "2", optional = true, default-features = false } [features] default = ["simple"] -simple = ["dep:getrandom", "dep:mcf", "dep:subtle"] +getrandom = ["password-hash/getrandom", "simple"] +simple = ["dep:mcf", "dep:password-hash", "dep:subtle"] [package.metadata.docs.rs] all-features = true diff --git a/sha-crypt/src/consts.rs b/sha-crypt/src/consts.rs index 30c95017..f7fb4c00 100644 --- a/sha-crypt/src/consts.rs +++ b/sha-crypt/src/consts.rs @@ -4,15 +4,8 @@ pub const BLOCK_SIZE_SHA256: usize = 32; /// Block size for SHA512 pub const BLOCK_SIZE_SHA512: usize = 64; -/// PWD part length of the password string of SHA256 -#[cfg(feature = "simple")] -pub const PW_SIZE_SHA256: usize = 43; - -/// Maximum length of a salt -#[cfg(feature = "simple")] -pub const SALT_MAX_LEN: usize = 16; - /// Inverse encoding map for SHA512. +#[cfg(feature = "simple")] #[rustfmt::skip] pub const MAP_SHA512: [u8; 64] = [ 42, 21, 0, @@ -40,6 +33,7 @@ pub const MAP_SHA512: [u8; 64] = [ ]; /// Inverse encoding map for SHA256. +#[cfg(feature = "simple")] #[rustfmt::skip] pub const MAP_SHA256: [u8; 32] = [ 20, 10, 0, diff --git a/sha-crypt/src/errors.rs b/sha-crypt/src/errors.rs index 0cfd5f48..d471cfdd 100644 --- a/sha-crypt/src/errors.rs +++ b/sha-crypt/src/errors.rs @@ -1,95 +1,29 @@ //! Error types. -use alloc::string; use core::fmt; -#[cfg(feature = "simple")] -use alloc::string::String; - /// Error type. #[derive(Debug)] -pub enum CryptError { +pub enum Error { /// Should be within range defs::ROUNDS_MIN < defs::ROUNDS_MIN RoundsError, - - /// RNG failed. - RandomError, - - /// UTF-8 error. - StringError(string::FromUtf8Error), } -impl From for CryptError { - fn from(e: string::FromUtf8Error) -> Self { - CryptError::StringError(e) - } -} - -impl core::error::Error for CryptError { - fn source(&self) -> Option<&(dyn core::error::Error + 'static)> { - match self { - CryptError::StringError(err) => Some(err), - _ => None, - } - } -} +impl core::error::Error for Error {} -impl fmt::Display for CryptError { +impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - CryptError::RoundsError => write!(f, "rounds error"), - CryptError::RandomError => write!(f, "random error"), - CryptError::StringError(_) => write!(f, "string error"), + Error::RoundsError => write!(f, "rounds error"), } } } -/// Errors which occur when verifying passwords. #[cfg(feature = "simple")] -#[derive(Debug)] -pub enum CheckError { - /// Format is invalid. - InvalidFormat(String), - - /// Cryptographic error. - Crypt(CryptError), - - /// Password hash doesn't match (invalid password). - HashMismatch, -} - -#[cfg(feature = "simple")] -impl core::error::Error for CheckError {} - -#[cfg(feature = "simple")] -impl fmt::Display for CheckError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::InvalidFormat(e) => write!(f, "invalid format: {e}"), - Self::Crypt(e) => write!(f, "cryptographic error: {e}"), - Self::HashMismatch => write!(f, "hash mismatch"), +impl From for password_hash::Error { + fn from(err: Error) -> Self { + match err { + Error::RoundsError => password_hash::Error::ParamInvalid { name: "rounds" }, } } } - -/// Decoding errors. -#[cfg(feature = "simple")] -#[derive(Debug)] -pub struct DecodeError; - -#[cfg(feature = "simple")] -impl From for CheckError { - fn from(_: DecodeError) -> CheckError { - CheckError::InvalidFormat("invalid B64".into()) - } -} - -#[cfg(feature = "simple")] -impl core::error::Error for DecodeError {} - -#[cfg(feature = "simple")] -impl fmt::Display for DecodeError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "decode error") - } -} diff --git a/sha-crypt/src/lib.rs b/sha-crypt/src/lib.rs index a3dd1764..0a75d9cb 100644 --- a/sha-crypt/src/lib.rs +++ b/sha-crypt/src/lib.rs @@ -1,33 +1,5 @@ -//! Pure Rust implementation of the [`SHA-crypt` password hash based on SHA-512][1], -//! a legacy password hashing scheme supported by the [POSIX crypt C library][2]. -//! -//! Password hashes using this algorithm start with `$6$` when encoded using the -//! [PHC string format][3]. -//! -//! # Usage -//! -//! ``` -//! # #[cfg(feature = "simple")] -//! # { -//! use sha_crypt::{Sha512Params, sha512_simple, sha512_check}; -//! -//! // First setup the Sha512Params arguments with: -//! // rounds = 10_000 -//! let params = Sha512Params::new(10_000).expect("RandomError!"); -//! -//! // Hash the password for storage -//! let hashed_password = sha512_simple("Not so secure password", ¶ms); -//! -//! // Verifying a stored password -//! assert!(sha512_check("Not so secure password", &hashed_password).is_ok()); -//! # } -//! ``` -//! -//! [1]: https://www.akkadia.org/drepper/SHA-crypt.txt -//! [2]: https://en.wikipedia.org/wiki/Crypt_(C) -//! [3]: https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md - #![no_std] +#![doc = include_str!("../README.md")] #![cfg_attr(docsrs, feature(doc_cfg))] #![doc( html_logo_url = "https://raw.githubusercontent.com/RustCrypto/media/8f1a9894/logo.svg", @@ -36,6 +8,25 @@ #![deny(unsafe_code)] #![warn(missing_docs, rust_2018_idioms)] +//! # Usage +//! +#![cfg_attr(feature = "getrandom", doc = "```")] +#![cfg_attr(not(feature = "getrandom"), doc = "```ignore")] +//! # fn main() -> password_hash::Result<()> { +//! // NOTE: example requires `getrandom` feature is enabled +//! +//! use sha_crypt::{SHA512_CRYPT, PasswordHasher, PasswordVerifier}; +//! +//! let password = b"pleaseletmein"; // don't actually use this as a password! +//! let password_hash = SHA512_CRYPT.hash_password(password)?; +//! assert!(password_hash.as_str().starts_with("$6$")); +//! +//! // verify password is correct for the given hash +//! SHA512_CRYPT.verify_password(password, &password_hash)?; +//! # Ok(()) +//! # } +//! ``` + // TODO(tarcieri): heapless support #[macro_use] extern crate alloc; @@ -43,26 +34,25 @@ extern crate alloc; mod consts; mod errors; mod params; +#[cfg(feature = "simple")] mod simple; pub use crate::{ consts::{BLOCK_SIZE_SHA256, BLOCK_SIZE_SHA512}, - errors::CryptError, + errors::Error, params::{ROUNDS_DEFAULT, ROUNDS_MAX, ROUNDS_MIN, Sha256Params, Sha512Params}, }; #[cfg(feature = "simple")] -pub use crate::simple::{sha256_check, sha256_simple, sha512_check, sha512_simple}; +pub use { + crate::simple::{SHA256_CRYPT, SHA512_CRYPT, ShaCrypt}, + mcf::{self, PasswordHash}, + password_hash::{self, CustomizedPasswordHasher, PasswordHasher, PasswordVerifier}, +}; -use alloc::{string::String, vec::Vec}; -use base64ct::{Base64ShaCrypt, Encoding}; +use alloc::vec::Vec; use sha2::{Digest, Sha256, Sha512}; -#[cfg(feature = "simple")] -pub use crate::errors::{CheckError, DecodeError}; - -use crate::consts::{MAP_SHA256, MAP_SHA512}; - /// The SHA512 crypt function returned as byte vector /// /// If the provided hash is longer than defs::SALT_MAX_LEN character, it will @@ -261,50 +251,6 @@ pub fn sha256_crypt( digest_c } -/// Same as sha512_crypt except base64 representation will be returned. -/// -/// # Arguments -/// - `password` - The password to process as a byte vector -/// - `salt` - The salt value to use as a byte vector -/// - `params` - The Sha512Params to use -/// **WARNING: Make sure to compare this value in constant time!** -/// -/// # Returns -/// - `Ok(())` if calculation was successful -/// - `Err(errors::CryptError)` otherwise -pub fn sha512_crypt_b64(password: &[u8], salt: &[u8], params: &Sha512Params) -> String { - let output = sha512_crypt(password, salt, params); - - let mut transposed = [0u8; BLOCK_SIZE_SHA512]; - for (i, &ti) in MAP_SHA512.iter().enumerate() { - transposed[i] = output[ti as usize]; - } - - Base64ShaCrypt::encode_string(&transposed) -} - -/// Same as sha256_crypt except base64 representation will be returned. -/// -/// # Arguments -/// - `password` - The password to process as a byte vector -/// - `salt` - The salt value to use as a byte vector -/// - `params` - The Sha256Params to use -/// **WARNING: Make sure to compare this value in constant time!** -/// -/// # Returns -/// - `Ok(())` if calculation was successful -/// - `Err(errors::CryptError)` otherwise -pub fn sha256_crypt_b64(password: &[u8], salt: &[u8], params: &Sha256Params) -> String { - let output = sha256_crypt(password, salt, params); - - let mut transposed = [0u8; BLOCK_SIZE_SHA256]; - for (i, &ti) in MAP_SHA256.iter().enumerate() { - transposed[i] = output[ti as usize]; - } - - Base64ShaCrypt::encode_string(&transposed) -} - fn produce_byte_seq(len: usize, fill_from: &[u8]) -> Vec { let bs = fill_from.len(); let mut seq: Vec = vec![0; len]; diff --git a/sha-crypt/src/params.rs b/sha-crypt/src/params.rs index a994e910..febadaf4 100644 --- a/sha-crypt/src/params.rs +++ b/sha-crypt/src/params.rs @@ -1,7 +1,11 @@ //! Algorithm parameters. use crate::errors; -use core::default::Default; +use core::{ + default::Default, + fmt::{self, Display}, + str::FromStr, +}; /// Default number of rounds. pub const ROUNDS_DEFAULT: u32 = 5_000; @@ -26,13 +30,27 @@ impl Default for Sha512Params { } } +impl Display for Sha512Params { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "rounds={}", self.rounds) + } +} + +impl FromStr for Sha512Params { + type Err = errors::Error; + + fn from_str(_s: &str) -> Result { + todo!() + } +} + impl Sha512Params { /// Create new algorithm parameters. - pub fn new(rounds: u32) -> Result { + pub fn new(rounds: u32) -> Result { if (ROUNDS_MIN..=ROUNDS_MAX).contains(&rounds) { Ok(Sha512Params { rounds }) } else { - Err(errors::CryptError::RoundsError) + Err(errors::Error::RoundsError) } } } @@ -51,13 +69,50 @@ impl Default for Sha256Params { } } +impl Display for Sha256Params { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "rounds={}", self.rounds) + } +} + +impl FromStr for Sha256Params { + type Err = errors::Error; + + fn from_str(_s: &str) -> Result { + todo!() + } +} + impl Sha256Params { /// Create new algorithm parameters. - pub fn new(rounds: u32) -> Result { + pub fn new(rounds: u32) -> Result { if (ROUNDS_MIN..=ROUNDS_MAX).contains(&rounds) { Ok(Sha256Params { rounds }) } else { - Err(errors::CryptError::RoundsError) + Err(errors::Error::RoundsError) } } } + +#[cfg(test)] +mod tests { + use super::{ROUNDS_MAX, ROUNDS_MIN, Sha256Params, Sha512Params}; + + #[test] + fn test_sha256_crypt_invalid_rounds() { + let params = Sha256Params::new(ROUNDS_MAX + 1); + assert!(params.is_err()); + + let params = Sha256Params::new(ROUNDS_MIN - 1); + assert!(params.is_err()); + } + + #[test] + fn test_sha512_crypt_invalid_rounds() { + let params = Sha512Params::new(ROUNDS_MAX + 1); + assert!(params.is_err()); + + let params = Sha512Params::new(ROUNDS_MIN - 1); + assert!(params.is_err()); + } +} diff --git a/sha-crypt/src/simple.rs b/sha-crypt/src/simple.rs index 56e2b827..53418c48 100644 --- a/sha-crypt/src/simple.rs +++ b/sha-crypt/src/simple.rs @@ -1,253 +1,258 @@ -//! "Simple" API which uses the Modular Crypt Format (MCF). - -#![cfg(feature = "simple")] +//! Implementation of the `password-hash` crate API. use crate::{ - CheckError, DecodeError, ROUNDS_DEFAULT, Sha256Params, Sha512Params, - consts::{ - BLOCK_SIZE_SHA256, BLOCK_SIZE_SHA512, MAP_SHA256, MAP_SHA512, PW_SIZE_SHA256, SALT_MAX_LEN, - }, - sha256_crypt, sha256_crypt_b64, sha512_crypt, sha512_crypt_b64, + BLOCK_SIZE_SHA256, BLOCK_SIZE_SHA512, ROUNDS_DEFAULT, Sha256Params, Sha512Params, + consts::{MAP_SHA256, MAP_SHA512}, + sha256_crypt, sha512_crypt, }; -use alloc::string::{String, ToString}; use base64ct::{Base64ShaCrypt, Encoding}; +use core::marker::PhantomData; +use mcf::{Base64, PasswordHash, PasswordHashRef}; +use password_hash::{ + CustomizedPasswordHasher, Error, PasswordHasher, PasswordVerifier, Result, Version, +}; +use sha2::{Digest, Sha256, Sha512}; const SHA256_MCF_ID: &str = "5"; const SHA512_MCF_ID: &str = "6"; const ROUNDS_PARAM: &str = "rounds="; -/// Simple interface for generating a SHA512 password hash. -/// -/// The salt will be chosen randomly. The output format will conform to [1]. -/// -/// `$$$` -/// -/// # Returns -/// - `Ok(String)` containing the full SHA512 password hash format on success -/// - `Err(CryptError)` if something went wrong. -/// -/// [1]: https://www.akkadia.org/drepper/SHA-crypt.txt -pub fn sha512_simple(password: &str, params: &Sha512Params) -> String { - let salt = random_salt(); - let out = sha512_crypt_b64(password.as_bytes(), salt.as_bytes(), params); - - let mut mcf_hash = mcf::PasswordHash::from_id(SHA512_MCF_ID).expect("should have valid ID"); - - if params.rounds != ROUNDS_DEFAULT { - mcf_hash - .push_str(&format!("{}{}", ROUNDS_PARAM, params.rounds)) - .expect("should be valid field"); - } +/// sha-crypt type for use with [`PasswordHasher`]. +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub struct ShaCrypt { + phantom: PhantomData, +} - mcf_hash.push_str(&salt).expect("should have valid salt"); - mcf_hash.push_str(&out).expect("should have valid hash"); +/// SHA-crypt initialized using SHA-256 +pub const SHA256_CRYPT: ShaCrypt = ShaCrypt { + phantom: PhantomData, +}; - mcf_hash.into() -} +/// SHA-crypt initialized using SHA-512 +pub const SHA512_CRYPT: ShaCrypt = ShaCrypt { + phantom: PhantomData, +}; + +impl CustomizedPasswordHasher for ShaCrypt { + type Params = Sha256Params; + + fn hash_password_customized( + &self, + password: &[u8], + salt: &[u8], + alg_id: Option<&str>, + version: Option, + params: Sha256Params, + ) -> Result { + match alg_id { + Some(SHA256_MCF_ID) | None => (), + _ => return Err(Error::Algorithm), + } + + if version.is_some() { + return Err(Error::Version); + } + + // We compute the function over the Base64-encoded salt + let salt = Base64ShaCrypt::encode_string(salt); + let out = sha256_crypt_transposed(password, salt.as_bytes(), ¶ms); + + let mut mcf_hash = PasswordHash::from_id(SHA256_MCF_ID).expect("should have valid ID"); + + if params.rounds != ROUNDS_DEFAULT { + mcf_hash + .push_str(&format!("{}{}", ROUNDS_PARAM, params.rounds)) + .expect("should be valid field"); + } -/// Simple interface for generating a SHA256 password hash. -/// -/// The salt will be chosen randomly. The output format will conform to [1]. -/// -/// `$$$` -/// -/// # Returns -/// - `Ok(String)` containing the full SHA256 password hash format on success -/// - `Err(CryptError)` if something went wrong. -/// -/// [1]: https://www.akkadia.org/drepper/SHA-crypt.txt -pub fn sha256_simple(password: &str, params: &Sha256Params) -> String { - let salt = random_salt(); - let out = sha256_crypt_b64(password.as_bytes(), salt.as_bytes(), params); - - let mut mcf_hash = mcf::PasswordHash::from_id(SHA256_MCF_ID).expect("should have valid ID"); - - if params.rounds != ROUNDS_DEFAULT { mcf_hash - .push_str(&format!("{}{}", ROUNDS_PARAM, params.rounds)) - .expect("should be valid field"); + .push_str(&salt) + .map_err(|_| Error::EncodingInvalid)?; + mcf_hash.push_base64(&out, Base64::ShaCrypt); + Ok(mcf_hash) } +} - mcf_hash.push_str(&salt).expect("should have valid salt"); - mcf_hash.push_str(&out).expect("should have valid hash"); +impl CustomizedPasswordHasher for ShaCrypt { + type Params = Sha512Params; + + fn hash_password_customized( + &self, + password: &[u8], + salt: &[u8], + alg_id: Option<&str>, + version: Option, + params: Sha512Params, + ) -> Result { + match alg_id { + Some(SHA512_MCF_ID) | None => (), + _ => return Err(Error::Algorithm), + } + + if version.is_some() { + return Err(Error::Version); + } + + // We compute the function over the Base64-encoded salt + let salt = Base64ShaCrypt::encode_string(salt); + let out = sha512_crypt_transposed(password, salt.as_bytes(), ¶ms); + + let mut mcf_hash = PasswordHash::from_id(SHA512_MCF_ID).expect("should have valid ID"); + + if params.rounds != ROUNDS_DEFAULT { + mcf_hash + .push_str(&format!("{}{}", ROUNDS_PARAM, params.rounds)) + .expect("should be valid field"); + } - mcf_hash.into() + mcf_hash + .push_str(&salt) + .map_err(|_| Error::EncodingInvalid)?; + mcf_hash.push_base64(&out, Base64::ShaCrypt); + Ok(mcf_hash) + } } -/// Checks that given password matches provided hash. -/// -/// # Arguments -/// - `password` - expected password -/// - `hashed_value` - the hashed value which should be used for checking, -/// should be of format mentioned in [1]: `$6$$` -/// -/// # Return -/// `OK(())` if password matches otherwise Err(CheckError) in case of invalid -/// format or password mismatch. -/// -/// [1]: https://www.akkadia.org/drepper/SHA-crypt.txt -pub fn sha512_check(password: &str, hashed_value: &str) -> Result<(), CheckError> { - let mut iter = hashed_value.split('$'); - - // Check that there are no characters before the first "$" - if iter.next() != Some("") { - return Err(CheckError::InvalidFormat( - "Should start with '$".to_string(), - )); +impl PasswordHasher for ShaCrypt { + fn hash_password_with_salt(&self, password: &[u8], salt: &[u8]) -> Result { + self.hash_password_customized(password, salt, None, None, Sha256Params::default()) } +} + +impl PasswordHasher for ShaCrypt { + fn hash_password_with_salt(&self, password: &[u8], salt: &[u8]) -> Result { + self.hash_password_customized(password, salt, None, None, Sha512Params::default()) + } +} - if iter.next() != Some("6") { - return Err(CheckError::InvalidFormat(format!( - "does not contain SHA512 identifier: '${SHA512_MCF_ID}$'", - ))); +impl PasswordVerifier for ShaCrypt { + fn verify_password(&self, password: &[u8], hash: &PasswordHash) -> Result<()> { + self.verify_password(password, hash.as_password_hash_ref()) } +} - let mut next = iter.next().ok_or_else(|| { - CheckError::InvalidFormat("Does not contain a rounds or salt nor hash string".to_string()) - })?; - let rounds = if next.starts_with(ROUNDS_PARAM) { - let rounds = next; - next = iter.next().ok_or_else(|| { - CheckError::InvalidFormat("Does not contain a salt nor hash string".to_string()) - })?; - - rounds[ROUNDS_PARAM.len()..].parse().map_err(|_| { - CheckError::InvalidFormat(format!("{ROUNDS_PARAM} specifier need to be a number",)) - })? - } else { - ROUNDS_DEFAULT - }; - - let salt = next; - - let hash = iter - .next() - .ok_or_else(|| CheckError::InvalidFormat("Does not contain a hash string".to_string()))?; - - // Make sure there is no trailing data after the final "$" - if iter.next().is_some() { - return Err(CheckError::InvalidFormat( - "Trailing characters present".to_string(), - )); +impl PasswordVerifier for ShaCrypt { + fn verify_password(&self, password: &[u8], hash: &PasswordHash) -> Result<()> { + self.verify_password(password, hash.as_password_hash_ref()) } +} + +impl PasswordVerifier for ShaCrypt { + fn verify_password(&self, password: &[u8], hash: &PasswordHashRef) -> Result<()> { + // verify id matches `$6` + if hash.id() != SHA256_MCF_ID { + return Err(Error::Algorithm); + } + + let mut fields = hash.fields(); + let mut next = fields.next().ok_or(Error::EncodingInvalid)?; + + let mut params = Sha256Params::default(); - let params = match Sha512Params::new(rounds) { - Ok(p) => p, - Err(e) => return Err(CheckError::Crypt(e)), - }; + // decode params + // TODO(tarcieri): `mcf::Field` helper methods for parsing params? + if let Some(rounds_str) = next.as_str().strip_prefix(ROUNDS_PARAM) { + let rounds = rounds_str.parse().map_err(|_| Error::EncodingInvalid)?; + params = Sha256Params::new(rounds)?; + next = fields.next().ok_or(Error::EncodingInvalid)?; + } - let output = sha512_crypt(password.as_bytes(), salt.as_bytes(), ¶ms); + let salt = next.as_str().as_bytes(); - let hash = decode_sha512(hash.as_bytes())?; + // decode expected password hash + let expected = fields + .next() + .ok_or(Error::EncodingInvalid)? + .decode_base64(Base64::ShaCrypt) + .map_err(|_| Error::EncodingInvalid)?; + + // should be the last field + if fields.next().is_some() { + return Err(Error::EncodingInvalid); + } + + let actual = sha256_crypt_transposed(password, salt, ¶ms); + + if subtle::ConstantTimeEq::ct_ne(actual.as_slice(), &expected).into() { + return Err(Error::PasswordInvalid); + } - use subtle::ConstantTimeEq; - if output.ct_eq(&hash).into() { Ok(()) - } else { - Err(CheckError::HashMismatch) } } -/// Checks that given password matches provided hash. -/// -/// # Arguments -/// - `password` - expected password -/// - `hashed_value` - the hashed value which should be used for checking, -/// should be of format mentioned in [1]: `$6$$` -/// -/// # Return -/// `OK(())` if password matches otherwise Err(CheckError) in case of invalid -/// format or password mismatch. -/// -/// [1]: https://www.akkadia.org/drepper/SHA-crypt.txt -pub fn sha256_check(password: &str, hashed_value: &str) -> Result<(), CheckError> { - let mut iter = hashed_value.split('$'); - - // Check that there are no characters before the first "$" - if iter.next() != Some("") { - return Err(CheckError::InvalidFormat( - "Should start with '$".to_string(), - )); - } +impl PasswordVerifier for ShaCrypt { + fn verify_password(&self, password: &[u8], hash: &PasswordHashRef) -> Result<()> { + // verify id matches `$6` + if hash.id() != SHA512_MCF_ID { + return Err(Error::Algorithm); + } - if iter.next() != Some("5") { - return Err(CheckError::InvalidFormat(format!( - "does not contain SHA256 identifier: '${SHA256_MCF_ID}$'", - ))); - } + let mut fields = hash.fields(); + let mut next = fields.next().ok_or(Error::EncodingInvalid)?; - let mut next = iter.next().ok_or_else(|| { - CheckError::InvalidFormat("Does not contain a rounds or salt nor hash string".to_string()) - })?; - let rounds = if next.starts_with(ROUNDS_PARAM) { - let rounds = next; - next = iter.next().ok_or_else(|| { - CheckError::InvalidFormat("Does not contain a salt nor hash string".to_string()) - })?; - - rounds[ROUNDS_PARAM.len()..].parse().map_err(|_| { - CheckError::InvalidFormat(format!("{ROUNDS_PARAM} specifier need to be a number",)) - })? - } else { - ROUNDS_DEFAULT - }; - - let salt = next; - - let hash = iter - .next() - .ok_or_else(|| CheckError::InvalidFormat("Does not contain a hash string".to_string()))?; - - // Make sure there is no trailing data after the final "$" - if iter.next().is_some() { - return Err(CheckError::InvalidFormat( - "Trailing characters present".to_string(), - )); - } + let mut params = Sha512Params::default(); + + // decode params + // TODO(tarcieri): `mcf::Field` helper methods for parsing params? + if let Some(rounds_str) = next.as_str().strip_prefix(ROUNDS_PARAM) { + let rounds = rounds_str.parse().map_err(|_| Error::EncodingInvalid)?; + params = Sha512Params::new(rounds)?; + next = fields.next().ok_or(Error::EncodingInvalid)?; + } + + let salt = next.as_str().as_bytes(); - let params = match Sha256Params::new(rounds) { - Ok(p) => p, - Err(e) => return Err(CheckError::Crypt(e)), - }; + // decode expected password hash + let expected = fields + .next() + .ok_or(Error::EncodingInvalid)? + .decode_base64(Base64::ShaCrypt) + .map_err(|_| Error::EncodingInvalid)?; - let output = sha256_crypt(password.as_bytes(), salt.as_bytes(), ¶ms); + // should be the last field + if fields.next().is_some() { + return Err(Error::EncodingInvalid); + } - let hash = decode_sha256(hash.as_bytes())?; + let actual = sha512_crypt_transposed(password, salt, ¶ms); + + if subtle::ConstantTimeEq::ct_ne(actual.as_slice(), &expected).into() { + return Err(Error::PasswordInvalid); + } - use subtle::ConstantTimeEq; - if output.ct_eq(&hash).into() { Ok(()) - } else { - Err(CheckError::HashMismatch) } } -/// Generate a random salt that is 16-bytes long. -fn random_salt() -> String { - // Create buffer containing raw bytes to encode as Base64 - let mut buf = [0u8; (SALT_MAX_LEN * 3).div_ceil(4)]; - getrandom::fill(&mut buf).expect("RNG failure"); - Base64ShaCrypt::encode_string(&buf) -} +/// Invokes sha256_crypt then runs the result through the SHA-256-specific transposition table. +fn sha256_crypt_transposed( + password: &[u8], + salt: &[u8], + params: &Sha256Params, +) -> [u8; BLOCK_SIZE_SHA256] { + let output = sha256_crypt(password, salt, params); -fn decode_sha512(source: &[u8]) -> Result<[u8; BLOCK_SIZE_SHA512], DecodeError> { - const BUF_SIZE: usize = 86; - let mut buf = [0u8; BUF_SIZE]; - Base64ShaCrypt::decode(source, &mut buf).map_err(|_| DecodeError)?; - let mut transposed = [0u8; BLOCK_SIZE_SHA512]; - for (i, &ti) in MAP_SHA512.iter().enumerate() { - transposed[ti as usize] = buf[i]; + let mut transposed = [0u8; BLOCK_SIZE_SHA256]; + for (i, &ti) in MAP_SHA256.iter().enumerate() { + transposed[i] = output[ti as usize]; } - Ok(transposed) + + transposed } -fn decode_sha256(source: &[u8]) -> Result<[u8; BLOCK_SIZE_SHA256], DecodeError> { - let mut buf = [0u8; PW_SIZE_SHA256]; - Base64ShaCrypt::decode(source, &mut buf).unwrap(); +/// Invokes sha512_crypt then runs the result through the SHA-512-specific transposition table. +fn sha512_crypt_transposed( + password: &[u8], + salt: &[u8], + params: &Sha512Params, +) -> [u8; BLOCK_SIZE_SHA512] { + let output = sha512_crypt(password, salt, params); - let mut transposed = [0u8; BLOCK_SIZE_SHA256]; - for (i, &ti) in MAP_SHA256.iter().enumerate() { - transposed[ti as usize] = buf[i]; + let mut transposed = [0u8; BLOCK_SIZE_SHA512]; + for (i, &ti) in MAP_SHA512.iter().enumerate() { + transposed[i] = output[ti as usize]; } - Ok(transposed) + + transposed } diff --git a/sha-crypt/tests/lib.rs b/sha-crypt/tests/lib.rs deleted file mode 100644 index 6cbb3af0..00000000 --- a/sha-crypt/tests/lib.rs +++ /dev/null @@ -1,234 +0,0 @@ -use sha_crypt::{ - ROUNDS_MAX, ROUNDS_MIN, Sha256Params, Sha512Params, sha256_crypt_b64, sha512_crypt_b64, -}; - -#[cfg(feature = "simple")] -use sha_crypt::{sha256_check, sha256_simple, sha512_check, sha512_simple}; - -struct TestVector { - input: &'static str, - salt: &'static str, - result_sha256: &'static str, - result_sha512: &'static str, - rounds: u32, -} - -const TEST_VECTORS: &[TestVector] = &[ - TestVector { - input: "Hello world!", - salt: "saltstring", - result_sha256: "5B8vYYiY.CVt1RlTTf8KbXBH3hsxY/GNooZaBBGWEc5", - result_sha512: "svn8UoSVapNtMuq1ukKS4tPQd8iKwSMHWjl/O817G3uBnIFNjnQJuesI68u4OTLiBFdcbYEdFCoEOfaS35inz1", - rounds: 5_000, - }, - TestVector { - input: "Hello world!", - salt: "saltstringsaltstring", - result_sha256: "3xv.VbSHBb41AL9AvLeujZkZRBAwqFMz2.opqey6IcA", - result_sha512: "OW1/O6BYHV6BcXZu8QVeXbDWra3Oeqh0sbHbbMCVNSnCM/UrjmM0Dp8vOuZeHBy/YTBmSK6H9qs/y3RnOaw5v.", - rounds: 10_000, - }, - TestVector { - input: "This is just a test", - salt: "toolongsaltstring", - result_sha256: "Un/5jzAHMgOGZ5.mWJpuVolil07guHPvOW8mGRcvxa5", - result_sha512: "lQ8jolhgVRVhY4b5pZKaysCLi0QBxGoNeKQzQ3glMhwllF7oGDZxUhx1yxdYcz/e1JSbq3y6JMxxl8audkUEm0", - rounds: 5_000, - }, - // 63 length password - TestVector { - input: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - salt: "kf8.jB3ZLrhG2/n1", - result_sha256: "DjWX2SQlslxT/jON7Gof6T4UodbHrqW0Lwl7xLT2gu8", - result_sha512: "BZk4ni5Rx3KgyM7vd48EpPgr8AoICCq5HRQPu6vNf6t6xnJ3xNu7MMMBXh/3eUZ5ql.mBqjNhlYUWHBqjKRkU/", - rounds: 5_000, - }, - // 64 length password - TestVector { - input: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", // 64 length - salt: "JKb8lkbDWryxdaRL", - result_sha256: "X2024tVMdQy9vnRU/dnWB0eL4dpfJvgD3g9o0eE95d7", - result_sha512: "dLVyYl.G1KhMak97BNNO7vV2upvwcQ3hKrQjO8xn.V/ucmN4ogytaGbIEfBrNv4YLtbpjgV240ldDgkP9M9S7.", - rounds: 5_000, - }, - // 65 length password - TestVector { - input: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", // 65 length - salt: "JmlLQtPDXkxMbdFc", - result_sha256: "MVZTo9AdKikQwK5sBlSQ/X7mUH19GoFGrmRr0XxcUr6", - result_sha512: "lO/BGRK6dKXMaRafyLMZl9wkxvdCobed0ppRHYJtCfatf6yGLghCs.rq.ifz4YezxCHmQG7lpqm4W46xsNnBm0", - rounds: 5_000, - }, - // 127 length password - TestVector { - input: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - salt: "/1VCrzVUrr9Nmmkl", - result_sha256: "SzBUUlAa0OOcJ4RkWAEcxPCu9KvgM3XV2Wt1Or7Qnm9", - result_sha512: "zp5KH/GGAMr3pQap8GbQ2Qgp3EjvI4o7kurGx9YNtwzN5eKvuWGuR/LNMa5qANyeHl2ROMMd0WkX24ttkiGIE1", - rounds: 5_000, - }, - // 128 length password - TestVector { - input: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - salt: "mpyXfdM3cczHJWG6", - result_sha256: "mDy6Ir4NZkSLjfI1jo.P8FQDl5cf51ZpOYO9VAiYzK2", - result_sha512: "vvNL55Todp53rsMLKgBJHsCC2lKj4AwYWWF/ywz7UVqBxj7F00UUI2an7R5amwBTL4DibkvKMb3Oj5dk4I1Y4.", - rounds: 5_000, - }, - // 129 length password - TestVector { - input: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - salt: "idU8Ptdv2tVGArtN", - result_sha256: "mijVpmWD4y0vuT.2Ux0XMwvpis59xfeu3Mhm1TIK7T9", - result_sha512: "uM8hU3Ot4nmDtcMvMyccUW2vT6uI2cM6MpWDslBlyO0jghVdKYB7RafnmQQPrA8QMauX8qrnX5Fs9ST5y/zUS1", - rounds: 5_000, - }, -]; - -#[test] -fn test_sha512_crypt() { - for t in TEST_VECTORS { - let params = Sha512Params::new(t.rounds).expect("Rounds error"); - let result = sha512_crypt_b64(t.input.as_bytes(), t.salt.as_bytes(), ¶ms); - assert!(result == t.result_sha512); - } -} - -#[test] -fn test_sha256_crypt() { - for t in TEST_VECTORS { - let params = Sha256Params::new(t.rounds).expect("Rounds error"); - let result = sha256_crypt_b64(t.input.as_bytes(), t.salt.as_bytes(), ¶ms); - println!("result {result:?}"); - println!("correct {:?}", t.result_sha256); - assert!(result == t.result_sha256); - } -} - -#[test] -fn test_sha512_crypt_invalid_rounds() { - let params = Sha512Params::new(ROUNDS_MAX + 1); - assert!(params.is_err()); - - let params = Sha512Params::new(ROUNDS_MIN - 1); - assert!(params.is_err()); -} - -#[test] -fn test_sha256_crypt_invalid_rounds() { - let params = Sha256Params::new(ROUNDS_MAX + 1); - assert!(params.is_err()); - - let params = Sha256Params::new(ROUNDS_MIN - 1); - assert!(params.is_err()); -} - -#[cfg(feature = "simple")] -#[test] -fn test_sha512_check() { - let pw = "foobar"; - let s = "$6$bbe605c2cce4c642$BiBOywFAm9kdv6ZPpj2GaKVqeh/.c21pf1uFBaq.e59KEE2Ej74iJleXaLXURYV6uh5LF4K7dDc4vtRtPiiKB/"; - assert!(sha512_check(pw, s).is_ok()); -} - -#[cfg(feature = "simple")] -#[test] -fn test_sha256_check() { - let pw = "foobar"; - let s = "$5$9aEeVXnCiCNHUjO/$FrVBcjyJukRaE6inMYazyQv1DBnwaKfom.71ebgQR/0"; - assert!(sha256_check(pw, s).is_ok()); -} - -#[cfg(feature = "simple")] -#[test] -fn test_sha512_check_with_rounds() { - let pw = "foobar"; - let s = "$6$rounds=100000$exn6tVc2j/MZD8uG$BI1Xh8qQSK9J4m14uwy7abn.ctj/TIAzlaVCto0MQrOFIeTXsc1iwzH16XEWo/a7c7Y9eVJvufVzYAs4EsPOy0"; - assert!(sha512_check(pw, s).is_ok()); -} - -#[cfg(feature = "simple")] -#[test] -fn test_sha256_check_with_rounds() { - let pw = "foobar"; - let s = "$5$rounds=100000$PhW/wpSsmgIMKsTW$d9kDD8dQNu3r0Ky.xcOEhdin6EQRebrHfNKDRwWP/pB"; - assert!(sha256_check(pw, s).is_ok()); -} - -#[cfg(feature = "simple")] -#[test] -fn test_sha512_simple_check_roundtrip() { - let pw = "this is my password"; - let params = Sha512Params::new(5_000).expect("Rounds error"); - - let hash = sha512_simple(pw, ¶ms); - dbg!(&hash); - - let c_r = sha512_check(pw, &hash); - dbg!(&c_r); - assert!(c_r.is_ok()); -} - -#[cfg(feature = "simple")] -#[test] -fn test_sha256_simple_check_roundtrip() { - let pw = "this is my password"; - let params = Sha256Params::new(5_000).expect("Rounds error"); - - let hash = sha256_simple(pw, ¶ms); - - let c_r = sha256_check(pw, &hash); - assert!(c_r.is_ok()); -} - -#[cfg(feature = "simple")] -#[test] -fn test_sha512_unexpected_prefix() { - let pw = "foobar"; - let s = "SHOULDNOTBEHERE$6$rounds=100000$exn6tVc2j/MZD8uG$BI1Xh8qQSK9J4m14uwy7abn.ctj/TIAzlaVCto0MQrOFIeTXsc1iwzH16XEWo/a7c7Y9eVJvufVzYAs4EsPOy0"; - assert!(sha512_check(pw, s).is_err()); -} - -#[cfg(feature = "simple")] -#[test] -fn test_sha256_unexpected_prefix() { - let pw = "foobar"; - let s = "SHOULDNOTBEHERE$6$rounds=100000$exn6tVc2j/MZD8uG$BI1Xh8qQSK9J4m14uwy7abn.ctj/TIAzlaVCto0MQrOFIeTXsc1iwzH16XEWo/a7c7Y9eVJvufVzYAs4EsPOy0"; - assert!(sha256_check(pw, s).is_err()); -} - -#[cfg(feature = "simple")] -#[test] -fn test_sha512_wrong_id() { - // wrong id '7' - let pw = "foobar"; - let s = "$7$rounds=100000$exn6tVc2j/MZD8uG$BI1Xh8qQSK9J4m14uwy7abn.ctj/TIAzlaVCto0MQrOFIeTXsc1iwzH16XEWo/a7c7Y9eVJvufVzYAs4EsPOy0"; - assert!(sha512_check(pw, s).is_err()); -} - -#[cfg(feature = "simple")] -#[test] -fn test_sha256_wrong_id() { - // wrong id '7' - let pw = "foobar"; - let s = "$7$rounds=100000$exn6tVc2j/MZD8uG$BI1Xh8qQSK9J4m14uwy7abn.ctj/TIAzlaVCto0MQrOFIeTXsc1iwzH16XEWo/a7c7Y9eVJvufVzYAs4EsPOy0"; - assert!(sha256_check(pw, s).is_err()); -} - -#[cfg(feature = "simple")] -#[test] -fn test_sha512_missing_trailing_slash() { - // Missing trailing slash - let pw = "abc"; - let s = "$6$rounds=656000$Ykk6fjI2sU3/uprV$Z6yV/9Z741lfroSSzB9MwxSRnGeI9Z74hBkgNsHuojQJxZ9XjPkHg9jqqGLvWZ586wqnSSx5vrXZdhrMSZZE4"; - assert!(sha512_check(pw, s).is_err()); -} - -#[cfg(feature = "simple")] -#[test] -fn test_sha256_missing_trailing_slash() { - // Missing trailing slash - let pw = "abc"; - let s = "$6$rounds=656000$Ykk6fjI2sU3/uprV$Z6yV/9Z741lfroSSzB9MwxSRnGeI9Z74hBkgNsHuojQJxZ9XjPkHg9jqqGLvWZ586wqnSSx5vrXZdhrMSZZE4"; - assert!(sha256_check(pw, s).is_err()); -} diff --git a/sha-crypt/tests/simple.rs b/sha-crypt/tests/simple.rs new file mode 100644 index 00000000..8da3c1e8 --- /dev/null +++ b/sha-crypt/tests/simple.rs @@ -0,0 +1,218 @@ +#![cfg(feature = "simple")] + +use base64ct::{Base64ShaCrypt, Encoding}; +use mcf::PasswordHash; +use sha_crypt::{ + SHA256_CRYPT, SHA512_CRYPT, Sha256Params, Sha512Params, + password_hash::{CustomizedPasswordHasher, Error, PasswordVerifier}, +}; + +struct TestVector { + input: &'static str, + salt: &'static str, + result_sha256: &'static str, + result_sha512: &'static str, + rounds: u32, +} + +const TEST_VECTORS: &[TestVector] = &[ + TestVector { + input: "Hello world!", + salt: "saltstring", + result_sha256: "5B8vYYiY.CVt1RlTTf8KbXBH3hsxY/GNooZaBBGWEc5", + result_sha512: "svn8UoSVapNtMuq1ukKS4tPQd8iKwSMHWjl/O817G3uBnIFNjnQJuesI68u4OTLiBFdcbYEdFCoEOfaS35inz1", + rounds: 5_000, + }, + TestVector { + input: "Hello world!", + salt: "saltstringsaltstring", + result_sha256: "3xv.VbSHBb41AL9AvLeujZkZRBAwqFMz2.opqey6IcA", + result_sha512: "OW1/O6BYHV6BcXZu8QVeXbDWra3Oeqh0sbHbbMCVNSnCM/UrjmM0Dp8vOuZeHBy/YTBmSK6H9qs/y3RnOaw5v.", + rounds: 10_000, + }, + TestVector { + input: "This is just a test", + salt: "toolongsaltstring", + result_sha256: "Un/5jzAHMgOGZ5.mWJpuVolil07guHPvOW8mGRcvxa5", + result_sha512: "lQ8jolhgVRVhY4b5pZKaysCLi0QBxGoNeKQzQ3glMhwllF7oGDZxUhx1yxdYcz/e1JSbq3y6JMxxl8audkUEm0", + rounds: 5_000, + }, + // 63 length password + TestVector { + input: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + salt: "kf8.jB3ZLrhG2/n1", + result_sha256: "DjWX2SQlslxT/jON7Gof6T4UodbHrqW0Lwl7xLT2gu8", + result_sha512: "BZk4ni5Rx3KgyM7vd48EpPgr8AoICCq5HRQPu6vNf6t6xnJ3xNu7MMMBXh/3eUZ5ql.mBqjNhlYUWHBqjKRkU/", + rounds: 5_000, + }, + // 64 length password + TestVector { + input: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", // 64 length + salt: "JKb8lkbDWryxdaRL", + result_sha256: "X2024tVMdQy9vnRU/dnWB0eL4dpfJvgD3g9o0eE95d7", + result_sha512: "dLVyYl.G1KhMak97BNNO7vV2upvwcQ3hKrQjO8xn.V/ucmN4ogytaGbIEfBrNv4YLtbpjgV240ldDgkP9M9S7.", + rounds: 5_000, + }, + // 65 length password + TestVector { + input: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", // 65 length + salt: "JmlLQtPDXkxMbdFc", + result_sha256: "MVZTo9AdKikQwK5sBlSQ/X7mUH19GoFGrmRr0XxcUr6", + result_sha512: "lO/BGRK6dKXMaRafyLMZl9wkxvdCobed0ppRHYJtCfatf6yGLghCs.rq.ifz4YezxCHmQG7lpqm4W46xsNnBm0", + rounds: 5_000, + }, + // 127 length password + TestVector { + input: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + salt: "/1VCrzVUrr9Nmmkl", + result_sha256: "SzBUUlAa0OOcJ4RkWAEcxPCu9KvgM3XV2Wt1Or7Qnm9", + result_sha512: "zp5KH/GGAMr3pQap8GbQ2Qgp3EjvI4o7kurGx9YNtwzN5eKvuWGuR/LNMa5qANyeHl2ROMMd0WkX24ttkiGIE1", + rounds: 5_000, + }, + // 128 length password + TestVector { + input: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + salt: "mpyXfdM3cczHJWG6", + result_sha256: "mDy6Ir4NZkSLjfI1jo.P8FQDl5cf51ZpOYO9VAiYzK2", + result_sha512: "vvNL55Todp53rsMLKgBJHsCC2lKj4AwYWWF/ywz7UVqBxj7F00UUI2an7R5amwBTL4DibkvKMb3Oj5dk4I1Y4.", + rounds: 5_000, + }, + // 129 length password + TestVector { + input: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + salt: "idU8Ptdv2tVGArtN", + result_sha256: "mijVpmWD4y0vuT.2Ux0XMwvpis59xfeu3Mhm1TIK7T9", + result_sha512: "uM8hU3Ot4nmDtcMvMyccUW2vT6uI2cM6MpWDslBlyO0jghVdKYB7RafnmQQPrA8QMauX8qrnX5Fs9ST5y/zUS1", + rounds: 5_000, + }, +]; + +#[test] +fn hash_sha256_crypt() { + let mut any = false; + + for t in TEST_VECTORS { + if let Ok(salt) = Base64ShaCrypt::decode_vec(&t.salt) { + let params = Sha256Params::new(t.rounds).unwrap(); + let result = SHA256_CRYPT + .hash_password_with_params(t.input.as_bytes(), &salt, params) + .unwrap(); + + assert_eq!(result.fields().last().unwrap().as_str(), t.result_sha256); + any = true; + } + } + + assert!(any) +} + +#[test] +fn hash_sha512_crypt() { + let mut any = false; + + for t in TEST_VECTORS { + if let Ok(salt) = Base64ShaCrypt::decode_vec(&t.salt) { + let params = Sha512Params::new(t.rounds).unwrap(); + let result = SHA512_CRYPT + .hash_password_with_params(t.input.as_bytes(), &salt, params) + .unwrap(); + + assert_eq!(result.fields().last().unwrap().as_str(), t.result_sha512); + any = true; + } + } + + assert!(any) +} + +#[test] +fn verify_sha256_crypt() { + for t in TEST_VECTORS { + let mut hash = PasswordHash::from_id("6").unwrap(); + hash.push_str(&format!("rounds={}", t.rounds)).unwrap(); + hash.push_str(t.salt).unwrap(); + hash.push_str(t.result_sha512).unwrap(); + + assert_eq!( + SHA512_CRYPT.verify_password(t.input.as_bytes(), &hash), + Ok(()) + ); + } + + assert_eq!( + SHA256_CRYPT.verify_password( + b"foobar", + &PasswordHash::new("$5$9aEeVXnCiCNHUjO/$FrVBcjyJukRaE6inMYazyQv1DBnwaKfom.71ebgQR/0") + .unwrap() + ), + Ok(()) + ); + + assert_eq!( + SHA256_CRYPT.verify_password( + b"foobar", + &PasswordHash::new( + "$5$rounds=100000$PhW/wpSsmgIMKsTW$d9kDD8dQNu3r0Ky.xcOEhdin6EQRebrHfNKDRwWP/pB" + ) + .unwrap() + ), + Ok(()) + ); +} + +#[test] +fn verify_sha512_crypt() { + for t in TEST_VECTORS { + let mut hash = PasswordHash::from_id("6").unwrap(); + hash.push_str(&format!("rounds={}", t.rounds)).unwrap(); + hash.push_str(t.salt).unwrap(); + hash.push_str(t.result_sha512).unwrap(); + + assert_eq!( + SHA512_CRYPT.verify_password(t.input.as_bytes(), &hash), + Ok(()) + ); + } + + assert_eq!( + SHA512_CRYPT.verify_password( + b"foobar", + &PasswordHash::new( + "$6$bbe605c2cce4c642$BiBOywFAm9kdv6ZPpj2GaKVqeh/.c21pf1uFBaq.e59KEE2Ej74iJleXaLXURYV6uh5LF4K7dDc4vtRtPiiKB/" + ).unwrap() + ), + Ok(()) + ); + + assert_eq!( + SHA512_CRYPT.verify_password( + b"foobar", + &PasswordHash::new( + "$6$rounds=100000$exn6tVc2j/MZD8uG$BI1Xh8qQSK9J4m14uwy7abn.ctj/TIAzlaVCto0MQrOFIeTXsc1iwzH16XEWo/a7c7Y9eVJvufVzYAs4EsPOy0" + ).unwrap() + ), + Ok(()) + ); +} + +#[cfg(feature = "simple")] +#[test] +fn test_sha256_wrong_id() { + let passwd = b"foobar"; + + // wrong id '7' + let hash = PasswordHash::new("$7$rounds=100000$exn6tVc2j/MZD8uG$BI1Xh8qQSK9J4m14uwy7abn.ctj/TIAzlaVCto0MQrOFIeTXsc1iwzH16XEWo/a7c7Y9eVJvufVzYAs4EsPOy0").unwrap(); + let res = SHA256_CRYPT.verify_password(passwd, &hash); + assert_eq!(res, Err(Error::Algorithm)); +} + +#[cfg(feature = "simple")] +#[test] +fn test_sha512_wrong_id() { + let passwd = b"foobar"; + + // wrong id '7' + let hash = PasswordHash::new("$7$rounds=100000$exn6tVc2j/MZD8uG$BI1Xh8qQSK9J4m14uwy7abn.ctj/TIAzlaVCto0MQrOFIeTXsc1iwzH16XEWo/a7c7Y9eVJvufVzYAs4EsPOy0").unwrap(); + let res = SHA512_CRYPT.verify_password(passwd, &hash); + assert_eq!(res, Err(Error::Algorithm)); +}