attrkey 0.1.0-alpha.1

Pure Rust implementation of a Selection-Sensitive Attribute-Based Key Derivation Scheme.
Documentation
use crate::errors::ArrkeyError;

/// Parameters for the Selection-Sensitive Attribute-Based Key Derivation
/// Scheme.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Params {
    /// Argon2 memory size in kibibytes, between 8 * `p_cost` and (2^32)-1.
    pub m_cost: u32,

    /// Argon2 number of iterations, between 1 and (2^32)-1.
    pub t_cost: u32,

    /// Argon2 degree of parallelism, between 1 and (2^24)-1.
    pub p_cost: u32,

    /// Annihilation proof-of-work constraint as a number of leading zero bits.
    pub constraint: u8,

    pub context: Option<String>,
}

/// Number of synchronization points between lanes per pass.
///
/// Constant not exported by the Argon2 crate, redefined here.
const SYNC_POINTS: usize = 4;

impl Params {
    /// Default memory cost for Argon2.
    pub const DEFAULT_M: u32 = 12288;

    // Minimum number of 1 KiB memory blocks
    const MIN_M: u32 = 2 * SYNC_POINTS as u32;

    /// Default number of iterations (time cost) for Argon2.
    pub const DEFAULT_T: u32 = 3;

    // Minimum number of iterations
    const MIN_T: u32 = 1;

    /// Default degree of parallelism for Argon2.
    pub const DEFAULT_P: u32 = 1;

    // Minimum and maximum number of threads
    const MIN_P: u32 = 1;
    const MAX_P: u32 = 0xFFFFFF;

    /// Default proof of work constraint for annihilation.
    pub const DEFAULT_CONSTRAINT: u8 = 16;

    // Minimum constraint
    const MIN_CONSTRAINT: u8 = 1;

    /// Default parameters.
    ///
    /// Following the OWASP Password Storage Cheat Sheet,
    /// and a recommended proof of work constraint.
    pub const DEFAULT: Self = Params {
        m_cost: Self::DEFAULT_M,
        t_cost: Self::DEFAULT_T,
        p_cost: Self::DEFAULT_P,
        constraint: Self::DEFAULT_CONSTRAINT,
        context: None,
    };

    /// Create new parameters for [argon2] and [annihilation].
    ///
    /// # Arguments
    /// - `m_cost`: memory size for Argon2 in 1 KiB blocks.
    /// - `t_cost`: number of iterations (time cost) for Argon2.
    /// - `p_cost`: degree of parallelism for Argon2.
    /// - `constraint`: Proof-of-work constraint for annihilation.
    pub fn new(
        m_cost: u32,
        t_cost: u32,
        p_cost: u32,
        constraint: u8,
        context: Option<&str>,
    ) -> Result<Self, ArrkeyError> {
        if m_cost < Self::MIN_M {
            return Err(ArrkeyError::MemoryTooSmall);
        }
        if m_cost < p_cost * 8 {
            return Err(ArrkeyError::MemoryTooSmall);
        }
        if t_cost < Self::MIN_T {
            return Err(ArrkeyError::TimeTooSmall);
        }
        if p_cost < Self::MIN_P {
            return Err(ArrkeyError::ThreadsTooFew);
        }
        if p_cost > Self::MAX_P {
            return Err(ArrkeyError::ThreadsTooMany);
        }
        if constraint < Self::MIN_CONSTRAINT {
            return Err(ArrkeyError::ConstraintTooSmall);
        }

        Ok(Self {
            m_cost,
            t_cost,
            p_cost,
            constraint,
            context: context.map(|s| s.to_string()),
        })
    }
}

impl Default for Params {
    fn default() -> Self {
        Params::DEFAULT
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn default() {
        let params = Params::default();

        assert_eq!(params.m_cost, Params::DEFAULT_M);
        assert_eq!(params.t_cost, Params::DEFAULT_T);
        assert_eq!(params.p_cost, Params::DEFAULT_P);
        assert_eq!(params.constraint, Params::DEFAULT_CONSTRAINT);
        assert_eq!(params.context, None);

        let params = Params::DEFAULT;
        assert_eq!(params.m_cost, Params::DEFAULT_M);
        assert_eq!(params.t_cost, Params::DEFAULT_T);
        assert_eq!(params.p_cost, Params::DEFAULT_P);
        assert_eq!(params.constraint, Params::DEFAULT_CONSTRAINT);
        assert_eq!(params.context, None);
    }

    #[test]
    fn new_valid_params() {
        let result = Params::new(12288, 3, 1, 16, None);
        assert!(result.is_ok());

        let params = result.unwrap();
        assert_eq!(params.m_cost, 12288);
        assert_eq!(params.t_cost, 3);
        assert_eq!(params.p_cost, 1);
        assert_eq!(params.constraint, 16);
        assert_eq!(params.context, None);
    }

    #[test]
    fn new_with_context() {
        let result = Params::new(12288, 3, 1, 16, Some("EOTWS_Variation1"));
        assert!(result.is_ok());

        let params = result.unwrap();
        assert_eq!(params.context, Some("EOTWS_Variation1".to_string()));
    }

    #[test]
    fn m_cost_too_small() {
        let result = Params::new(1, 3, 1, 16, None);
        assert_eq!(result.unwrap_err(), ArrkeyError::MemoryTooSmall);
    }

    #[test]
    fn m_cost_too_small_p_cost() {
        let result = Params::new(15, 3, 2, 16, None);
        assert_eq!(result.unwrap_err(), ArrkeyError::MemoryTooSmall);
    }

    #[test]
    fn t_cost_too_small() {
        let result = Params::new(12288, 0, 1, 16, None);
        assert_eq!(result.unwrap_err(), ArrkeyError::TimeTooSmall);
    }

    #[test]
    fn p_cost_min_p() {
        let result = Params::new(12288, 3, 0, 16, None);
        assert_eq!(result.unwrap_err(), ArrkeyError::ThreadsTooFew);
    }

    #[test]
    fn p_cost_max_p() {
        let result = Params::new(0x1000000 * 8, 3, 0x1000000, 16, None);
        assert_eq!(result.unwrap_err(), ArrkeyError::ThreadsTooMany);
    }

    #[test]
    fn min_constraint() {
        let result = Params::new(12288, 3, 1, 0, None);
        assert_eq!(result.unwrap_err(), ArrkeyError::ConstraintTooSmall);
    }
}