attrkey 0.1.0-alpha.2

Pure Rust implementation of a Selection-Sensitive Attribute-Based Key Derivation Scheme.
Documentation
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;

/// `Attributes` represent a collection of attribute values and that
/// can be hardened into a [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,
{
    /// Construct a new collection of `Attributes`.
    ///
    /// Requires at least two attribute values, and a password hasher.
    /// Returns an error when fewer than two attributes are provided.
    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,
        })
    }

    /// Harden the attributes into a cryptographic [Keyspace].
    ///
    /// Applies [password hashing](PasswordHasher) to harden all attributes
    /// in [parallel](rayon) with salts derived from each attribute and its
    /// position. All hardened attributes are used to produce a keyspace, from
    /// which attribute-based keys can be selected.
    ///
    /// Returns a `Keyspace` of the hardened attributes on success, or an error
    /// if password hashing fails.
    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))?
    }

    /// Harden the attributes into a cryptographic [Keyspace].
    ///
    /// Applies [password hashing](PasswordHasher) to harden all attributes
    /// in [parallel](rayon) with provided salts. All hardened attributes are
    /// used to produce a keyspace, from which attribute-based keys can be
    /// selected.
    ///
    /// Returns a `Keyspace` of the hardened attributes on success, or an error
    /// if password hashing fails.
    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))?
    }

    /// Return the number of attributes in this collection.
    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};

    /// Testing Attributes type.
    type TestAttributes = Attributes<Scrypt, ChaCha20Rng>;

    /// Triggers the [PwdHasherFail](AttrkeyError::PwdHasherFail) error path
    /// for [Attributes] methods.
    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;

    /// Triggers the [HashOutputAbsent](AttrkeyError::HashOutputAbsent) error
    /// path for [Attributes] methods.
    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);

        // Valid attribute values with password hasher must yield Ok result
        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();

        // Zero attributes must result in an error
        let result = TestAttributes::new(arr_empty, make_scrypt());
        assert!(matches!(result, Err(AttrkeyError::AttributesTooFew)));

        // One attribute must result in an error
        let result = TestAttributes::new(arr_one, make_scrypt());
        assert!(matches!(result, Err(AttrkeyError::AttributesTooFew)));
    }

    #[test]
    fn harden_produces_keyspace() {
        let attributes = setup();

        // Hardening attributes with context must produce keyspace
        let result = attributes.harden(4, None);
        assert!(result.is_ok());
    }

    #[test]
    fn harden_with_context_produces_keyspace() {
        let attributes = setup();

        // Hardening attributes with context must produce keyspace
        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);

        // Password hasher failure in hardening must result in an error
        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);

        // No password hash output in hardening must reuslt in an error
        let result = attributes.harden(4, None);
        assert!(matches!(result, Err(AttrkeyError::HashOutputAbsent)));
    }

    #[test]
    fn harden_with_salts_produces_keyspace() {
        let attributes = setup();

        // Hardening attributes with salts must produce keyspace
        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();

        // Hardening attributes with salts and context must produce keyspace
        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();

        // Fewer salts than attributes must result in an error
        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();

        // More salts than attributes must result in an error
        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);

        // Password hasher failure in hardening must result in an error
        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);

        // No password hash output in hardening must reuslt in an error
        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();

        // Returned length must match original number of attributes
        let len = attributes.len();
        assert_eq!(len, expected);
    }
}