Skip to main content

devolutions_crypto/
argon2parameters.rs

1use std::{
2    convert::TryFrom,
3    io::{Cursor, Read, Write},
4};
5
6use argon2::{Config, Variant, Version};
7use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
8use rand::TryRng;
9use typed_builder::TypedBuilder;
10
11#[cfg(feature = "wbindgen")]
12use wasm_bindgen::prelude::*;
13
14use super::Error;
15use super::Result;
16
17pub mod defaults {
18    use super::Error;
19    use super::Result;
20    use argon2::Variant;
21    use argon2::Version;
22    use rand::TryRng;
23
24    pub const LENGTH: u32 = 32;
25    pub const LANES: u32 = 1;
26    pub const MEMORY: u32 = 4096;
27    pub const ITERATIONS: u32 = 2;
28    pub const VARIANT: Variant = Variant::Argon2id;
29    pub const VERSION: Version = Version::Version13;
30    pub const DC_VERSION: u32 = 1;
31
32    pub fn salt() -> Result<Vec<u8>> {
33        let mut salt = vec![0u8; 16];
34        rand::rngs::SysRng
35            .try_fill_bytes(salt.as_mut_slice())
36            .map_err(|_| Error::RandomError)?;
37        Ok(salt)
38    }
39}
40
41/// Parameters used to derive the password into an Argon2 hash.
42///
43/// It is used to derive a password into a keypair.
44/// You should use the default, although this may be tweakable by the user in some cases.
45/// Once serialized, you can save it along the user information as it is not sensitive data.
46/// If the hash should never be computed in a non-threaded environment,
47///  you can raise the "lanes" value to enable multi-threading.
48///
49/// Note that calling `default()` will also generate a new random salt,
50///  so two calls to `default()` will not generate the same structure.
51#[cfg_attr(feature = "wbindgen", wasm_bindgen(inspectable))]
52#[derive(Clone, TypedBuilder)]
53pub struct Argon2Parameters {
54    /// Length of the desired hash. Should be 32 in most case.
55    #[builder(default=defaults::LENGTH)]
56    pub length: u32,
57    /// Number of parallel jobs to run. Only use if always computed in a multithreaded environment.
58    #[builder(default=defaults::LANES)]
59    pub lanes: u32,
60    /// Memory used by the algorithm in KiB. Higher is better.
61    #[builder(default=defaults::MEMORY)]
62    pub memory: u32,
63    /// Number of iterations(time cost). Higher is better.
64    #[builder(default=defaults::ITERATIONS)]
65    pub iterations: u32,
66    /// The variant to use. You should almost always use Argon2Id.
67    #[builder(default=defaults::VARIANT)]
68    variant: Variant,
69    /// The version of Argon2 to use. Use the latest.
70    #[builder(default=defaults::VERSION)]
71    version: Version,
72    /// Version of this structure in DevolutionsCrypto.
73    #[builder(default=defaults::DC_VERSION)]
74    dc_version: u32,
75    /// Authenticated but not secret data.
76    #[builder(default)]
77    associated_data: Vec<u8>,
78    /// Secret key to sign the hash. Note that this is not serialized.
79    #[builder(default)]
80    secret_key: Vec<u8>,
81    /// A 16-bytes salt to use that is generated automatically. Should not be accessed directly.
82    #[builder(default = defaults::salt().unwrap())]
83    salt: Vec<u8>,
84}
85
86impl Argon2Parameters {
87    pub fn get_salt_as_slice(&self) -> &[u8] {
88        self.salt.as_slice()
89    }
90
91    pub fn set_salt(&mut self, salt: Vec<u8>) {
92        self.salt = salt;
93    }
94}
95
96/// Implements the default parameters.
97impl Default for Argon2Parameters {
98    fn default() -> Self {
99        let mut salt = vec![0u8; 16];
100
101        rand::rngs::SysRng
102            .try_fill_bytes(salt.as_mut_slice())
103            .unwrap();
104
105        Argon2Parameters {
106            associated_data: Vec::new(),
107            secret_key: Vec::new(),
108            length: 32,
109            lanes: 1,
110            memory: 4096,
111            iterations: 2,
112            variant: Variant::Argon2id,
113            version: Version::Version13,
114            dc_version: 1,
115            salt,
116        }
117    }
118}
119
120impl From<&Argon2Parameters> for Vec<u8> {
121    fn from(params: &Argon2Parameters) -> Self {
122        // Data is encoded this way:
123        // All the u32 data -> enums(as u8) -> Vectors(length as u32 + vec))
124        // Note that the secret key is not serialized.
125        // Length is calculated this way:
126        // 5 * u32 + 2 * u8(enums) + 3 *u32(lengths) + 2 * vec.len();
127        let mut data = Vec::with_capacity(
128            5 * 4 + 2 + 2 * 4 + params.associated_data.len() + params.salt.len(),
129        );
130        data.write_u32::<LittleEndian>(params.dc_version).unwrap();
131        data.write_u32::<LittleEndian>(params.length).unwrap();
132        data.write_u32::<LittleEndian>(params.lanes).unwrap();
133        data.write_u32::<LittleEndian>(params.memory).unwrap();
134        data.write_u32::<LittleEndian>(params.iterations).unwrap();
135        data.write_u8(params.variant.as_u32() as u8).unwrap();
136        data.write_u8(params.version.as_u32() as u8).unwrap();
137
138        data.write_u32::<LittleEndian>(params.associated_data.len() as u32)
139            .unwrap();
140
141        data.write_all(&params.associated_data).unwrap();
142
143        data.write_u32::<LittleEndian>(params.salt.len() as u32)
144            .unwrap();
145
146        data.write_all(&params.salt).unwrap();
147
148        data
149    }
150}
151
152impl TryFrom<&[u8]> for Argon2Parameters {
153    type Error = Error;
154
155    fn try_from(data: &[u8]) -> Result<Self> {
156        let mut data_cursor = Cursor::new(data);
157        let dc_version = data_cursor.read_u32::<LittleEndian>()?;
158        let length = data_cursor.read_u32::<LittleEndian>()?;
159        let lanes = data_cursor.read_u32::<LittleEndian>()?;
160        let memory = data_cursor.read_u32::<LittleEndian>()?;
161        let iterations = data_cursor.read_u32::<LittleEndian>()?;
162
163        // Check if the versions works
164        let (variant, version) = match (
165            Variant::from_u32(data_cursor.read_u8()? as u32),
166            Version::from_u32(data_cursor.read_u8()? as u32),
167        ) {
168            (Ok(variant), Ok(version)) => (variant, version),
169            _ => return Err(Error::InvalidData),
170        };
171
172        let associated_data_length = data_cursor.read_u32::<LittleEndian>()? as usize;
173        let remaining = data.len() - (data_cursor.position() as usize);
174        if remaining < associated_data_length {
175            return Err(Error::InvalidLength);
176        }
177
178        let mut associated_data = vec![0u8; associated_data_length];
179        data_cursor.read_exact(&mut associated_data)?;
180
181        let salt_length = data_cursor.read_u32::<LittleEndian>()? as usize;
182        let remaining = data.len() - (data_cursor.position() as usize);
183        if remaining < salt_length {
184            return Err(Error::InvalidLength);
185        }
186
187        let mut salt = vec![0u8; salt_length];
188        data_cursor.read_exact(&mut salt)?;
189
190        Ok(Argon2Parameters {
191            associated_data,
192            secret_key: Vec::new(),
193            length,
194            lanes,
195            memory,
196            iterations,
197            variant,
198            version,
199            dc_version,
200            salt,
201        })
202    }
203}
204
205impl Argon2Parameters {
206    /// Compute the Argon2 hash using the password and the parameters.
207    pub fn compute(&self, password: &[u8]) -> Result<Vec<u8>> {
208        let config = Config {
209            ad: &self.associated_data,
210            secret: &self.secret_key,
211            hash_length: self.length,
212            lanes: self.lanes,
213            mem_cost: self.memory,
214            time_cost: self.iterations,
215            variant: self.variant,
216            version: self.version,
217            thread_mode: argon2::ThreadMode::Sequential,
218        };
219
220        Ok(argon2::hash_raw(password, &self.salt, &config)?)
221    }
222}
223
224#[test]
225fn test_argon2() {
226    use std::convert::TryInto;
227
228    let mut config = Argon2Parameters::default();
229    config.iterations = 2;
230    config.memory = 32;
231
232    // Computes the first hash
233    let hash1 = config.compute(b"Password1").unwrap();
234    let config_vec: Vec<u8> = (&config).into();
235
236    assert_ne!(config_vec.len(), 0);
237
238    let config: Argon2Parameters = config_vec.as_slice().try_into().unwrap();
239
240    // Compute a 2nd hash with the same params
241    let hash2 = config.compute(b"Password1").unwrap();
242
243    // Same params, different password.
244    let hash3 = config.compute(b"Password2").unwrap();
245
246    // Same Params, Same password, different salt
247    let mut config = Argon2Parameters::default();
248    config.iterations = 2;
249    config.memory = 32;
250    let hash4 = config.compute(b"Password1").unwrap();
251
252    // Test length params.
253    let mut config5 = Argon2Parameters::default();
254    config5.iterations = 2;
255    config5.memory = 32;
256    config5.length = 41;
257    let hash5 = config5.compute(b"Password1").unwrap();
258
259    assert_eq!(hash1.len(), config.length as usize);
260    assert_eq!(hash1, hash2);
261    assert_ne!(hash1, hash3);
262    assert_ne!(hash1, hash4);
263    assert_eq!(hash5.len(), config5.length as usize);
264}