diff --git a/Cargo.lock b/Cargo.lock index 4a2748d15..5df257cc3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -324,10 +324,7 @@ dependencies = [ name = "password-hash" version = "0.6.0-rc.3" dependencies = [ - "base64ct", - "getrandom", - "rand_core", - "subtle", + "phc", ] [[package]] @@ -339,6 +336,15 @@ dependencies = [ "base64ct", ] +[[package]] +name = "phc" +version = "0.3.0-pre" +source = "git+https://github.com/RustCrypto/formats#1e7caeb8e57b89d5cff1fba6ab500928fb3eccf7" +dependencies = [ + "base64ct", + "subtle", +] + [[package]] name = "pkcs8" version = "0.11.0-rc.8" diff --git a/Cargo.toml b/Cargo.toml index 408b9a7f0..1bf315815 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,3 +18,4 @@ digest = { path = "digest" } signature = { path = "signature" } getrandom = { git = "https://github.com/rust-random/getrandom" } +phc = { git = "https://github.com/RustCrypto/formats" } diff --git a/password-hash/Cargo.toml b/password-hash/Cargo.toml index 993282500..b4cbe4ba3 100644 --- a/password-hash/Cargo.toml +++ b/password-hash/Cargo.toml @@ -17,19 +17,10 @@ as well as a `no_std`-friendly implementation of the PHC string format """ [dependencies] -base64ct = "1.7" -subtle = { version = "2", default-features = false } - -# optional dependencies -getrandom = { version = "0.3", optional = true, default-features = false } -rand_core = { version = "0.10.0-rc-2", optional = true, default-features = false } +phc = { version = "0.3.0-pre", optional = true, default-features = false } [features] -default = ["phc", "rand_core"] -alloc = ["base64ct/alloc"] -getrandom = ["dep:getrandom"] -phc = [] -rand_core = ["dep:rand_core"] +alloc = ["phc?/alloc"] [package.metadata.docs.rs] all-features = true diff --git a/password-hash/src/error.rs b/password-hash/src/error.rs new file mode 100644 index 000000000..811072bc0 --- /dev/null +++ b/password-hash/src/error.rs @@ -0,0 +1,63 @@ +//! Error types. + +use core::fmt; + +/// Result type. +pub type Result = core::result::Result; + +/// Password hashing errors. +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub enum Error { + /// Unsupported algorithm. + Algorithm, + + /// Cryptographic error. + Crypto, + + /// Encoding errors (e.g. Base64). + EncodingInvalid, + + /// Out of memory (heap allocation failure). + OutOfMemory, + + /// Output size invalid. + OutputSize, + + /// Invalid named parameter. + ParamInvalid { + /// Parameter name. + name: &'static str, + }, + + /// Invalid parameters. + ParamsInvalid, + + /// Invalid password. + PasswordInvalid, + + /// Invalid salt. + SaltInvalid, + + /// Invalid algorithm version. + Version, +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> core::result::Result<(), fmt::Error> { + match self { + Self::Algorithm => write!(f, "unsupported algorithm"), + Self::Crypto => write!(f, "cryptographic error"), + Self::EncodingInvalid => write!(f, "invalid encoding"), + Self::OutOfMemory => write!(f, "out of memory"), + Self::OutputSize => write!(f, "password hash output size invalid"), + Self::ParamInvalid { name } => write!(f, "invalid parameter: {name:?}"), + Self::ParamsInvalid => write!(f, "invalid parameters"), + Self::PasswordInvalid => write!(f, "invalid password"), + Self::SaltInvalid => write!(f, "invalid salt"), + Self::Version => write!(f, "invalid algorithm version"), + } + } +} + +impl core::error::Error for Error {} diff --git a/password-hash/src/errors.rs b/password-hash/src/errors.rs deleted file mode 100644 index efa72fb5d..000000000 --- a/password-hash/src/errors.rs +++ /dev/null @@ -1,178 +0,0 @@ -//! Error types. - -pub use base64ct::Error as B64Error; - -use core::cmp::Ordering; -use core::fmt; - -/// Result type. -pub type Result = core::result::Result; - -/// Password hashing errors. -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -#[non_exhaustive] -pub enum Error { - /// Unsupported algorithm. - Algorithm, - - /// "B64" encoding error. - B64Encoding(B64Error), - - /// Cryptographic error. - Crypto, - - /// Out of memory (heap allocation failure). - OutOfMemory, - - /// Output size unexpected. - OutputSize { - /// Indicates why the output size is unexpected. - /// - /// - [`Ordering::Less`]: Size is too small. - /// - [`Ordering::Equal`]: Size is not exactly as `expected`. - /// - [`Ordering::Greater`]: Size is too long. - provided: Ordering, - /// Expected output size in relation to `provided`. - /// - /// - [`Ordering::Less`]: Minimum size. - /// - [`Ordering::Equal`]: Expected size. - /// - [`Ordering::Greater`]: Maximum size. - expected: usize, - }, - - /// Duplicate parameter name encountered. - ParamNameDuplicated, - - /// Invalid parameter name. - ParamNameInvalid, - - /// Invalid parameter value. - ParamValueInvalid(InvalidValue), - - /// Maximum number of parameters exceeded. - ParamsMaxExceeded, - - /// Invalid password. - Password, - - /// Password hash string invalid. - PhcStringField, - - /// Password hash string contains trailing data. - PhcStringTrailingData, - - /// Salt invalid. - SaltInvalid(InvalidValue), - - /// Value exceeds the maximum allowed length. - TooLong, - - /// Value does not satisfy the minimum length. - TooShort, - - /// Invalid algorithm version. - Version, -} - -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> core::result::Result<(), fmt::Error> { - match self { - Self::Algorithm => write!(f, "unsupported algorithm"), - Self::B64Encoding(err) => write!(f, "{err}"), - Self::Crypto => write!(f, "cryptographic error"), - Self::OutOfMemory => write!(f, "out of memory"), - Self::OutputSize { provided, expected } => match provided { - Ordering::Less => write!( - f, - "output size too short, expected at least {expected} bytes", - ), - Ordering::Equal => write!(f, "output size unexpected, expected {expected} bytes"), - Ordering::Greater => { - write!(f, "output size too long, expected at most {expected} bytes") - } - }, - Self::ParamNameDuplicated => f.write_str("duplicate parameter"), - Self::ParamNameInvalid => f.write_str("invalid parameter name"), - Self::ParamValueInvalid(val_err) => write!(f, "invalid parameter value: {val_err}"), - Self::ParamsMaxExceeded => f.write_str("maximum number of parameters reached"), - Self::Password => write!(f, "invalid password"), - Self::PhcStringField => write!(f, "password hash string missing field"), - Self::PhcStringTrailingData => { - write!(f, "password hash string contains trailing characters") - } - Self::SaltInvalid(val_err) => write!(f, "salt invalid: {val_err}"), - Self::TooLong => f.write_str("value to long"), - Self::TooShort => f.write_str("value to short"), - Self::Version => write!(f, "invalid algorithm version"), - } - } -} - -impl core::error::Error for Error { - fn source(&self) -> Option<&(dyn core::error::Error + 'static)> { - match self { - Self::B64Encoding(err) => Some(err), - Self::ParamValueInvalid(err) => Some(err), - Self::SaltInvalid(err) => Some(err), - _ => None, - } - } -} - -impl From for Error { - fn from(err: B64Error) -> Error { - Error::B64Encoding(err) - } -} - -impl From for Error { - fn from(_: base64ct::InvalidLengthError) -> Error { - Error::B64Encoding(B64Error::InvalidLength) - } -} - -/// Parse errors relating to invalid parameter values or salts. -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -#[non_exhaustive] -pub enum InvalidValue { - /// Character is not in the allowed set. - InvalidChar(char), - - /// Format is invalid. - InvalidFormat, - - /// Value is malformed. - Malformed, - - /// Value exceeds the maximum allowed length. - TooLong, - - /// Value does not satisfy the minimum length. - TooShort, -} - -impl InvalidValue { - /// Create an [`Error::ParamValueInvalid`] which warps this error. - pub fn param_error(self) -> Error { - Error::ParamValueInvalid(self) - } - - /// Create an [`Error::SaltInvalid`] which wraps this error. - pub fn salt_error(self) -> Error { - Error::SaltInvalid(self) - } -} - -impl fmt::Display for InvalidValue { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> core::result::Result<(), fmt::Error> { - match self { - Self::InvalidChar(c) => write!(f, "contains invalid character: '{c}'"), - Self::InvalidFormat => f.write_str("value format is invalid"), - Self::Malformed => f.write_str("value malformed"), - Self::TooLong => f.write_str("value to long"), - Self::TooShort => f.write_str("value to short"), - } - } -} - -impl core::error::Error for InvalidValue {} diff --git a/password-hash/src/lib.rs b/password-hash/src/lib.rs index b79dfa66d..ee55eb152 100644 --- a/password-hash/src/lib.rs +++ b/password-hash/src/lib.rs @@ -29,14 +29,12 @@ #[allow(unused_extern_crates)] extern crate alloc; -#[cfg(feature = "rand_core")] -pub use rand_core; +mod error; -pub mod errors; -#[cfg(feature = "phc")] -pub mod phc; +pub use crate::error::{Error, Result}; -pub use crate::errors::{Error, Result}; +#[cfg(feature = "phc")] +pub use phc; /// DEPRECATED: import this as `password_hash::phc::PasswordHash`. #[cfg(feature = "phc")] @@ -46,11 +44,11 @@ pub use crate::errors::{Error, Result}; )] pub type PasswordHash = phc::PasswordHash; -/// DEPRECATED: import this as `password_hash::phc::PasswordHashString`. +/// DEPRECATED: use `password_hash::phc::PasswordHash` or `String` #[cfg(all(feature = "alloc", feature = "phc"))] #[deprecated( since = "0.6.0", - note = "import as `password_hash::phc::PasswordHashString` instead" + note = "use `password_hash::phc::PasswordHash` or `String`" )] pub type PasswordHashString = phc::PasswordHashString; @@ -97,13 +95,12 @@ pub trait CustomizedPasswordHasher { /// Trait for password verification. /// -/// Generic around a password hash to be returned (typically [`PasswordHash`]) +/// Generic around a password hash to be returned (typically [`phc::PasswordHash`]) /// -/// Automatically impl'd for any type that impls [`PasswordHasher`] with [`PasswordHash`] as `H`. +/// Automatically impl'd for type that impl [`PasswordHasher`] with [`phc::PasswordHash`] as `H`. /// /// This trait is object safe and can be used to implement abstractions over -/// multiple password hashing algorithms. One such abstraction is provided by -/// the [`PasswordHash::verify_password`] method. +/// multiple password hashing algorithms. pub trait PasswordVerifier { /// Compute this password hashing function against the provided password /// using the parameters from the provided password hash and see if the @@ -135,7 +132,7 @@ impl> PasswordVerifier (), } - Err(Error::Password) + Err(Error::PasswordInvalid) } } diff --git a/password-hash/src/phc.rs b/password-hash/src/phc.rs deleted file mode 100644 index 76348e407..000000000 --- a/password-hash/src/phc.rs +++ /dev/null @@ -1,334 +0,0 @@ -//! Password Hashing Competition string format implementation. - -mod ident; -mod output; -mod params; -mod salt; -mod string_buf; -mod value; - -pub use ident::Ident; -pub use output::Output; -pub use params::ParamsString; -pub use salt::{Salt, SaltString}; -pub use value::{Decimal, Value}; - -use crate::{Error, PasswordHasher, PasswordVerifier}; -use core::{fmt, str::FromStr}; -use string_buf::StringBuf; - -#[cfg(feature = "alloc")] -use alloc::string::{String, ToString}; - -/// Separator character used in password hashes (e.g. `$6$...`). -const PASSWORD_HASH_SEPARATOR: char = '$'; - -/// Password hash. -/// -/// This type corresponds to the parsed representation of a PHC string as -/// described in the [PHC string format specification][1]. -/// -/// PHC strings have the following format: -/// -/// ```text -/// $[$v=][$=(,=)*][$[$]] -/// ``` -/// -/// where: -/// -/// - `` is the symbolic name for the function -/// - `` is the algorithm version -/// - `` is a parameter name -/// - `` is a parameter value -/// - `` is an encoding of the salt -/// - `` is an encoding of the hash output -/// -/// The string is then the concatenation, in that order, of: -/// -/// - a `$` sign; -/// - the function symbolic name; -/// - optionally, a `$` sign followed by the algorithm version with a `v=version` format; -/// - optionally, a `$` sign followed by one or several parameters, each with a `name=value` format; -/// the parameters are separated by commas; -/// - optionally, a `$` sign followed by the (encoded) salt value; -/// - optionally, a `$` sign followed by the (encoded) hash output (the hash output may be present -/// only if the salt is present). -/// -/// [1]: https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md#specification -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct PasswordHash { - /// Password hashing algorithm identifier. - /// - /// This corresponds to the `` field in a PHC string, a.k.a. the - /// symbolic name for the function. - pub algorithm: Ident, - - /// Optional version field. - /// - /// This corresponds to the `` field in a PHC string. - pub version: Option, - - /// Algorithm-specific parameters. - /// - /// This corresponds to the set of `$=(,=)*` - /// name/value pairs in a PHC string. - pub params: ParamsString, - - /// [`Salt`] string for personalizing a password hash output. - /// - /// This corresponds to the `` value in a PHC string. - pub salt: Option, - - /// Password hashing function [`Output`], a.k.a. hash/digest. - /// - /// This corresponds to the `` output in a PHC string. - pub hash: Option, -} - -impl PasswordHash { - /// Parse a password hash from a string in the PHC string format. - pub fn new(s: &str) -> crate::Result { - if s.is_empty() { - return Err(Error::PhcStringField); - } - - let mut fields = s.split(PASSWORD_HASH_SEPARATOR); - let beginning = fields.next().expect("no first field"); - - if beginning.chars().next().is_some() { - return Err(Error::PhcStringField); - } - - let algorithm = fields - .next() - .ok_or(Error::PhcStringField) - .and_then(Ident::from_str)?; - - let mut version = None; - let mut params = ParamsString::new(); - let mut salt = None; - let mut hash = None; - - let mut next_field = fields.next(); - - if let Some(field) = next_field { - // v= - if field.starts_with("v=") && !field.contains(params::PARAMS_DELIMITER) { - version = Some(Value::new(&field[2..]).and_then(|value| value.decimal())?); - next_field = None; - } - } - - if next_field.is_none() { - next_field = fields.next(); - } - - if let Some(field) = next_field { - // = - if field.contains(params::PAIR_DELIMITER) { - params = field.parse()?; - next_field = None; - } - } - - if next_field.is_none() { - next_field = fields.next(); - } - - if let Some(s) = next_field { - salt = Some(s.parse()?); - } - - if let Some(field) = fields.next() { - hash = Some(Output::decode(field)?); - } - - if fields.next().is_some() { - return Err(Error::PhcStringTrailingData); - } - - Ok(Self { - algorithm, - version, - params, - salt, - hash, - }) - } - - /// Generate a password hash using the supplied algorithm. - pub fn generate( - phf: impl PasswordHasher, - password: impl AsRef<[u8]>, - salt: &[u8], - ) -> crate::Result { - phf.hash_password(password.as_ref(), salt) - } - - /// Verify this password hash using the specified set of supported - /// [`PasswordHasher`] trait objects. - pub fn verify_password( - &self, - phfs: &[&dyn PasswordVerifier], - password: impl AsRef<[u8]>, - ) -> crate::Result<()> { - for &phf in phfs { - if phf.verify_password(password.as_ref(), self).is_ok() { - return Ok(()); - } - } - - Err(Error::Password) - } - - /// Serialize this [`PasswordHash`] as a [`PasswordHashString`]. - #[cfg(feature = "alloc")] - pub fn serialize(&self) -> PasswordHashString { - self.into() - } -} - -impl FromStr for PasswordHash { - type Err = Error; - - fn from_str(s: &str) -> crate::Result { - Self::new(s) - } -} - -impl TryFrom<&str> for PasswordHash { - type Error = Error; - - fn try_from(s: &str) -> crate::Result { - Self::new(s) - } -} - -impl fmt::Display for PasswordHash { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}{}", PASSWORD_HASH_SEPARATOR, self.algorithm)?; - - if let Some(version) = self.version { - write!(f, "{PASSWORD_HASH_SEPARATOR}v={version}")?; - } - - if !self.params.is_empty() { - write!(f, "{}{}", PASSWORD_HASH_SEPARATOR, self.params)?; - } - - if let Some(salt) = &self.salt { - write!(f, "{PASSWORD_HASH_SEPARATOR}{salt}")?; - - if let Some(hash) = &self.hash { - write!(f, "{PASSWORD_HASH_SEPARATOR}{hash}")?; - } - } - - Ok(()) - } -} - -/// Serialized [`PasswordHash`]. -/// -/// This type contains a serialized password hash string which is ensured to -/// parse successfully. -// TODO(tarcieri): cached parsed representations? or at least structural data -#[cfg(feature = "alloc")] -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct PasswordHashString { - /// String value - string: String, -} - -#[cfg(feature = "alloc")] -#[allow(clippy::len_without_is_empty)] -impl PasswordHashString { - /// Parse a password hash from a string in the PHC string format. - pub fn new(s: &str) -> crate::Result { - PasswordHash::new(s).map(Into::into) - } - - /// Parse this owned string as a [`PasswordHash`]. - pub fn password_hash(&self) -> PasswordHash { - PasswordHash::new(&self.string).expect("malformed password hash") - } - - /// Borrow this value as a `str`. - pub fn as_str(&self) -> &str { - self.string.as_str() - } - - /// Borrow this value as bytes. - pub fn as_bytes(&self) -> &[u8] { - self.as_str().as_bytes() - } - - /// Get the length of this value in ASCII characters. - pub fn len(&self) -> usize { - self.as_str().len() - } - - /// Password hashing algorithm identifier. - pub fn algorithm(&self) -> Ident { - self.password_hash().algorithm - } - - /// Optional version field. - pub fn version(&self) -> Option { - self.password_hash().version - } - - /// Algorithm-specific parameters. - pub fn params(&self) -> ParamsString { - self.password_hash().params - } - - /// [`Salt`] string for personalizing a password hash output. - pub fn salt(&self) -> Option { - self.password_hash().salt - } - - /// Password hashing function [`Output`], a.k.a. hash/digest. - pub fn hash(&self) -> Option { - self.password_hash().hash - } -} - -#[cfg(feature = "alloc")] -impl AsRef for PasswordHashString { - fn as_ref(&self) -> &str { - self.as_str() - } -} - -#[cfg(feature = "alloc")] -impl From for PasswordHashString { - fn from(hash: PasswordHash) -> PasswordHashString { - PasswordHashString::from(&hash) - } -} - -#[cfg(feature = "alloc")] -impl From<&PasswordHash> for PasswordHashString { - fn from(hash: &PasswordHash) -> PasswordHashString { - PasswordHashString { - string: hash.to_string(), - } - } -} - -#[cfg(feature = "alloc")] -impl FromStr for PasswordHashString { - type Err = Error; - - fn from_str(s: &str) -> crate::Result { - Self::new(s) - } -} - -#[cfg(feature = "alloc")] -impl fmt::Display for PasswordHashString { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(self.as_str()) - } -} diff --git a/password-hash/src/phc/ident.rs b/password-hash/src/phc/ident.rs deleted file mode 100644 index 302010389..000000000 --- a/password-hash/src/phc/ident.rs +++ /dev/null @@ -1,185 +0,0 @@ -//! Algorithm or parameter identifier. -//! -//! Implements the following parts of the [PHC string format specification][1]: -//! -//! > The function symbolic name is a sequence of characters in: `[a-z0-9-]` -//! > (lowercase letters, digits, and the minus sign). No other character is -//! > allowed. Each function defines its own identifier (or identifiers in case -//! > of a function family); identifiers should be explicit (human readable, -//! > not a single digit), with a length of about 5 to 10 characters. An -//! > identifier name MUST NOT exceed 32 characters in length. -//! > -//! > Each parameter name shall be a sequence of characters in: `[a-z0-9-]` -//! > (lowercase letters, digits, and the minus sign). No other character is -//! > allowed. Parameter names SHOULD be readable for a human user. A -//! > parameter name MUST NOT exceed 32 characters in length. -//! -//! [1]: https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md - -use super::StringBuf; -use crate::{Error, Result}; -use core::{ - fmt, - ops::Deref, - str::{self, FromStr}, -}; - -/// Algorithm or parameter identifier. -/// -/// This type encompasses both the "function symbolic name" and "parameter name" -/// use cases as described in the [PHC string format specification][1]. -/// -/// # Constraints -/// - ASCII-encoded string consisting of the characters `[a-z0-9-]` -/// (lowercase letters, digits, and the minus sign) -/// - Minimum length: 1 ASCII character (i.e. 1-byte) -/// - Maximum length: 32 ASCII characters (i.e. 32-bytes) -/// -/// [1]: https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md -#[derive(Copy, Clone, Eq, Hash, PartialEq, PartialOrd, Ord)] -pub struct Ident(StringBuf<{ Ident::MAX_LENGTH }>); - -impl Ident { - /// Maximum length of an [`Ident`] - 32 ASCII characters (i.e. 32-bytes). - /// - /// This value corresponds to the maximum size of a function symbolic names - /// and parameter names according to the PHC string format. - /// Maximum length of an [`Ident`] - 32 ASCII characters (i.e. 32-bytes). - /// - /// This value corresponds to the maximum size of a function symbolic names - /// and parameter names according to the PHC string format. - const MAX_LENGTH: usize = 32; - - /// Parse an [`Ident`] from a string. - /// - /// String must conform to the constraints given in the type-level - /// documentation. - pub const fn new(s: &str) -> Result { - let input = s.as_bytes(); - - match input.len() { - 1..=Self::MAX_LENGTH => { - let mut i = 0; - - while i < input.len() { - if !matches!(input[i], b'a'..=b'z' | b'0'..=b'9' | b'-') { - return Err(Error::ParamNameInvalid); - } - - i += 1; - } - - match StringBuf::new(s) { - Ok(buf) => Ok(Self(buf)), - Err(e) => Err(e), - } - } - _ => Err(Error::ParamNameInvalid), - } - } - - /// Parse an [`Ident`] from a string, panicking on parse errors. - /// - /// This function exists as a workaround for `unwrap` not yet being - /// stable in `const fn` contexts, and is intended to allow the result to - /// be bound to a constant value. - pub const fn new_unwrap(s: &str) -> Self { - assert!(!s.is_empty(), "PHC ident string can't be empty"); - assert!(s.len() <= Self::MAX_LENGTH, "PHC ident string too long"); - - match Self::new(s) { - Ok(ident) => ident, - Err(_) => panic!("invalid PHC string format identifier"), - } - } - - /// Borrow this ident as a `str` - pub fn as_str(&self) -> &str { - &self.0 - } -} - -impl AsRef for Ident { - fn as_ref(&self) -> &str { - self.as_str() - } -} - -impl Deref for Ident { - type Target = str; - - fn deref(&self) -> &str { - self.as_str() - } -} - -impl FromStr for Ident { - type Err = Error; - - fn from_str(s: &str) -> Result { - Self::new(s) - } -} - -impl TryFrom<&str> for Ident { - type Error = Error; - - fn try_from(s: &str) -> Result { - Self::new(s) - } -} - -impl fmt::Display for Ident { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(self) - } -} - -impl fmt::Debug for Ident { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_tuple("Ident").field(&self.as_ref()).finish() - } -} - -#[cfg(test)] -mod tests { - use super::{Error, Ident}; - - // Invalid ident examples - const INVALID_EMPTY: &str = ""; - const INVALID_CHAR: &str = "argon2;d"; - const INVALID_TOO_LONG: &str = "012345678911234567892123456789312"; - const INVALID_CHAR_AND_TOO_LONG: &str = "0!2345678911234567892123456789312"; - - #[test] - fn parse_valid() { - let valid_examples = ["6", "x", "argon2d", "01234567891123456789212345678931"]; - - for &example in &valid_examples { - assert_eq!(example, &*Ident::new(example).unwrap()); - } - } - - #[test] - fn reject_empty() { - assert_eq!(Ident::new(INVALID_EMPTY), Err(Error::ParamNameInvalid)); - } - - #[test] - fn reject_invalid() { - assert_eq!(Ident::new(INVALID_CHAR), Err(Error::ParamNameInvalid)); - } - - #[test] - fn reject_too_long() { - assert_eq!(Ident::new(INVALID_TOO_LONG), Err(Error::ParamNameInvalid)); - } - - #[test] - fn reject_invalid_char_and_too_long() { - assert_eq!( - Ident::new(INVALID_CHAR_AND_TOO_LONG), - Err(Error::ParamNameInvalid) - ); - } -} diff --git a/password-hash/src/phc/output.rs b/password-hash/src/phc/output.rs deleted file mode 100644 index e5c1fb2e2..000000000 --- a/password-hash/src/phc/output.rs +++ /dev/null @@ -1,325 +0,0 @@ -//! Outputs from password hashing functions. - -use crate::{Error, Result}; -use base64ct::{Base64Unpadded as B64, Encoding}; -use core::{cmp::Ordering, fmt, str::FromStr}; -use subtle::{Choice, ConstantTimeEq}; - -/// Output from password hashing functions, i.e. the "hash" or "digest" -/// as raw bytes. -/// -/// The [`Output`] type implements the RECOMMENDED best practices described in -/// the [PHC string format specification][1], namely: -/// -/// > The hash output, for a verification, must be long enough to make preimage -/// > attacks at least as hard as password guessing. To promote wide acceptance, -/// > a default output size of 256 bits (32 bytes, encoded as 43 characters) is -/// > recommended. Function implementations SHOULD NOT allow outputs of less -/// > than 80 bits to be used for password verification. -/// -/// # Recommended length -/// Per the description above, the recommended default length for an [`Output`] -/// of a password hashing function is **32-bytes** (256-bits). -/// -/// # Constraints -/// The above guidelines are interpreted into the following constraints: -/// -/// - Minimum length: **10**-bytes (80-bits) -/// - Maximum length: **64**-bytes (512-bits) -/// -/// The specific recommendation of a 64-byte maximum length is taken as a best -/// practice from the hash output guidelines for [Argon2 Encoding][2] given in -/// the same document: -/// -/// > The hash output...length shall be between 12 and 64 bytes (16 and 86 -/// > characters, respectively). The default output length is 32 bytes -/// > (43 characters). -/// -/// Based on this guidance, this type enforces an upper bound of 64-bytes -/// as a reasonable maximum, and recommends using 32-bytes. -/// -/// # Constant-time comparisons -/// The [`Output`] type impls the [`ConstantTimeEq`] trait from the [`subtle`] -/// crate and uses it to perform constant-time comparisons. -/// -/// Additionally the [`PartialEq`] and [`Eq`] trait impls for [`Output`] use -/// [`ConstantTimeEq`] when performing comparisons. -/// -/// ## Attacks on non-constant-time password hash comparisons -/// Comparing password hashes in constant-time is known to mitigate at least -/// one [poorly understood attack][3] involving an adversary with the following -/// knowledge/capabilities: -/// -/// - full knowledge of what password hashing algorithm is being used -/// including any relevant configurable parameters -/// - knowledge of the salt for a particular victim -/// - ability to accurately measure a timing side-channel on comparisons -/// of the password hash over the network -/// -/// An attacker with the above is able to perform an offline computation of -/// the hash for any chosen password in such a way that it will match the -/// hash computed by the server. -/// -/// As noted above, they also measure timing variability in the server's -/// comparison of the hash it computes for a given password and a target hash -/// the attacker is trying to learn. -/// -/// When the attacker observes a hash comparison that takes longer than their -/// previous attempts, they learn that they guessed another byte in the -/// password hash correctly. They can leverage repeated measurements and -/// observations with different candidate passwords to learn the password -/// hash a byte-at-a-time in a manner similar to other such timing side-channel -/// attacks. -/// -/// The attack may seem somewhat counterintuitive since learning prefixes of a -/// password hash does not reveal any additional information about the password -/// itself. However, the above can be combined with an offline dictionary -/// attack where the attacker is able to determine candidate passwords to send -/// to the server by performing a brute force search offline and selecting -/// candidate passwords whose hashes match the portion of the prefix they have -/// learned so far. -/// -/// As the attacker learns a longer and longer prefix of the password hash, -/// they are able to more effectively eliminate candidate passwords offline as -/// part of a dictionary attack, until they eventually guess the correct -/// password or exhaust their set of candidate passwords. -/// -/// ## Mitigations -/// While we have taken care to ensure password hashes are compared in constant -/// time, we would also suggest preventing such attacks by using randomly -/// generated salts and keeping those salts secret. -/// -/// The [`SaltString::from_rng`][`crate::phc::SaltString::from_rng`] and -/// [`SaltString::try_from_rng`][`crate::phc::SaltString::try_from_rng`] functions can be -/// used to generate random high-entropy salt values. -/// -/// [1]: https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md#function-duties -/// [2]: https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md#argon2-encoding -/// [3]: https://web.archive.org/web/20130208100210/http://security-assessment.com/files/documents/presentations/TimingAttackPresentation2012.pdf -#[derive(Copy, Clone, Eq)] -pub struct Output { - /// Byte array containing a password hashing function output. - bytes: [u8; Self::MAX_LENGTH], - - /// Length of the password hashing function output in bytes. - length: u8, -} - -#[allow(clippy::len_without_is_empty)] -impl Output { - /// Minimum length of a [`Output`] string: 10-bytes. - pub const MIN_LENGTH: usize = 10; - - /// Maximum length of [`Output`] string: 64-bytes. - /// - /// See type-level documentation about [`Output`] for more information. - pub const MAX_LENGTH: usize = 64; - - /// Maximum length of [`Output`] when encoded as B64 string: 86-bytes - /// (i.e. 86 ASCII characters) - pub const B64_MAX_LENGTH: usize = ((Self::MAX_LENGTH * 4) / 3) + 1; - - /// Create a [`Output`] from the given byte slice, validating it according - /// to [`Output::MIN_LENGTH`] and [`Output::MAX_LENGTH`] restrictions. - pub fn new(input: &[u8]) -> Result { - Self::init_with(input.len(), |bytes| { - bytes.copy_from_slice(input); - Ok(()) - }) - } - - /// Initialize an [`Output`] using the provided method, which is given - /// a mutable byte slice into which it should write the output. - /// - /// The `output_size` (in bytes) must be known in advance, as well as at - /// least [`Output::MIN_LENGTH`] bytes and at most [`Output::MAX_LENGTH`] - /// bytes. - pub fn init_with(output_size: usize, f: F) -> Result - where - F: FnOnce(&mut [u8]) -> Result<()>, - { - if output_size < Self::MIN_LENGTH { - return Err(Error::OutputSize { - provided: Ordering::Less, - expected: Self::MIN_LENGTH, - }); - } - - if output_size > Self::MAX_LENGTH { - return Err(Error::OutputSize { - provided: Ordering::Greater, - expected: Self::MAX_LENGTH, - }); - } - - let mut bytes = [0u8; Self::MAX_LENGTH]; - f(&mut bytes[..output_size])?; - - Ok(Self { - bytes, - length: output_size as u8, - }) - } - - /// Borrow the output value as a byte slice. - pub fn as_bytes(&self) -> &[u8] { - &self.bytes[..self.len()] - } - - /// Get the length of the output value as a byte slice. - pub fn len(&self) -> usize { - usize::from(self.length) - } - - /// Parse "B64"-encoded [`Output`], i.e. using the PHC string specification's restricted - /// interpretation of Base64. - pub fn decode(input: &str) -> Result { - let mut bytes = [0u8; Self::MAX_LENGTH]; - B64::decode(input, &mut bytes) - .map_err(Into::into) - .and_then(Self::new) - } - - /// Write "B64"-encoded [`Output`] to the provided buffer, returning a sub-slice containing the - /// encoded data. - /// - /// Returns an error if the buffer is too short to contain the output. - pub fn encode<'a>(&self, out: &'a mut [u8]) -> Result<&'a str> { - Ok(B64::encode(self.as_ref(), out)?) - } - - /// Get the length of this [`Output`] when encoded as "B64". - pub fn encoded_len(&self) -> usize { - B64::encoded_len(self.as_ref()) - } - - /// DEPRECATED: parse B64-encoded [`Output`], i.e. using the PHC string specification's - /// restricted interpretation of Base64. - #[deprecated(since = "0.6.0", note = "Use `Output::decode` instead")] - pub fn b64_decode(input: &str) -> Result { - Self::decode(input) - } - - /// DEPRECATED: write B64-encoded [`Output`] to the provided buffer, returning a sub-slice - /// containing the encoded data. - /// - /// Returns an error if the buffer is too short to contain the output. - #[deprecated(since = "0.6.0", note = "Use `Output::encode` instead")] - pub fn b64_encode<'a>(&self, out: &'a mut [u8]) -> Result<&'a str> { - self.encode(out) - } - - /// Get the length of this [`Output`] when encoded as B64. - #[deprecated(since = "0.6.0", note = "Use `Output::encoded_len` instead")] - pub fn b64_len(&self) -> usize { - self.encoded_len() - } -} - -impl AsRef<[u8]> for Output { - fn as_ref(&self) -> &[u8] { - self.as_bytes() - } -} - -impl ConstantTimeEq for Output { - fn ct_eq(&self, other: &Self) -> Choice { - self.as_ref().ct_eq(other.as_ref()) - } -} - -impl FromStr for Output { - type Err = Error; - - fn from_str(s: &str) -> Result { - Self::decode(s) - } -} - -impl PartialEq for Output { - fn eq(&self, other: &Self) -> bool { - self.ct_eq(other).into() - } -} - -impl TryFrom<&[u8]> for Output { - type Error = Error; - - fn try_from(input: &[u8]) -> Result { - Self::new(input) - } -} - -impl fmt::Display for Output { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut buffer = [0u8; Self::B64_MAX_LENGTH]; - self.encode(&mut buffer) - .map_err(|_| fmt::Error) - .and_then(|encoded| f.write_str(encoded)) - } -} - -impl fmt::Debug for Output { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Output(\"{self}\")") - } -} - -#[cfg(test)] -mod tests { - use super::{Error, Ordering, Output}; - - #[test] - fn new_with_valid_min_length_input() { - let bytes = [10u8; 10]; - let output = Output::new(&bytes).unwrap(); - assert_eq!(output.as_ref(), &bytes); - } - - #[test] - fn new_with_valid_max_length_input() { - let bytes = [64u8; 64]; - let output = Output::new(&bytes).unwrap(); - assert_eq!(output.as_ref(), &bytes); - } - - #[test] - fn reject_new_too_short() { - let bytes = [9u8; 9]; - let err = Output::new(&bytes).err().unwrap(); - assert_eq!( - err, - Error::OutputSize { - provided: Ordering::Less, - expected: Output::MIN_LENGTH - } - ); - } - - #[test] - fn reject_new_too_long() { - let bytes = [65u8; 65]; - let err = Output::new(&bytes).err().unwrap(); - assert_eq!( - err, - Error::OutputSize { - provided: Ordering::Greater, - expected: Output::MAX_LENGTH - } - ); - } - - #[test] - fn partialeq_true() { - let a = Output::new(&[1u8; 32]).unwrap(); - let b = Output::new(&[1u8; 32]).unwrap(); - assert_eq!(a, b); - } - - #[test] - fn partialeq_false() { - let a = Output::new(&[1u8; 32]).unwrap(); - let b = Output::new(&[2u8; 32]).unwrap(); - assert_ne!(a, b); - } -} diff --git a/password-hash/src/phc/params.rs b/password-hash/src/phc/params.rs deleted file mode 100644 index a51e37b08..000000000 --- a/password-hash/src/phc/params.rs +++ /dev/null @@ -1,403 +0,0 @@ -//! Algorithm parameters. - -use super::{Decimal, Ident, StringBuf, Value}; -use crate::{Error, Result, errors::InvalidValue}; -use base64ct::{Base64Unpadded as B64, Encoding}; -use core::{ - fmt::{self, Debug, Write as _}, - str::{self, FromStr}, -}; - -/// Individual parameter name/value pair. -pub type Pair<'a> = (Ident, Value<'a>); - -/// Delimiter character between name/value pairs. -pub(crate) const PAIR_DELIMITER: char = '='; - -/// Delimiter character between parameters. -pub(crate) const PARAMS_DELIMITER: char = ','; - -/// Maximum serialized length of parameters. -const MAX_LENGTH: usize = 127; - -/// Error message used with `expect` for when internal invariants are violated -/// (i.e. the contents of a [`ParamsString`] should always be valid) -const INVARIANT_VIOLATED_MSG: &str = "PHC params invariant violated"; - -/// Algorithm parameter string. -/// -/// The [PHC string format specification][1] defines a set of optional -/// algorithm-specific name/value pairs which can be encoded into a -/// PHC-formatted parameter string as follows: -/// -/// ```text -/// $=(,=)* -/// ``` -/// -/// This type represents that set of parameters. -/// -/// [1]: https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md#specification -#[derive(Clone, Default, Eq, PartialEq)] -pub struct ParamsString(StringBuf); - -impl ParamsString { - /// Create new empty [`ParamsString`]. - pub fn new() -> Self { - Self::default() - } - - /// Add the given byte value to the [`ParamsString`], encoding it as "B64". - pub fn add_b64_bytes(&mut self, name: impl TryInto, bytes: &[u8]) -> Result<()> { - if !self.is_empty() { - self.0 - .write_char(PARAMS_DELIMITER) - .map_err(|_| Error::ParamsMaxExceeded)? - } - - let name = name.try_into().map_err(|_| Error::ParamNameInvalid)?; - - // Add param name - let offset = self.0.length; - if write!(self.0, "{name}=").is_err() { - self.0.length = offset; - return Err(Error::ParamsMaxExceeded); - } - - // Encode B64 value - let offset = self.0.length as usize; - let written = B64::encode(bytes, &mut self.0.bytes[offset..])?.len(); - - self.0.length += written as u8; - Ok(()) - } - - /// Add a key/value pair with a decimal value to the [`ParamsString`]. - pub fn add_decimal(&mut self, name: impl TryInto, value: Decimal) -> Result<()> { - let name = name.try_into().map_err(|_| Error::ParamNameInvalid)?; - self.add(name, value) - } - - /// Add a key/value pair with a string value to the [`ParamsString`]. - pub fn add_str<'a>( - &mut self, - name: impl TryInto, - value: impl TryInto>, - ) -> Result<()> { - let name = name.try_into().map_err(|_| Error::ParamNameInvalid)?; - - let value = value - .try_into() - .map_err(|_| Error::ParamValueInvalid(InvalidValue::InvalidFormat))?; - - self.add(name, value) - } - - /// Borrow the contents of this [`ParamsString`] as a byte slice. - pub fn as_bytes(&self) -> &[u8] { - self.as_str().as_bytes() - } - - /// Borrow the contents of this [`ParamsString`] as a `str`. - pub fn as_str(&self) -> &str { - self.0.as_ref() - } - - /// Get the count of the number ASCII characters in this [`ParamsString`]. - pub fn len(&self) -> usize { - self.as_str().len() - } - - /// Is this set of parameters empty? - pub fn is_empty(&self) -> bool { - self.len() == 0 - } - - /// Iterate over the parameters. - pub fn iter(&self) -> Iter<'_> { - Iter::new(self.as_str()) - } - - /// Get a parameter [`Value`] by name. - pub fn get(&self, name: impl TryInto) -> Option> { - let name = name.try_into().ok()?; - - for (n, v) in self.iter() { - if name == n { - return Some(v); - } - } - - None - } - - /// Get a parameter as a `str`. - pub fn get_str(&self, name: impl TryInto) -> Option<&str> { - self.get(name).map(|value| value.as_str()) - } - - /// Get a parameter as a [`Decimal`]. - /// - /// See [`Value::decimal`] for format information. - pub fn get_decimal(&self, name: impl TryInto) -> Option { - self.get(name).and_then(|value| value.decimal().ok()) - } - - /// Add a value to this [`ParamsString`] using the provided callback. - fn add(&mut self, name: Ident, value: impl fmt::Display) -> Result<()> { - if self.get(name).is_some() { - return Err(Error::ParamNameDuplicated); - } - - let orig_len = self.0.length; - - if !self.is_empty() { - self.0 - .write_char(PARAMS_DELIMITER) - .map_err(|_| Error::ParamsMaxExceeded)? - } - - if write!(self.0, "{name}={value}").is_err() { - self.0.length = orig_len; - return Err(Error::ParamsMaxExceeded); - } - - Ok(()) - } -} - -impl FromStr for ParamsString { - type Err = Error; - - fn from_str(s: &str) -> Result { - if s.len() > MAX_LENGTH { - return Err(Error::ParamsMaxExceeded); - } - - if s.is_empty() { - return Ok(ParamsString::new()); - } - - // Validate the string is well-formed - for mut param in s.split(PARAMS_DELIMITER).map(|p| p.split(PAIR_DELIMITER)) { - // Validate name - param - .next() - .ok_or(Error::ParamNameInvalid) - .and_then(Ident::from_str)?; - - // Validate value - param - .next() - .ok_or(Error::ParamValueInvalid(InvalidValue::Malformed)) - .and_then(Value::try_from)?; - - if param.next().is_some() { - return Err(Error::ParamValueInvalid(InvalidValue::Malformed)); - } - } - - let mut bytes = [0u8; MAX_LENGTH]; - bytes[..s.len()].copy_from_slice(s.as_bytes()); - - Ok(Self(StringBuf { - bytes, - length: s.len() as u8, - })) - } -} - -impl<'a> FromIterator> for ParamsString { - fn from_iter(iter: I) -> Self - where - I: IntoIterator>, - { - let mut params = ParamsString::new(); - - for pair in iter { - params.add_str(pair.0, pair.1).expect("PHC params error"); - } - - params - } -} - -impl fmt::Display for ParamsString { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(self.as_str()) - } -} - -impl fmt::Debug for ParamsString { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_map().entries(self.iter()).finish() - } -} - -/// Iterator over algorithm parameters stored in a [`ParamsString`] struct. -#[derive(Debug)] -pub struct Iter<'a> { - inner: Option>, -} - -impl<'a> Iter<'a> { - /// Create a new [`Iter`]. - fn new(s: &'a str) -> Self { - if s.is_empty() { - Self { inner: None } - } else { - Self { - inner: Some(s.split(PARAMS_DELIMITER)), - } - } - } -} - -impl<'a> Iterator for Iter<'a> { - type Item = Pair<'a>; - - fn next(&mut self) -> Option> { - let mut param = self.inner.as_mut()?.next()?.split(PAIR_DELIMITER); - - let name = param - .next() - .and_then(|id| Ident::from_str(id).ok()) - .expect(INVARIANT_VIOLATED_MSG); - - let value = param - .next() - .and_then(|value| Value::try_from(value).ok()) - .expect(INVARIANT_VIOLATED_MSG); - - debug_assert_eq!(param.next(), None); - Some((name, value)) - } -} - -#[cfg(test)] -mod tests { - use super::{Error, Ident, ParamsString, Value}; - - #[cfg(feature = "alloc")] - use alloc::string::ToString; - use core::str::FromStr; - - #[test] - fn add() { - let mut params = ParamsString::new(); - params.add_str("a", "1").unwrap(); - params.add_decimal("b", 2).unwrap(); - params.add_str("c", "3").unwrap(); - - assert_eq!(params.iter().count(), 3); - assert_eq!(params.get_decimal("a").unwrap(), 1); - assert_eq!(params.get_decimal("b").unwrap(), 2); - assert_eq!(params.get_decimal("c").unwrap(), 3); - } - - #[test] - #[cfg(feature = "alloc")] - fn add_b64_bytes() { - let mut params = ParamsString::new(); - params.add_b64_bytes("a", &[1]).unwrap(); - params.add_b64_bytes("b", &[2, 3]).unwrap(); - params.add_b64_bytes("c", &[4, 5, 6]).unwrap(); - assert_eq!(params.to_string(), "a=AQ,b=AgM,c=BAUG"); - } - - #[test] - fn duplicate_names() { - let name = Ident::new("a").unwrap(); - let mut params = ParamsString::new(); - params.add_decimal(name, 1).unwrap(); - - let err = params.add_decimal(name, 2u32).err().unwrap(); - assert_eq!(err, Error::ParamNameDuplicated); - } - - #[test] - fn from_iter() { - let params = ParamsString::from_iter( - [ - (Ident::new("a").unwrap(), Value::try_from("1").unwrap()), - (Ident::new("b").unwrap(), Value::try_from("2").unwrap()), - (Ident::new("c").unwrap(), Value::try_from("3").unwrap()), - ] - .iter() - .cloned(), - ); - - assert_eq!(params.iter().count(), 3); - assert_eq!(params.get_decimal("a").unwrap(), 1); - assert_eq!(params.get_decimal("b").unwrap(), 2); - assert_eq!(params.get_decimal("c").unwrap(), 3); - } - - #[test] - fn iter() { - let mut params = ParamsString::new(); - params.add_str("a", "1").unwrap(); - params.add_str("b", "2").unwrap(); - params.add_str("c", "3").unwrap(); - - let mut i = params.iter(); - - for (name, value) in &[("a", "1"), ("b", "2"), ("c", "3")] { - let name = Ident::new(name).unwrap(); - let value = Value::try_from(*value).unwrap(); - assert_eq!(i.next(), Some((name, value))); - } - - assert_eq!(i.next(), None); - } - - // - // `FromStr` tests - // - - #[test] - fn parse_empty() { - let params = ParamsString::from_str("").unwrap(); - assert!(params.is_empty()); - } - - #[test] - fn parse_one() { - let params = ParamsString::from_str("a=1").unwrap(); - assert_eq!(params.iter().count(), 1); - assert_eq!(params.get("a").unwrap().decimal().unwrap(), 1); - } - - #[test] - fn parse_many() { - let params = ParamsString::from_str("a=1,b=2,c=3").unwrap(); - assert_eq!(params.iter().count(), 3); - assert_eq!(params.get_decimal("a").unwrap(), 1); - assert_eq!(params.get_decimal("b").unwrap(), 2); - assert_eq!(params.get_decimal("c").unwrap(), 3); - } - - // - // `Display` tests - // - - #[test] - #[cfg(feature = "alloc")] - fn display_empty() { - let params = ParamsString::new(); - assert_eq!(params.to_string(), ""); - } - - #[test] - #[cfg(feature = "alloc")] - fn display_one() { - let params = ParamsString::from_str("a=1").unwrap(); - assert_eq!(params.to_string(), "a=1"); - } - - #[test] - #[cfg(feature = "alloc")] - fn display_many() { - let params = ParamsString::from_str("a=1,b=2,c=3").unwrap(); - assert_eq!(params.to_string(), "a=1,b=2,c=3"); - } -} diff --git a/password-hash/src/phc/salt.rs b/password-hash/src/phc/salt.rs deleted file mode 100644 index b31b65bc8..000000000 --- a/password-hash/src/phc/salt.rs +++ /dev/null @@ -1,400 +0,0 @@ -//! Salt string support. - -use super::StringBuf; -use crate::{Error, Result, errors::InvalidValue}; -use base64ct::{Base64Unpadded as B64, Encoding}; -use core::{ - fmt, - ops::Deref, - str::{self, FromStr}, -}; -#[cfg(feature = "rand_core")] -use rand_core::{CryptoRng, TryCryptoRng}; - -/// Error message used with `expect` for when internal invariants are violated -/// (i.e. the contents of a [`Salt`] should always be valid) -const INVARIANT_VIOLATED_MSG: &str = "salt string invariant violated"; - -/// In password hashing, a "salt" is an additional value used to -/// personalize/tweak the output of a password hashing function for a given -/// input password. -/// -/// Salts help defend against attacks based on precomputed tables of hashed -/// passwords, i.e. "[rainbow tables][1]". -/// -/// The [`Salt`] type implements the RECOMMENDED best practices for salts -/// described in the [PHC string format specification][2], namely: -/// -/// > - Maximum lengths for salt, output and parameter values are meant to help -/// > consumer implementations, in particular written in C and using -/// > stack-allocated buffers. These buffers must account for the worst case, -/// > i.e. the maximum defined length. Therefore, keep these lengths low. -/// > - The role of salts is to achieve uniqueness. A random salt is fine for -/// > that as long as its length is sufficient; a 16-byte salt would work well -/// > (by definition, UUID are very good salts, and they encode over exactly -/// > 16 bytes). 16 bytes encode as 22 characters in B64. Functions should -/// > disallow salt values that are too small for security (4 bytes should be -/// > viewed as an absolute minimum). -/// -/// # Recommended length -/// The recommended default length for a salt string is **16-bytes** (128-bits). -/// -/// See [`Salt::RECOMMENDED_LENGTH`] for more information. -/// -/// # Constraints -/// Salt strings are constrained to the following set of characters per the -/// PHC spec: -/// -/// > The salt consists in a sequence of characters in: `[a-zA-Z0-9/+.-]` -/// > (lowercase letters, uppercase letters, digits, `/`, `+`, `.` and `-`). -/// -/// Additionally, the following length restrictions are enforced based on the -/// guidelines from the spec: -/// -/// - Minimum length: **8**-bytes -/// - Maximum length: **48**-bytes -/// -/// A maximum length is enforced based on the above recommendation for -/// supporting stack-allocated buffers (which this library uses), and the -/// specific determination of 48-bytes is taken as a best practice from the -/// [Argon2 Encoding][3] specification in the same document: -/// -/// > The length in bytes of the salt is between 8 and 48 bytes, thus -/// > yielding a length in characters between 11 and 64 characters (and that -/// > length is never equal to 1 modulo 4). The default byte length of the salt -/// > is 16 bytes (22 characters in B64 encoding). An encoded UUID, or a -/// > sequence of 16 bytes produced with a cryptographically strong PRNG, are -/// > appropriate salt values. -/// > -/// > The Argon2 specification states that the salt can be much longer, up -/// > to 2^32-1 bytes, but this makes little sense for password hashing. -/// > Specifying a relatively small maximum length allows for parsing with a -/// > stack allocated buffer. -/// -/// Based on this guidance, this type enforces an upper bound of 48-bytes -/// as a reasonable maximum, and recommends using 16-bytes. -/// -/// [1]: https://en.wikipedia.org/wiki/Rainbow_table -/// [2]: https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md#function-duties -/// [3]: https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md#argon2-encoding -#[derive(Copy, Clone, Eq, PartialEq)] -pub struct Salt { - /// Length of the salt in bytes. - pub(super) length: u8, - - /// Byte array containing an ASCII-encoded string. - pub(super) bytes: [u8; Self::MAX_LENGTH], -} - -#[allow(clippy::len_without_is_empty)] -impl Salt { - /// Minimum length of a [`Salt`] (after "B64" decoding): 8-bytes. - pub const MIN_LENGTH: usize = 8; - - /// Maximum length of a [`Salt`] (after "B64" decoding): 48-bytes. - /// - /// See type-level documentation about [`Salt`] for more information. - pub const MAX_LENGTH: usize = 48; - - /// Recommended length of a salt: 16-bytes. - /// - /// This recommendation comes from the [PHC string format specification]: - /// - /// > The role of salts is to achieve uniqueness. A *random* salt is fine - /// > for that as long as its length is sufficient; a 16-byte salt would - /// > work well (by definition, UUID are very good salts, and they encode - /// > over exactly 16 bytes). 16 bytes encode as 22 characters in B64. - /// - /// [PHC string format specification]: https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md#function-duties - pub const RECOMMENDED_LENGTH: usize = 16; - - /// Generate a random [`Salt`] with the `RECOMMENDED_LENGTH`.. - #[cfg(feature = "getrandom")] - pub fn generate() -> Self { - let mut bytes = [0u8; Self::RECOMMENDED_LENGTH]; - getrandom::fill(&mut bytes).expect("RNG failure"); - Self::new(&bytes).expect(INVARIANT_VIOLATED_MSG) - } - - /// Generate a random [`Salt`] from the given [`CryptoRng`]. - #[cfg(feature = "rand_core")] - pub fn from_rng(rng: &mut R) -> Self { - let Ok(out) = Self::try_from_rng(rng); - out - } - - /// Generate a random [`Salt`] from the given [`TryCryptoRng`]. - #[cfg(feature = "rand_core")] - pub fn try_from_rng( - rng: &mut R, - ) -> core::result::Result { - let mut bytes = [0u8; Self::RECOMMENDED_LENGTH]; - rng.try_fill_bytes(&mut bytes)?; - Ok(Self::new(&bytes).expect(INVARIANT_VIOLATED_MSG)) - } - - /// Create a new [`Salt`] from the given byte slice. - pub fn new(slice: &[u8]) -> Result { - if slice.len() < Self::MIN_LENGTH { - return Err(Error::SaltInvalid(InvalidValue::TooShort)); - } - - let mut bytes = [0; Self::MAX_LENGTH]; - bytes - .get_mut(..slice.len()) - .ok_or(Error::SaltInvalid(InvalidValue::TooLong))? - .copy_from_slice(slice); - - debug_assert!(slice.len() >= Self::MIN_LENGTH); - debug_assert!(slice.len() <= Self::MAX_LENGTH); - - Ok(Self { - bytes, - length: slice.len() as u8, - }) - } - - /// Create a [`Salt`] from the given B64-encoded input string, validating - /// [`Salt::MIN_LENGTH`] and [`Salt::MAX_LENGTH`] restrictions. - pub fn from_b64(b64: &str) -> Result { - if b64.len() < SaltString::MIN_LENGTH { - return Err(Error::SaltInvalid(InvalidValue::TooShort)); - } - - if b64.len() > SaltString::MAX_LENGTH { - return Err(Error::SaltInvalid(InvalidValue::TooLong)); - } - - let mut bytes = [0; Self::MAX_LENGTH]; - let length = B64::decode(b64, &mut bytes)?.len(); - debug_assert!(length <= Self::MAX_LENGTH); - - Ok(Self { - bytes, - length: length as u8, - }) - } - - /// Encode this [`Salt`] as a "B64" [`SaltString`]. - pub fn to_salt_string(&self) -> SaltString { - self.into() - } -} - -impl AsRef<[u8]> for Salt { - fn as_ref(&self) -> &[u8] { - &self.bytes[..(self.length as usize)] - } -} - -impl Deref for Salt { - type Target = [u8]; - - fn deref(&self) -> &[u8] { - self.as_ref() - } -} - -impl FromStr for Salt { - type Err = Error; - - fn from_str(b64: &str) -> Result { - Self::from_b64(b64) - } -} - -impl TryFrom<&[u8]> for Salt { - type Error = Error; - - fn try_from(slice: &[u8]) -> Result { - Self::new(slice) - } -} - -impl TryFrom<&str> for Salt { - type Error = Error; - - fn try_from(b64: &str) -> Result { - Self::from_b64(b64) - } -} - -impl fmt::Display for Salt { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.to_salt_string().fmt(f) - } -} - -impl fmt::Debug for Salt { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_tuple("Salt").field(&self.as_ref()).finish() - } -} - -/// Owned stack-allocated equivalent of [`Salt`]. -#[derive(Clone, Eq)] -pub struct SaltString(StringBuf<{ SaltString::MAX_LENGTH }>); - -#[allow(clippy::len_without_is_empty)] -impl SaltString { - /// Minimum length of "B64"-encoded [`SaltString`] string: 11-bytes (4-bytes encoded as "B64") - pub const MIN_LENGTH: usize = 11; - - /// Maximum length of a "B64"-encoded [`SaltString`]: 64-bytes (48-bytes encoded as "B64") - /// - /// See type-level documentation about [`Salt`] for more information. - pub const MAX_LENGTH: usize = 64; - - /// Generate a random B64-encoded [`SaltString`]. - #[cfg(feature = "getrandom")] - pub fn generate() -> Self { - Salt::generate().into() - } - - /// Generate a random B64-encoded [`SaltString`] from [`CryptoRng`]. - #[cfg(feature = "rand_core")] - pub fn from_rng(rng: &mut R) -> Self { - let Ok(out) = Self::try_from_rng(rng); - out - } - - /// Generate a random B64-encoded [`SaltString`] from [`TryCryptoRng`]. - #[cfg(feature = "rand_core")] - pub fn try_from_rng( - rng: &mut R, - ) -> core::result::Result { - Ok(Salt::try_from_rng(rng)?.to_salt_string()) - } - - /// Create a new [`SaltString`] from the given B64-encoded input string, - /// validating [`Salt::MIN_LENGTH`] and [`Salt::MAX_LENGTH`] restrictions. - pub fn from_b64(s: &str) -> Result { - // Assert `s` parses successfully as a `Salt` - Salt::from_b64(s)?; - Ok(Self(s.parse()?)) - } - - /// Decode this "B64" string, returning a [`Salt`] containing the decoded bytes. - pub fn to_salt(&self) -> Salt { - self.into() - } -} - -impl AsRef for SaltString { - fn as_ref(&self) -> &str { - &self.0 - } -} - -impl Deref for SaltString { - type Target = str; - - fn deref(&self) -> &str { - &self.0 - } -} - -impl From for SaltString { - fn from(salt: Salt) -> Self { - SaltString::from(&salt) - } -} - -impl From<&Salt> for SaltString { - fn from(salt: &Salt) -> Self { - let mut buf = [0; SaltString::MAX_LENGTH]; - let b64 = B64::encode(salt, &mut buf).expect(INVARIANT_VIOLATED_MSG); - SaltString(b64.parse().expect(INVARIANT_VIOLATED_MSG)) - } -} - -impl From for Salt { - fn from(salt: SaltString) -> Self { - Salt::from(&salt) - } -} - -impl From<&SaltString> for Salt { - fn from(salt: &SaltString) -> Self { - Salt::from_b64(salt.as_ref()).expect(INVARIANT_VIOLATED_MSG) - } -} - -impl FromStr for SaltString { - type Err = Error; - - fn from_str(s: &str) -> Result { - Self::from_b64(s) - } -} - -impl PartialEq for SaltString { - fn eq(&self, other: &Self) -> bool { - // Ensure comparisons always honor the initialized portion of the buffer - self.as_ref().eq(other.as_ref()) - } -} - -impl fmt::Display for SaltString { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(self.as_ref()) - } -} - -impl fmt::Debug for SaltString { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "SaltString({:?})", self.as_ref()) - } -} - -#[cfg(test)] -mod tests { - use super::{Error, Salt}; - use crate::errors::InvalidValue; - - #[test] - fn new_with_valid_min_length_input() { - let s = "abcdabcdabc"; - let salt = Salt::from_b64(s).unwrap(); - assert_eq!( - salt.as_ref(), - &[0x69, 0xb7, 0x1d, 0x69, 0xb7, 0x1d, 0x69, 0xb7] - ); - } - - #[test] - fn new_with_valid_max_length_input() { - let s = "012345678911234567892123456789312345678941234567"; - let salt = Salt::from_b64(s).unwrap(); - assert_eq!( - salt.as_ref(), - &[ - 0xd3, 0x5d, 0xb7, 0xe3, 0x9e, 0xbb, 0xf3, 0xdd, 0x75, 0xdb, 0x7e, 0x39, 0xeb, 0xbf, - 0x3d, 0xdb, 0x5d, 0xb7, 0xe3, 0x9e, 0xbb, 0xf3, 0xdd, 0xf5, 0xdb, 0x7e, 0x39, 0xeb, - 0xbf, 0x3d, 0xe3, 0x5d, 0xb7, 0xe3, 0x9e, 0xbb - ] - ); - } - - #[test] - fn reject_new_too_short() { - for &too_short in &["", "a", "ab", "abc"] { - let err = Salt::from_b64(too_short).err().unwrap(); - assert_eq!(err, Error::SaltInvalid(InvalidValue::TooShort)); - } - } - - #[test] - fn reject_new_too_long() { - let s = "01234567891123456789212345678931234567894123456785234567896234567"; - let err = Salt::from_b64(s).err().unwrap(); - assert_eq!(err, Error::SaltInvalid(InvalidValue::TooLong)); - } - - #[test] - fn reject_new_invalid_char() { - let s = "01234_abcde"; - let err = Salt::from_b64(s).err().unwrap(); - assert_eq!(err, Error::B64Encoding(base64ct::Error::InvalidEncoding)); - } -} diff --git a/password-hash/src/phc/string_buf.rs b/password-hash/src/phc/string_buf.rs deleted file mode 100644 index 1596e85d7..000000000 --- a/password-hash/src/phc/string_buf.rs +++ /dev/null @@ -1,108 +0,0 @@ -use crate::{Error, Result}; -use core::{ - fmt, - ops::Deref, - str::{self, FromStr}, -}; - -/// Buffer for storing short stack-allocated strings. -#[derive(Clone, Copy, Eq, Hash, PartialEq, PartialOrd, Ord)] -pub(super) struct StringBuf { - /// Length of the string in ASCII characters (i.e. bytes). - pub(super) length: u8, - - /// Byte array containing an ASCII-encoded string. - pub(super) bytes: [u8; N], -} - -impl StringBuf { - /// Create a new string buffer containing the given string - pub(super) const fn new(s: &str) -> Result { - if s.len() > N || s.len() > u8::MAX as usize { - return Err(Error::TooLong); - } - - let mut bytes = [0u8; N]; - let mut i = 0; - - while i < s.len() { - bytes[i] = s.as_bytes()[i]; - i += 1; - } - - Ok(Self { - bytes, - length: s.len() as u8, - }) - } -} - -impl AsRef for StringBuf { - fn as_ref(&self) -> &str { - str::from_utf8(&self.bytes[..(self.length as usize)]).expect("should be valid UTF-8") - } -} - -impl Default for StringBuf { - fn default() -> Self { - StringBuf { - bytes: [0u8; N], - length: 0, - } - } -} - -impl Deref for StringBuf { - type Target = str; - - fn deref(&self) -> &str { - self.as_ref() - } -} - -impl FromStr for StringBuf { - type Err = Error; - - fn from_str(s: &str) -> Result { - Self::new(s) - } -} - -impl TryFrom<&str> for StringBuf { - type Error = Error; - - fn try_from(s: &str) -> Result { - Self::new(s) - } -} - -impl fmt::Debug for StringBuf { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(self.as_ref()) - } -} - -impl fmt::Display for StringBuf { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(self.as_ref()) - } -} - -impl fmt::Write for StringBuf { - fn write_str(&mut self, input: &str) -> fmt::Result { - const { debug_assert!(N <= u8::MAX as usize) } - - let bytes = input.as_bytes(); - let length = self.length as usize; - let new_length = length.checked_add(bytes.len()).ok_or(fmt::Error)?; - - if new_length > N { - return Err(fmt::Error); - } - - self.bytes[length..new_length].copy_from_slice(bytes); - self.length = new_length.try_into().map_err(|_| fmt::Error)?; - - Ok(()) - } -} diff --git a/password-hash/src/phc/value.rs b/password-hash/src/phc/value.rs deleted file mode 100644 index fc7080120..000000000 --- a/password-hash/src/phc/value.rs +++ /dev/null @@ -1,304 +0,0 @@ -//! Algorithm parameter value as defined by the [PHC string format]. -//! -//! Implements the following parts of the specification: -//! -//! > The value for each parameter consists in characters in: `[a-zA-Z0-9/+.-]` -//! > (lowercase letters, uppercase letters, digits, /, +, . and -). No other -//! > character is allowed. Interpretation of the value depends on the -//! > parameter and the function. The function specification MUST unambiguously -//! > define the set of valid parameter values. The function specification MUST -//! > define a maximum length (in characters) for each parameter. For numerical -//! > parameters, functions SHOULD use plain decimal encoding (other encodings -//! > are possible as long as they are clearly defined). -//! -//! [1]: https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md - -use crate::{Error, Result, errors::InvalidValue}; -use base64ct::{Base64Unpadded as B64, Encoding}; -use core::{fmt, str}; - -/// Type used to represent decimal (i.e. integer) values. -pub type Decimal = u32; - -/// Algorithm parameter value string. -/// -/// Parameter values are defined in the [PHC string format specification][1]. -/// -/// # Constraints -/// - ASCII-encoded string consisting of the characters `[a-zA-Z0-9/+.-]` -/// (lowercase letters, digits, and the minus sign) -/// - Minimum length: 0 (i.e. empty values are allowed) -/// - Maximum length: 64 ASCII characters (i.e. 64-bytes) -/// -/// # Additional Notes -/// The PHC spec allows for algorithm-defined maximum lengths for parameter -/// values, however this library defines a [`Value::MAX_LENGTH`] of 64 ASCII -/// characters. -/// -/// [1]: https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md -/// [2]: https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md#argon2-encoding -#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)] -pub struct Value<'a>(&'a str); - -impl<'a> Value<'a> { - /// Maximum length of an [`Value`] - 64 ASCII characters (i.e. 64-bytes). - /// - /// This value is selected to match the maximum length of a [`Salt`][`crate::phc::Salt`] - /// as this library internally uses this type to represent salts. - pub const MAX_LENGTH: usize = 64; - - /// Parse a [`Value`] from the provided `str`, validating it according to - /// the PHC string format's rules. - pub fn new(input: &'a str) -> Result { - if input.len() > Self::MAX_LENGTH { - return Err(Error::ParamValueInvalid(InvalidValue::TooLong)); - } - - // Check that the characters are permitted in a PHC parameter value. - assert_valid_value(input)?; - Ok(Self(input)) - } - - /// Attempt to decode a B64-encoded [`Value`], writing the decoded - /// result into the provided buffer, and returning a slice of the buffer - /// containing the decoded result on success. - /// - /// Examples of "B64"-encoded parameters in practice are the `keyid` and - /// `data` parameters used by the [Argon2 Encoding][1] as described in the - /// PHC string format specification. - /// - /// [1]: https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md#argon2-encoding - pub fn b64_decode<'b>(&self, buf: &'b mut [u8]) -> Result<&'b [u8]> { - Ok(B64::decode(self.as_str(), buf)?) - } - - /// Borrow this value as a `str`. - pub fn as_str(&self) -> &'a str { - self.0 - } - - /// Borrow this value as bytes. - pub fn as_bytes(&self) -> &'a [u8] { - self.as_str().as_bytes() - } - - /// Get the length of this value in ASCII characters. - pub fn len(&self) -> usize { - self.as_str().len() - } - - /// Is this value empty? - pub fn is_empty(&self) -> bool { - self.as_str().is_empty() - } - - /// Attempt to parse this [`Value`] as a PHC-encoded decimal (i.e. integer). - /// - /// Decimal values are integers which follow the rules given in the - /// ["Decimal Encoding" section of the PHC string format specification][1]. - /// - /// The decimal encoding rules are as follows: - /// > For an integer value x, its decimal encoding consist in the following: - /// > - /// > - If x < 0, then its decimal encoding is the minus sign - followed by the decimal - /// > encoding of -x. - /// > - If x = 0, then its decimal encoding is the single character 0. - /// > - If x > 0, then its decimal encoding is the smallest sequence of ASCII digits that - /// > matches its value (i.e. there is no leading zero). - /// > - /// > Thus, a value is a valid decimal for an integer x if and only if all of the following hold true: - /// > - /// > - The first character is either a - sign, or an ASCII digit. - /// > - All characters other than the first are ASCII digits. - /// > - If the first character is - sign, then there is at least another character, and the - /// > second character is not a 0. - /// > - If the string consists in more than one character, then the first one cannot be a 0. - /// - /// Note: this implementation does not support negative decimals despite - /// them being allowed per the spec above. If you need to parse a negative - /// number, please parse it from the string representation directly e.g. - /// `value.as_str().parse::()` - /// - /// [1]: https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md#decimal-encoding - pub fn decimal(&self) -> Result { - let value = self.as_str(); - - // Empty strings aren't decimals - if value.is_empty() { - return Err(Error::ParamValueInvalid(InvalidValue::Malformed)); - } - - // Ensure all characters are digits - for c in value.chars() { - if !c.is_ascii_digit() { - return Err(Error::ParamValueInvalid(InvalidValue::InvalidChar(c))); - } - } - - // Disallow leading zeroes - if value.starts_with('0') && value.len() > 1 { - return Err(Error::ParamValueInvalid(InvalidValue::InvalidFormat)); - } - - value.parse().map_err(|_| { - // In theory a value overflow should be the only potential error here. - // When `ParseIntError::kind` is stable it might be good to double check: - // - Error::ParamValueInvalid(InvalidValue::InvalidFormat) - }) - } - - /// Does this value parse successfully as a decimal? - pub fn is_decimal(&self) -> bool { - self.decimal().is_ok() - } -} - -impl AsRef for Value<'_> { - fn as_ref(&self) -> &str { - self.as_str() - } -} - -impl<'a> TryFrom<&'a str> for Value<'a> { - type Error = Error; - - fn try_from(input: &'a str) -> Result { - Self::new(input) - } -} - -impl<'a> TryFrom> for Decimal { - type Error = Error; - - fn try_from(value: Value<'a>) -> Result { - Decimal::try_from(&value) - } -} - -impl<'a> TryFrom<&Value<'a>> for Decimal { - type Error = Error; - - fn try_from(value: &Value<'a>) -> Result { - value.decimal() - } -} - -impl fmt::Display for Value<'_> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(self.as_str()) - } -} - -/// Are all of the given bytes allowed in a [`Value`]? -fn assert_valid_value(input: &str) -> Result<()> { - for c in input.chars() { - if !is_char_valid(c) { - return Err(Error::ParamValueInvalid(InvalidValue::InvalidChar(c))); - } - } - - Ok(()) -} - -/// Ensure the given ASCII character (i.e. byte) is allowed in a [`Value`]. -fn is_char_valid(c: char) -> bool { - matches!(c, 'A' ..= 'Z' | 'a'..='z' | '0'..='9' | '/' | '+' | '.' | '-') -} - -#[cfg(test)] -mod tests { - use super::{Error, InvalidValue, Value}; - - // Invalid value examples - const INVALID_CHAR: &str = "x;y"; - const INVALID_TOO_LONG: &str = - "01234567891123456789212345678931234567894123456785234567896234567"; - const INVALID_CHAR_AND_TOO_LONG: &str = - "0!234567891123456789212345678931234567894123456785234567896234567"; - - // - // Decimal parsing tests - // - - #[test] - fn decimal_value() { - let valid_decimals = &[("0", 0u32), ("1", 1u32), ("4294967295", u32::MAX)]; - - for &(s, i) in valid_decimals { - let value = Value::new(s).unwrap(); - assert!(value.is_decimal()); - assert_eq!(value.decimal().unwrap(), i) - } - } - - #[test] - fn reject_decimal_with_leading_zero() { - let value = Value::new("01").unwrap(); - let err = u32::try_from(value).err().unwrap(); - assert!(matches!( - err, - Error::ParamValueInvalid(InvalidValue::InvalidFormat) - )); - } - - #[test] - fn reject_overlong_decimal() { - let value = Value::new("4294967296").unwrap(); - let err = u32::try_from(value).err().unwrap(); - assert_eq!(err, Error::ParamValueInvalid(InvalidValue::InvalidFormat)); - } - - #[test] - fn reject_negative() { - let value = Value::new("-1").unwrap(); - let err = u32::try_from(value).err().unwrap(); - assert!(matches!( - err, - Error::ParamValueInvalid(InvalidValue::InvalidChar(_)) - )); - } - - // - // String parsing tests - // - - #[test] - fn string_value() { - let valid_examples = [ - "", - "X", - "x", - "xXx", - "a+b.c-d", - "1/2", - "01234567891123456789212345678931", - ]; - - for &example in &valid_examples { - let value = Value::new(example).unwrap(); - assert_eq!(value.as_str(), example); - } - } - - #[test] - fn reject_invalid_char() { - let err = Value::new(INVALID_CHAR).err().unwrap(); - assert!(matches!( - err, - Error::ParamValueInvalid(InvalidValue::InvalidChar(_)) - )); - } - - #[test] - fn reject_too_long() { - let err = Value::new(INVALID_TOO_LONG).err().unwrap(); - assert_eq!(err, Error::ParamValueInvalid(InvalidValue::TooLong)); - } - - #[test] - fn reject_invalid_char_and_too_long() { - let err = Value::new(INVALID_CHAR_AND_TOO_LONG).err().unwrap(); - assert_eq!(err, Error::ParamValueInvalid(InvalidValue::TooLong)); - } -} diff --git a/password-hash/tests/encoding.rs b/password-hash/tests/encoding.rs deleted file mode 100644 index 1334044fd..000000000 --- a/password-hash/tests/encoding.rs +++ /dev/null @@ -1,36 +0,0 @@ -//! Base64 encoding tests. -//! -//! # B64 Notes -//! -//! "B64" is a ubset of the standard Base64 encoding (RFC 4648, section 4) which -//! omits padding (`=`) as well as extra whitespace, as described in the PHC -//! string format specification: -//! -//! - -#![cfg(feature = "phc")] - -use password_hash::phc::{Output, Salt}; - -// Example salt encoded as a B64 string. -const EXAMPLE_SALT_B64: &str = "REVBREJFRUZERUFEQkVFRg"; -const EXAMPLE_SALT_RAW: &[u8] = b"DEADBEEFDEADBEEF"; - -// Example PHF output encoded as a B64 string. -const EXAMPLE_OUTPUT_B64: &str = - "REVBREJFRUZERUFEQkVFRkRFQURCRUVGREVBREJFRUZERUFEQkVFRkRFQURCRUVGREVBREJFRUZERUFEQkVFRg"; -const EXAMPLE_OUTPUT_RAW: &[u8] = - b"DEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF"; - -#[test] -fn salt_roundtrip() { - let salt = Salt::from_b64(EXAMPLE_SALT_B64).unwrap(); - assert_eq!(salt.as_ref(), EXAMPLE_SALT_RAW); -} - -#[test] -fn output_roundtrip() { - let out = EXAMPLE_OUTPUT_B64.parse::().unwrap(); - assert_eq!(out.as_ref(), EXAMPLE_OUTPUT_RAW); - assert_eq!(out.to_string(), EXAMPLE_OUTPUT_B64); -} diff --git a/password-hash/tests/password_hash.rs b/password-hash/tests/password_hash.rs deleted file mode 100644 index 0177a700e..000000000 --- a/password-hash/tests/password_hash.rs +++ /dev/null @@ -1,148 +0,0 @@ -//! Tests for `PasswordHash` encoding/decoding. -//! -//! Each test implements a different permutation of the possible combinations -//! of the string encoding, and ensures password hashes round trip under each -//! of the conditions. - -#![cfg(feature = "phc")] - -use password_hash::phc::{Ident, ParamsString, PasswordHash, Salt}; - -const EXAMPLE_ALGORITHM: Ident = Ident::new_unwrap("argon2d"); -const EXAMPLE_SALT: &str = "saltsaltsaltsaltsalt"; -const EXAMPLE_HASH: &[u8] = &[ - 0x85, 0xab, 0x21, 0x85, 0xab, 0x21, 0x85, 0xab, 0x21, 0x85, 0xab, 0x21, 0x85, 0xab, 0x21, 0x85, - 0xab, 0x21, 0x85, 0xab, 0x21, 0x85, 0xab, 0x21, 0x85, 0xab, 0x21, 0x85, 0xab, 0x21, 0x85, 0xab, -]; - -/// Example parameters -fn example_params() -> ParamsString { - let mut params = ParamsString::new(); - params.add_decimal("a", 1).unwrap(); - params.add_decimal("b", 2).unwrap(); - params.add_decimal("c", 3).unwrap(); - params -} - -#[test] -fn algorithm_alone() { - let ph = PasswordHash::new("$argon2d").unwrap(); - assert_eq!(ph.algorithm, EXAMPLE_ALGORITHM); - - let s = ph.to_string(); - assert_eq!(s, "$argon2d"); - - let ph2 = PasswordHash::try_from(s.as_str()).unwrap(); - assert_eq!(ph, ph2); -} - -#[test] -fn params() { - let ph = PasswordHash { - algorithm: EXAMPLE_ALGORITHM, - version: None, - params: example_params(), - salt: None, - hash: None, - }; - - let s = ph.to_string(); - assert_eq!(s, "$argon2d$a=1,b=2,c=3"); - - let ph2 = PasswordHash::try_from(s.as_str()).unwrap(); - assert_eq!(ph, ph2); -} - -#[test] -fn salt() { - let ph = PasswordHash { - algorithm: EXAMPLE_ALGORITHM, - version: None, - params: ParamsString::new(), - salt: Some(Salt::from_b64(EXAMPLE_SALT).unwrap()), - hash: None, - }; - - let s = ph.to_string(); - assert_eq!(s, "$argon2d$saltsaltsaltsaltsalt"); - - let ph2 = PasswordHash::try_from(s.as_str()).unwrap(); - assert_eq!(ph, ph2); -} - -#[test] -fn one_param_and_salt() { - let mut params = ParamsString::new(); - params.add_decimal("a", 1).unwrap(); - - let ph = PasswordHash { - algorithm: EXAMPLE_ALGORITHM, - version: None, - params, - salt: Some(Salt::from_b64(EXAMPLE_SALT).unwrap()), - hash: None, - }; - - let s = ph.to_string(); - assert_eq!(s, "$argon2d$a=1$saltsaltsaltsaltsalt"); - - let ph2 = PasswordHash::try_from(s.as_str()).unwrap(); - assert_eq!(ph, ph2); -} - -#[test] -fn params_and_salt() { - let ph = PasswordHash { - algorithm: EXAMPLE_ALGORITHM, - version: None, - params: example_params(), - salt: Some(Salt::from_b64(EXAMPLE_SALT).unwrap()), - hash: None, - }; - - let s = ph.to_string(); - assert_eq!(s, "$argon2d$a=1,b=2,c=3$saltsaltsaltsaltsalt"); - - let ph2 = PasswordHash::try_from(s.as_str()).unwrap(); - assert_eq!(ph, ph2); -} - -#[test] -fn salt_and_hash() { - let ph = PasswordHash { - algorithm: EXAMPLE_ALGORITHM, - version: None, - params: ParamsString::default(), - salt: Some(Salt::from_b64(EXAMPLE_SALT).unwrap()), - hash: Some(EXAMPLE_HASH.try_into().unwrap()), - }; - - let s = ph.to_string(); - assert_eq!( - s, - "$argon2d$saltsaltsaltsaltsalt$hashhashhashhashhashhashhashhashhashhashhas" - ); - - let ph2 = PasswordHash::try_from(s.as_str()).unwrap(); - assert_eq!(ph, ph2); -} - -#[test] -fn all_fields() { - let ph = PasswordHash { - algorithm: EXAMPLE_ALGORITHM, - version: None, - params: example_params(), - salt: Some(Salt::from_b64(EXAMPLE_SALT).unwrap()), - hash: Some(EXAMPLE_HASH.try_into().unwrap()), - }; - - let s = ph.to_string(); - assert_eq!( - s, - "$argon2d$a=1,b=2,c=3$saltsaltsaltsaltsalt$hashhashhashhashhashhashhashhashhashhashhas" - ); - - let ph2 = PasswordHash::try_from(s.as_str()).unwrap(); - assert_eq!(ph, ph2); -} diff --git a/password-hash/tests/test_vectors.rs b/password-hash/tests/test_vectors.rs deleted file mode 100644 index 69d32f16f..000000000 --- a/password-hash/tests/test_vectors.rs +++ /dev/null @@ -1,44 +0,0 @@ -//! Test vectors for commonly used password hashing algorithms. - -#![cfg(feature = "phc")] - -use password_hash::phc::{Ident, PasswordHash}; - -const ARGON2D_HASH: &str = - "$argon2d$v=19$m=512,t=3,p=2$5VtWOO3cGWYQHEMaYGbsfQ$AcmqasQgW/wI6wAHAMk4aQ"; -const SCRYPT_HASH: &str = - "$scrypt$epIxT/h6HbbwHaehFnh/bw$7H0vsXlY8UxxyW/BWx/9GuY7jEvGjT71GFd6O4SZND0"; - -#[test] -fn argon2id() { - let ph = PasswordHash::new(ARGON2D_HASH).unwrap(); - assert_eq!(ph.algorithm, Ident::new("argon2d").unwrap()); - assert_eq!(ph.version, Some(19)); - assert_eq!(ph.params.iter().count(), 3); - assert_eq!(ph.params.get_decimal("m").unwrap(), 512); - assert_eq!(ph.params.get_decimal("t").unwrap(), 3); - assert_eq!(ph.params.get_decimal("p").unwrap(), 2); - assert_eq!( - ph.salt.unwrap().as_ref(), - &[ - 0xe5, 0x5b, 0x56, 0x38, 0xed, 0xdc, 0x19, 0x66, 0x10, 0x1c, 0x43, 0x1a, 0x60, 0x66, - 0xec, 0x7d - ] - ); - assert_eq!(ph.hash.unwrap().to_string(), "AcmqasQgW/wI6wAHAMk4aQ"); - assert_eq!(ph.to_string(), ARGON2D_HASH); -} - -#[test] -fn scrypt() { - let ph = PasswordHash::new(SCRYPT_HASH).unwrap(); - assert_eq!(ph.algorithm, Ident::new("scrypt").unwrap()); - assert_eq!(ph.version, None); - assert_eq!(ph.params.len(), 0); - assert_eq!(ph.salt.unwrap().to_string(), "epIxT/h6HbbwHaehFnh/bw"); - assert_eq!( - ph.hash.unwrap().to_string(), - "7H0vsXlY8UxxyW/BWx/9GuY7jEvGjT71GFd6O4SZND0" - ); - assert_eq!(ph.to_string(), SCRYPT_HASH); -} diff --git a/password-hash/tests/hashing.rs b/password-hash/tests/traits.rs similarity index 74% rename from password-hash/tests/hashing.rs rename to password-hash/tests/traits.rs index ae018d605..de63e7cdb 100644 --- a/password-hash/tests/hashing.rs +++ b/password-hash/tests/traits.rs @@ -3,11 +3,8 @@ #![cfg(feature = "phc")] use core::{fmt::Display, str::FromStr}; -use password_hash::{ - CustomizedPasswordHasher, PasswordHasher, - errors::{Error, Result}, - phc::{Decimal, Ident, Output, ParamsString, PasswordHash, Salt}, -}; +use password_hash::{CustomizedPasswordHasher, Error, PasswordHasher, Result}; +use phc::{Decimal, Ident, Output, ParamsString, PasswordHash, Salt}; const ALG: Ident = Ident::new_unwrap("example"); @@ -25,11 +22,11 @@ impl CustomizedPasswordHasher for StubPasswordHasher { version: Option, params: StubParams, ) -> Result { - let salt = Salt::new(salt)?; + let salt = Salt::new(salt).map_err(|_| Error::SaltInvalid)?; let mut output = Vec::new(); if let Some(alg) = algorithm { - if Ident::new(alg)? != ALG { + if Ident::new(alg).map_err(|_| Error::Algorithm)? != ALG { return Err(Error::Algorithm); } } @@ -38,7 +35,7 @@ impl CustomizedPasswordHasher for StubPasswordHasher { output.extend_from_slice(slice); } - let hash = Output::new(&output)?; + let hash = Output::new(&output).map_err(|_| Error::OutputSize)?; Ok(PasswordHash { algorithm: ALG, @@ -84,9 +81,11 @@ impl TryFrom for ParamsString { #[test] fn verify_password_hash() { - let valid_password = "test password"; + let valid_password = b"test password"; let salt = Salt::from_b64("testsalt000").unwrap(); - let hash = PasswordHash::generate(StubPasswordHasher, valid_password, &salt).unwrap(); + let hash = StubPasswordHasher + .hash_password(valid_password, &salt) + .unwrap(); // Sanity tests for StubFunction impl above assert_eq!(hash.algorithm, ALG); @@ -94,15 +93,4 @@ fn verify_password_hash() { hash.salt.unwrap().as_ref(), &[0xb5, 0xeb, 0x2d, 0xb1, 0xa9, 0x6d, 0xd3, 0x4d] ); - - // Tests for generic password verification logic - assert_eq!( - hash.verify_password(&[&StubPasswordHasher], valid_password), - Ok(()) - ); - - assert_eq!( - hash.verify_password(&[&StubPasswordHasher], "wrong password"), - Err(Error::Password) - ); }