dexios_core/
key.rs

1//! This module handles key-related functionality within `dexios-core`
2//!
3//! It contains methods for `argon2id` and `balloon` hashing, and securely generating a salt
4//!
5//! # Examples
6//!
7//! ```rust,ignore
8//! let salt = gen_salt();
9//! let secret_data = "secure key".as_bytes().to_vec();
10//! let raw_key = Protected::new(secret_data);
11//! let key = argon2id_hash(raw_key, &salt, &HeaderVersion::V3).unwrap();
12//! ```
13use anyhow::Result;
14use rand::{prelude::StdRng, Rng, SeedableRng};
15use zeroize::Zeroize;
16
17use crate::cipher::Ciphers;
18use crate::header::{Header, HeaderVersion};
19use crate::primitives::{MASTER_KEY_LEN, SALT_LEN};
20use crate::protected::Protected;
21
22/// This handles `argon2id` hashing of a raw key
23///
24/// It requires a user to generate the salt
25///
26/// `HeaderVersion` is required as the parameters are linked to specific header versions
27///
28/// It returns a `Protected<[u8; 32]>` - `Protected` wrappers are used for all sensitive information within `dexios-core`
29///
30/// This function ensures that `raw_key` is securely erased from memory once hashed
31///
32/// # Examples
33///
34/// ```rust,ignore
35/// let salt = gen_salt();
36/// let secret_data = "secure key".as_bytes().to_vec();
37/// let raw_key = Protected::new(secret_data);
38/// let key = argon2id_hash(raw_key, &salt, &HeaderVersion::V3).unwrap();
39/// ```
40///
41pub fn argon2id_hash(
42    raw_key: Protected<Vec<u8>>,
43    salt: &[u8; SALT_LEN],
44    version: &HeaderVersion,
45) -> Result<Protected<[u8; 32]>> {
46    use argon2::Argon2;
47    use argon2::Params;
48
49    let params = match version {
50        HeaderVersion::V1 => {
51            // 8MiB of memory, 8 iterations, 4 levels of parallelism
52            Params::new(8192, 8, 4, Some(Params::DEFAULT_OUTPUT_LEN))
53                .map_err(|_| anyhow::anyhow!("Error initialising argon2id parameters"))?
54        }
55        HeaderVersion::V2 => {
56            // 256MiB of memory, 8 iterations, 4 levels of parallelism
57            Params::new(262_144, 8, 4, Some(Params::DEFAULT_OUTPUT_LEN))
58                .map_err(|_| anyhow::anyhow!("Error initialising argon2id parameters"))?
59        }
60        HeaderVersion::V3 => {
61            // 256MiB of memory, 10 iterations, 4 levels of parallelism
62            Params::new(262_144, 10, 4, Some(Params::DEFAULT_OUTPUT_LEN))
63                .map_err(|_| anyhow::anyhow!("Error initialising argon2id parameters"))?
64        }
65        HeaderVersion::V4 | HeaderVersion::V5 => {
66            return Err(anyhow::anyhow!(
67                "argon2id is not supported on header versions above V3."
68            ))
69        }
70    };
71
72    let mut key = [0u8; 32];
73    let argon2 = Argon2::new(argon2::Algorithm::Argon2id, argon2::Version::V0x13, params);
74    let result = argon2.hash_password_into(raw_key.expose(), salt, &mut key);
75    drop(raw_key);
76
77    if result.is_err() {
78        return Err(anyhow::anyhow!("Error while hashing your key"));
79    }
80
81    Ok(Protected::new(key))
82}
83
84/// This handles BLAKE3-Balloon hashing of a raw key
85///
86/// It requires a user to generate the salt
87///
88/// `HeaderVersion` is required as the parameters are linked to specific header versions
89///
90/// It's only supported on header versions V4 and above.
91///
92/// It returns a `Protected<[u8; 32]>` - `Protected` wrappers are used for all sensitive information within `dexios-core`
93///
94/// This function ensures that `raw_key` is securely erased from memory once hashed
95///
96/// # Examples
97///
98/// ```rust,ignore
99/// let salt = gen_salt();
100/// let secret_data = "secure key".as_bytes().to_vec();
101/// let raw_key = Protected::new(secret_data);
102/// let key = balloon_hash(raw_key, &salt, &HeaderVersion::V4).unwrap();
103/// ```
104///
105pub fn balloon_hash(
106    raw_key: Protected<Vec<u8>>,
107    salt: &[u8; SALT_LEN],
108    version: &HeaderVersion,
109) -> Result<Protected<[u8; 32]>> {
110    use balloon_hash::Balloon;
111
112    let params = match version {
113        HeaderVersion::V1 | HeaderVersion::V2 | HeaderVersion::V3 => {
114            return Err(anyhow::anyhow!(
115                "Balloon hashing is not supported in header versions below V4."
116            ));
117        }
118        HeaderVersion::V4 => balloon_hash::Params::new(262_144, 1, 1)
119            .map_err(|_| anyhow::anyhow!("Error initialising balloon hashing parameters"))?,
120        HeaderVersion::V5 => balloon_hash::Params::new(278_528, 1, 1)
121            .map_err(|_| anyhow::anyhow!("Error initialising balloon hashing parameters"))?,
122    };
123
124    let mut key = [0u8; 32];
125    let balloon = Balloon::<blake3::Hasher>::new(balloon_hash::Algorithm::Balloon, params, None);
126    let result = balloon.hash_into(raw_key.expose(), salt, &mut key);
127    drop(raw_key);
128
129    if result.is_err() {
130        return Err(anyhow::anyhow!("Error while hashing your key"));
131    }
132
133    Ok(Protected::new(key))
134}
135
136
137/// This is a helper function for retrieving the key used for encrypting the data
138/// 
139/// In header versions below V4, this is just the hashed password
140/// 
141/// In header versions >= V4, this is a cryptographically-secure random value
142/// 
143/// In header versions >= V4, this function will iterate through all available keyslots, looking for a match. If it finds a match, it will return the decrypted master key.
144#[allow(clippy::module_name_repetitions)]
145pub fn decrypt_master_key(
146    raw_key: Protected<Vec<u8>>,
147    header: &Header,
148    // TODO: use custom error instead of anyhow
149) -> Result<Protected<[u8; MASTER_KEY_LEN]>> {
150    match header.header_type.version {
151        HeaderVersion::V1 | HeaderVersion::V2 | HeaderVersion::V3 => {
152            argon2id_hash(raw_key, &header.salt.ok_or_else(|| anyhow::anyhow!("Missing salt within the header!"))?, &header.header_type.version)
153        }
154        HeaderVersion::V4 => {
155            let keyslots = header.keyslots.as_ref().ok_or_else(|| anyhow::anyhow!("Unable to find a keyslot!"))?;
156            let keyslot = keyslots.first().ok_or_else(|| anyhow::anyhow!("Unable to find a match with the key you provided (maybe you supplied the wrong key?)"))?;
157            let key = keyslot.hash_algorithm.hash(raw_key, &keyslot.salt)?;
158
159            let cipher = Ciphers::initialize(key, &header.header_type.algorithm)?;
160            cipher
161                .decrypt(&keyslot.nonce, keyslot.encrypted_key.as_slice())
162                .map(vec_to_arr)
163                .map(Protected::new)
164                .map_err(|_| anyhow::anyhow!("Cannot decrypt master key"))
165        }
166        HeaderVersion::V5 => {
167            header
168                .keyslots
169                .as_ref()
170                .ok_or_else(|| anyhow::anyhow!("Unable to find a keyslot!"))?
171                .iter()
172                .find_map(|keyslot| {
173                    let key = keyslot.hash_algorithm.hash(raw_key.clone(), &keyslot.salt).ok()?;
174
175                    let cipher = Ciphers::initialize(key, &header.header_type.algorithm).ok()?;
176                    cipher
177                        .decrypt(&keyslot.nonce, keyslot.encrypted_key.as_slice())
178                        .map(vec_to_arr)
179                        .map(Protected::new)
180                        .ok()
181                })
182                .ok_or_else(|| anyhow::anyhow!("Unable to find a match with the key you provided (maybe you supplied the wrong key?)"))
183        }
184    }
185}
186
187// TODO: choose better place for this util
188/// This is a simple helper function, used for converting the 32-byte master key `Vec<u8>`s to `[u8; 32]`
189#[must_use]
190pub fn vec_to_arr<const N: usize>(mut master_key_vec: Vec<u8>) -> [u8; N] {
191    let mut master_key = [0u8; N];
192    let len = N.min(master_key_vec.len());
193    master_key[..len].copy_from_slice(&master_key_vec[..len]);
194    master_key_vec.zeroize();
195    master_key
196}
197
198/// This function is used for autogenerating a passphrase, from a wordlist
199/// 
200/// It consists of 3 words, from the EFF wordlist, and 6 random digits appended to the end
201/// 
202/// Each word, and the block of digits, are separated with `-`
203/// 
204/// This provides adequate protection, while also remaining somewhat memorable.
205#[must_use]
206pub fn generate_passphrase() -> Protected<String> {
207    let collection = include_str!("wordlist.lst");
208    let words = collection.lines().collect::<Vec<_>>();
209
210    let mut passphrase = String::new();
211
212    for _ in 0..3 {
213        let index = StdRng::from_entropy().gen_range(0..=words.len());
214        let word = words[index];
215        let capitalized_word = word
216            .char_indices()
217            .map(|(i, ch)| match i {
218                0 => ch.to_ascii_uppercase(),
219                _ => ch,
220            })
221            .collect::<String>();
222        passphrase.push_str(&capitalized_word);
223        passphrase.push('-');
224    }
225
226    for _ in 0..6 {
227        let number: i64 = StdRng::from_entropy().gen_range(0..=9);
228        passphrase.push_str(&number.to_string());
229    }
230
231    Protected::new(passphrase)
232}