From 4e8736a86d669e022d5d70085aaea3385476daa1 Mon Sep 17 00:00:00 2001 From: Tony Arcieri Date: Fri, 28 May 2021 11:46:14 -0700 Subject: [PATCH] argon2: make `Params` always available; fix `hash_password_simple` (#182) Closes #179. This commit makes the `Params` struct always available regardless of enabled crate features, adding on functionality when the `password-hash` feature is enabled. Per the added TODOs in this commit, it really seems like in the next breaking release, `Argon2::new` should accept `Params` as an argument rather than duplicating all of the individual parameter fields and passing them in as arguments. After that, they can be validated and stored within the `Argon2` struct itself. But that would be a breaking change, so for now they're stored piecemeal. Additionally, this commit now ensures enough context is available in the `Argon2` struct in order to implement `hash_password_simple` that uses the configured params rather than the defaults, which addresses the aforementioned issue. --- argon2/src/algorithm.rs | 137 +++++++++++++++++++ argon2/src/lib.rs | 288 +++++++++++++++++----------------------- argon2/src/params.rs | 46 +++++-- 3 files changed, 289 insertions(+), 182 deletions(-) create mode 100644 argon2/src/algorithm.rs diff --git a/argon2/src/algorithm.rs b/argon2/src/algorithm.rs new file mode 100644 index 000000000..3f126039c --- /dev/null +++ b/argon2/src/algorithm.rs @@ -0,0 +1,137 @@ +//! Argon2 algorithms (e.g. Argon2d, Argon2i, Argon2id). + +use crate::Error; +use core::{ + fmt::{self, Display}, + str::FromStr, +}; + +#[cfg(feature = "password-hash")] +use {core::convert::TryFrom, password_hash::Ident}; + +/// Argon2d algorithm identifier +#[cfg(feature = "password-hash")] +#[cfg_attr(docsrs, doc(cfg(feature = "password-hash")))] +pub const ARGON2D_IDENT: Ident<'_> = Ident::new("argon2d"); + +/// Argon2i algorithm identifier +#[cfg(feature = "password-hash")] +#[cfg_attr(docsrs, doc(cfg(feature = "password-hash")))] +pub const ARGON2I_IDENT: Ident<'_> = Ident::new("argon2i"); + +/// Argon2id algorithm identifier +#[cfg(feature = "password-hash")] +#[cfg_attr(docsrs, doc(cfg(feature = "password-hash")))] +pub const ARGON2ID_IDENT: Ident<'_> = Ident::new("argon2id"); + +/// Argon2 primitive type: variants of the algorithm. +#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] +pub enum Algorithm { + /// Optimizes against GPU cracking attacks but vulnerable to side-channels. + /// + /// Accesses the memory array in a password dependent order, reducing the + /// possibility of time–memory tradeoff (TMTO) attacks. + Argon2d = 0, + + /// Optimized to resist side-channel attacks. + /// + /// Accesses the memory array in a password independent order, increasing the + /// possibility of time-memory tradeoff (TMTO) attacks. + Argon2i = 1, + + /// Hybrid that mixes Argon2i and Argon2d passes (*default*). + /// + /// Uses the Argon2i approach for the first half pass over memory and + /// Argon2d approach for subsequent passes. This effectively places it in + /// the "middle" between the other two: it doesn't provide as good + /// TMTO/GPU cracking resistance as Argon2d, nor as good of side-channel + /// resistance as Argon2i, but overall provides the most well-rounded + /// approach to both classes of attacks. + Argon2id = 2, +} + +impl Default for Algorithm { + fn default() -> Algorithm { + Algorithm::Argon2id + } +} + +impl Algorithm { + /// Parse an [`Algorithm`] from the provided string. + pub fn new(id: impl AsRef) -> Result { + id.as_ref().parse() + } + + /// Get the identifier string for this PBKDF2 [`Algorithm`]. + pub fn as_str(&self) -> &str { + match self { + Algorithm::Argon2d => "argon2d", + Algorithm::Argon2i => "argon2i", + Algorithm::Argon2id => "argon2id", + } + } + + /// Get the [`Ident`] that corresponds to this Argon2 [`Algorithm`]. + #[cfg(feature = "password-hash")] + #[cfg_attr(docsrs, doc(cfg(feature = "password-hash")))] + pub fn ident(&self) -> Ident<'static> { + match self { + Algorithm::Argon2d => ARGON2D_IDENT, + Algorithm::Argon2i => ARGON2I_IDENT, + Algorithm::Argon2id => ARGON2ID_IDENT, + } + } + + /// Serialize primitive type as little endian bytes + pub(crate) fn to_le_bytes(self) -> [u8; 4] { + (self as u32).to_le_bytes() + } +} + +impl AsRef for Algorithm { + fn as_ref(&self) -> &str { + self.as_str() + } +} + +impl Display for Algorithm { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.as_str()) + } +} + +impl FromStr for Algorithm { + type Err = Error; + + fn from_str(s: &str) -> Result { + match s { + "argon2d" => Ok(Algorithm::Argon2d), + "argon2i" => Ok(Algorithm::Argon2i), + "argon2id" => Ok(Algorithm::Argon2id), + _ => Err(Error::AlgorithmInvalid), + } + } +} + +#[cfg(feature = "password-hash")] +#[cfg_attr(docsrs, doc(cfg(feature = "password-hash")))] +impl From for Ident<'static> { + fn from(alg: Algorithm) -> Ident<'static> { + alg.ident() + } +} + +#[cfg(feature = "password-hash")] +#[cfg_attr(docsrs, doc(cfg(feature = "password-hash")))] +impl<'a> TryFrom> for Algorithm { + type Error = password_hash::Error; + + fn try_from(ident: Ident<'a>) -> Result { + match ident { + ARGON2D_IDENT => Ok(Algorithm::Argon2d), + ARGON2I_IDENT => Ok(Algorithm::Argon2i), + ARGON2ID_IDENT => Ok(Algorithm::Argon2id), + _ => Err(password_hash::Error::Algorithm), + } + } +} diff --git a/argon2/src/lib.rs b/argon2/src/lib.rs index 1d8113521..3096b39b6 100644 --- a/argon2/src/lib.rs +++ b/argon2/src/lib.rs @@ -76,21 +76,20 @@ #[macro_use] extern crate alloc; +mod algorithm; mod block; mod error; mod instance; mod memory; -mod version; - -#[cfg(feature = "password-hash")] mod params; +mod version; -pub use crate::{error::Error, version::Version}; +pub use crate::{algorithm::Algorithm, error::Error, params::Params, version::Version}; #[cfg(feature = "password-hash")] #[cfg_attr(docsrs, doc(cfg(feature = "password-hash")))] pub use { - params::Params, + crate::algorithm::{ARGON2D_IDENT, ARGON2ID_IDENT, ARGON2I_IDENT}, password_hash::{self, PasswordHash, PasswordHasher, PasswordVerifier}, }; @@ -100,10 +99,6 @@ use crate::{ memory::{Memory, SYNC_POINTS}, }; use blake2::{digest, Blake2b, Digest}; -use core::{ - fmt::{self, Display}, - str::FromStr, -}; #[cfg(feature = "password-hash")] use { @@ -156,133 +151,6 @@ pub const MAX_SALT_LENGTH: usize = 0xFFFFFFFF; /// Maximum key length in bytes pub const MAX_SECRET: usize = 0xFFFFFFFF; -/// Argon2d algorithm identifier -#[cfg(feature = "password-hash")] -#[cfg_attr(docsrs, doc(cfg(feature = "password-hash")))] -pub const ARGON2D_IDENT: Ident<'_> = Ident::new("argon2d"); - -/// Argon2i algorithm identifier -#[cfg(feature = "password-hash")] -#[cfg_attr(docsrs, doc(cfg(feature = "password-hash")))] -pub const ARGON2I_IDENT: Ident<'_> = Ident::new("argon2i"); - -/// Argon2id algorithm identifier -#[cfg(feature = "password-hash")] -#[cfg_attr(docsrs, doc(cfg(feature = "password-hash")))] -pub const ARGON2ID_IDENT: Ident<'_> = Ident::new("argon2id"); - -/// Argon2 primitive type: variants of the algorithm. -#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] -pub enum Algorithm { - /// Optimizes against GPU cracking attacks but vulnerable to side-channels. - /// - /// Accesses the memory array in a password dependent order, reducing the - /// possibility of time–memory tradeoff (TMTO) attacks. - Argon2d = 0, - - /// Optimized to resist side-channel attacks. - /// - /// Accesses the memory array in a password independent order, increasing the - /// possibility of time-memory tradeoff (TMTO) attacks. - Argon2i = 1, - - /// Hybrid that mixes Argon2i and Argon2d passes (*default*). - /// - /// Uses the Argon2i approach for the first half pass over memory and - /// Argon2d approach for subsequent passes. This effectively places it in - /// the "middle" between the other two: it doesn't provide as good - /// TMTO/GPU cracking resistance as Argon2d, nor as good of side-channel - /// resistance as Argon2i, but overall provides the most well-rounded - /// approach to both classes of attacks. - Argon2id = 2, -} - -impl Default for Algorithm { - fn default() -> Algorithm { - Algorithm::Argon2id - } -} - -impl Algorithm { - /// Parse an [`Algorithm`] from the provided string. - pub fn new(id: impl AsRef) -> Result { - id.as_ref().parse() - } - - /// Get the identifier string for this PBKDF2 [`Algorithm`]. - pub fn as_str(&self) -> &str { - match self { - Algorithm::Argon2d => "argon2d", - Algorithm::Argon2i => "argon2i", - Algorithm::Argon2id => "argon2id", - } - } - - /// Get the [`Ident`] that corresponds to this Argon2 [`Algorithm`]. - #[cfg(feature = "password-hash")] - #[cfg_attr(docsrs, doc(cfg(feature = "password-hash")))] - pub fn ident(&self) -> Ident<'static> { - match self { - Algorithm::Argon2d => ARGON2D_IDENT, - Algorithm::Argon2i => ARGON2I_IDENT, - Algorithm::Argon2id => ARGON2ID_IDENT, - } - } - - /// Serialize primitive type as little endian bytes - fn to_le_bytes(self) -> [u8; 4] { - (self as u32).to_le_bytes() - } -} - -impl AsRef for Algorithm { - fn as_ref(&self) -> &str { - self.as_str() - } -} - -impl Display for Algorithm { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(self.as_str()) - } -} - -impl FromStr for Algorithm { - type Err = Error; - - fn from_str(s: &str) -> Result { - match s { - "argon2d" => Ok(Algorithm::Argon2d), - "argon2i" => Ok(Algorithm::Argon2i), - "argon2id" => Ok(Algorithm::Argon2id), - _ => Err(Error::AlgorithmInvalid), - } - } -} - -#[cfg(feature = "password-hash")] -#[cfg_attr(docsrs, doc(cfg(feature = "password-hash")))] -impl From for Ident<'static> { - fn from(alg: Algorithm) -> Ident<'static> { - alg.ident() - } -} - -#[cfg(feature = "password-hash")] -#[cfg_attr(docsrs, doc(cfg(feature = "password-hash")))] -impl<'a> TryFrom> for Algorithm { - type Error = password_hash::Error; - - fn try_from(ident: Ident<'a>) -> Result { - match ident { - ARGON2D_IDENT => Ok(Algorithm::Argon2d), - ARGON2I_IDENT => Ok(Algorithm::Argon2i), - ARGON2ID_IDENT => Ok(Algorithm::Argon2id), - _ => Err(password_hash::Error::Algorithm), - } - } -} - /// Argon2 context. /// /// Holds the following Argon2 inputs: @@ -310,35 +178,53 @@ impl<'a> TryFrom> for Algorithm { /// /// You want to erase the password, but you're OK with last pass not being /// erased. +// TODO(tarcieri): replace `Params`-related fields with an internally-stored struct #[derive(Clone)] pub struct Argon2<'key> { /// Key array secret: Option<&'key [u8]>, - /// Number of passes - t_cost: u32, + /// Default algorithm. + algorithm: Option, - /// Amount of memory requested (kB) + /// Version number + version: Version, + + /// Amount of memory requested (kB). m_cost: u32, - /// Number of lanes + /// Number of passes. + t_cost: u32, + + /// Number of lanes. lanes: u32, - /// Maximum number of threads + /// Maximum number of threads. threads: u32, - /// Version number - version: Version, + /// Enforce a required output size. + output_size: Option, } impl Default for Argon2<'_> { fn default() -> Self { - Self::new(None, 3, 4096, 1, Version::default()).expect("invalid default Argon2 params") + // TODO(tarcieri): use `Params` as argument to `Argon2::new` in the next breaking release + let params = Params::default(); + + Self::new( + None, + params.t_cost, + params.m_cost, + params.p_cost, + params.version, + ) + .expect("invalid default Argon2 params") } } impl<'key> Argon2<'key> { - /// Create a new Argon2 context + /// Create a new Argon2 context. + // TODO(tarcieri): use `Params` as argument to `Argon2::new` in the next breaking release pub fn new( secret: Option<&'key [u8]>, t_cost: u32, @@ -392,10 +278,12 @@ impl<'key> Argon2<'key> { Ok(Self { secret, + algorithm: None, t_cost, m_cost, lanes, threads: parallelism, + output_size: None, version, }) } @@ -409,12 +297,17 @@ impl<'key> Argon2<'key> { ad: &[u8], out: &mut [u8], ) -> Result<(), Error> { + // TODO(tarcieri): move algorithm selection entirely to `Argon2::new` + if self.algorithm.is_some() && Some(alg) != self.algorithm { + return Err(Error::AlgorithmInvalid); + } + // Validate output length - if MIN_OUTLEN > out.len() { + if out.len() < self.output_size.unwrap_or(MIN_OUTLEN) { return Err(Error::OutputTooShort); } - if MAX_OUTLEN < out.len() { + if out.len() > self.output_size.unwrap_or(MAX_OUTLEN) { return Err(Error::OutputTooLong); } @@ -448,6 +341,18 @@ impl<'key> Argon2<'key> { Instance::hash(self, alg, initial_hash, memory, out) } + /// Get default configured [`Params`]. + // TODO(tarcieri): store `Params` field in the `Argon2` struct. + pub fn params(&self) -> Params { + Params { + m_cost: self.m_cost, + t_cost: self.t_cost, + p_cost: self.threads, + output_size: self.output_size.unwrap_or(Params::DEFAULT_OUTPUT_SIZE), + version: self.version, + } + } + /// Hashes all the inputs into `blockhash[PREHASH_DIGEST_LENGTH]`. pub(crate) fn initial_hash( &self, @@ -487,26 +392,52 @@ impl<'key> Argon2<'key> { impl PasswordHasher for Argon2<'_> { type Params = Params; + fn hash_password_simple<'a, S>( + &self, + password: &[u8], + salt: &'a S, + ) -> password_hash::Result> + where + S: AsRef + ?Sized, + { + let algorithm = self.algorithm.unwrap_or_default(); + + let salt = Salt::try_from(salt.as_ref())?; + let mut salt_arr = [0u8; 64]; + let salt_bytes = salt.b64_decode(&mut salt_arr)?; + + // TODO(tarcieri): support the `data` parameter (i.e. associated data) + let ad = b""; + let output_size = self.output_size.unwrap_or(Params::DEFAULT_OUTPUT_SIZE); + + let output = password_hash::Output::init_with(output_size, |out| { + Ok(self.hash_password_into(algorithm, password, salt_bytes, ad, out)?) + })?; + + Ok(PasswordHash { + algorithm: algorithm.ident(), + version: Some(self.version.into()), + params: self.params().try_into()?, + salt: Some(salt), + hash: Some(output), + }) + } + fn hash_password<'a>( &self, password: &[u8], alg_id: Option>, params: Params, salt: impl Into>, - ) -> Result, password_hash::Error> { + ) -> password_hash::Result> { let algorithm = alg_id .map(Algorithm::try_from) .transpose()? .unwrap_or_default(); let salt = salt.into(); - let mut salt_arr = [0u8; 64]; - let salt_bytes = salt.b64_decode(&mut salt_arr)?; - - // TODO(tarcieri): support the `data` parameter (i.e. associated data) - let ad = b""; - let hasher = Self::new( + let mut hasher = Self::new( self.secret, params.t_cost, params.m_cost, @@ -515,31 +446,24 @@ impl PasswordHasher for Argon2<'_> { ) .map_err(|_| password_hash::Error::ParamValueInvalid)?; - if MAX_PWD_LENGTH < password.len() { - return Err(password_hash::Error::Password); - } - - let output = password_hash::Output::init_with(params.output_size, |out| { - Ok(hasher.hash_password_into(algorithm, password, salt_bytes, ad, out)?) - })?; + // TODO(tarcieri): pass these via `Params` when `Argon::new` accepts `Params` + hasher.algorithm = Some(algorithm); + hasher.output_size = Some(params.output_size); - Ok(PasswordHash { - algorithm: algorithm.ident(), - version: Some(params.version.into()), - params: params.try_into()?, - salt: Some(salt), - hash: Some(output), - }) + hasher.hash_password_simple(password, salt.as_str()) } } #[cfg(all(test, feature = "password-hash"))] mod tests { - use super::{Argon2, Params, PasswordHasher, Salt}; + use crate::{Argon2, Params, PasswordHasher, Salt, Version}; /// Example password only: don't use this as a real password!!! const EXAMPLE_PASSWORD: &[u8] = b"hunter42"; + /// Example salt value. Don't use a static salt value!!! + const EXAMPLE_SALT: &str = "examplesalt"; + #[test] fn decoded_salt_too_short() { let argon2 = Argon2::default(); @@ -550,4 +474,30 @@ mod tests { let res = argon2.hash_password(EXAMPLE_PASSWORD, None, Params::default(), salt); assert_eq!(res, Err(password_hash::Error::SaltTooShort)); } + + #[test] + fn hash_simple_retains_configured_params() { + // Non-default but valid parameters + let t_cost = 4; + let m_cost = 2048; + let p_cost = 2; + let version = Version::V0x10; + + let hasher = Argon2::new(None, t_cost, m_cost, p_cost, version).unwrap(); + let hash = hasher + .hash_password_simple(EXAMPLE_PASSWORD, EXAMPLE_SALT) + .unwrap(); + + assert_eq!(hash.version.unwrap(), version.into()); + + for &(param, value) in &[("t", t_cost), ("m", m_cost), ("p", p_cost)] { + assert_eq!( + hash.params + .get(param) + .and_then(|p| p.decimal().ok()) + .unwrap(), + value + ); + } + } } diff --git a/argon2/src/params.rs b/argon2/src/params.rs index fb9bd436f..d8e50fd05 100644 --- a/argon2/src/params.rs +++ b/argon2/src/params.rs @@ -1,51 +1,69 @@ //! Argon2 password hash parameters. -use crate::{Argon2, Version}; -use core::convert::{TryFrom, TryInto}; -use password_hash::{Decimal, ParamsString, PasswordHash}; +use crate::Version; + +#[cfg(feature = "password-hash")] +use { + core::convert::{TryFrom, TryInto}, + password_hash::{ParamsString, PasswordHash}, +}; /// Argon2 password hash parameters. /// /// These are parameters which can be encoded into a PHC hash string. -#[cfg_attr(docsrs, doc(cfg(feature = "password-hash")))] #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub struct Params { /// Memory size, expressed in kilobytes, between 1 and (2^32)-1. /// /// Value is an integer in decimal (1 to 10 digits). - pub m_cost: Decimal, + pub m_cost: u32, /// Number of iterations, between 1 and (2^32)-1. /// /// Value is an integer in decimal (1 to 10 digits). - pub t_cost: Decimal, + pub t_cost: u32, /// Degree of parallelism, between 1 and 255. /// /// Value is an integer in decimal (1 to 3 digits). - pub p_cost: Decimal, + pub p_cost: u32, /// Size of the output (in bytes) pub output_size: usize, /// Algorithm version + // TODO(tarcieri): make this separate from params in the next breaking release? pub version: Version, } +impl Params { + /// Default memory cost. + pub const DEFAULT_M_COST: u32 = 4096; + + /// Default number of iterations. + pub const DEFAULT_T_COST: u32 = 3; + + /// Default degree of parallelism. + pub const DEFAULT_P_COST: u32 = 1; + + /// Default output size. + pub const DEFAULT_OUTPUT_SIZE: usize = 32; +} + impl Default for Params { fn default() -> Params { - let ctx = Argon2::default(); - Params { - m_cost: ctx.m_cost, - t_cost: ctx.t_cost, - p_cost: ctx.threads, - output_size: 32, + m_cost: Self::DEFAULT_M_COST, + t_cost: Self::DEFAULT_T_COST, + p_cost: Self::DEFAULT_P_COST, + output_size: Self::DEFAULT_OUTPUT_SIZE, version: Version::default(), } } } +#[cfg(feature = "password-hash")] +#[cfg_attr(docsrs, doc(cfg(feature = "password-hash")))] impl<'a> TryFrom<&'a PasswordHash<'a>> for Params { type Error = password_hash::Error; @@ -77,6 +95,8 @@ impl<'a> TryFrom<&'a PasswordHash<'a>> for Params { } } +#[cfg(feature = "password-hash")] +#[cfg_attr(docsrs, doc(cfg(feature = "password-hash")))] impl<'a> TryFrom for ParamsString { type Error = password_hash::Error;