use password_hash::{PasswordHasher, phc::PasswordHash};
use rand_core::{Rng, SeedableRng};
use rayon::iter::{
IndexedParallelIterator, IntoParallelRefIterator, ParallelIterator,
};
use sha2::{Digest, Sha256};
use std::marker::PhantomData;
use zeroize::{Zeroize, ZeroizeOnDrop};
use crate::errors::AttrkeyError;
use crate::keyspace::Keyspace;
#[derive(Clone, Zeroize, ZeroizeOnDrop)]
pub struct Attributes<H, R> {
arr: Vec<Vec<u8>>,
#[zeroize(skip)]
password_hasher: H,
_rng: PhantomData<R>,
}
impl<H, R> Attributes<H, R>
where
H: PasswordHasher<PasswordHash> + Sync,
R: SeedableRng + Rng + Sync,
R::Seed: AsMut<[u8]> + Default + Zeroize,
{
pub fn new(
arr: Vec<Vec<u8>>,
password_hasher: H,
) -> Result<Self, AttrkeyError> {
if arr.len() <= 1 {
return Err(AttrkeyError::AttributesTooFew);
}
Ok(Self {
arr,
password_hasher,
_rng: PhantomData,
})
}
pub fn harden(
&self,
constraint: u8,
context: Option<&[u8]>,
) -> Result<Keyspace<R>, AttrkeyError> {
let hardened: Vec<Vec<u8>> = self
.arr
.par_iter()
.enumerate()
.map(|(pos, pwd)| -> Result<Vec<u8>, AttrkeyError> {
let mut hasher = Sha256::new();
hasher.update(pwd);
hasher.update(&pos.to_le_bytes());
let mut salt: [u8; 32] = hasher.finalize().into();
let password_hash = self
.password_hasher
.hash_password_with_salt(pwd, &salt)
.map_err(|_| {
salt.zeroize();
AttrkeyError::PwdHasherFail
})?
.hash;
salt.zeroize();
Ok(password_hash
.ok_or(AttrkeyError::HashOutputAbsent)?
.as_bytes()
.to_vec())
})
.collect::<Result<Vec<_>, _>>()?;
Ok(Keyspace::<R>::new(hardened, constraint, context))?
}
pub fn harden_with_salts(
&self,
salts: &[Vec<u8>],
constraint: u8,
context: Option<&[u8]>,
) -> Result<Keyspace<R>, AttrkeyError> {
if salts.len() < self.arr.len() {
return Err(AttrkeyError::SaltsTooFew);
}
if salts.len() > self.arr.len() {
return Err(AttrkeyError::SaltsTooMany);
}
let hardened: Vec<Vec<u8>> = self
.arr
.par_iter()
.zip(salts.par_iter())
.map(|(pwd, salt)| -> Result<Vec<u8>, AttrkeyError> {
let password_hash = self
.password_hasher
.hash_password_with_salt(pwd, salt)
.map_err(|_| AttrkeyError::PwdHasherFail)?
.hash;
Ok(password_hash
.ok_or(AttrkeyError::HashOutputAbsent)?
.as_bytes()
.to_vec())
})
.collect::<Result<Vec<_>, _>>()?;
Ok(Keyspace::<R>::new(hardened, constraint, context))?
}
pub fn len(&self) -> usize {
self.arr.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
use chacha20::ChaCha20Rng;
use hex_literal::hex;
use password_hash::{
PasswordHasher,
phc::{Ident, ParamsString, PasswordHash},
};
use scrypt::{Params, Scrypt};
type TestAttributes = Attributes<Scrypt, ChaCha20Rng>;
struct FailingHasher;
impl PasswordHasher<PasswordHash> for FailingHasher {
fn hash_password_with_salt(
&self,
_password: &[u8],
_salt: &[u8],
) -> password_hash::Result<PasswordHash> {
Err(password_hash::Error::PasswordInvalid)
}
}
struct NoOutputHasher;
impl PasswordHasher<PasswordHash> for NoOutputHasher {
fn hash_password_with_salt(
&self,
_password: &[u8],
_salt: &[u8],
) -> password_hash::Result<PasswordHash> {
Ok(PasswordHash {
algorithm: Ident::new_unwrap("mock"),
version: None,
params: ParamsString::default(),
salt: None,
hash: None,
})
}
}
fn get_arr() -> Vec<Vec<u8>> {
vec![
b"Pigs on the Wing I".to_vec(),
b"Dogs".to_vec(),
b"Pigs (Three Different Ones)".to_vec(),
b"Sheep".to_vec(),
b"Pigs on the Wing II".to_vec(),
]
}
fn get_salts() -> Vec<Vec<u8>> {
vec![
hex!("b1e44c103e8771c050b4e55926d64300").to_vec(),
hex!("195755baffb5d472d11bbe91389c54d3").to_vec(),
hex!("8c8c33219b786561836944bc93d4e35f").to_vec(),
hex!("2c1b7554be4b741f9405b3c50abd7d6d").to_vec(),
hex!("3db2240e7a8aa6ab26ad526e50aae3b0").to_vec(),
]
}
fn make_scrypt() -> Scrypt {
let params = Params::new(4, 8, 1).unwrap();
Scrypt::new_with_params(params)
}
fn setup() -> TestAttributes {
TestAttributes::new(get_arr(), make_scrypt())
.expect("failed to construct attributes collection")
}
fn setup_with_hasher<H>(hasher: H) -> Attributes<H, ChaCha20Rng>
where
H: PasswordHasher<PasswordHash> + Sync,
{
Attributes::<H, ChaCha20Rng>::new(get_arr(), hasher)
.expect("failed to construct attributes collection")
}
#[test]
fn new_produces_attributes_collection() {
let params = Params::new(4, 8, 1).unwrap();
let scrypt = Scrypt::new_with_params(params);
let result = TestAttributes::new(get_arr(), scrypt);
assert!(result.is_ok());
}
#[test]
fn new_fails_attributes_too_few() {
let arr_empty: Vec<Vec<u8>> = vec![];
let arr_one: Vec<Vec<u8>> = get_arr()[..1].to_vec();
let result = TestAttributes::new(arr_empty, make_scrypt());
assert!(matches!(result, Err(AttrkeyError::AttributesTooFew)));
let result = TestAttributes::new(arr_one, make_scrypt());
assert!(matches!(result, Err(AttrkeyError::AttributesTooFew)));
}
#[test]
fn harden_produces_keyspace() {
let attributes = setup();
let result = attributes.harden(4, None);
assert!(result.is_ok());
}
#[test]
fn harden_with_context_produces_keyspace() {
let attributes = setup();
let result = attributes.harden(4, Some(b"Animals"));
assert!(result.is_ok());
}
#[test]
fn harden_fails_pwd_hasher_failure() {
let attributes = setup_with_hasher(FailingHasher);
let result = attributes.harden(4, None);
assert!(matches!(result, Err(AttrkeyError::PwdHasherFail)));
}
#[test]
fn harden_fails_absent_hash_output() {
let attributes = setup_with_hasher(NoOutputHasher);
let result = attributes.harden(4, None);
assert!(matches!(result, Err(AttrkeyError::HashOutputAbsent)));
}
#[test]
fn harden_with_salts_produces_keyspace() {
let attributes = setup();
let result = attributes.harden_with_salts(&get_salts(), 4, None);
assert!(result.is_ok());
}
#[test]
fn harden_with_salts_with_context_produces_keyspace() {
let attributes = setup();
let result =
attributes.harden_with_salts(&get_salts(), 4, Some(b"Animals"));
assert!(result.is_ok());
}
#[test]
fn harden_with_salts_fails_too_few_salts() {
let attributes = setup();
let too_few = get_salts()[..2].to_vec();
let result = attributes.harden_with_salts(&too_few, 4, None);
assert!(matches!(result, Err(AttrkeyError::SaltsTooFew)));
}
#[test]
fn harden_with_salts_fails_too_many_salts() {
let attributes = setup();
let mut too_many = get_salts();
too_many.push(hex!("ffffffffffffffffffffffffffffffff").to_vec());
let result = attributes.harden_with_salts(&too_many, 4, None);
assert!(matches!(result, Err(AttrkeyError::SaltsTooMany)));
}
#[test]
fn harden_with_salts_fails_pwd_hasher_failure() {
let attributes = setup_with_hasher(FailingHasher);
let result = attributes.harden_with_salts(&get_salts(), 4, None);
assert!(matches!(result, Err(AttrkeyError::PwdHasherFail)));
}
#[test]
fn harden_with_salts_fails_absent_hash_output() {
let attributes = setup_with_hasher(NoOutputHasher);
let result = attributes.harden_with_salts(&get_salts(), 4, None);
assert!(matches!(result, Err(AttrkeyError::HashOutputAbsent)));
}
#[test]
fn len_returns_collection_length() {
let expected = get_arr().len();
let attributes = setup();
let len = attributes.len();
assert_eq!(len, expected);
}
}