Switched from sea-orm to sqlx

This commit is contained in:
StNicolay 2023-11-25 19:29:06 +03:00
parent e4e33f52b1
commit 9f967e82d5
Signed by: StNicolay
GPG Key ID: 9693D04DCD962B0D
40 changed files with 434 additions and 1110 deletions

View File

@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "INSERT INTO account VALUES (?, ?, ?, ?, ?)",
"describe": {
"columns": [],
"parameters": {
"Right": 5
},
"nullable": []
},
"hash": "06b0592a51c20f754e0c4d9ac2e6c266b47fb9ca86ff37657fe17538b3fb21c6"
}

View File

@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "INSERT INTO master_pass VALUES (?, ?, ?)",
"describe": {
"columns": [],
"parameters": {
"Right": 3
},
"nullable": []
},
"hash": "1fc824ca1ca447a990c3e68ee4f2a15b8e5bc260641057d914bb7ff871b51aa3"
}

View File

@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "UPDATE account SET enc_password = ? WHERE user_id = ? AND name = ?",
"describe": {
"columns": [],
"parameters": {
"Right": 3
},
"nullable": []
},
"hash": "20f824c521c2261a9e6c85a8972d83ccdffc2fa110ad152356d6392eb7d84326"
}

View File

@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "UPDATE account SET enc_login = ? WHERE user_id = ? AND name = ?",
"describe": {
"columns": [],
"parameters": {
"Right": 3
},
"nullable": []
},
"hash": "5d0690b7fff7d8b3d8d76631360a2b749d401d5722c1a08186da379cdea35cb9"
}

View File

@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "DELETE FROM account WHERE user_id = ?",
"describe": {
"columns": [],
"parameters": {
"Right": 1
},
"nullable": []
},
"hash": "65cf7d5bfa7f322b3294755fcf3678807d31104ef8431f1842cfa05494af1bfd"
}

View File

@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "DELETE FROM account WHERE user_id = ? AND name = ?",
"describe": {
"columns": [],
"parameters": {
"Right": 2
},
"nullable": []
},
"hash": "d24ba49dfa1ba9dc0c3534fe543c8feb3557f8e2bfee2fac0dc37ede2562acfd"
}

View File

@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "DELETE FROM master_pass WHERE user_id = ?",
"describe": {
"columns": [],
"parameters": {
"Right": 1
},
"nullable": []
},
"hash": "d2b33865cf969bb28341212d7dcac28a2f5832b90cfd7ed6a061e4fe203205a7"
}

View File

@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "UPDATE account SET name = ? WHERE user_id = ? AND name = ?",
"describe": {
"columns": [],
"parameters": {
"Right": 3
},
"nullable": []
},
"hash": "f9f6cbb6958f2d35d4ba57e9db9b9d7fc9d67c8bbf9d60fd7e55bebd7188cd84"
}

