stegano_seasmoke/
lib.rs

1//! # Password Hashing
2//! This little lib explores on
3
4use argon2::password_hash::rand_core::{OsRng, RngCore};
5use argon2::{Argon2, ParamsBuilder};
6use chacha20poly1305::aead::{Aead, AeadCore};
7use chacha20poly1305::{KeyInit, XChaCha20Poly1305};
8// use rand::RngCore;
9use zeroize::Zeroize;
10
11pub mod error;
12pub mod ffi;
13pub mod ffi_utils;
14
15pub use crate::error::SeasmokeError;
16
17const NONCE_LEN: usize = 24;
18const SALT_LEN: usize = 32;
19const KEY_LEN: usize = 32;
20
21pub type Result<T> = std::result::Result<T, SeasmokeError>;
22pub type Key = [u8; KEY_LEN];
23
24/// decrypt data with password, it uses argon2id for key derivation and XChaCha20Poly1305 for encryption
25pub fn decrypt_data(password: &str, data: &[u8]) -> Result<Vec<u8>> {
26    assert!(data.len() >= SALT_LEN + NONCE_LEN, "data is too short");
27    let salt = &data[data.len() - SALT_LEN..];
28    let nonce = &data[data.len() - SALT_LEN - NONCE_LEN..data.len() - SALT_LEN];
29    let key = derive_key(password.as_bytes(), salt)?;
30
31    let decryptor = XChaCha20Poly1305::new(&key.into());
32    let decipher_data = decryptor
33        .decrypt(nonce.into(), &data[0..data.len() - SALT_LEN - NONCE_LEN])
34        .map_err(SeasmokeError::DecryptionError)?;
35
36    // todo write tests for enc and decrypt
37    Ok(decipher_data)
38}
39
40/// encrypt data with password, it uses argon2id for key derivation and XChaCha20Poly1305 for encryption
41pub fn encrypt_data(password: &str, data: &[u8]) -> Result<Vec<u8>> {
42    // https://kerkour.com/rust-file-encryption-chacha20poly1305-argon2
43    let mut salt = [0u8; SALT_LEN];
44    OsRng
45        .try_fill_bytes(&mut salt)
46        .map_err(SeasmokeError::RandomSaltError)?;
47    let key = derive_key(password.as_bytes(), &salt)?;
48
49    let mut nonce = XChaCha20Poly1305::generate_nonce(&mut OsRng);
50    assert!(nonce.len() == NONCE_LEN);
51
52    let encryptor = XChaCha20Poly1305::new(&key.into());
53    let mut cipher_data = encryptor
54        .encrypt(&nonce, data)
55        .map_err(SeasmokeError::EncryptionError)?;
56    cipher_data.extend_from_slice(&nonce);
57    cipher_data.extend_from_slice(&salt);
58
59    nonce.zeroize();
60    salt.zeroize();
61
62    Ok(cipher_data)
63}
64
65fn default_secure_argon<'key>() -> Result<Argon2<'key>> {
66    // increased time costs to make it more secure
67    let params = ParamsBuilder::default()
68        .t_cost(10)
69        .output_len(32)
70        .build()
71        .map_err(SeasmokeError::KeyDerivationParamEarror)?;
72
73    Ok(Argon2::new(
74        argon2::Algorithm::Argon2id,
75        argon2::Version::V0x13,
76        params,
77    ))
78}
79
80fn derive_key(password: &[u8], salt: &[u8]) -> Result<[u8; 32]> {
81    let mut output_key_material = [0u8; 32]; // Can be any desired size
82    default_secure_argon()?
83        .hash_password_into(password, salt, &mut output_key_material)
84        .map_err(SeasmokeError::KeyDerivationError)?;
85
86    Ok(output_key_material)
87}
88
89#[cfg(test)]
90mod tests {
91    use argon2::{password_hash::SaltString, PasswordHash, PasswordVerifier};
92
93    use super::*;
94
95    #[test]
96    fn test_kye_derivation() {
97        let password = b"hunter42"; // Bad password; don't actually use!
98        let mut rnd = argon2::password_hash::rand_core::OsRng;
99        let mut salt = [0; 32];
100        rnd.try_fill_bytes(&mut salt).unwrap();
101
102        let mut output_key_material = [0u8; 32]; // Can be any desired size
103        Argon2::default()
104            .hash_password_into(password, &salt, &mut output_key_material)
105            .unwrap();
106
107        assert_ne!(salt, [0u8; 32]);
108        assert_ne!(output_key_material, [0u8; 32]);
109    }
110
111    #[test]
112    fn test_password_hash() {
113        // Can be: `$argon2`, `$pbkdf2`, or `$scrypt`
114        let hash_string = "$argon2i$v=19$m=65536,t=1,p=1$c29tZXNhbHQAAAAAAAAAAA$+r0d29hqEB0yasKr55ZgICsQGSkl0v0kgwhd+U3wyRo";
115        let input_password = "password";
116
117        let password_hash = PasswordHash::new(hash_string).expect("invalid password hash");
118
119        // Trait objects for algorithms to support
120        let algs: &[&dyn PasswordVerifier] = &[&Argon2::default()];
121
122        password_hash
123            .verify_password(algs, input_password)
124            .expect("invalid password");
125    }
126
127    #[test]
128    fn test_generate_a_password_hash() {
129        let password = b"hunter42"; // Bad password; don't actually use!
130        let mut rnd = OsRng;
131        let salt = SaltString::generate(&mut rnd);
132        let password_hash = PasswordHash::generate(Argon2::default(), password, &salt).unwrap();
133
134        assert_ne!(password_hash.to_string(), "");
135    }
136
137    #[test]
138    fn test_encryption_round_trip() {
139        let password = "resistance is futile";
140        // some stupind data
141        let data = b"lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.";
142
143        let cipher_data = encrypt_data(password, data).unwrap();
144        let decipher_data = decrypt_data(password, &cipher_data).unwrap();
145
146        assert_ne!(data, cipher_data.as_slice());
147        assert_eq!(data, decipher_data.as_slice());
148    }
149}