Sepparated the code out into 3 more library crates: cryptography, entity and pass_manager
This commit is contained in:
		
							
								
								
									
										18
									
								
								cryptography/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								cryptography/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,18 @@
 | 
			
		||||
[package]
 | 
			
		||||
name = "cryptography"
 | 
			
		||||
version = "0.1.0"
 | 
			
		||||
edition = "2021"
 | 
			
		||||
 | 
			
		||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 | 
			
		||||
 | 
			
		||||
[dependencies]
 | 
			
		||||
sha2 = "0.10.6"
 | 
			
		||||
scrypt = { version = "0.11.0", default-features = false, features = ["std"] }
 | 
			
		||||
pbkdf2 = "0.12.1"
 | 
			
		||||
thiserror = "1.0.40"
 | 
			
		||||
entity = { version = "0.1.0", path = "../entity" }
 | 
			
		||||
chacha20poly1305 = { version = "0.10.1", features = ["std"] }
 | 
			
		||||
rand = { version = "0.8.5", default-features = false, features = ["std_rng"] }
 | 
			
		||||
sea-orm = "0.11.3"
 | 
			
		||||
bitflags = "2.3.1"
 | 
			
		||||
arrayvec = "0.7.2"
 | 
			
		||||
							
								
								
									
										88
									
								
								cryptography/src/account.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								cryptography/src/account.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,88 @@
 | 
			
		||||
use chacha20poly1305::{aead::Aead, AeadCore, ChaCha20Poly1305, KeyInit};
 | 
			
		||||
use entity::account;
 | 
			
		||||
use pbkdf2::pbkdf2_hmac_array;
 | 
			
		||||
use rand::{rngs::OsRng, RngCore};
 | 
			
		||||
use sea_orm::ActiveValue::Set;
 | 
			
		||||
use sha2::Sha256;
 | 
			
		||||
 | 
			
		||||
struct Cipher {
 | 
			
