From cda07b4d845c19b61444135d210091e6ed14adcc Mon Sep 17 00:00:00 2001 From: StNicolay Date: Thu, 1 Jun 2023 14:42:35 +0300 Subject: [PATCH] Sepparated the code out into 3 more library crates: cryptography, entity and pass_manager --- .vscode/settings.json | 5 ++ Cargo.lock | 32 +++++-- Cargo.toml | 10 +-- cryptography/Cargo.toml | 18 ++++ cryptography/src/account.rs | 88 +++++++++++++++++++ cryptography/src/lib.rs | 17 ++++ cryptography/src/master_pass.rs | 45 ++++++++++ .../src/password_generation.rs | 26 +----- cryptography/src/prelude.rs | 2 + entity/Cargo.toml | 11 +++ {src/entity => entity/src}/account.rs | 82 ++--------------- entity/src/lib.rs | 7 ++ entity/src/master_pass.rs | 45 ++++++++++ {src/entity => entity/src}/prelude.rs | 2 +- .../callbacks/delete_message.rs | 2 +- src/{handlers => }/callbacks/mod.rs | 0 src/{handlers => }/commands/add_account.rs | 5 +- src/{handlers => }/commands/cancel.rs | 2 +- src/{handlers => }/commands/delete.rs | 10 +-- src/{handlers => }/commands/delete_all.rs | 11 +-- src/{handlers => }/commands/export.rs | 6 +- src/commands/gen_password.rs | 20 +++++ src/{handlers => }/commands/get_account.rs | 13 ++- src/{handlers => }/commands/get_accounts.rs | 5 +- src/{handlers => }/commands/help.rs | 2 +- src/{handlers => }/commands/import.rs | 4 +- src/{handlers => }/commands/mod.rs | 0 .../commands/set_master_pass.rs | 10 +-- src/{handlers => }/commands/start.rs | 0 src/{handlers => }/default.rs | 2 +- src/entity/master_pass.rs | 77 ---------------- src/entity/mod.rs | 5 -- src/{handlers/mod.rs => lib.rs} | 4 + src/main.rs | 10 +-- src/{handlers => }/markups.rs | 2 +- src/{handlers => }/master_password_check.rs | 5 +- src/models.rs | 4 +- src/{handlers => }/state/generic.rs | 8 +- src/{handlers => }/state/get_account_name.rs | 12 ++- src/{handlers => }/state/get_login.rs | 2 +- src/{handlers => }/state/get_master_pass.rs | 8 +- src/{handlers => }/state/get_password.rs | 2 +- src/{handlers => }/state/get_user.rs | 5 +- src/{handlers => }/state/handler.rs | 2 +- src/{handlers => }/state/mod.rs | 0 src/{handlers => }/utils.rs | 0 46 files changed, 360 insertions(+), 268 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 cryptography/Cargo.toml create mode 100644 cryptography/src/account.rs create mode 100644 cryptography/src/lib.rs create mode 100644 cryptography/src/master_pass.rs rename src/handlers/commands/gen_password.rs => cryptography/src/password_generation.rs (61%) create mode 100644 cryptography/src/prelude.rs create mode 100644 entity/Cargo.toml rename {src/entity => entity/src}/account.rs (50%) create mode 100644 entity/src/lib.rs create mode 100644 entity/src/master_pass.rs rename {src/entity => entity/src}/prelude.rs (65%) rename src/{handlers => }/callbacks/delete_message.rs (94%) rename src/{handlers => }/callbacks/mod.rs (100%) rename src/{handlers => }/commands/add_account.rs (95%) rename src/{handlers => }/commands/cancel.rs (86%) rename src/{handlers => }/commands/delete.rs (92%) rename src/{handlers => }/commands/delete_all.rs (85%) rename src/{handlers => }/commands/export.rs (94%) create mode 100644 src/commands/gen_password.rs rename src/{handlers => }/commands/get_account.rs (92%) rename src/{handlers => }/commands/get_accounts.rs (90%) rename src/{handlers => }/commands/help.rs (86%) rename src/{handlers => }/commands/import.rs (96%) rename src/{handlers => }/commands/mod.rs (100%) rename src/{handlers => }/commands/set_master_pass.rs (89%) rename src/{handlers => }/commands/start.rs (100%) rename src/{handlers => }/default.rs (89%) delete mode 100644 src/entity/master_pass.rs delete mode 100644 src/entity/mod.rs rename src/{handlers/mod.rs => lib.rs} (96%) rename src/{handlers => }/markups.rs (96%) rename src/{handlers => }/master_password_check.rs (94%) rename src/{handlers => }/state/generic.rs (90%) rename src/{handlers => }/state/get_account_name.rs (93%) rename src/{handlers => }/state/get_login.rs (93%) rename src/{handlers => }/state/get_master_pass.rs (92%) rename src/{handlers => }/state/get_password.rs (93%) rename src/{handlers => }/state/get_user.rs (95%) rename src/{handlers => }/state/handler.rs (97%) rename src/{handlers => }/state/mod.rs (100%) rename src/{handlers => }/utils.rs (100%) diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..352a626 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "rust-analyzer.linkedProjects": [ + "./Cargo.toml" + ] +} \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 552b2c5..f0b8844 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -460,6 +460,22 @@ dependencies = [ "typenum", ] +[[package]] +name = "cryptography" +version = "0.1.0" +dependencies = [ + "arrayvec", + "bitflags 2.3.1", + "chacha20poly1305", + "entity", + "pbkdf2", + "rand", + "scrypt", + "sea-orm", + "sha2", + "thiserror", +] + [[package]] name = "darling" version = "0.13.4" @@ -566,6 +582,14 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "entity" +version = "0.1.0" +dependencies = [ + "futures", + "sea-orm", +] + [[package]] name = "env_logger" version = "0.10.0" @@ -1314,21 +1338,17 @@ version = "0.1.0" dependencies = [ "anyhow", "arrayvec", - "bitflags 2.3.1", - "chacha20poly1305", + "cryptography", "dotenv", + "entity", "futures", "itertools 0.10.5", "log", "migration", - "pbkdf2", "pretty_env_logger", - "rand", - "scrypt", "sea-orm", "serde", "serde_json", - "sha2", "teloxide", "thiserror", "tokio", diff --git a/Cargo.toml b/Cargo.toml index a194dfd..3b806c2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,26 +9,22 @@ edition = "2021" strip = true [workspace] -members = [".", "migration"] +members = [".", "migration", "entity", "cryptography"] [dependencies] anyhow = "1.0.71" arrayvec = "0.7.2" -bitflags = "2.3.1" -chacha20poly1305 = { version = "0.10.1", features = ["std"] } +cryptography = { version = "0.1.0", path = "cryptography" } dotenv = "0.15.0" +entity = { version = "0.1.0", path = "entity" } futures = "0.3.28" itertools = "0.10.5" log = "0.4.17" migration = { version = "0.2.0", path = "migration" } -pbkdf2 = "0.12.1" pretty_env_logger = "0.5.0" -rand = { version = "0.8.5", default-features = false, features = ["std_rng"] } -scrypt = { version = "0.11.0", default-features = false, features = ["std"] } sea-orm = { version = "0.11.3", features = ["sqlx-mysql", "runtime-tokio-rustls"] } serde = "1.0.163" serde_json = "1.0.96" -sha2 = "0.10.6" teloxide = { version = "0.12.2", features = ["macros", "ctrlc_handler", "rustls", "throttle"], default-features = false } thiserror = "1.0.40" tokio = { version = "1.28.1", features = ["macros", "rt-multi-thread"] } diff --git a/cryptography/Cargo.toml b/cryptography/Cargo.toml new file mode 100644 index 0000000..a02ea15 --- /dev/null +++ b/cryptography/Cargo.toml @@ -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" diff --git a/cryptography/src/account.rs b/cryptography/src/account.rs new file mode 100644 index 0000000..628a484 --- /dev/null +++ b/cryptography/src/account.rs @@ -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::(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) + } +} + +pub trait AccountFromUnencryptedExt { + fn from_unencrypted( + user_id: u64, + name: String, + login: &str, + password: &str, + master_pass: &str, + ) -> crate::Result; +} + +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 { + 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)) + } +} diff --git a/cryptography/src/lib.rs b/cryptography/src/lib.rs new file mode 100644 index 0000000..db8f12f --- /dev/null +++ b/cryptography/src/lib.rs @@ -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 = std::result::Result; diff --git a/cryptography/src/master_pass.rs b/cryptography/src/master_pass.rs new file mode 100644 index 0000000..6943b3c --- /dev/null +++ b/cryptography/src/master_pass.rs @@ -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, + } + } +} diff --git a/src/handlers/commands/gen_password.rs b/cryptography/src/password_generation.rs similarity index 61% rename from src/handlers/commands/gen_password.rs rename to cryptography/src/password_generation.rs index 8336be7..8c84f54 100644 --- a/src/handlers/commands/gen_password.rs +++ b/cryptography/src/password_generation.rs @@ -1,14 +1,10 @@ -use crate::handlers::markups::deletion_markup; use arrayvec::{ArrayString, ArrayVec}; use rand::{rngs::OsRng, seq::SliceRandom}; -use std::{fmt::Write, str::from_utf8_unchecked}; -use teloxide::{adaptors::Throttle, prelude::*, types::ParseMode}; -use tokio::task::spawn_blocking; +use std::str::from_utf8_unchecked; const CHARS: &[u8] = br##"!"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_abcdefghijklmnopqrstuvwxyz{|}~"##; bitflags::bitflags! { - #[derive(PartialEq)] struct PasswordFlags: u8 { const LOWERCASE = 0b0001; const UPPERCASE = 0b0010; @@ -18,7 +14,7 @@ bitflags::bitflags! { } /// Returns true if the generated master password is valid. -/// It checks that it has at least one lowercase, one lowercase and one punctuation char +/// 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(); @@ -32,7 +28,7 @@ fn check_generated_password(password: &[u8]) -> bool { } _ => (), } - if flags == PasswordFlags::all() { + if flags.is_all() { return true; } } @@ -41,7 +37,7 @@ fn check_generated_password(password: &[u8]) -> bool { /// Continuously generates the password until it passes the checks #[inline] -fn generate_passwords() -> [ArrayString<32>; 10] { +pub fn generate_passwords() -> [ArrayString<32>; 10] { let mut passwords = ArrayVec::new_const(); while !passwords.is_full() { let password: ArrayVec = (0..32) @@ -55,17 +51,3 @@ fn generate_passwords() -> [ArrayString<32>; 10] { } unsafe { passwords.into_inner_unchecked() } } - -/// Handles /gen_password command by generating 10 copyable passwords and sending them to the user -pub async fn gen_password(bot: Throttle, msg: Message) -> crate::Result<()> { - let mut message: ArrayString<{ 10 + 35 * 10 }> = "Passwords:".try_into().unwrap(); - let passwords = spawn_blocking(generate_passwords).await?; - for password in passwords { - write!(message, "\n`{password}`").unwrap(); - } - bot.send_message(msg.chat.id, message.as_str()) - .parse_mode(ParseMode::MarkdownV2) - .reply_markup(deletion_markup()) - .await?; - Ok(()) -} diff --git a/cryptography/src/prelude.rs b/cryptography/src/prelude.rs new file mode 100644 index 0000000..fe1020f --- /dev/null +++ b/cryptography/src/prelude.rs @@ -0,0 +1,2 @@ +pub use super::account::*; +pub use super::master_pass::*; diff --git a/entity/Cargo.toml b/entity/Cargo.toml new file mode 100644 index 0000000..5ea9eb2 --- /dev/null +++ b/entity/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "entity" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +futures = "0.3.28" +sea-orm = "0.11.3" + diff --git a/src/entity/account.rs b/entity/src/account.rs similarity index 50% rename from src/entity/account.rs rename to entity/src/account.rs index 7dc5505..eecfdc1 100644 --- a/src/entity/account.rs +++ b/entity/src/account.rs @@ -1,9 +1,7 @@ -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; +//! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.3 + +use futures::Stream; +use sea_orm::{entity::prelude::*, QueryOrder, QuerySelect}; #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] #[sea_orm(table_name = "account")] @@ -25,74 +23,6 @@ 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] @@ -104,7 +34,7 @@ impl Entity { .filter(Column::UserId.eq(user_id)) .stream(db) .await?; - Ok(result.err_into()) + Ok(result) } /// Gets a list of account names of a user @@ -122,7 +52,7 @@ impl Entity { select = select.order_by_asc(Column::Name); } let result = select.into_tuple().stream(db).await?; - Ok(result.err_into()) + Ok(result) } /// Checks if the account exists diff --git a/entity/src/lib.rs b/entity/src/lib.rs new file mode 100644 index 0000000..e40efc2 --- /dev/null +++ b/entity/src/lib.rs @@ -0,0 +1,7 @@ +pub mod account; +pub mod master_pass; +pub mod prelude; + +use sea_orm::DbErr; + +type Result = std::result::Result; diff --git a/entity/src/master_pass.rs b/entity/src/master_pass.rs new file mode 100644 index 0000000..c853353 --- /dev/null +++ b/entity/src/master_pass.rs @@ -0,0 +1,45 @@ +//! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.3 + +use sea_orm::{entity::prelude::*, QuerySelect}; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[sea_orm(table_name = "master_pass")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub user_id: u64, + #[sea_orm(column_type = "Binary(BlobSize::Blob(Some(64)))")] + pub salt: Vec, + #[sea_orm(column_type = "Binary(BlobSize::Blob(Some(64)))")] + pub password_hash: Vec, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation {} + +impl ActiveModelBehavior for ActiveModel {} + +impl Entity { + /// Gets the master password from the database + #[inline] + pub async fn get(user_id: u64, db: &DatabaseConnection) -> crate::Result> { + Self::find_by_id(user_id).one(db).await + } + + /// Checks if the master password for the user exists + #[inline] + pub async fn exists(user_id: u64, db: &DatabaseConnection) -> Result { + let id = Self::find_by_id(user_id) + .select_only() + .column(Column::UserId) + .into_tuple::() + .one(db) + .await?; + Ok(id.is_some()) + } + + /// Removes a master password of the user from the database + pub async fn remove(user_id: u64, db: &DatabaseConnection) -> Result<(), DbErr> { + Self::delete_by_id(user_id).exec(db).await?; + Ok(()) + } +} diff --git a/src/entity/prelude.rs b/entity/src/prelude.rs similarity index 65% rename from src/entity/prelude.rs rename to entity/src/prelude.rs index d18dff6..ef066e2 100644 --- a/src/entity/prelude.rs +++ b/entity/src/prelude.rs @@ -1,4 +1,4 @@ -//! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.2 +//! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.3 pub use super::account::{self, Entity as Account}; pub use super::master_pass::{self, Entity as MasterPass}; diff --git a/src/handlers/callbacks/delete_message.rs b/src/callbacks/delete_message.rs similarity index 94% rename from src/handlers/callbacks/delete_message.rs rename to src/callbacks/delete_message.rs index 9f80183..fc01d4d 100644 --- a/src/handlers/callbacks/delete_message.rs +++ b/src/callbacks/delete_message.rs @@ -1,4 +1,4 @@ -use crate::handlers::markups::deletion_markup; +use crate::markups::deletion_markup; use teloxide::{adaptors::Throttle, dispatching::DpHandlerDescription, prelude::*}; /// Deletes the message from the callback diff --git a/src/handlers/callbacks/mod.rs b/src/callbacks/mod.rs similarity index 100% rename from src/handlers/callbacks/mod.rs rename to src/callbacks/mod.rs diff --git a/src/handlers/commands/add_account.rs b/src/commands/add_account.rs similarity index 95% rename from src/handlers/commands/add_account.rs rename to src/commands/add_account.rs index e1d778c..20abc25 100644 --- a/src/handlers/commands/add_account.rs +++ b/src/commands/add_account.rs @@ -1,7 +1,6 @@ use crate::{ - errors::NoUserInfo, - handlers::{markups::deletion_markup, state::NameCheckKind, Handler, MainDialogue, State}, - models::DecryptedAccount, + errors::NoUserInfo, markups::deletion_markup, models::DecryptedAccount, state::NameCheckKind, + Handler, MainDialogue, State, }; use sea_orm::prelude::*; use teloxide::{adaptors::Throttle, prelude::*}; diff --git a/src/handlers/commands/cancel.rs b/src/commands/cancel.rs similarity index 86% rename from src/handlers/commands/cancel.rs rename to src/commands/cancel.rs index f73de99..8ed2a22 100644 --- a/src/handlers/commands/cancel.rs +++ b/src/commands/cancel.rs @@ -1,4 +1,4 @@ -use crate::handlers::markups::deletion_markup; +use crate::markups::deletion_markup; use teloxide::{adaptors::Throttle, prelude::*}; /// Handles /cancel command when there's no active state diff --git a/src/handlers/commands/delete.rs b/src/commands/delete.rs similarity index 92% rename from src/handlers/commands/delete.rs rename to src/commands/delete.rs index e525461..8912158 100644 --- a/src/handlers/commands/delete.rs +++ b/src/commands/delete.rs @@ -1,12 +1,10 @@ use crate::{ - entity::prelude::Account, errors::NoUserInfo, - handlers::{ - markups::{self, deletion_markup}, - state::NameCheckKind, - Handler, MainDialogue, State, - }, + markups::{self, deletion_markup}, + state::NameCheckKind, + Handler, MainDialogue, State, }; +use entity::prelude::*; use sea_orm::prelude::*; use teloxide::{adaptors::Throttle, prelude::*}; diff --git a/src/handlers/commands/delete_all.rs b/src/commands/delete_all.rs similarity index 85% rename from src/handlers/commands/delete_all.rs rename to src/commands/delete_all.rs index 2cd3d5a..f3cf43a 100644 --- a/src/handlers/commands/delete_all.rs +++ b/src/commands/delete_all.rs @@ -1,9 +1,6 @@ -use crate::{ - entity::prelude::*, - errors::NoUserInfo, - handlers::{markups::deletion_markup, Handler, MainDialogue, State}, -}; -use sea_orm::prelude::*; +use crate::{errors::NoUserInfo, markups::deletion_markup, Handler, MainDialogue, State}; +use entity::prelude::*; +use sea_orm::DatabaseConnection; use teloxide::{adaptors::Throttle, prelude::*}; use tokio::join; @@ -23,7 +20,7 @@ async fn get_master_pass( MasterPass::remove(user_id, &db), ) { (Ok(_), Ok(_)) => (), - (Err(err), _) | (Ok(_), Err(err)) => return Err(err), + (Err(err), _) | (Ok(_), Err(err)) => return Err(err.into()), }; bot.send_message(msg.chat.id, "Everything was deleted") .reply_markup(deletion_markup()) diff --git a/src/handlers/commands/export.rs b/src/commands/export.rs similarity index 94% rename from src/handlers/commands/export.rs rename to src/commands/export.rs index a1422d9..24201a1 100644 --- a/src/handlers/commands/export.rs +++ b/src/commands/export.rs @@ -1,9 +1,10 @@ use crate::{ - entity::prelude::Account, errors::NoUserInfo, - handlers::{markups::deletion_markup, Handler, MainDialogue, State}, + markups::deletion_markup, models::{DecryptedAccount, User}, + Handler, MainDialogue, State, }; +use entity::prelude::*; use futures::TryStreamExt; use sea_orm::DatabaseConnection; use serde_json::to_vec_pretty; @@ -26,6 +27,7 @@ async fn get_master_pass( let master_pass: Arc = master_pass.into(); Account::get_all(user_id, &db) .await? + .err_into::() .try_for_each_concurrent(None, |account| { let master_pass = Arc::clone(&master_pass); async move { diff --git a/src/commands/gen_password.rs b/src/commands/gen_password.rs new file mode 100644 index 0000000..1fc320d --- /dev/null +++ b/src/commands/gen_password.rs @@ -0,0 +1,20 @@ +use crate::markups::deletion_markup; +use arrayvec::ArrayString; +use cryptography::password_generation::generate_passwords; +use std::fmt::Write; +use teloxide::{adaptors::Throttle, prelude::*, types::ParseMode}; +use tokio::task::spawn_blocking; + +/// Handles /gen_password command by generating 10 copyable passwords and sending them to the user +pub async fn gen_password(bot: Throttle, msg: Message) -> crate::Result<()> { + let mut message: ArrayString<{ 10 + 35 * 10 }> = "Passwords:".try_into().unwrap(); + let passwords = spawn_blocking(generate_passwords).await?; + for password in passwords { + write!(message, "\n`{password}`").unwrap(); + } + bot.send_message(msg.chat.id, message.as_str()) + .parse_mode(ParseMode::MarkdownV2) + .reply_markup(deletion_markup()) + .await?; + Ok(()) +} diff --git a/src/handlers/commands/get_account.rs b/src/commands/get_account.rs similarity index 92% rename from src/handlers/commands/get_account.rs rename to src/commands/get_account.rs index e6b4d36..2431cf4 100644 --- a/src/handlers/commands/get_account.rs +++ b/src/commands/get_account.rs @@ -1,13 +1,12 @@ use crate::{ - entity::prelude::Account, errors::NoUserInfo, - handlers::{ - markups::{self, deletion_markup}, - state::NameCheckKind, - Handler, MainDialogue, State, - }, + markups::{self, deletion_markup}, + state::NameCheckKind, + Handler, MainDialogue, State, }; -use sea_orm::prelude::*; +use cryptography::prelude::*; +use entity::prelude::*; +use sea_orm::DatabaseConnection; use teloxide::{adaptors::Throttle, prelude::*, types::ParseMode}; use tokio::task::spawn_blocking; diff --git a/src/handlers/commands/get_accounts.rs b/src/commands/get_accounts.rs similarity index 90% rename from src/handlers/commands/get_accounts.rs rename to src/commands/get_accounts.rs index bb27924..11392cd 100644 --- a/src/handlers/commands/get_accounts.rs +++ b/src/commands/get_accounts.rs @@ -1,6 +1,7 @@ -use crate::{entity::prelude::Account, errors::NoUserInfo, handlers::markups::deletion_markup}; +use crate::{errors::NoUserInfo, markups::deletion_markup}; +use entity::prelude::*; use futures::TryStreamExt; -use sea_orm::prelude::*; +use sea_orm::DatabaseConnection; use std::fmt::Write; use teloxide::{adaptors::Throttle, prelude::*, types::ParseMode}; diff --git a/src/handlers/commands/help.rs b/src/commands/help.rs similarity index 86% rename from src/handlers/commands/help.rs rename to src/commands/help.rs index 90d3f46..8658cde 100644 --- a/src/handlers/commands/help.rs +++ b/src/commands/help.rs @@ -1,4 +1,4 @@ -use crate::handlers::{markups::deletion_markup, Command}; +use crate::{markups::deletion_markup, Command}; use teloxide::{adaptors::Throttle, prelude::*, utils::command::BotCommands}; /// Handles the help command by sending the passwords descryptions diff --git a/src/handlers/commands/import.rs b/src/commands/import.rs similarity index 96% rename from src/handlers/commands/import.rs rename to src/commands/import.rs index a3e698e..b49600d 100644 --- a/src/handlers/commands/import.rs +++ b/src/commands/import.rs @@ -1,7 +1,5 @@ use crate::{ - errors::NoUserInfo, - handlers::{markups::deletion_markup, Handler, MainDialogue, State}, - models::User, + errors::NoUserInfo, markups::deletion_markup, models::User, Handler, MainDialogue, State, }; use futures::{future, stream::FuturesUnordered, StreamExt}; use itertools::Itertools; diff --git a/src/handlers/commands/mod.rs b/src/commands/mod.rs similarity index 100% rename from src/handlers/commands/mod.rs rename to src/commands/mod.rs diff --git a/src/handlers/commands/set_master_pass.rs b/src/commands/set_master_pass.rs similarity index 89% rename from src/handlers/commands/set_master_pass.rs rename to src/commands/set_master_pass.rs index 9d553d3..ad8ebba 100644 --- a/src/handlers/commands/set_master_pass.rs +++ b/src/commands/set_master_pass.rs @@ -1,8 +1,6 @@ -use crate::{ - entity::prelude::*, - errors::NoUserInfo, - handlers::{markups::deletion_markup, Handler, MainDialogue, State}, -}; +use crate::{errors::NoUserInfo, markups::deletion_markup, Handler, MainDialogue, State}; +use cryptography::prelude::*; +use entity::prelude::*; use sea_orm::prelude::*; use teloxide::{adaptors::Throttle, prelude::*}; use tokio::task; @@ -20,7 +18,7 @@ async fn get_master_pass( let model = task::spawn_blocking(move || { master_pass::ActiveModel::from_unencrypted(user_id, &master_password) }) - .await??; + .await?; model.insert(&db).await?; bot.send_message(msg.chat.id, "Success") .reply_markup(deletion_markup()) diff --git a/src/handlers/commands/start.rs b/src/commands/start.rs similarity index 100% rename from src/handlers/commands/start.rs rename to src/commands/start.rs diff --git a/src/handlers/default.rs b/src/default.rs similarity index 89% rename from src/handlers/default.rs rename to src/default.rs index e6ab59d..4b836b3 100644 --- a/src/handlers/default.rs +++ b/src/default.rs @@ -1,4 +1,4 @@ -use crate::handlers::markups::deletion_markup; +use crate::markups::deletion_markup; use teloxide::{adaptors::Throttle, prelude::*}; /// Handles the messages which weren't matched by any commands or states diff --git a/src/entity/master_pass.rs b/src/entity/master_pass.rs deleted file mode 100644 index 7f9328c..0000000 --- a/src/entity/master_pass.rs +++ /dev/null @@ -1,77 +0,0 @@ -use rand::{rngs::OsRng, RngCore}; -use scrypt::{scrypt, Params}; -use sea_orm::{entity::prelude::*, ActiveValue::Set, QuerySelect}; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] -#[sea_orm(table_name = "master_pass")] -pub struct Model { - #[sea_orm(primary_key, auto_increment = false)] - pub user_id: u64, - #[sea_orm(column_type = "Binary(BlobSize::Blob(Some(64)))")] - pub salt: Vec, - #[sea_orm(column_type = "Binary(BlobSize::Blob(Some(64)))")] - pub password_hash: Vec, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} - -/// Hashes the password with Scrypt with the given salt -#[inline] -fn hash_password(password: &[u8], salt: &[u8]) -> crate::Result<[u8; 64]> { - let params = Params::new(14, Params::RECOMMENDED_R, Params::RECOMMENDED_P, 64)?; - let mut password_hash = [0; 64]; - scrypt(password, salt, ¶ms, &mut password_hash)?; - Ok(password_hash) -} - -impl Model { - /// Checks that the given password hash matches the one of the model - pub fn verify(&self, password: &str) -> crate::Result { - let hashed = hash_password(password.as_bytes(), &self.salt)?; - Ok(hashed == self.password_hash.as_slice()) - } -} - -impl ActiveModel { - /// Hashes the password and creates an ActiveModel with all fields set to Set variant - #[inline] - pub fn from_unencrypted(user_id: u64, password: &str) -> crate::Result { - let mut salt = vec![0; 64]; - OsRng.fill_bytes(&mut salt); - let password_hash = Set(hash_password(password.as_bytes(), &salt)?.to_vec()); - Ok(Self { - user_id: Set(user_id), - salt: Set(salt), - password_hash, - }) - } -} - -impl Entity { - /// Gets the master password from the database - #[inline] - pub async fn get(user_id: u64, db: &DatabaseConnection) -> crate::Result> { - Self::find_by_id(user_id).one(db).await.map_err(Into::into) - } - - /// Checks if the master password for the user exists - #[inline] - pub async fn exists(user_id: u64, db: &DatabaseConnection) -> crate::Result { - let id = Self::find_by_id(user_id) - .select_only() - .column(Column::UserId) - .into_tuple::() - .one(db) - .await?; - Ok(id.is_some()) - } - - /// Removes a master password of the user from the database - pub async fn remove(user_id: u64, db: &DatabaseConnection) -> crate::Result<()> { - Self::delete_by_id(user_id).exec(db).await?; - Ok(()) - } -} diff --git a/src/entity/mod.rs b/src/entity/mod.rs deleted file mode 100644 index 97ca813..0000000 --- a/src/entity/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -//! Entities to work with the database - -pub mod account; -pub mod master_pass; -pub mod prelude; diff --git a/src/handlers/mod.rs b/src/lib.rs similarity index 96% rename from src/handlers/mod.rs rename to src/lib.rs index 81e161d..70adbca 100644 --- a/src/handlers/mod.rs +++ b/src/lib.rs @@ -1,12 +1,16 @@ mod callbacks; mod commands; mod default; +mod errors; mod markups; mod master_password_check; +mod models; mod state; mod utils; +use anyhow::{Error, Result}; use commands::Command; +use futures::future::BoxFuture as PinnedFuture; use sea_orm::prelude::*; use state::{Handler, MainDialogue, State}; use teloxide::{ diff --git a/src/main.rs b/src/main.rs index dc36696..e25eb3b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,13 +1,7 @@ -mod entity; -mod errors; -mod handlers; -mod models; - -use anyhow::{Error, Result}; +use anyhow::Result; use dotenv::dotenv; -use futures::future::BoxFuture as PinnedFuture; -use handlers::get_dispatcher; use migration::{Migrator, MigratorTrait}; +use pass_manager::get_dispatcher; use sea_orm::Database; use std::env; diff --git a/src/handlers/markups.rs b/src/markups.rs similarity index 96% rename from src/handlers/markups.rs rename to src/markups.rs index fe42320..a77d571 100644 --- a/src/handlers/markups.rs +++ b/src/markups.rs @@ -1,4 +1,4 @@ -use crate::entity::prelude::Account; +use entity::prelude::Account; use futures::TryStreamExt; use sea_orm::prelude::*; use teloxide::types::{InlineKeyboardButton, InlineKeyboardMarkup, KeyboardButton, KeyboardMarkup}; diff --git a/src/handlers/master_password_check.rs b/src/master_password_check.rs similarity index 94% rename from src/handlers/master_password_check.rs rename to src/master_password_check.rs index 252de57..267b65a 100644 --- a/src/handlers/master_password_check.rs +++ b/src/master_password_check.rs @@ -1,5 +1,6 @@ -use crate::{entity::prelude::MasterPass, errors::NoUserInfo}; -use sea_orm::prelude::*; +use crate::errors::NoUserInfo; +use entity::prelude::*; +use sea_orm::DatabaseConnection; use teloxide::{adaptors::Throttle, dispatching::DpHandlerDescription, prelude::*}; use super::markups::deletion_markup; diff --git a/src/models.rs b/src/models.rs index 9113418..0b53b98 100644 --- a/src/models.rs +++ b/src/models.rs @@ -1,6 +1,7 @@ //! Models to export and import the accounts -use crate::entity::prelude::*; +use cryptography::prelude::*; +use entity::prelude::*; use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize)] @@ -36,6 +37,7 @@ impl DecryptedAccount { &self.password, master_pass, ) + .map_err(Into::into) } /// Returns true if the account's fields are valid diff --git a/src/handlers/state/generic.rs b/src/state/generic.rs similarity index 90% rename from src/handlers/state/generic.rs rename to src/state/generic.rs index 8940809..ebd062c 100644 --- a/src/handlers/state/generic.rs +++ b/src/state/generic.rs @@ -1,8 +1,4 @@ -use crate::{ - errors::HandlerUsed, - handlers::{markups::deletion_markup, utils::delete_optional}, - PinnedFuture, -}; +use crate::{errors::HandlerUsed, markups::deletion_markup, utils::delete_optional, PinnedFuture}; use sea_orm::prelude::*; use teloxide::{adaptors::Throttle, prelude::*}; @@ -12,7 +8,7 @@ pub async fn generic( bot: Throttle, msg: Message, db: DatabaseConnection, - dialogue: crate::handlers::MainDialogue, + dialogue: crate::MainDialogue, check: F, no_text_message: impl Into, next: super::PackagedHandler, diff --git a/src/handlers/state/get_account_name.rs b/src/state/get_account_name.rs similarity index 93% rename from src/handlers/state/get_account_name.rs rename to src/state/get_account_name.rs index 360fafb..23e7d1c 100644 --- a/src/handlers/state/get_account_name.rs +++ b/src/state/get_account_name.rs @@ -1,13 +1,11 @@ use crate::{ - entity::prelude::Account, errors::{HandlerUsed, NoUserInfo}, - handlers::{ - markups::{account_markup, deletion_markup}, - utils::{delete_optional, validate_field}, - MainDialogue, - }, + markups::{account_markup, deletion_markup}, + utils::{delete_optional, validate_field}, + MainDialogue, }; -use sea_orm::prelude::*; +use entity::prelude::*; +use sea_orm::DatabaseConnection; use teloxide::{adaptors::Throttle, prelude::*}; /// Specifies the kind of checks to be run during the account name validation diff --git a/src/handlers/state/get_login.rs b/src/state/get_login.rs similarity index 93% rename from src/handlers/state/get_login.rs rename to src/state/get_login.rs index b5d8a85..b1dbfdb 100644 --- a/src/handlers/state/get_login.rs +++ b/src/state/get_login.rs @@ -1,4 +1,4 @@ -use crate::handlers::{utils::validate_field, MainDialogue}; +use crate::{utils::validate_field, MainDialogue}; use sea_orm::prelude::*; use teloxide::{adaptors::Throttle, prelude::*}; diff --git a/src/handlers/state/get_master_pass.rs b/src/state/get_master_pass.rs similarity index 92% rename from src/handlers/state/get_master_pass.rs rename to src/state/get_master_pass.rs index dd620de..65a099f 100644 --- a/src/handlers/state/get_master_pass.rs +++ b/src/state/get_master_pass.rs @@ -1,6 +1,8 @@ -use crate::{entity::prelude::MasterPass, errors::NoUserInfo, handlers::MainDialogue}; +use crate::{errors::NoUserInfo, MainDialogue}; +use cryptography::prelude::*; +use entity::prelude::*; use log::error; -use sea_orm::prelude::*; +use sea_orm::DatabaseConnection; use teloxide::{adaptors::Throttle, prelude::*}; use tokio::task::spawn_blocking; @@ -18,7 +20,7 @@ async fn check_master_pass( let is_valid = match model { Some(model) => { let master_pass = master_pass.to_owned(); - spawn_blocking(move || model.verify(&master_pass)).await?? + spawn_blocking(move || model.verify(&master_pass)).await? } None => { error!("User was put into the GetMasterPass state with no master password set"); diff --git a/src/handlers/state/get_password.rs b/src/state/get_password.rs similarity index 93% rename from src/handlers/state/get_password.rs rename to src/state/get_password.rs index 0affc1b..4ce8db7 100644 --- a/src/handlers/state/get_password.rs +++ b/src/state/get_password.rs @@ -1,7 +1,7 @@ use sea_orm::prelude::*; use teloxide::{adaptors::Throttle, prelude::*}; -use crate::handlers::{utils::validate_field, MainDialogue}; +use crate::{utils::validate_field, MainDialogue}; /// Function to handle GetPassword state pub async fn get_password( diff --git a/src/handlers/state/get_user.rs b/src/state/get_user.rs similarity index 95% rename from src/handlers/state/get_user.rs rename to src/state/get_user.rs index bb80888..bec4b7d 100644 --- a/src/handlers/state/get_user.rs +++ b/src/state/get_user.rs @@ -1,7 +1,6 @@ use crate::{ - errors::HandlerUsed, - handlers::{markups::deletion_markup, utils::delete_optional, MainDialogue}, - models::User, + errors::HandlerUsed, markups::deletion_markup, models::User, utils::delete_optional, + MainDialogue, }; use futures::TryStreamExt; use sea_orm::prelude::*; diff --git a/src/handlers/state/handler.rs b/src/state/handler.rs similarity index 97% rename from src/handlers/state/handler.rs rename to src/state/handler.rs index e3c5c5b..569174c 100644 --- a/src/handlers/state/handler.rs +++ b/src/state/handler.rs @@ -1,4 +1,4 @@ -use crate::handlers::MainDialogue; +use crate::MainDialogue; use sea_orm::prelude::*; use std::{future::Future, sync::Arc}; use teloxide::{adaptors::Throttle, prelude::*}; diff --git a/src/handlers/state/mod.rs b/src/state/mod.rs similarity index 100% rename from src/handlers/state/mod.rs rename to src/state/mod.rs diff --git a/src/handlers/utils.rs b/src/utils.rs similarity index 100% rename from src/handlers/utils.rs rename to src/utils.rs