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

This commit is contained in:
StNicolay 2023-06-01 14:42:35 +03:00
parent c40ffafd69
commit cda07b4d84
Signed by: StNicolay
GPG Key ID: 9693D04DCD962B0D
46 changed files with 360 additions and 268 deletions

5
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,5 @@
{
"rust-analyzer.linkedProjects": [
"./Cargo.toml"
]
}

32
Cargo.lock generated
View File

@ -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",

View File

@ -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"] }

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

@ -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<u8, 32> = (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<Bot>, 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(())
}

View File

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

11
entity/Cargo.toml Normal file
View File

@ -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"

View File

@ -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::<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)
}
}
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<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,
})
}
}
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

7
entity/src/lib.rs Normal file
View File

@ -0,0 +1,7 @@
pub mod account;
pub mod master_pass;
pub mod prelude;
use sea_orm::DbErr;
type Result<T> = std::result::Result<T, DbErr>;

45
entity/src/master_pass.rs Normal file
View File

@ -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<u8>,
#[sea_orm(column_type = "Binary(BlobSize::Blob(Some(64)))")]
pub password_hash: Vec<u8>,
}
#[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<Option<Model>> {
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<bool, DbErr> {
let id = Self::find_by_id(user_id)
.select_only()
.column(Column::UserId)
.into_tuple::<u64>()
.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(())
}
}

View File

@ -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};

View File

@ -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

View File

@ -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::*};

View File

@ -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

View File

@ -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::*};

View File

@ -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())

View File

@ -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<str> = master_pass.into();
Account::get_all(user_id, &db)
.await?
.err_into::<crate::Error>()
.try_for_each_concurrent(None, |account| {
let master_pass = Arc::clone(&master_pass);
async move {

View File

@ -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<Bot>, 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(())
}

View File

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

View File

@ -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};

View File

@ -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

View File

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

View File

@ -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())

View File

@ -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

View File

@ -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<u8>,
#[sea_orm(column_type = "Binary(BlobSize::Blob(Some(64)))")]
pub password_hash: Vec<u8>,
}
#[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, &params, &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<bool> {
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<Self> {
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<Option<Model>> {
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<bool> {
let id = Self::find_by_id(user_id)
.select_only()
.column(Column::UserId)
.into_tuple::<u64>()
.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(())
}
}

View File

@ -1,5 +0,0 @@
//! Entities to work with the database
pub mod account;
pub mod master_pass;
pub mod prelude;

View File

@ -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::{

View File

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

View File

@ -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};

View File

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

View File

@ -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

View File

@ -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<F>(
bot: Throttle<Bot>,
msg: Message,
db: DatabaseConnection,
dialogue: crate::handlers::MainDialogue,
dialogue: crate::MainDialogue,
check: F,
no_text_message: impl Into<String>,
next: super::PackagedHandler<String>,

View File

@ -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

View File

@ -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::*};

View File

@ -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");

View File

@ -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(

View File

@ -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::*;

View File

@ -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::*};