		||||
    chacha: ChaCha20Poly1305,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Cipher {
 | 
			
		||||
    /// Creates a new cipher from a master password and the salt
 | 
			
		||||
    #[inline]
 | 
			
		||||
    fn new(password: &[u8], salt: &[u8]) -> Self {
 | 
			
		||||
        let key = pbkdf2_hmac_array::<Sha256, 32>(password, salt, 480000);
 | 
			
		||||
 | 
			
		||||
        Self {
 | 
			
		||||
            chacha: ChaCha20Poly1305::new(&key.into()),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// 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>> {
 | 
			
		||||
        let nonce = ChaCha20Poly1305::generate_nonce(&mut OsRng);
 | 
			
		||||
        let mut result = self.chacha.encrypt(&nonce, value)?;
 | 
			
		||||
        result.extend(nonce);
 | 
			
		||||
        Ok(result)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Decrypts the value with the current cipher. The 12 byte nonce is expected to be at the end of the value
 | 
			
		||||
    #[inline]
 | 
			
		||||
    fn decrypt(&self, value: &[u8]) -> crate::Result<Vec<u8>> {
 | 
			
		||||
        let (data, nonce) = value.split_at(value.len() - 12);
 | 
			
		||||
        self.chacha.decrypt(nonce.into(), data).map_err(Into::into)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub trait AccountFromUnencryptedExt {
 | 
			
		||||
    fn from_unencrypted(
 | 
			
		||||
        user_id: u64,
 | 
			
		||||
        name: String,
 | 
			
		||||
        login: &str,
 | 
			
		||||
        password: &str,
 | 
			
		||||
        master_pass: &str,
 | 
			
		||||
    ) -> crate::Result<account::ActiveModel>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl AccountFromUnencryptedExt for account::ActiveModel {
 | 
			
		||||
    /// Encryptes the provided data by the master password and creates the ActiveModel with all fields set to Set variant
 | 
			
		||||
    #[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())?);
 | 
			
		||||
        Ok(Self {
 | 
			
		||||
            name: Set(name),
 | 
			
		||||
            user_id: Set(user_id),
 | 
			
		||||
            salt: Set(salt),
 | 
			
		||||
            enc_login,
 | 
			
		||||
            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
 | 
			
		||||
    #[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))
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										17
									
								
								cryptography/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								cryptography/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,17 @@
 | 
			
		||||
//! Functions to encrypt the database models
 | 
			
		||||
 | 
			
		||||
pub mod account;
 | 
			
		||||
pub mod master_pass;
 | 
			
		||||
pub mod password_generation;
 | 
			
		||||
pub mod prelude;
 | 
			
		||||
 | 
			
		||||
#[derive(thiserror::Error, Debug)]
 | 
			
		||||
pub enum Error {
 | 
			
		||||
    #[error(transparent)]
 | 
			
		||||
    ChaChaError(#[from] chacha20poly1305::Error),
 | 
			
		||||
 | 
			
		||||
    #[error(transparent)]
 | 
			
		||||
    InvalidUTF8(#[from] std::string::FromUtf8Error),
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Result<T> = std::result::Result<T, Error>;
 | 
			
		||||
							
								
								
									
										45
									
								
								cryptography/src/master_pass.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								cryptography/src/master_pass.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,45 @@
 | 
			
		||||
use entity::master_pass;
 | 
			
		||||
use rand::{rngs::OsRng, RngCore};
 | 
			
		||||
use scrypt::{scrypt, Params};
 | 
			
		||||
use sea_orm::ActiveValue::Set;
 | 
			
		||||
 | 
			
		||||
/// Hashes the password with Scrypt with the given salt
 | 
			
		||||
#[inline]
 | 
			
		||||
fn hash_password(password: &[u8], salt: &[u8]) -> [u8; 64] {
 | 
			
		||||
    let params = Params::new(14, Params::RECOMMENDED_R, Params::RECOMMENDED_P, 64).unwrap();
 | 
			
		||||
    let mut password_hash = [0; 64];
 | 
			
		||||
    scrypt(password, salt, ¶ms, &mut password_hash).unwrap();
 | 
			
		||||
    password_hash
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub trait VerifyMasterPassExt {
 | 
			
		||||
    fn verify(&self, password: &str) -> bool;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl VerifyMasterPassExt for master_pass::Model {
 | 
			
		||||
    /// Checks that the given password hash matches the one of the model
 | 
			
		||||
    #[inline]
 | 
			
		||||
    fn verify(&self, password: &str) -> bool {
 | 
			
		||||
        let hashed = hash_password(password.as_bytes(), &self.salt);
 | 
			
		||||
        hashed == self.password_hash.as_slice()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub trait MasterPassFromUnencryptedExt {
 | 
			
		||||
    fn from_unencrypted(user_id: u64, password: &str) -> master_pass::ActiveModel;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl MasterPassFromUnencryptedExt for master_pass::ActiveModel {
 | 
			
		||||
    /// Hashes the password and creates an ActiveModel with all fields set to Set variant
 | 
			
		||||
    #[inline]
 | 
			
		||||
    fn from_unencrypted(user_id: u64, password: &str) -> Self {
 | 
			
		||||
        let mut salt = vec![0; 64];
 | 
			
		||||
        OsRng.fill_bytes(&mut salt);
 | 
			
		||||
        let password_hash = Set(hash_password(password.as_bytes(), &salt).to_vec());
 | 
			
		||||
        Self {
 | 
			
		||||
            user_id: Set(user_id),
 | 
			
		||||
            salt: Set(salt),
 | 
			
		||||
            password_hash,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										53
									
								
								cryptography/src/password_generation.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								cryptography/src/password_generation.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,53 @@
 | 
			
		||||
use arrayvec::{ArrayString, ArrayVec};
 | 
			
		||||
use rand::{rngs::OsRng, seq::SliceRandom};
 | 
			
		||||
use std::str::from_utf8_unchecked;
 | 
			
		||||
 | 
			
		||||
const CHARS: &[u8] = br##"!"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_abcdefghijklmnopqrstuvwxyz{|}~"##;
 | 
			
		||||
 | 
			
		||||
bitflags::bitflags! {
 | 
			
		||||
    struct PasswordFlags: u8 {
 | 
			
		||||
        const LOWERCASE = 0b0001;
 | 
			
		||||
        const UPPERCASE = 0b0010;
 | 
			
		||||
        const NUMBER = 0b0100;
 | 
			
		||||
        const SPECIAL_CHARACTER = 0b1000;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Returns true if the generated master password is valid.
 | 
			
		||||
/// It checks that it has at least one lowercase, one uppercase, one number and one punctuation char
 | 
			
		||||
#[inline]
 | 
			
		||||
fn check_generated_password(password: &[u8]) -> bool {
 | 
			
		||||
    let mut flags = PasswordFlags::empty();
 | 
			
		||||
    for &byte in password {
 | 
			
		||||
        match byte {
 | 
			
		||||
            b'a'..=b'z' => flags |= PasswordFlags::LOWERCASE,
 | 
			
		||||
            b'A'..=b'Z' => flags |= PasswordFlags::UPPERCASE,
 | 
			
		||||
            b'0'..=b'9' => flags |= PasswordFlags::NUMBER,
 | 
			
		||||
            b'!'..=b'/' | b':'..=b'@' | b'['..=b'`' | b'{'..=b'~' => {
 | 
			
		||||
                flags |= PasswordFlags::SPECIAL_CHARACTER
 | 
			
		||||
            }
 | 
			
		||||
            _ => (),
 | 
			
		||||
        }
 | 
			
		||||
        if flags.is_all() {
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Continuously generates the password until it passes the checks
 | 
			
		||||
#[inline]
 | 
			
		||||
pub fn generate_passwords() -> [ArrayString<32>; 10] {
 | 
			
		||||
    let mut passwords = ArrayVec::new_const();
 | 
			
		||||
    while !passwords.is_full() {
 | 
			
		||||
        let password: ArrayVec<u8, 32> = (0..32)
 | 
			
		||||
            .map(|_| *CHARS.choose(&mut OsRng).unwrap())
 | 
			
		||||
            .collect();
 | 
			
		||||
        if check_generated_password(&password) {
 | 
			
		||||
            let mut string = ArrayString::<32>::new_const();
 | 
			
		||||
            unsafe { string.push_str(from_utf8_unchecked(&password)) };
 | 
			
		||||
            passwords.push(string)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    unsafe { passwords.into_inner_unchecked() }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										2
									
								
								cryptography/src/prelude.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								cryptography/src/prelude.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,2 @@
 | 
			
		||||
pub use super::account::*;
 | 
			
		||||
pub use super::master_pass::*;
 | 
			
		||||
		Reference in New Issue
	
	Block a user