use super::Error;
use chacha20poly1305::{
ChaCha20Poly1305,
aead::{Aead, AeadCore, KeyInit, OsRng, Payload},
};
use generic_array::GenericArray;
use pbkdf2::{password_hash::SaltString, pbkdf2_hmac_array};
use sha2::Sha256;
const KEY_LENGTH: usize = 32;
const SALT_LEN: usize = 22;
const NONCE_LEN: usize = 12;
pub struct Chacha {
pbkdf_rounds: u32,
}
impl Chacha {
#[must_use]
pub fn with_pbkdf2_rounds(pbkdf_rounds: u32) -> Self {
Self { pbkdf_rounds }
}
fn derive_key_from_password(&self, password: &str, salt: &[u8]) -> [u8; KEY_LENGTH] {
pbkdf2_hmac_array::<Sha256, KEY_LENGTH>(password.as_bytes(), salt, self.pbkdf_rounds)
}
pub fn encrypt(&self, secret: &[u8], pw: &str) -> Result<Cipher, Error> {
self.encrypt_auth(secret, &[0_u8; 0], pw)
}
#[allow(clippy::missing_panics_doc)]
pub fn encrypt_auth(&self, secret: &[u8], auth: &[u8], pw: &str) -> Result<Cipher, Error> {
let salt = SaltString::generate(&mut OsRng);
debug_assert_eq!(salt.len(), SALT_LEN);
let key = self.derive_key_from_password(pw, salt.as_str().as_bytes());
let nonce = ChaCha20Poly1305::generate_nonce(&mut OsRng);
debug_assert_eq!(nonce.len(), NONCE_LEN);
Ok(Cipher {
salt: salt.to_string(),
ciphertext: ChaCha20Poly1305::new_from_slice(&key)
.unwrap()
.encrypt(
&nonce,
Payload {
msg: secret,
aad: auth,
},
)
.map_err(|e| {
Error::Encrypt(format!("can't encrypt the connection store, due to {e}"))
})?,
nonce: nonce.to_vec(),
})
}
#[allow(clippy::missing_panics_doc)]
pub fn decrypt(&self, cipher: &Cipher, pw: &str) -> Result<Vec<u8>, Error> {
self.decrypt_auth(cipher, "".as_bytes(), pw)
}
#[allow(clippy::missing_panics_doc)]
pub fn decrypt_auth(&self, cipher: &Cipher, auth: &[u8], pw: &str) -> Result<Vec<u8>, Error> {
let key = self.derive_key_from_password(pw, cipher.salt.as_bytes());
let ccp = ChaCha20Poly1305::new_from_slice(&key).unwrap();
ccp.decrypt(
GenericArray::from_slice(&cipher.nonce),
Payload {
msg: &cipher.ciphertext,
aad: auth,
},
)
.map_err(|e| Error::Decrypt(e.to_string()))
}
}
pub struct Cipher {
pub salt: String,
pub ciphertext: Vec<u8>,
pub nonce: Vec<u8>,
}
#[cfg(test)]
mod test {
use super::Chacha;
#[test]
fn test_encrypt_decrypt() {
let test_data = String::from("daewörjlser,dk gjxlre.t98i1df.lskejr lewiri23r9ß iu4ötirjf");
let pw = "LOIUo98zkjhB";
let chacha = Chacha::with_pbkdf2_rounds(123_456);
let cipher = chacha.encrypt(test_data.as_bytes(), pw).unwrap();
let result = String::from_utf8(chacha.decrypt(&cipher, pw).unwrap()).unwrap();
assert_eq!(test_data, result);
}
#[test]
fn test_encrypt_auth_decrypt_auth() {
let test_data = String::from("daewörjlser,dk gjxlre.t98i1df.lskejr lewiri23r9ß iu4ötirjf");
let test_auth = "oirlyrkfdösadkflsdkgnfm";
let pw = "LOIUo98zkjhB";
let chacha = Chacha::with_pbkdf2_rounds(123_456);
let cipher = chacha
.encrypt_auth(test_data.as_bytes(), test_auth.as_bytes(), pw)
.unwrap();
let result = String::from_utf8(
chacha
.decrypt_auth(&cipher, test_auth.as_bytes(), pw)
.unwrap(),
)
.unwrap();
assert_eq!(test_data, result);
}
}