use chacha20poly1305::{aead::Aead, AeadCore, ChaCha20Poly1305, KeyInit}; use futures::{Stream, TryStreamExt}; use pbkdf2::pbkdf2_hmac_array; use rand::{rngs::OsRng, RngCore}; use sea_orm::{prelude::*, ActiveValue::Set, QueryOrder, QuerySelect}; use sha2::Sha256; #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] #[sea_orm(table_name = "account")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub user_id: u64, #[sea_orm(primary_key, auto_increment = false)] pub name: String, #[sea_orm(column_type = "Binary(BlobSize::Blob(Some(64)))")] pub salt: Vec, #[sea_orm(column_type = "VarBinary(256)")] pub enc_login: Vec, #[sea_orm(column_type = "VarBinary(256)")] pub enc_password: Vec, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] pub enum Relation {} impl ActiveModelBehavior for ActiveModel {} 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::(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> { 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> { let (data, nonce) = value.split_at(value.len() - 12); self.chacha.decrypt(nonce.into(), data).map_err(Into::into) } } impl ActiveModel { /// Encryptes the provided data by the master password and creates the ActiveModel with all fields set to Set variant #[inline] pub 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())?); Ok(Self { name: Set(name), user_id: Set(user_id), salt: Set(salt), enc_login, enc_password, }) } } impl Model { /// Returns the decrypted login and password of the account #[inline] pub 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)) } } impl Entity { /// Gets all user's account from DB #[inline] pub async fn get_all( user_id: u64, db: &DatabaseConnection, ) -> crate::Result> + '_> { let result = Self::find() .filter(Column::UserId.eq(user_id)) .stream(db) .await?; Ok(result.err_into()) } /// Gets a list of account names of a user #[inline] pub async fn get_names( user_id: u64, db: &DatabaseConnection, ordered: bool, ) -> crate::Result> + '_> { let mut select = Self::find() .select_only() .column(Column::Name) .filter(Column::UserId.eq(user_id)); if ordered { select = select.order_by_asc(Column::Name); } let result = select.into_tuple().stream(db).await?; Ok(result.err_into()) } /// Checks if the account exists #[inline] pub async fn exists( user_id: u64, account_name: impl Into, db: &DatabaseConnection, ) -> crate::Result { let result = Self::find_by_id((user_id, account_name.into())) .select_only() .column(Column::UserId) .into_tuple::() .one(db) .await?; Ok(result.is_some()) } /// Gets the account from the DB #[inline] pub async fn get( user_id: u64, account_name: impl Into, db: &DatabaseConnection, ) -> crate::Result> { Self::find_by_id((user_id, account_name.into())) .one(db) .await .map_err(Into::into) } /// Deletes all the user's accounts from DB #[inline] pub async fn delete_all(user_id: u64, db: &DatabaseConnection) -> crate::Result<()> { Self::delete_many() .filter(Column::UserId.eq(user_id)) .exec(db) .await?; Ok(()) } }