ncrypt_me/
lib.rs

1//! ncrypt_me - Secure Data Encryption
2//!
3//!
4//! ## How the Data is Encrypted
5//!
6//! Given some `Credentials` (username and password):
7//!
8//! - **Hashing**: Both the password and username are hashed using **Argon2**.
9//!   - The resulting hash of the **password** is used as the **key** for the **XChaCha20Poly1305** cipher.
10//!   - The resulting hash of the **username** is used as the **Additional Authenticated Data (AAD)** for the cipher.
11//!
12//! - **Encryption**: With the key and AAD set, the data is encrypted using the **XChaCha20Poly1305** cipher.
13//!
14//! - **Output**: The encrypted data is then returned.
15//!
16//! ### Example
17//!
18//!
19//! ```
20//! use ncrypt_me::{encrypt_data, decrypt_data, Credentials, Argon2, secure_types::{SecureString, SecureBytes}};
21//!
22//! let exposed_data: Vec<u8> = vec![1, 2, 3, 4];
23//! let credentials = Credentials::new(
24//!  SecureString::from("username"),
25//!  SecureString::from("password"),
26//!  SecureString::from("password"),
27//! );
28//!
29//! // I don't recommend using such low values, this is just an example
30//!
31//! let m_cost = 24_000;
32//! let t_cost = 3;
33//! let p_cost = 4;
34//!
35//! let argon2 = Argon2::new(m_cost, t_cost, p_cost);
36//! let secure_data = SecureBytes::from_vec(exposed_data.clone()).unwrap();
37//! let encrypted_data = encrypt_data(argon2, secure_data, credentials.clone()).unwrap();
38//!
39//! let decrypted_data = decrypt_data(encrypted_data, credentials).unwrap();
40//!
41//! decrypted_data.slice_scope(|decrypted_slice| {
42//!  assert_eq!(&exposed_data, decrypted_slice);
43//! });
44//! ```
45
46pub mod credentials;
47pub mod decrypt;
48pub mod encrypt;
49pub mod error;
50
51pub use secure_types;
52pub use zeroize;
53
54pub use credentials::Credentials;
55pub use decrypt::decrypt_data;
56pub use encrypt::encrypt_data;
57
58use bincode::{Decode, Encode, config::standard, decode_from_slice};
59use error::{Error, map_argon2_error};
60use secure_types::{SecureBytes, SecureString};
61use zeroize::Zeroize;
62
63use argon2_sys::{ARGON2_FLAG_CLEAR_PASSWORD, argon2_context, argon2_ctx};
64
65const HEADER_LEN: usize = 8;
66const ENCRYPTED_INFO_START: usize = 12;
67pub const RECOMMENDED_SALT_LEN: usize = 64;
68pub const RECOMMENDED_HASH_LENGTH: u64 = 64;
69
70/// Argon2 primitive type: variants of the algorithm.
71#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Default, Ord, Encode, Decode)]
72pub enum Algorithm {
73   /// Optimizes against GPU cracking attacks but vulnerable to side-channels.
74   ///
75   /// Accesses the memory array in a password dependent order, reducing the
76   /// possibility of time–memory tradeoff (TMTO) attacks.
77   Argon2d = 0,
78
79   /// Optimized to resist side-channel attacks.
80   ///
81   /// Accesses the memory array in a password independent order, increasing the
82   /// possibility of time-memory tradeoff (TMTO) attacks.
83   Argon2i = 1,
84
85   /// Hybrid that mixes Argon2i and Argon2d passes (*default*).
86   ///
87   /// Uses the Argon2i approach for the first half pass over memory and
88   /// Argon2d approach for subsequent passes. This effectively places it in
89   /// the "middle" between the other two: it doesn't provide as good
90   /// TMTO/GPU cracking resistance as Argon2d, nor as good of side-channel
91   /// resistance as Argon2i, but overall provides the most well-rounded
92   /// approach to both classes of attacks.
93   #[default]
94   Argon2id = 2,
95}
96
97/// Version of the algorithm.
98#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, PartialOrd, Ord, Encode, Decode)]
99#[repr(u32)]
100pub enum Version {
101   /// Version 16 (0x10 in hex)
102   ///
103   /// Performs overwrite internally
104   V0x10 = 0x10,
105
106   /// Version 19 (0x13 in hex, default)
107   ///
108   /// Performs XOR internally
109   #[default]
110   V0x13 = 0x13,
111}
112
113pub(crate) fn extract_encrypted_info_and_data(
114   encrypted_data: &[u8],
115) -> Result<(Vec<u8>, Vec<u8>), Error> {
116   let encrypted_info_length = u32::from_le_bytes(
117      encrypted_data[HEADER_LEN..ENCRYPTED_INFO_START]
118         .try_into()
119         .map_err(|_| Error::EncryptedInfo)?,
120   );
121
122   let encrypted_info_end = ENCRYPTED_INFO_START + (encrypted_info_length as usize);
123   let encrypted_info = &encrypted_data[ENCRYPTED_INFO_START..encrypted_info_end];
124   let encrypted_data = &encrypted_data[encrypted_info_end..];
125   Ok((encrypted_info.to_vec(), encrypted_data.to_vec()))
126}
127
128#[derive(Default, Clone, Debug, Encode, Decode)]
129pub struct EncryptedInfo {
130   pub password_salt: Vec<u8>,
131   pub username_salt: Vec<u8>,
132   pub cipher_nonce: Vec<u8>,
133   pub argon2: Argon2,
134}
135
136impl EncryptedInfo {
137   pub fn new(
138      password_salt: Vec<u8>,
139      username_salt: Vec<u8>,
140      cipher_nonce: Vec<u8>,
141      argon2: Argon2,
142   ) -> Self {
143      Self {
144         password_salt,
145         username_salt,
146         cipher_nonce,
147         argon2,
148      }
149   }
150
151   pub fn from_encrypted_data(data: &[u8]) -> Result<Self, Error> {
152      let (encrypted_info, _) = extract_encrypted_info_and_data(data)?;
153
154      let info: (EncryptedInfo, usize) = decode_from_slice(&encrypted_info, standard())
155         .map_err(|e| Error::DecodingFailed(e.to_string()))?;
156
157      Ok(info.0)
158   }
159}
160
161/// Argon2 instance
162#[derive(Default, Clone, Debug, Encode, Decode)]
163pub struct Argon2 {
164   pub m_cost: u32,
165   pub t_cost: u32,
166   pub p_cost: u32,
167   pub hash_length: u64,
168   /// By default we use the Argon2id
169   pub algorithm: Algorithm,
170   /// By default we use the version 0x13
171   pub version: Version,
172}
173
174impl Argon2 {
175   pub fn new(m_cost: u32, t_cost: u32, p_cost: u32) -> Self {
176      Self {
177         m_cost,
178         t_cost,
179         p_cost,
180         hash_length: RECOMMENDED_HASH_LENGTH,
181         ..Default::default()
182      }
183   }
184
185   pub fn with_algorithm(mut self, algorithm: Algorithm) -> Self {
186      self.algorithm = algorithm;
187      self
188   }
189
190   pub fn with_version(mut self, version: Version) -> Self {
191      self.version = version;
192      self
193   }
194
195   pub fn with_hash_length(mut self, hash_length: u64) -> Self {
196      self.hash_length = hash_length;
197      self
198   }
199
200   /// Hashes the given password
201   ///
202   /// ## Arguments
203   ///
204   /// - `password` - The password to hash
205   /// - `salt` - The salt to use for hashing
206   /// - `params` - The Argon2 parameters to use for hashing
207   ///
208   ///
209   /// ## Returns
210   ///
211   /// The hash of the password in its raw byte form
212   pub fn hash_password(
213      &self,
214      password: &SecureString,
215      mut salt: Vec<u8>,
216   ) -> Result<SecureBytes, Error> {
217      let mut hash_buffer = vec![0u8; self.hash_length as usize];
218
219      let code = password.str_scope(|password_str| {
220         let mut context = argon2_context {
221            out: hash_buffer.as_mut_ptr(),
222            outlen: self.hash_length as u32,
223            pwd: password_str.as_bytes().as_ptr() as *mut u8,
224            pwdlen: password_str.len() as u32,
225            salt: salt.as_mut_ptr(),
226            saltlen: salt.len() as u32,
227            secret: std::ptr::null_mut(),
228            secretlen: 0,
229            ad: std::ptr::null_mut(),
230            adlen: 0,
231            t_cost: self.t_cost,
232            m_cost: self.m_cost,
233            lanes: self.p_cost,
234            threads: self.p_cost,
235            version: self.version as u32,
236            allocate_cbk: None,
237            free_cbk: None,
238            flags: ARGON2_FLAG_CLEAR_PASSWORD,
239         };
240
241         unsafe {
242            argon2_ctx(&mut context, self.algorithm as u32)
243         }
244      });
245
246      salt.zeroize();
247
248      if code != 0 {
249         return Err(Error::Argon2(map_argon2_error(code)));
250      }
251
252      let secured_buffer =
253         SecureBytes::from_vec(hash_buffer).map_err(|e| Error::Custom(e.to_string()))?;
254      Ok(secured_buffer)
255   }
256}
257
258// Argon2 Presets
259impl Argon2 {
260   /// Not recommended
261   pub fn very_fast() -> Self {
262      Self {
263         m_cost: 128_000,
264         t_cost: 16,
265         p_cost: 4,
266         hash_length: RECOMMENDED_HASH_LENGTH,
267         ..Default::default()
268      }
269   }
270
271   /// Not recommended
272   pub fn fast() -> Self {
273      Self {
274         m_cost: 256_000,
275         t_cost: 8,
276         p_cost: 4,
277         hash_length: RECOMMENDED_HASH_LENGTH,
278         ..Default::default()
279      }
280   }
281
282   pub fn balanced() -> Self {
283      Self {
284         m_cost: 1024_000,
285         t_cost: 16,
286         p_cost: 4,
287         hash_length: RECOMMENDED_HASH_LENGTH,
288         ..Default::default()
289      }
290   }
291
292   pub fn slow() -> Self {
293      Self {
294         m_cost: 2048_000,
295         t_cost: 16,
296         p_cost: 4,
297         hash_length: RECOMMENDED_HASH_LENGTH,
298         ..Default::default()
299      }
300   }
301
302   pub fn very_slow() -> Self {
303      Self {
304         m_cost: 3072_000,
305         t_cost: 24,
306         p_cost: 4,
307         hash_length: RECOMMENDED_HASH_LENGTH,
308         ..Default::default()
309      }
310   }
311}
312
313#[cfg(test)]
314mod tests {
315   use super::*;
316   use secure_types::{SecureBytes, SecureString};
317
318   #[test]
319   fn can_encrypt_decrypt() {
320      let exposed_data: Vec<u8> = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
321      let credentials = Credentials::new(
322         SecureString::from("username"),
323         SecureString::from("password"),
324         SecureString::from("password"),
325      );
326
327      let m_cost = 24_000;
328      let t_cost = 3;
329      let p_cost = 1;
330
331      let argon2 = Argon2::new(m_cost, t_cost, p_cost);
332
333      let secure_data = SecureBytes::from_vec(exposed_data.clone()).unwrap();
334
335      let encrypted_data = encrypt_data(argon2, secure_data, credentials.clone()).unwrap();
336      let decrypted_data = decrypt_data(encrypted_data, credentials).unwrap();
337
338      decrypted_data.slice_scope(|decrypted_data| {
339         assert_eq!(exposed_data, decrypted_data);
340      });
341   }
342}