Sepparated the code out into 3 more library crates: cryptography, entity and pass_manager

This commit is contained in:
2023-06-01 14:42:35 +03:00
parent c40ffafd69
commit cda07b4d84
46 changed files with 360 additions and 268 deletions

18
cryptography/Cargo.toml Normal file
View 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"

View 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
View 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>;

View 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, &params, &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,
}
}
}

View 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() }
}

View File

@ -0,0 +1,2 @@
pub use super::account::*;
pub use super::master_pass::*;