837
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -9,7 +9,7 @@ edition = "2021"
strip = true strip = true
[workspace] [workspace]
members = [".", "migration", "entity", "cryptography"] members = [".", "entity", "cryptography"]
[workspace.lints.clippy] [workspace.lints.clippy]
pedantic = "warn" pedantic = "warn"
@ -31,16 +31,12 @@ futures = "0.3"
hex = "0.4" hex = "0.4"
itertools = "0.12" itertools = "0.12"
log = "0.4" log = "0.4"
migration = { version = "0.2", path = "migration" }
parking_lot = "0.12" parking_lot = "0.12"
pretty_env_logger = "0.5" pretty_env_logger = "0.5"
sea-orm = { version = "0.12", features = [
"sqlx-mysql",
"runtime-tokio-rustls",
] }
serde = "1" serde = "1"
serde_json = "1" serde_json = "1"
sha2 = "0.10" sha2 = "0.10"
sqlx = { version = "0.7", features = ["mysql", "runtime-tokio-rustls", "macros", "migrate"], default-features = false }
teloxide = { version = "0.12", features = [ teloxide = { version = "0.12", features = [
"macros", "macros",
"ctrlc_handler", "ctrlc_handler",

5
build.rs Normal file
View File

@ -0,0 +1,5 @@
// generated by `sqlx migrate build-script`
fn main() {
// trigger recompilation when a new migration is added
println!("cargo:rerun-if-changed=migrations");
}

View File

@ -19,7 +19,6 @@ rand = { version = "0.8", default-features = false, features = [
"std_rng", "std_rng",
"std", "std",
] } ] }
sea-orm = "0.12"
bitflags = "2" bitflags = "2"
arrayvec = "0.7" arrayvec = "0.7"
subtle = "2" subtle = "2"

View File

@ -1,8 +1,7 @@
use chacha20poly1305::{AeadCore, AeadInPlace, ChaCha20Poly1305, KeyInit}; use chacha20poly1305::{AeadCore, AeadInPlace, ChaCha20Poly1305, KeyInit};
use entity::account::{self, ActiveModel}; use entity::account::Account;
use pbkdf2::pbkdf2_hmac_array; use pbkdf2::pbkdf2_hmac_array;
use rand::{rngs::OsRng, RngCore}; use rand::{rngs::OsRng, RngCore};
use sea_orm::ActiveValue::Set;
use sha2::Sha256; use sha2::Sha256;
pub struct Cipher { pub struct Cipher {
@ -62,7 +61,7 @@ impl Decrypted {
/// ///
/// Returns an error if the tag doesn't match the ciphertext or if the decrypted data isn't valid UTF-8 /// Returns an error if the tag doesn't match the ciphertext or if the decrypted data isn't valid UTF-8
#[inline] #[inline]
pub fn from_account(mut account: account::Model, master_pass: &str) -> crate::Result<Self> { pub fn from_account(mut account: Account, master_pass: &str) -> crate::Result<Self> {
let cipher = Cipher::new(master_pass.as_bytes(), &account.salt); let cipher = Cipher::new(master_pass.as_bytes(), &account.salt);
cipher.decrypt(&mut account.enc_login)?; cipher.decrypt(&mut account.enc_login)?;
cipher.decrypt(&mut account.enc_password)?; cipher.decrypt(&mut account.enc_password)?;
@ -77,22 +76,22 @@ impl Decrypted {
/// Constructs `ActiveModel` with eath field Set by encrypting `self` /// Constructs `ActiveModel` with eath field Set by encrypting `self`
#[inline] #[inline]
#[must_use] #[must_use]
pub fn into_account(self, user_id: u64, master_pass: &str) -> account::ActiveModel { pub fn into_account(self, user_id: u64, master_pass: &str) -> Account {
let mut login = self.login.into_bytes(); let mut enc_login = self.login.into_bytes();
let mut password = self.password.into_bytes(); let mut enc_password = self.password.into_bytes();
let mut salt = vec![0; 64]; let mut salt = vec![0; 64];
OsRng.fill_bytes(&mut salt); OsRng.fill_bytes(&mut salt);
let cipher = Cipher::new(master_pass.as_bytes(), &salt); let cipher = Cipher::new(master_pass.as_bytes(), &salt);
cipher.encrypt(&mut login); cipher.encrypt(&mut enc_login);
cipher.encrypt(&mut password); cipher.encrypt(&mut enc_password);
ActiveModel { Account {
user_id: Set(user_id), user_id,
name: Set(self.name), name: self.name,
salt: Set(salt), salt,
enc_login: Set(login), enc_login,
enc_password: Set(password), enc_password,
} }
} }

View File

@ -1,4 +1,4 @@
use entity::master_pass; use entity::master_pass::MasterPass;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use rand::{rngs::OsRng, RngCore}; use rand::{rngs::OsRng, RngCore};
use scrypt::{scrypt, Params}; use scrypt::{scrypt, Params};
@ -55,9 +55,9 @@ where
} }
} }
impl<'a> From<&'a master_pass::Model> for HashedBytes<&'a [u8], &'a [u8]> { impl<'a> From<&'a MasterPass> for HashedBytes<&'a [u8], &'a [u8]> {
#[inline] #[inline]
fn from(value: &'a master_pass::Model) -> Self { fn from(value: &'a MasterPass) -> Self {
HashedBytes { HashedBytes {
hash: &value.password_hash, hash: &value.password_hash,
salt: &value.salt, salt: &value.salt,
@ -65,8 +65,8 @@ impl<'a> From<&'a master_pass::Model> for HashedBytes<&'a [u8], &'a [u8]> {
} }
} }
impl From<master_pass::Model> for HashedBytes<Vec<u8>, Vec<u8>> { impl From<MasterPass> for HashedBytes<Vec<u8>, Vec<u8>> {
fn from(value: master_pass::Model) -> Self { fn from(value: MasterPass) -> Self {
Self { Self {
hash: value.password_hash, hash: value.password_hash,
salt: value.salt, salt: value.salt,

View File

@ -1,20 +1,19 @@
use super::hashing::HashedBytes; use super::hashing::HashedBytes;
use entity::master_pass; use entity::master_pass;
use sea_orm::ActiveValue::Set;
pub trait FromUnencryptedExt { pub trait FromUnencryptedExt {
fn from_unencrypted(user_id: u64, password: &str) -> master_pass::ActiveModel; fn from_unencrypted(user_id: u64, password: &str) -> master_pass::MasterPass;
} }
impl FromUnencryptedExt for master_pass::ActiveModel { impl FromUnencryptedExt for master_pass::MasterPass {
/// Hashes the password and creates an `ActiveModel` with all fields set to Set variant /// Hashes the password and creates an `ActiveModel` with all fields set to Set variant
#[inline] #[inline]
fn from_unencrypted(user_id: u64, password: &str) -> Self { fn from_unencrypted(user_id: u64, password: &str) -> Self {
let hash = HashedBytes::new(password.as_bytes()); let hash = HashedBytes::new(password.as_bytes());
Self { Self {
user_id: Set(user_id), user_id,
password_hash: Set(hash.hash.to_vec()), password_hash: hash.hash.to_vec(),
salt: Set(hash.salt.to_vec()), salt: hash.salt.to_vec(),
} }
} }
} }

View File

@ -10,4 +10,4 @@ workspace = true
[dependencies] [dependencies]
futures = "0.3" futures = "0.3"
sea-orm = "0.12" sqlx = "0.7.2"

View File

@ -1,90 +1,96 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.3 use super::Pool;
use futures::{Stream, TryStreamExt};
use sqlx::{query, query_as, Executor, FromRow, MySql};
use futures::Stream; #[derive(Clone, Debug, PartialEq, Eq, FromRow, Default)]
use sea_orm::{entity::prelude::*, ActiveValue::Set, QueryOrder, QuerySelect, Statement}; pub struct Account {
#[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, pub user_id: u64,
#[sea_orm(primary_key, auto_increment = false)]
pub name: String, pub name: String,
#[sea_orm(column_type = "Binary(BlobSize::Blob(Some(64)))")]
pub salt: Vec<u8>, pub salt: Vec<u8>,
#[sea_orm(column_type = "VarBinary(256)")]
pub enc_login: Vec<u8>, pub enc_login: Vec<u8>,
#[sea_orm(column_type = "VarBinary(256)")]
pub enc_password: Vec<u8>, pub enc_password: Vec<u8>,
} }
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] impl Account {
pub enum Relation {} // Inserts the account into DB
#[inline]
pub async fn insert(&self, pool: &Pool) -> crate::Result<()> {
query!(
"INSERT INTO account VALUES (?, ?, ?, ?, ?)",
self.user_id,
self.name,
self.salt,
self.enc_login,
self.enc_password
)
.execute(pool)
.await
.map(|_| ())
}
impl ActiveModelBehavior for ActiveModel {}
impl Entity {
/// Gets all user's account from DB /// Gets all user's account from DB
#[inline] #[inline]
pub async fn get_all( pub fn get_all(user_id: u64, pool: &Pool) -> impl Stream<Item = crate::Result<Self>> + '_ {
user_id: u64, query_as("SELECT * FROM account WHERE user_id = ?")
db: &DatabaseConnection, .bind(user_id)
) -> crate::Result<impl Stream<Item = crate::Result<Model>> + '_> { .fetch(pool)
Self::find()
.filter(Column::UserId.eq(user_id))
.stream(db)
.await
} }
/// Streams the names of the user accounts /// Streams the names of the user accounts
#[inline] #[inline]
pub async fn get_names( pub fn get_names(user_id: u64, pool: &Pool) -> impl Stream<Item = crate::Result<String>> + '_ {
user_id: u64, query_as::<_, (String,)>("SELECT name FROM account WHERE user_id = ?")
db: &DatabaseConnection, .bind(user_id)
) -> crate::Result<impl Stream<Item = crate::Result<String>> + '_> { .fetch(pool)
Self::find() .map_ok(|(name,)| name)
.select_only()
.column(Column::Name)
.filter(Column::UserId.eq(user_id))
.order_by_asc(Column::Name)
.into_tuple()
.stream(db)
.await
} }
/// Checks if the account exists /// Checks if the account exists
#[inline] #[inline]
pub async fn exists( pub async fn exists(user_id: u64, account_name: &str, pool: &Pool) -> crate::Result<bool> {
user_id: u64, query_as::<_, (bool,)>(
account_name: impl Into<String> + Send, "SELECT EXISTS(SELECT * FROM account WHERE user_id = ? AND name = ? LIMIT 1) as value",
db: &DatabaseConnection, )
) -> crate::Result<bool> { .bind(user_id)
let count = Self::find_by_id((user_id, account_name.into())) .bind(account_name)
.count(db) .fetch_one(pool)
.await?; .await
Ok(count != 0) .map(|(exists,)| exists)
} }
/// Gets the account from the DB /// Gets the account from the DB
#[inline] #[inline]
pub async fn get( pub async fn get(user_id: u64, account_name: &str, pool: &Pool) -> crate::Result<Option<Self>> {
user_id: u64, query_as("SELECT * FROM account WHERE user_id = ? AND name = ?")
account_name: impl Into<String> + Send, .bind(user_id)
db: &DatabaseConnection, .bind(account_name)
) -> crate::Result<Option<Model>> { .fetch_optional(pool)
Self::find_by_id((user_id, account_name.into()))
.one(db)
.await .await
} }
// Deletes the account from DB
#[inline]
pub async fn delete(user_id: u64, name: &str, pool: &Pool) -> crate::Result<()> {
query!(
"DELETE FROM account WHERE user_id = ? AND name = ?",
user_id,
name
)
.execute(pool)
.await
.map(|_| ())
}
/// Deletes all the user's accounts from DB /// Deletes all the user's accounts from DB
#[inline] #[inline]
pub async fn delete_all(user_id: u64, db: &impl ConnectionTrait) -> crate::Result<()> { pub async fn delete_all(
Self::delete_many() user_id: u64,
.filter(Column::UserId.eq(user_id)) pool: impl Executor<'_, Database = MySql>,
.exec(db) ) -> crate::Result<()> {
.await?; query!("DELETE FROM account WHERE user_id = ?", user_id)
Ok(()) .execute(pool)
.await
.map(|_| ())
} }
/// Gets a name by a hex of a SHA256 hash of the name /// Gets a name by a hex of a SHA256 hash of the name
@ -92,49 +98,82 @@ impl Entity {
pub async fn get_name_by_hash( pub async fn get_name_by_hash(
user_id: u64, user_id: u64,
hash: String, hash: String,
db: &DatabaseConnection, pool: &Pool,
) -> crate::Result<Option<String>> { ) -> crate::Result<Option<String>> {
db.query_one(Statement::from_sql_and_values( let name = query_as::<_, (String,)>(
sea_orm::DatabaseBackend::MySql,
"SELECT `name` FROM `account` WHERE SHA2(`name`, 256) = ? AND `user_id` = ?;", "SELECT `name` FROM `account` WHERE SHA2(`name`, 256) = ? AND `user_id` = ?;",
[hash.into(), user_id.into()], )
)) .bind(hash)
.await? .bind(user_id)
.map(|result| result.try_get_by_index(0)) .fetch_optional(pool)
.transpose() .await?;
Ok(name.map(|(name,)| name))
} }
#[inline] #[inline]
pub async fn get_salt( pub async fn get_salt(user_id: u64, name: &str, pool: &Pool) -> crate::Result<Option<Vec<u8>>> {
user_id: u64, let salt =
name: String, query_as::<_, (Vec<u8>,)>("SELECT salt FROM account WHERE user_id = ? AND name = ?")
db: &DatabaseConnection, .bind(user_id)
) -> crate::Result<Option<Vec<u8>>> { .bind(name)
Self::find_by_id((user_id, name)) .fetch_optional(pool)
.select_only() .await?;
.column(Column::Salt)
.into_tuple() Ok(salt.map(|(salt,)| salt))
.one(db)
.await
} }
#[inline] #[inline]
pub async fn update_name( pub async fn update_name(
user_id: u64, user_id: u64,
original_name: String, original_name: &str,
new_name: String, new_name: &str,
db: &DatabaseConnection, pool: &Pool,
) -> crate::Result<()> { ) -> crate::Result<()> {
Self::update_many() query!(
.set(ActiveModel { "UPDATE account SET name = ? WHERE user_id = ? AND name = ?",
name: Set(new_name), new_name,
..Default::default() user_id,
}) original_name
.filter(Column::UserId.eq(user_id)) )
.filter(Column::Name.eq(original_name)) .execute(pool)
.exec(db) .await
.await?; .map(|_| ())
}
Ok(()) #[inline]
pub async fn update_login(
user_id: u64,
name: &str,
login: Vec<u8>,
pool: &Pool,
) -> crate::Result<()> {
query!(
"UPDATE account SET enc_login = ? WHERE user_id = ? AND name = ?",
login,
user_id,
name
)
.execute(pool)
.await
.map(|_| ())
}
#[inline]
pub async fn update_password(
user_id: u64,
name: &str,
password: Vec<u8>,
pool: &Pool,
) -> crate::Result<()> {
query!(
"UPDATE account SET enc_password = ? WHERE user_id = ? AND name = ?",
password,
user_id,
name
)
.execute(pool)
.await
.map(|_| ())
} }
} }

View File

@ -5,6 +5,6 @@ pub mod account;
pub mod master_pass; pub mod master_pass;
pub mod prelude; pub mod prelude;
use sea_orm::DbErr; pub use sqlx::Result;
type Result<T> = std::result::Result<T, DbErr>; pub type Pool = sqlx::mysql::MySqlPool;

View File

@ -1,40 +1,57 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.3 use super::Pool;
use sqlx::{prelude::FromRow, query, query_as, Executor, MySql};
use sea_orm::entity::prelude::*; #[derive(Clone, Debug, PartialEq, FromRow, Eq)]
pub struct MasterPass {
#[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, pub user_id: u64,
#[sea_orm(column_type = "Binary(BlobSize::Blob(Some(64)))")]
pub salt: Vec<u8>, pub salt: Vec<u8>,
#[sea_orm(column_type = "Binary(BlobSize::Blob(Some(64)))")]
pub password_hash: Vec<u8>, pub password_hash: Vec<u8>,
} }
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] impl MasterPass {
pub enum Relation {} // Inserts the master password into DB
#[inline]
pub async fn insert(&self, pool: &Pool) -> crate::Result<()> {
query!(
"INSERT INTO master_pass VALUES (?, ?, ?)",
self.user_id,
self.salt,
self.password_hash
)
.execute(pool)
.await
.map(|_| ())
}
impl ActiveModelBehavior for ActiveModel {}
impl Entity {
/// Gets the master password from the database /// Gets the master password from the database
#[inline] #[inline]
pub async fn get(user_id: u64, db: &DatabaseConnection) -> crate::Result<Option<Model>> { pub async fn get(user_id: u64, pool: &Pool) -> crate::Result<Option<Self>> {
Self::find_by_id(user_id).one(db).await query_as("SELECT * FROM master_pass WHERE user_id = ?")
.bind(user_id)
.fetch_optional(pool)
.await
} }
/// Checks if the master password for the user exists /// Checks if the master password for the user exists
#[inline] #[inline]
pub async fn exists(user_id: u64, db: &DatabaseConnection) -> Result<bool, DbErr> { pub async fn exists(user_id: u64, pool: &Pool) -> crate::Result<bool> {
let count = Self::find_by_id(user_id).count(db).await?; query_as::<_, (bool,)>(
Ok(count != 0) "SELECT EXISTS(SELECT * FROM master_pass WHERE user_id = ? LIMIT 1) as value",
)
.bind(user_id)
.fetch_one(pool)
.await
.map(|(exists,)| exists)
} }
/// Removes a master password of the user from the database /// Removes a master password of the user from the database
pub async fn remove(user_id: u64, db: &impl ConnectionTrait) -> Result<(), DbErr> { pub async fn remove(
Self::delete_by_id(user_id).exec(db).await?; user_id: u64,
Ok(()) pool: impl Executor<'_, Database = MySql>,
) -> crate::Result<()> {
query!("DELETE FROM master_pass WHERE user_id = ?", user_id)
.execute(pool)
.await
.map(|_| ())
} }
} }

View File

@ -1,4 +1,2 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.3 pub use crate::account::Account;
pub use crate::master_pass::MasterPass;
pub use crate::account::{self, Entity as Account};
pub use crate::master_pass::{self, Entity as MasterPass};

View File

@ -1,12 +0,0 @@
[package]
name = "migration"
version = "0.2.0"
edition = "2021"
[lints]
workspace = true
[dependencies.sea-orm-migration]
version = "0.12"
features = ["runtime-tokio-rustls", "sqlx-mysql"]
default-features = false

View File

@ -1,16 +0,0 @@
pub use sea_orm_migration::prelude::*;
mod m20220101_000001_create_table;
mod m20230427_142510_change_password_hash_size;
pub struct Migrator;
#[async_trait::async_trait]
impl MigratorTrait for Migrator {
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
vec![
Box::new(m20220101_000001_create_table::Migration),
Box::new(m20230427_142510_change_password_hash_size::Migration),
]
}
}

View File

@ -1,80 +0,0 @@
use sea_orm_migration::prelude::*;
#[derive(Iden)]
enum MasterPass {
Table,
#[iden = "user_id"]
UserId,
Salt,
#[iden = "password_hash"]
PasswordHash,
}
#[derive(Iden)]
enum Account {
Table,
#[iden = "user_id"]
UserId,
Name,
Salt,
#[iden = "enc_login"]
EncLogin,
#[iden = "enc_password"]
EncPassword,
}
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(MasterPass::Table)
.if_not_exists()
.col(
ColumnDef::new(MasterPass::UserId)
.big_unsigned()
.primary_key()
.not_null(),
)
.col(ColumnDef::new(MasterPass::Salt).binary_len(64).not_null())
.col(
ColumnDef::new(MasterPass::PasswordHash)
.binary_len(128)
.not_null(),
)
.to_owned(),
)
.await?;
manager
.create_table(
Table::create()
.table(Account::Table)
.if_not_exists()
.col(ColumnDef::new(Account::UserId).big_unsigned().not_null())
.col(ColumnDef::new(Account::Name).string_len(256).not_null())
.col(ColumnDef::new(Account::Salt).binary_len(64).not_null())
.col(ColumnDef::new(Account::EncLogin).var_binary(256).not_null())
.col(
ColumnDef::new(Account::EncPassword)
.var_binary(256)
.not_null(),
)
.primary_key(Index::create().col(Account::UserId).col(Account::Name))
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(sea_query::Table::drop().table(MasterPass::Table).to_owned())
.await?;
manager
.drop_table(sea_query::Table::drop().table(Account::Table).to_owned())
.await
}
}

View File

@ -1,44 +0,0 @@
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[derive(Iden)]
enum MasterPass {
Table,
#[iden = "password_hash"]
PasswordHash,
}
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.alter_table(
sea_query::Table::alter()
.table(MasterPass::Table)
.modify_column(
ColumnDef::new(MasterPass::PasswordHash)
.binary_len(64)
.not_null(),
)
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.alter_table(
sea_query::Table::alter()
.table(MasterPass::Table)
.modify_column(
ColumnDef::new(MasterPass::PasswordHash)
.binary_len(128)
.not_null(),
)
.to_owned(),
)
.await
}
}

View File

@ -0,0 +1,3 @@
DROP TABLE account;
DROP TABLE master_pass;

View File

@ -0,0 +1,16 @@
CREATE TABLE
master_pass (
user_id BIGINT UNSIGNED NOT NULL PRIMARY KEY,
salt BINARY(64) NOT NULL,
password_hash BINARY(64) NOT NULL
);
CREATE TABLE
account (
user_id BIGINT UNSIGNED NOT NULL,
name VARCHAR(255) NOT NULL,
salt BINARY(64) NOT NULL,
enc_login VARBINARY(256) NOT NULL,
enc_password VARBINARY(256) NOT NULL,
PRIMARY KEY (user_id, name)
);

View File

@ -1,9 +1,7 @@
use super::AlterableField::{self, Login, Name, Pass}; use super::AlterableField::{self, Login, Name, Pass};
use crate::{change_state, prelude::*}; use crate::{change_state, prelude::*};
use account::ActiveModel;
use cryptography::account::Cipher; use cryptography::account::Cipher;
use futures::TryFutureExt; use futures::TryFutureExt;
use sea_orm::ActiveValue::Set;
use tokio::{task::spawn_blocking, try_join}; use tokio::{task::spawn_blocking, try_join};
#[inline] #[inline]
@ -16,12 +14,12 @@ async fn update_account(
master_pass: String, master_pass: String,
) -> crate::Result<()> { ) -> crate::Result<()> {
if field == Name { if field == Name {
Account::update_name(user_id, name, field_value, db).await?; Account::update_name(user_id, &name, &field_value, db).await?;
return Ok(()); return Ok(());
} }
let salt = Account::get_salt(user_id, name.clone(), db).await?.unwrap(); let salt = Account::get_salt(user_id, &name, db).await?.unwrap();
let field_value = spawn_blocking(move || { let field_value = spawn_blocking(move || {
let cipher = Cipher::new(master_pass.as_bytes(), &salt); let cipher = Cipher::new(master_pass.as_bytes(), &salt);
@ -31,20 +29,12 @@ async fn update_account(
}) })
.await?; .await?;
let mut model = ActiveModel {
user_id: Set(user_id),
name: Set(name),
..Default::default()
};
match field { match field {
Login => model.enc_login = Set(field_value), Login => Account::update_login(user_id, &name, field_value, db).await?,
Pass => model.enc_password = Set(field_value), Pass => Account::update_password(user_id, &name, field_value, db).await?,
Name => unreachable!(), Name => unreachable!(),
} }
model.update(db).await?;
Ok(()) Ok(())
} }

View File

@ -16,7 +16,7 @@ async fn get_master_pass(
dialogue.exit().await?; dialogue.exit().await?;
let user_id = msg.from().ok_or(NoUserInfo)?.id.0; let user_id = msg.from().ok_or(NoUserInfo)?.id.0;
Account::delete_by_id((user_id, name)).exec(&db).await?; Account::delete(user_id, &name, &db).await?;
ids.alter_message( ids.alter_message(
&bot, &bot,

View File

@ -5,10 +5,7 @@ use tokio::task::spawn_blocking;
pub async fn delete(bot: Throttle<Bot>, msg: Message, db: DatabaseConnection) -> crate::Result<()> { pub async fn delete(bot: Throttle<Bot>, msg: Message, db: DatabaseConnection) -> crate::Result<()> {
let user_id = msg.from().ok_or(NoUserInfo)?.id.0; let user_id = msg.from().ok_or(NoUserInfo)?.id.0;
let names: Vec<String> = Account::get_names(user_id, &db) let names: Vec<String> = Account::get_names(user_id, &db).try_collect().await?;
.await?
.try_collect()
.await?;
if names.is_empty() { if names.is_empty() {
bot.send_message(msg.chat.id, "You don't have any accounts") bot.send_message(msg.chat.id, "You don't have any accounts")

View File

@ -1,7 +1,5 @@
use crate::prelude::*; use crate::prelude::*;
use log::error; use log::error;
use sea_orm::TransactionTrait;
use tokio::try_join;
/// Gets the master password, deletes the accounts and the master password from DB. /// Gets the master password, deletes the accounts and the master password from DB.
/// Although it doesn't use the master password, we get it to be sure that it's the user who used that command /// Although it doesn't use the master password, we get it to be sure that it's the user who used that command
@ -16,17 +14,17 @@ async fn get_master_pass(
) -> crate::Result<()> { ) -> crate::Result<()> {
dialogue.exit().await?; dialogue.exit().await?;
let user_id = msg.from().ok_or(NoUserInfo)?.id.0; let user_id = msg.from().ok_or(NoUserInfo)?.id.0;
let txn = db.begin().await?; let mut txn = db.begin().await?;
let result = try_join!( let result = (
Account::delete_all(user_id, &txn), Account::delete_all(user_id, &mut *txn).await,
MasterPass::remove(user_id, &txn), MasterPass::remove(user_id, &mut *txn).await,
); );
let text = match result { let text = match result {
Ok(_) => { (Ok(()), Ok(())) => {
txn.commit().await?; txn.commit().await?;
"Everything was deleted" "Everything was deleted"
} }
Err(err) => { (Err(err), _) | (_, Err(err)) => {
error!("{}", crate::Error::from(err)); error!("{}", crate::Error::from(err));
txn.rollback().await?; txn.rollback().await?;
"Something went wrong. Try again later" "Something went wrong. Try again later"

View File

@ -7,7 +7,7 @@ use tokio::task::spawn_blocking;
/// Decryptes the account on a worker thread and adds it to the accounts vector /// Decryptes the account on a worker thread and adds it to the accounts vector
#[inline] #[inline]
async fn decrypt_account( async fn decrypt_account(
account: account::Model, account: Account,
master_pass: Arc<str>, master_pass: Arc<str>,
accounts: &Mutex<&mut Vec<DecryptedAccount>>, accounts: &Mutex<&mut Vec<DecryptedAccount>>,
) -> crate::Result<()> { ) -> crate::Result<()> {
@ -38,7 +38,6 @@ async fn get_master_pass(
let master_pass: Arc<str> = master_pass.into(); let master_pass: Arc<str> = master_pass.into();
Account::get_all(user_id, &db) Account::get_all(user_id, &db)
.await?
.err_into::<crate::Error>() .err_into::<crate::Error>()
.try_for_each_concurrent(3, |account| { .try_for_each_concurrent(3, |account| {
decrypt_account(account, master_pass.clone(), &accounts) decrypt_account(account, master_pass.clone(), &accounts)

View File

@ -9,10 +9,7 @@ pub async fn get_account(
) -> crate::Result<()> { ) -> crate::Result<()> {
let user_id = msg.from().ok_or(NoUserInfo)?.id.0; let user_id = msg.from().ok_or(NoUserInfo)?.id.0;
let names: Vec<String> = Account::get_names(user_id, &db) let names: Vec<String> = Account::get_names(user_id, &db).try_collect().await?;
.await?
.try_collect()
.await?;
if names.is_empty() { if names.is_empty() {
bot.send_message(msg.chat.id, "You don't have any accounts") bot.send_message(msg.chat.id, "You don't have any accounts")

View File

@ -11,7 +11,7 @@ pub async fn get_accounts(
db: DatabaseConnection, db: DatabaseConnection,
) -> crate::Result<()> { ) -> crate::Result<()> {
let user_id = msg.from().ok_or(NoUserInfo)?.id.0; let user_id = msg.from().ok_or(NoUserInfo)?.id.0;
let mut account_names = Account::get_names(user_id, &db).await?; let mut account_names = Account::get_names(user_id, &db);
let mut text = if let Some(name) = account_names.try_next().await? { let mut text = if let Some(name) = account_names.try_next().await? {
format!("Accounts:\n`{name}`") format!("Accounts:\n`{name}`")

View File

@ -18,7 +18,7 @@ async fn encrypt_account(
let name = account.name.clone(); let name = account.name.clone();
match spawn_blocking(move || account.into_account(user_id, &master_pass)).await { match spawn_blocking(move || account.into_account(user_id, &master_pass)).await {
Ok(account) => match account.insert(db).await { Ok(account) => match account.insert(db).await {
Ok(_) => (), Ok(()) => (),
Err(_) => failed.lock().push(name), Err(_) => failed.lock().push(name),
}, },
_ => failed.lock().push(name), _ => failed.lock().push(name),

View File

@ -5,10 +5,7 @@ use tokio::task::spawn_blocking;
pub async fn menu(bot: Throttle<Bot>, msg: Message, db: DatabaseConnection) -> crate::Result<()> { pub async fn menu(bot: Throttle<Bot>, msg: Message, db: DatabaseConnection) -> crate::Result<()> {
let user_id = msg.from().ok_or(NoUserInfo)?.id.0; let user_id = msg.from().ok_or(NoUserInfo)?.id.0;
let names: Vec<String> = Account::get_names(user_id, &db) let names: Vec<String> = Account::get_names(user_id, &db).try_collect().await?;
.await?
.try_collect()
.await?;
if names.is_empty() { if names.is_empty() {
bot.send_message(msg.chat.id, "You don't have any accounts") bot.send_message(msg.chat.id, "You don't have any accounts")

View File

@ -1,6 +1,5 @@
use crate::{change_state, prelude::*}; use crate::{change_state, prelude::*};
use cryptography::hashing::HashedBytes; use cryptography::hashing::HashedBytes;
use sea_orm::ActiveValue::Set;
use tokio::task::spawn_blocking; use tokio::task::spawn_blocking;
#[inline] #[inline]
@ -14,7 +13,7 @@ async fn get_master_pass2(
master_pass: String, master_pass: String,
) -> crate::Result<()> { ) -> crate::Result<()> {
dialogue.exit().await?; dialogue.exit().await?;
let user_id = Set(msg.from().ok_or(NoUserInfo)?.id.0); let user_id = msg.from().ok_or(NoUserInfo)?.id.0;
if !hash.verify(master_pass.as_bytes()) { if !hash.verify(master_pass.as_bytes()) {
ids.alter_message( ids.alter_message(
@ -28,10 +27,10 @@ async fn get_master_pass2(
return Ok(()); return Ok(());
} }
let model = master_pass::ActiveModel { let model = MasterPass {
user_id, user_id,
password_hash: Set(hash.hash.to_vec()), password_hash: hash.hash.to_vec(),
salt: Set(hash.salt.to_vec()), salt: hash.salt.to_vec(),
}; };
model.insert(&db).await?; model.insert(&db).await?;

View File

@ -1,6 +1,3 @@
// #![warn(clippy::pedantic, clippy::all, clippy::nursery)]
// #![allow(clippy::single_match_else)]
mod callbacks; mod callbacks;
mod commands; mod commands;
mod default; mod default;
@ -16,9 +13,7 @@ mod utils;
use anyhow::{Error, Result}; use anyhow::{Error, Result};
use dotenvy::dotenv; use dotenvy::dotenv;
use migration::{Migrator, MigratorTrait};
use prelude::*; use prelude::*;
use sea_orm::Database;
use std::env; use std::env;
use teloxide::{adaptors::throttle::Limits, dispatching::dialogue::InMemStorage, filter_command}; use teloxide::{adaptors::throttle::Limits, dispatching::dialogue::InMemStorage, filter_command};
@ -92,8 +87,10 @@ async fn main() -> Result<()> {
let token = env::var("TOKEN").expect("expected TOKEN in the enviroment"); let token = env::var("TOKEN").expect("expected TOKEN in the enviroment");
let database_url = env::var("DATABASE_URL").expect("expected DATABASE_URL in the enviroment"); let database_url = env::var("DATABASE_URL").expect("expected DATABASE_URL in the enviroment");
let db = Database::connect(database_url).await?; let pool = sqlx::mysql::MySqlPool::connect(&database_url).await?;
Migrator::up(&db, None).await?;
get_dispatcher(token, db).dispatch().await; sqlx::migrate!().run(&pool).await?;
get_dispatcher(token, pool).dispatch().await;
Ok(()) Ok(())
} }

View File

@ -32,7 +32,7 @@ pub async fn menu_markup(
db: &DatabaseConnection, db: &DatabaseConnection,
) -> crate::Result<InlineKeyboardMarkup> { ) -> crate::Result<InlineKeyboardMarkup> {
let command: String = command.into(); let command: String = command.into();
let names: Vec<String> = Account::get_names(user_id, db).await?.try_collect().await?; let names: Vec<String> = Account::get_names(user_id, db).try_collect().await?;
spawn_blocking(move || menu_markup_sync(&command, names)) spawn_blocking(move || menu_markup_sync(&command, names))
.await .await

View File

@ -8,7 +8,6 @@ pub use crate::{
utils::*, utils::*,
}; };
pub use cryptography::prelude::*; pub use cryptography::prelude::*;
pub use entity::prelude::*; pub use entity::{prelude::*, Pool as DatabaseConnection};
pub use futures::{StreamExt, TryStreamExt}; pub use futures::{StreamExt, TryStreamExt};
pub use sea_orm::prelude::*;
pub use teloxide::{adaptors::Throttle, prelude::*}; pub use teloxide::{adaptors::Throttle, prelude::*};

View File

@ -165,7 +165,6 @@ pub async fn get_user(
let existing_names = async { let existing_names = async {
Account::get_names(user_id, &db) Account::get_names(user_id, &db)
.await?
.try_collect() .try_collect()
.await .await
.map_err(Into::into) .map_err(Into::into)