Updated cryptography functions to be in-place
This commit is contained in:
		
							
								
								
									
										5
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										5
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							@@ -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",
 | 
			
		||||
 
 | 
			
		||||
@@ -24,3 +24,4 @@ bitflags = "2"
 | 
			
		||||
arrayvec = "0.7"
 | 
			
		||||
subtle = "2"
 | 
			
		||||
once_cell = "1"
 | 
			
		||||
serde = { version = "1", features = ["derive"] }
 | 
			
		||||
 
 | 
			
		||||
@@ -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<Vec<u8>> {
 | 
			
		||||
    #[allow(clippy::missing_panics_doc)]
 | 
			
		||||
    pub fn encrypt(&self, value: &mut Vec<u8>) {
 | 
			
		||||
        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<Vec<u8>> {
 | 
			
		||||
        let (data, nonce) = value.split_at(value.len() - 12);
 | 
			
		||||
    pub fn decrypt(&self, value: &mut Vec<u8>) -> 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<account::ActiveModel>;
 | 
			
		||||
#[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<Self> {
 | 
			
		||||
        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<Self> {
 | 
			
		||||
        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)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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),
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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,
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -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?;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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,
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
 
 | 
			
		||||
@@ -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),
 | 
			
		||||
        },
 | 
			
		||||
 
 | 
			
		||||
@@ -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<Self> {
 | 
			
		||||
        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> {
 | 
			
		||||
        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<DecryptedAccount>,
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										11
									
								
								src/utils.rs
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								src/utils.rs
									
									
									
									
									
								
							@@ -6,17 +6,6 @@ pub async fn delete_message(bot: Throttle<Bot>, 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,
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user