Skip to main content

attrkey/
attributes.rs

1use argon2::{Algorithm, Argon2, Version};
2use digest::{
3    Digest, HashMarker, OutputSizeUser,
4    block_buffer::Eager,
5    core_api::{
6        BlockSizeUser, BufferKindUser, CoreProxy, FixedOutputCore, UpdateCore,
7    },
8    typenum::{IsLess, Le, NonZero, U256},
9};
10use rayon::prelude::*;
11use std::marker::PhantomData;
12use zeroize::Zeroize;
13
14use crate::errors::ArrkeyError;
15use crate::keyspace::Keyspace;
16use crate::params::Params;
17
18/// Represents a collection of attribute values and that can be hardened into
19/// a `Keyspace` using memory-hard key derivation.
20#[derive(Clone, Debug)]
21pub struct Attributes<'a, D> {
22    arr: Vec<Vec<u8>>,
23    argon2_context: Argon2<'a>,
24    context_string: Option<String>,
25    constraint: u8,
26    _digest: PhantomData<D>,
27}
28
29impl<'a, D> Attributes<'a, D>
30where
31    D: Digest + CoreProxy + OutputSizeUser + Sync,
32    D::Core: Sync
33        + HashMarker
34        + UpdateCore
35        + FixedOutputCore
36        + BufferKindUser<BufferKind = Eager>
37        + Default
38        + Clone
39        + BlockSizeUser,
40    <D::Core as BlockSizeUser>::BlockSize: IsLess<U256>,
41    Le<<D::Core as BlockSizeUser>::BlockSize, U256>: NonZero,
42{
43    /// Construct a new collection of `Attributes`.
44    ///
45    /// Requires at least two attribute values, and a set of valid parameters.
46    /// Returns an error when fewer than two attributes are provided.
47    pub fn new(arr: Vec<Vec<u8>>, params: Params) -> Result<Self, ArrkeyError> {
48        if arr.len() <= 1 {
49            return Err(ArrkeyError::AttributesTooFew);
50        }
51
52        let argon2_params = argon2::Params::new(
53            params.m_cost,
54            params.t_cost,
55            params.p_cost,
56            Some(32),
57        )
58        .expect("password hashing params already validated");
59
60        let argon2_context =
61            Argon2::new(Algorithm::Argon2id, Version::V0x13, argon2_params);
62
63        Ok(Self {
64            arr,
65            argon2_context,
66            context_string: params.context,
67            constraint: params.constraint,
68            _digest: PhantomData,
69        })
70    }
71
72    /// Harden the attributes into a cryptographic [Keyspace].
73    ///
74    /// Applies a memory-hard key derivation function
75    /// ([Argon2id](argon2::Argon2)) to harden all attributes in parallel,
76    /// using per-attribute salts derived from a shared base salt. All hardened
77    /// attributes are used to produce a keyspace, from which attribute-based
78    /// keys can be selected.
79    ///
80    /// Returns a [Keyspace] of the hardened attributes on success, or an error
81    /// when the memory-hard key derivation process fails.
82    pub fn harden(&self, salt: &[u8]) -> Result<Keyspace<D>, ArrkeyError> {
83        let mut arr_salts = self.derive_salts(salt);
84
85        let hardened: Vec<[u8; 32]> = self
86            .arr
87            .par_iter()
88            .zip(arr_salts.par_iter())
89            .map(|(pwd, salt)| -> Result<[u8; 32], ArrkeyError> {
90                let mut tmp = [0u8; 32];
91                self.argon2_context
92                    .hash_password_into(pwd, salt, &mut tmp)
93                    .map_err(|_| ArrkeyError::HardenFail)?;
94                Ok(tmp)
95            })
96            .collect::<Result<Vec<_>, _>>()?;
97        arr_salts.zeroize();
98
99        Ok(Keyspace::new(hardened, self.constraint))
100    }
101
102    /// Return the number of attributes in this collection.
103    pub fn len(&self) -> usize {
104        self.arr.len()
105    }
106
107    fn derive_salts(&self, base_salt: &[u8]) -> Vec<Vec<u8>> {
108        (0..self.arr.len())
109            .map(|i| {
110                let mut hasher = D::new();
111
112                hasher.update(base_salt);
113                if let Some(ctx) = &self.context_string {
114                    hasher.update(ctx.as_bytes());
115                }
116                hasher.update(&i.to_le_bytes());
117
118                hasher.finalize().to_vec()
119            })
120            .collect()
121    }
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127    use sha2::Sha256;
128
129    const ATTR_1: &'static [u8; 18] = b"Pigs on the Wing I";
130    const ATTR_2: &'static [u8; 4] = b"Dogs";
131    const ATTR_3: &'static [u8; 27] = b"Pigs (Three Different Ones)";
132    const ATTR_4: &'static [u8; 5] = b"Sheep";
133    const ATTR_5: &'static [u8; 19] = b"Pigs on the Wing II";
134
135    fn get_arr() -> Vec<Vec<u8>> {
136        vec![
137            ATTR_1.to_vec(),
138            ATTR_2.to_vec(),
139            ATTR_3.to_vec(),
140            ATTR_4.to_vec(),
141            ATTR_5.to_vec(),
142        ]
143    }
144
145    #[test]
146    fn new_produces_attributes_collection() {
147        let arr = get_arr();
148
149        // Valid attribute values and params must yield Ok result
150        let result = Attributes::<Sha256>::new(arr, Params::default());
151        assert!(result.is_ok());
152    }
153
154    #[test]
155    fn new_too_few() {
156        let arr_empty: Vec<Vec<u8>> = vec![];
157        let arr_one: Vec<Vec<u8>> = vec![ATTR_1.to_vec()];
158
159        // Too few attributes (zero attributes) must result in an error
160        let result = Attributes::<Sha256>::new(arr_empty, Params::default());
161        assert_eq!(result.unwrap_err(), ArrkeyError::AttributesTooFew);
162
163        // Too few attributes (one attribute) must result in an error
164        let result = Attributes::<Sha256>::new(arr_one, Params::default());
165        assert_eq!(result.unwrap_err(), ArrkeyError::AttributesTooFew);
166    }
167
168    #[test]
169    fn harden_produces_keyspace() {
170        let arr = get_arr();
171        let salt = b"Animals";
172
173        // Should construct collection of attributes
174        let result = Attributes::<Sha256>::new(arr, Params::default());
175        assert!(result.is_ok());
176        let attributes = result.unwrap();
177
178        // Hardening from attribute collection must produce keyspace
179        let result = attributes.harden(salt);
180        assert!(result.is_ok());
181    }
182
183    #[test]
184    fn harden_fails_argon2_failure() {
185        let huge_attribute = vec![0u8; (u32::MAX as usize) + 1];
186        let arr = vec![ATTR_1.to_vec(), huge_attribute];
187        let salt = b"Animals";
188
189        // Should construct collection of attributes
190        let result = Attributes::<Sha256>::new(arr, Params::default());
191        assert!(result.is_ok());
192        let attributes = result.unwrap();
193
194        // Hardening must fail for attribute too large for Argon2
195        let result = attributes.harden(salt);
196        assert_eq!(result.unwrap_err(), ArrkeyError::HardenFail);
197    }
198
199    #[test]
200    fn len_returns_collection_length() {
201        let arr = get_arr();
202        let arr_len = arr.len();
203
204        // Should construct collection of attributes
205        let result = Attributes::<Sha256>::new(arr, Params::default());
206        assert!(result.is_ok());
207        let attributes = result.unwrap();
208
209        // Returned length must match original number of attributes
210        let len = attributes.len();
211        assert_eq!(len, arr_len);
212    }
213}