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