use chacha20poly1305::{AeadCore, AeadInPlace, ChaCha20Poly1305, KeyInit}; use entity::account::Account; use pbkdf2::pbkdf2_hmac_array; use rand::{rngs::OsRng, RngCore}; use sha2::Sha256; pub struct Cipher { chacha: ChaCha20Poly1305, } impl Cipher { /// Creates a new cipher from a master password and the salt #[inline] #[must_use] pub fn new(password: &[u8], salt: &[u8]) -> Self { let key = pbkdf2_hmac_array::(password, salt, 480_000); Self { chacha: ChaCha20Poly1305::new(&key.into()), } } /// Encrypts the value with the current cipher. The 12 byte nonce is appended to the result #[inline] #[allow(clippy::missing_panics_doc)] pub fn encrypt(&self, value: &mut Vec) { let nonce = ChaCha20Poly1305::generate_nonce(&mut OsRng); self.chacha.encrypt_in_place(&nonce, b"", value).unwrap(); value.extend_from_slice(&nonce); } /// Decrypts the value with the current cipher. The 12 byte nonce is expected to be at the end of the value /// /// # Errors /// /// Returns an error if the tag doesn't match the ciphertext #[inline] #[allow(clippy::missing_panics_doc)] pub fn decrypt(&self, value: &mut Vec) -> crate::Result<()> { if value.len() <= 12 { return Err(crate::Error::InvalidInputLength); } let nonce: [u8; 12] = value[value.len() - 12..].try_into().unwrap(); value.truncate(value.len() - 12); self.chacha .decrypt_in_place(nonce.as_slice().into(), b"", value) .map_err(Into::into) } } #[derive(serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq, Debug)] pub struct Decrypted { pub name: String, pub login: String, pub password: String, } impl Decrypted { /// Constructs `DecryptedAccount` by decrypting the provided account /// /// # Errors /// /// Returns an error if the tag doesn't match the ciphertext or if the decrypted data isn't valid UTF-8 #[inline] pub fn from_account(mut account: Account, master_pass: &str) -> crate::Result { let cipher = Cipher::new(master_pass.as_bytes(), &account.salt); cipher.decrypt(&mut account.enc_login)?; cipher.decrypt(&mut account.enc_password)?; Ok(Self { name: account.name, login: String::from_utf8(account.enc_login)?, password: String::from_utf8(account.enc_password)?, }) } /// Constructs `ActiveModel` with eath field Set by encrypting `self` #[inline] #[must_use] pub fn into_account(self, user_id: u64, master_pass: &str) -> Account { let mut enc_login = self.login.into_bytes(); let mut enc_password = self.password.into_bytes(); let mut salt = vec![0; 64]; OsRng.fill_bytes(&mut salt); let cipher = Cipher::new(master_pass.as_bytes(), &salt); cipher.encrypt(&mut enc_login); cipher.encrypt(&mut enc_password); Account { user_id, name: self.name, salt, enc_login, enc_password, } } /// Returns true if the account's fields are valid #[inline] #[must_use] pub fn validate(&self) -> bool { [ self.name.as_str(), self.login.as_str(), self.password.as_str(), ] .into_iter() .all(super::validate_field) } } #[cfg(test)] mod tests { use super::*; use once_cell::sync::Lazy; const TESTING_MASTER_PASSWORD: &str = "VeryStr^n#M@$terP@$$!word"; static CIPHER: Lazy = Lazy::new(|| { let mut salt = [0; 64]; OsRng.fill_bytes(&mut salt); Cipher::new(TESTING_MASTER_PASSWORD.as_bytes(), &salt) }); #[test] fn cipher_test() -> crate::Result<()> { const ORIGINAL: &[u8] = b"Data to protect"; let mut data = ORIGINAL.to_owned(); CIPHER.encrypt(&mut data); CIPHER.decrypt(&mut data)?; assert_eq!(ORIGINAL, data); Ok(()) } #[test] fn account_encryption() -> crate::Result<()> { let original = Decrypted { name: "Account Name".to_owned(), login: "StrongLogin@mail.com".to_owned(), password: "StrongP@$$word!".to_owned(), }; let account = original.clone().into_account(1, TESTING_MASTER_PASSWORD); let decrypted = Decrypted::from_account(account, TESTING_MASTER_PASSWORD)?; assert_eq!(original, decrypted); Ok(()) } #[test] fn decrypt_invalid_input_length() { let mut bytes = vec![0]; assert!(matches!( CIPHER.decrypt(&mut bytes), Err(crate::Error::InvalidInputLength) )); } }