diff --git a/Cargo.lock b/Cargo.lock index ad89314..3dc0e3a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -462,6 +462,7 @@ dependencies = [ "rand", "scrypt", "sea-orm", + "serde", "sha2", "subtle", "thiserror", @@ -1865,9 +1866,9 @@ dependencies = [ [[package]] name = "rust_decimal" -version = "1.33.0" +version = "1.33.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "076ba1058b036d3ca8bcafb1d54d0b0572e99d7ecd3e4222723e18ca8e9ca9a8" +checksum = "06676aec5ccb8fc1da723cc8c0f9a46549f21ebb8753d3915c6c41db1e7f1dc4" dependencies = [ "arrayvec", "borsh", diff --git a/cryptography/Cargo.toml b/cryptography/Cargo.toml index 41dce7f..e885c1c 100644 --- a/cryptography/Cargo.toml +++ b/cryptography/Cargo.toml @@ -24,3 +24,4 @@ bitflags = "2" arrayvec = "0.7" subtle = "2" once_cell = "1" +serde = { version = "1", features = ["derive"] } diff --git a/cryptography/src/account.rs b/cryptography/src/account.rs index bb81605..454284d 100644 --- a/cryptography/src/account.rs +++ b/cryptography/src/account.rs @@ -1,5 +1,5 @@ -use chacha20poly1305::{aead::Aead, AeadCore, ChaCha20Poly1305, KeyInit}; -use entity::account; +use chacha20poly1305::{AeadCore, AeadInPlace, ChaCha20Poly1305, KeyInit}; +use entity::account::{self, ActiveModel}; use pbkdf2::pbkdf2_hmac_array; use rand::{rngs::OsRng, RngCore}; use sea_orm::ActiveValue::Set; @@ -23,68 +23,89 @@ impl Cipher { /// Encrypts the value with the current cipher. The 12 byte nonce is appended to the result #[inline] - pub fn encrypt(&self, value: &[u8]) -> crate::Result> { + #[allow(clippy::missing_panics_doc)] + pub fn encrypt(&self, value: &mut Vec) { let nonce = ChaCha20Poly1305::generate_nonce(&mut OsRng); - let mut result = self.chacha.encrypt(&nonce, value)?; - result.extend(nonce); - Ok(result) + 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] - pub fn decrypt(&self, value: &[u8]) -> crate::Result> { - let (data, nonce) = value.split_at(value.len() - 12); + pub fn decrypt(&self, value: &mut Vec) -> crate::Result<()> { + let nonce: [u8; 12] = value[value.len() - 12..] + .try_into() + .map_err(|_| crate::Error::InvalidInputLength)?; + value.truncate(value.len() - 12); - self.chacha.decrypt(nonce.into(), data).map_err(Into::into) + self.chacha + .decrypt_in_place(nonce.as_slice().into(), b"", value) + .map_err(Into::into) } } -pub trait FromUnencryptedExt { - fn from_unencrypted( - user_id: u64, - name: String, - login: &str, - password: &str, - master_pass: &str, - ) -> crate::Result; +#[derive(serde::Serialize, serde::Deserialize)] +pub struct Decrypted { + pub name: String, + pub login: String, + pub password: String, } -impl FromUnencryptedExt for account::ActiveModel { - /// Encryptes the provided data by the master password and creates the `ActiveModel` with all fields set to Set variant +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] - fn from_unencrypted( - user_id: u64, - name: String, - login: &str, - password: &str, - master_pass: &str, - ) -> crate::Result { - let mut salt = vec![0; 64]; - OsRng.fill_bytes(&mut salt); - let cipher = Cipher::new(master_pass.as_bytes(), &salt); - let enc_login = Set(cipher.encrypt(login.as_bytes())?); - let enc_password = Set(cipher.encrypt(password.as_bytes())?); + pub fn from_account(mut account: account::Model, 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: Set(name), - user_id: Set(user_id), - salt: Set(salt), - enc_login, - enc_password, + name: account.name, + login: String::from_utf8(account.enc_login)?, + password: String::from_utf8(account.enc_password)?, }) } -} -pub trait DecryptAccountExt { - fn decrypt(&self, master_pass: &str) -> crate::Result<(String, String)>; -} - -impl DecryptAccountExt for account::Model { - /// Returns the decrypted login and password of the account + /// Constructs `ActiveModel` with eath field Set by encrypting `self` #[inline] - fn decrypt(&self, master_pass: &str) -> crate::Result<(String, String)> { - let cipher = Cipher::new(master_pass.as_bytes(), &self.salt); - let login = String::from_utf8(cipher.decrypt(&self.enc_login)?)?; - let password = String::from_utf8(cipher.decrypt(&self.enc_password)?)?; - Ok((login, password)) + #[must_use] + pub fn into_account(self, user_id: u64, master_pass: &str) -> account::ActiveModel { + let mut login = self.login.into_bytes(); + let mut 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 login); + cipher.encrypt(&mut password); + + ActiveModel { + user_id: Set(user_id), + name: Set(self.name), + salt: Set(salt), + enc_login: Set(login), + enc_password: Set(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) } } diff --git a/cryptography/src/lib.rs b/cryptography/src/lib.rs index 6846138..6bfffc1 100644 --- a/cryptography/src/lib.rs +++ b/cryptography/src/lib.rs @@ -1,5 +1,4 @@ //! Functions to encrypt the database models -#![allow(clippy::missing_errors_doc)] pub mod account; pub mod hashing; @@ -7,8 +6,23 @@ pub mod master_pass; pub mod passwords; pub mod prelude; +/// Returns true if the field is valid +#[inline] +#[must_use] +pub fn validate_field(field: &str) -> bool { + if !(1..255).contains(&field.len()) { + return false; + } + field + .chars() + .all(|char| !['`', '\\', '\n', '\t'].contains(&char)) +} + #[derive(thiserror::Error, Debug)] pub enum Error { + #[error("Invalid input length")] + InvalidInputLength, + #[error(transparent)] ChaChaError(#[from] chacha20poly1305::Error), diff --git a/cryptography/src/prelude.rs b/cryptography/src/prelude.rs index dde5bf6..ad9dacc 100644 --- a/cryptography/src/prelude.rs +++ b/cryptography/src/prelude.rs @@ -1,2 +1,3 @@ -pub use crate::account::{DecryptAccountExt as _, FromUnencryptedExt as _}; -pub use crate::master_pass::FromUnencryptedExt as _; +pub use crate::{ + account::Decrypted as DecryptedAccount, master_pass::FromUnencryptedExt as _, validate_field, +}; diff --git a/src/callbacks/alter.rs b/src/callbacks/alter.rs index 83f5e4b..ac3877c 100644 --- a/src/callbacks/alter.rs +++ b/src/callbacks/alter.rs @@ -25,7 +25,9 @@ async fn update_account( let field_value = spawn_blocking(move || { let cipher = Cipher::new(master_pass.as_bytes(), &salt); - cipher.encrypt(field_value.as_bytes()).unwrap() + let mut field = field_value.into_bytes(); + cipher.encrypt(&mut field); + field }) .await?; diff --git a/src/callbacks/decrypt.rs b/src/callbacks/decrypt.rs index 16c213e..45b8a69 100644 --- a/src/callbacks/decrypt.rs +++ b/src/callbacks/decrypt.rs @@ -23,8 +23,13 @@ async fn get_master_pass( return Ok(()); }; - let (login, password) = spawn_blocking(move || account.decrypt(&master_pass)).await??; - let text = format!("Name:\n`{name}`\nLogin:\n`{login}`\nPassword:\n`{password}`"); + let account = + spawn_blocking(move || DecryptedAccount::from_account(account, &master_pass)).await??; + + let text = format!( + "Name:\n`{name}`\nLogin:\n`{}`\nPassword:\n`{}`", + account.login, account.password + ); ids.alter_message( &bot, diff --git a/src/commands/add_account.rs b/src/commands/add_account.rs index 29ffa5d..fc07715 100644 --- a/src/commands/add_account.rs +++ b/src/commands/add_account.rs @@ -20,9 +20,14 @@ async fn get_master_pass( let user_id = msg.from().ok_or(NoUserInfo)?.id.0; let account = spawn_blocking(move || { - account::ActiveModel::from_unencrypted(user_id, name, &login, &password, &master_pass) + DecryptedAccount { + name, + login, + password, + } + .into_account(user_id, &master_pass) }) - .await??; + .await?; account.insert(&db).await?; ids.alter_message(&bot, "Success", deletion_markup(), None) diff --git a/src/commands/import.rs b/src/commands/import.rs index b57288b..6f05171 100644 --- a/src/commands/import.rs +++ b/src/commands/import.rs @@ -17,7 +17,7 @@ async fn encrypt_account( ) { let name = account.name.clone(); match spawn_blocking(move || account.into_account(user_id, &master_pass)).await { - Ok(Ok(account)) => match account.insert(db).await { + Ok(account) => match account.insert(db).await { Ok(_) => (), Err(_) => failed.lock().push(name), }, diff --git a/src/models.rs b/src/models.rs index 0edd51f..3806398 100644 --- a/src/models.rs +++ b/src/models.rs @@ -1,58 +1,8 @@ //! Models to export and import the accounts use crate::prelude::*; -use serde::{Deserialize, Serialize}; -#[derive(Serialize, Deserialize)] -pub struct DecryptedAccount { - pub name: String, - pub login: String, - pub password: String, -} - -impl DecryptedAccount { - /// Constructs `DecryptedAccount` by decrypting the provided account - #[inline] - pub fn from_account(account: account::Model, master_pass: &str) -> crate::Result { - let (login, password) = account.decrypt(master_pass)?; - Ok(Self { - name: account.name, - login, - password, - }) - } - - /// Constructs `ActiveModel` with eath field Set by encrypting `self` - #[inline] - pub fn into_account( - self, - user_id: u64, - master_pass: &str, - ) -> crate::Result { - account::ActiveModel::from_unencrypted( - user_id, - self.name, - &self.login, - &self.password, - master_pass, - ) - .map_err(Into::into) - } - - /// Returns true if the account's fields are valid - #[inline] - pub fn validate(&self) -> bool { - [ - self.name.as_str(), - self.login.as_str(), - self.password.as_str(), - ] - .into_iter() - .all(validate_field) - } -} - -#[derive(Serialize, Deserialize)] +#[derive(serde::Serialize, serde::Deserialize)] #[repr(transparent)] pub struct User { pub accounts: Vec, diff --git a/src/utils.rs b/src/utils.rs index 33b1093..1dd805f 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -6,17 +6,6 @@ pub async fn delete_message(bot: Throttle, msg: Message) { let _ = bot.delete_message(msg.chat.id, msg.id).await; } -/// Returns true if the field is valid -#[inline] -pub fn validate_field(field: &str) -> bool { - if !(1..255).contains(&field.len()) { - return false; - } - field - .chars() - .all(|char| !['`', '\\', '\n', '\t'].contains(&char)) -} - #[inline] pub async fn name_from_hash( db: &DatabaseConnection,