#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use subtle::ConstantTimeEq;
use zeroize::Zeroize;
use crate::classic::crypto_pwhash;
use crate::constants::*;
use crate::error::Error;
use crate::keypair;
use crate::rng::copy_randombytes;
use crate::types::*;
pub type Salt = Vec<u8>;
pub type Hash = Vec<u8>;
#[cfg_attr(
feature = "serde",
derive(Zeroize, Clone, Debug, Serialize, Deserialize)
)]
#[cfg_attr(not(feature = "serde"), derive(Zeroize, Clone, Debug))]
pub struct Config {
algorithm: crypto_pwhash::PasswordHashAlgorithm,
hash_length: usize,
memlimit: usize,
opslimit: u64,
salt_length: usize,
}
impl Config {
#[must_use]
pub fn with_salt_length(self, salt_length: usize) -> Self {
Self {
salt_length,
..self
}
}
#[must_use]
pub fn with_hash_length(self, hash_length: usize) -> Self {
Self {
hash_length,
..self
}
}
#[must_use]
pub fn with_memlimit(self, memlimit: usize) -> Self {
Self { memlimit, ..self }
}
#[must_use]
pub fn with_opslimit(self, opslimit: u64) -> Self {
Self { opslimit, ..self }
}
pub fn interactive() -> Self {
Self {
algorithm: crypto_pwhash::PasswordHashAlgorithm::Argon2id13,
opslimit: CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE,
memlimit: CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE,
salt_length: CRYPTO_PWHASH_SALTBYTES,
hash_length: crypto_pwhash::STR_HASHBYTES,
}
}
pub fn moderate() -> Self {
Self {
algorithm: crypto_pwhash::PasswordHashAlgorithm::Argon2id13,
opslimit: CRYPTO_PWHASH_OPSLIMIT_MODERATE,
memlimit: CRYPTO_PWHASH_MEMLIMIT_MODERATE,
salt_length: CRYPTO_PWHASH_SALTBYTES,
hash_length: crypto_pwhash::STR_HASHBYTES,
}
}
pub fn sensitive() -> Self {
Self {
algorithm: crypto_pwhash::PasswordHashAlgorithm::Argon2id13,
opslimit: CRYPTO_PWHASH_OPSLIMIT_SENSITIVE,
memlimit: CRYPTO_PWHASH_MEMLIMIT_SENSITIVE,
salt_length: CRYPTO_PWHASH_SALTBYTES,
hash_length: crypto_pwhash::STR_HASHBYTES,
}
}
}
impl Default for Config {
fn default() -> Self {
Self::interactive()
}
}
#[cfg_attr(
feature = "serde",
derive(Zeroize, Clone, Debug, Serialize, Deserialize)
)]
#[cfg_attr(not(feature = "serde"), derive(Zeroize, Clone, Debug))]
pub struct PwHash<Hash: Bytes, Salt: Bytes> {
hash: Hash,
salt: Salt,
config: Config,
}
pub type VecPwHash = PwHash<Hash, Salt>;
#[cfg(any(feature = "nightly", all(doc, not(doctest))))]
#[cfg_attr(all(feature = "nightly", doc), doc(cfg(feature = "nightly")))]
pub mod protected {
use super::*;
pub use crate::protected::*;
pub use crate::types::*;
pub type Salt = HeapBytes;
pub type Hash = HeapBytes;
pub type LockedPwHash = PwHash<Locked<Hash>, Locked<Salt>>;
}
impl<Hash: NewBytes + ResizableBytes, Salt: NewBytes + ResizableBytes> PwHash<Hash, Salt> {
pub fn hash<Password: Bytes>(password: &Password, config: Config) -> Result<Self, Error> {
let mut hash = Hash::new_bytes();
let mut salt = Salt::new_bytes();
hash.resize(config.hash_length, 0);
salt.resize(config.salt_length, 0);
copy_randombytes(salt.as_mut_slice());
crypto_pwhash::crypto_pwhash(
hash.as_mut_slice(),
password.as_slice(),
salt.as_slice(),
config.opslimit,
config.memlimit,
config.algorithm.clone(),
)?;
Ok(Self { hash, salt, config })
}
pub fn hash_interactive<Password: Bytes>(password: &Password) -> Result<Self, Error> {
Self::hash(password, Config::interactive())
}
pub fn hash_moderate<Password: Bytes>(password: &Password) -> Result<Self, Error> {
Self::hash(password, Config::moderate())
}
pub fn hash_sensitive<Password: Bytes>(password: &Password) -> Result<Self, Error> {
Self::hash(password, Config::sensitive())
}
#[cfg(any(feature = "base64", all(doc, not(doctest))))]
#[cfg_attr(all(feature = "nightly", doc), doc(cfg(feature = "base64")))]
#[allow(clippy::inherent_to_string)]
pub fn to_string(&self) -> String {
let (t_cost, m_cost) =
crypto_pwhash::convert_costs(self.config.opslimit, self.config.memlimit);
crypto_pwhash::pwhash_to_string(t_cost, m_cost, self.salt.as_slice(), self.hash.as_slice())
}
}
impl<Hash: NewBytes + ResizableBytes, Salt: Bytes + Clone> PwHash<Hash, Salt> {
pub fn verify<Password: Bytes>(&self, password: &Password) -> Result<(), Error> {
let computed = Self::hash_with_salt(password, self.salt.clone(), self.config.clone())?;
if self
.hash
.as_slice()
.ct_eq(computed.hash.as_slice())
.unwrap_u8()
== 1
{
Ok(())
} else {
Err(dryoc_error!("hashes do not match"))
}
}
pub fn hash_with_salt<Password: Bytes>(
password: &Password,
salt: Salt,
config: Config,
) -> Result<Self, Error> {
let mut hash = Hash::new_bytes();
hash.resize(config.hash_length, 0);
crypto_pwhash::crypto_pwhash(
hash.as_mut_slice(),
password.as_slice(),
salt.as_slice(),
config.opslimit,
config.memlimit,
config.algorithm.clone(),
)?;
Ok(Self { hash, salt, config })
}
}
#[cfg(any(feature = "base64", all(doc, not(doctest))))]
#[cfg_attr(all(feature = "nightly", doc), doc(cfg(feature = "base64")))]
impl<Hash: Bytes + From<Vec<u8>>, Salt: Bytes + From<Vec<u8>>> PwHash<Hash, Salt> {
pub fn from_string(hashed_password: &str) -> Result<Self, Error> {
let parsed_pwhash = crypto_pwhash::Pwhash::parse_encoded_pwhash(hashed_password)?;
let opslimit = parsed_pwhash.t_cost.unwrap() as u64;
let memlimit = 1024 * (parsed_pwhash.m_cost.unwrap() as usize);
let hash_length = parsed_pwhash.pwhash.as_ref().unwrap().len();
let salt_length = parsed_pwhash.salt.as_ref().unwrap().len();
let algorithm = parsed_pwhash.type_.unwrap();
Ok(Self {
hash: parsed_pwhash.pwhash.unwrap().into(),
salt: parsed_pwhash.salt.unwrap().into(),
config: Config {
algorithm,
hash_length,
memlimit,
opslimit,
salt_length,
},
})
}
}
impl<Hash: Bytes, Salt: Bytes> PwHash<Hash, Salt> {
pub fn from_parts(hash: Hash, salt: Salt, config: Config) -> Self {
Self { hash, salt, config }
}
pub fn into_parts(self) -> (Hash, Salt, Config) {
(self.hash, self.salt, self.config)
}
}
impl<Salt: Bytes> PwHash<Hash, Salt> {
pub fn derive_keypair<
Password: Bytes,
PublicKey: NewByteArray<CRYPTO_BOX_PUBLICKEYBYTES>,
SecretKey: NewByteArray<CRYPTO_BOX_SECRETKEYBYTES>,
>(
password: &Password,
salt: Salt,
config: Config,
) -> Result<keypair::KeyPair<PublicKey, SecretKey>, Error> {
let mut secret_key = SecretKey::new_byte_array();
crypto_pwhash::crypto_pwhash(
secret_key.as_mut_slice(),
password.as_slice(),
salt.as_slice(),
config.opslimit,
config.memlimit,
config.algorithm,
)?;
Ok(keypair::KeyPair::<PublicKey, SecretKey>::from_secret_key(
secret_key,
))
}
}
impl PwHash<Hash, Salt> {
pub fn hash_with_defaults<Password: Bytes>(password: &Password) -> Result<Self, Error> {
Self::hash_interactive(password)
}
#[cfg(any(feature = "base64", all(doc, not(doctest))))]
#[cfg_attr(all(feature = "nightly", doc), doc(cfg(feature = "base64")))]
pub fn from_string_with_defaults(hashed_password: &str) -> Result<Self, Error> {
Self::from_string(hashed_password)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pwhash() {
let password = b"super secrit password";
let pwhash = PwHash::hash_with_defaults(password).expect("unable to hash");
pwhash.verify(password).expect("verification failed");
pwhash
.verify(b"invalid password")
.expect_err("verification should have failed");
}
#[cfg(feature = "base64")]
#[test]
fn test_pwhash_str() {
let password = b"super secrit password";
let pwhash = PwHash::hash_with_defaults(password).expect("unable to hash");
let pw_string = pwhash.to_string();
let parsed_pwhash =
PwHash::from_string_with_defaults(&pw_string).expect("couldn't parse hashed password");
parsed_pwhash.verify(password).expect("verification failed");
parsed_pwhash
.verify(b"invalid password")
.expect_err("verification should have failed");
}
#[test]
#[cfg(feature = "nightly")]
fn test_protected() {
use crate::pwhash::protected::*;
let password =
HeapBytes::from_slice_into_locked(b"juicy password").expect("couldn't lock password");
let pwhash: LockedPwHash =
PwHash::hash(&password, Config::interactive()).expect("unable to hash");
pwhash.verify(&password).expect("verification failed");
pwhash
.verify(b"invalid password")
.expect_err("verification should have failed");
}
}