diff --git a/Cargo.toml b/Cargo.toml index de227c0..e092980 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,14 @@ strip = true [workspace] members = [".", "migration", "entity", "cryptography"] +[workspace.lints.clippy] +pedantic = "warn" +all = "warn" +nursery = "warn" + +[lints] +workspace = true + [dependencies] ahash = "0.8.3" anyhow = "1" diff --git a/cryptography/Cargo.toml b/cryptography/Cargo.toml index 4e10fea..41dce7f 100644 --- a/cryptography/Cargo.toml +++ b/cryptography/Cargo.toml @@ -5,6 +5,9 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[lints] +workspace = true + [dependencies] sha2 = "0.10" scrypt = { version = "0.11", default-features = false, features = ["std"] } diff --git a/cryptography/src/account.rs b/cryptography/src/account.rs index 33bc0b5..bb81605 100644 --- a/cryptography/src/account.rs +++ b/cryptography/src/account.rs @@ -12,8 +12,9 @@ pub struct Cipher { impl Cipher { /// Creates a new cipher from a master password and the salt #[inline] + #[must_use] pub fn new(password: &[u8], salt: &[u8]) -> Self { - let key = pbkdf2_hmac_array::(password, salt, 480000); + let key = pbkdf2_hmac_array::(password, salt, 480_000); Self { chacha: ChaCha20Poly1305::new(&key.into()), @@ -33,11 +34,12 @@ impl Cipher { #[inline] pub 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 { +pub trait FromUnencryptedExt { fn from_unencrypted( user_id: u64, name: String, @@ -47,8 +49,8 @@ pub trait AccountFromUnencryptedExt { ) -> 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 +impl FromUnencryptedExt 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, diff --git a/cryptography/src/hashing.rs b/cryptography/src/hashing.rs index 8bcce8d..95063ce 100644 --- a/cryptography/src/hashing.rs +++ b/cryptography/src/hashing.rs @@ -11,6 +11,8 @@ static PARAMS: Lazy = Lazy::new(|| Params::new(14, 8, 1, HASH_LENGTH).un /// Hashes the bytes with Scrypt with the given salt #[inline] +#[must_use] +#[allow(clippy::missing_panics_doc)] pub fn hash_scrypt(bytes: &[u8], salt: &[u8]) -> [u8; HASH_LENGTH] { let mut hash = [0; HASH_LENGTH]; scrypt(bytes, salt, &PARAMS, &mut hash).unwrap(); @@ -29,6 +31,7 @@ where impl HashedBytes<[u8; HASH_LENGTH], [u8; SALT_LENGTH]> { #[inline] + #[must_use] pub fn new(bytes: &[u8]) -> Self { let mut salt = [0; 64]; OsRng.fill_bytes(&mut salt); @@ -45,6 +48,7 @@ where U: AsRef<[u8]>, { #[inline] + #[must_use] pub fn verify(&self, bytes: &[u8]) -> bool { let hash = hash_scrypt(bytes, self.salt.as_ref()); hash.ct_eq(self.hash.as_ref()).into() diff --git a/cryptography/src/lib.rs b/cryptography/src/lib.rs index 0eae6d0..6846138 100644 --- a/cryptography/src/lib.rs +++ b/cryptography/src/lib.rs @@ -1,4 +1,5 @@ //! Functions to encrypt the database models +#![allow(clippy::missing_errors_doc)] pub mod account; pub mod hashing; diff --git a/cryptography/src/master_pass.rs b/cryptography/src/master_pass.rs index f6f8eb0..b4db34d 100644 --- a/cryptography/src/master_pass.rs +++ b/cryptography/src/master_pass.rs @@ -2,12 +2,12 @@ use super::hashing::HashedBytes; use entity::master_pass; use sea_orm::ActiveValue::Set; -pub trait MasterPassFromUnencryptedExt { +pub trait FromUnencryptedExt { 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 +impl FromUnencryptedExt 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 hash = HashedBytes::new(password.as_bytes()); diff --git a/cryptography/src/passwords.rs b/cryptography/src/passwords.rs index 54e0cb3..c452e8b 100644 --- a/cryptography/src/passwords.rs +++ b/cryptography/src/passwords.rs @@ -25,6 +25,7 @@ bitflags::bitflags! { /// Returns true if the generated master password is valid. /// It checks that it has at least one lowercase, one uppercase, one number and one punctuation char #[inline] +#[must_use] fn check_generated_password(password: &[u8; LENGTH]) -> bool { let mut flags = PasswordFlags::empty(); for &byte in password { @@ -33,7 +34,7 @@ fn check_generated_password(password: &[u8; LENGTH]) -> boo b'A'..=b'Z' => flags |= PasswordFlags::UPPERCASE, b'0'..=b'9' => flags |= PasswordFlags::NUMBER, b'!'..=b'/' | b':'..=b'@' | b'['..=b'`' | b'{'..=b'~' => { - flags |= PasswordFlags::SPECIAL_CHARACTER + flags |= PasswordFlags::SPECIAL_CHARACTER; } _ => (), } @@ -46,6 +47,7 @@ fn check_generated_password(password: &[u8; LENGTH]) -> boo /// Continuously generates the password until it passes the checks #[inline] +#[must_use] fn generate_password(rng: &mut R) -> ArrayString where R: Rng + CryptoRng, @@ -59,6 +61,8 @@ where } #[inline] +#[must_use] +#[allow(clippy::module_name_repetitions)] pub fn generate_passwords( ) -> [ArrayString; AMOUNT] { let mut rng = thread_rng(); @@ -66,6 +70,7 @@ pub fn generate_passwords( } #[inline] +#[must_use] pub fn check_master_pass(password: &str) -> PasswordValidity { let mut count = 0; let mut chars = password.chars(); @@ -74,13 +79,13 @@ pub fn check_master_pass(password: &str) -> PasswordValidity { for char in &mut chars { count += 1; if char.is_lowercase() { - flags.remove(PasswordValidity::NO_LOWERCASE) + flags.remove(PasswordValidity::NO_LOWERCASE); } else if char.is_uppercase() { - flags.remove(PasswordValidity::NO_UPPERCASE) + flags.remove(PasswordValidity::NO_UPPERCASE); } else if char.is_ascii_digit() { - flags.remove(PasswordValidity::NO_NUMBER) + flags.remove(PasswordValidity::NO_NUMBER); } else if char.is_ascii_punctuation() { - flags.remove(PasswordValidity::NO_SPECIAL_CHARACTER) + flags.remove(PasswordValidity::NO_SPECIAL_CHARACTER); } if flags == PasswordValidity::TOO_SHORT { @@ -90,7 +95,7 @@ pub fn check_master_pass(password: &str) -> PasswordValidity { } if count >= 8 { - flags.remove(PasswordValidity::TOO_SHORT) + flags.remove(PasswordValidity::TOO_SHORT); } flags @@ -102,6 +107,6 @@ mod tests { #[test] fn chars_must_be_ascii() { - assert!(CHARS.is_ascii()) + assert!(CHARS.is_ascii()); } } diff --git a/cryptography/src/prelude.rs b/cryptography/src/prelude.rs index fb986b6..dde5bf6 100644 --- a/cryptography/src/prelude.rs +++ b/cryptography/src/prelude.rs @@ -1,2 +1,2 @@ -pub use crate::account::*; -pub use crate::master_pass::*; +pub use crate::account::{DecryptAccountExt as _, FromUnencryptedExt as _}; +pub use crate::master_pass::FromUnencryptedExt as _; diff --git a/entity/Cargo.toml b/entity/Cargo.toml index be037a8..ad233aa 100644 --- a/entity/Cargo.toml +++ b/entity/Cargo.toml @@ -5,6 +5,9 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[lints] +workspace = true + [dependencies] futures = "0.3" sea-orm = "0.12" diff --git a/entity/src/account.rs b/entity/src/account.rs index c395f12..3460cb6 100644 --- a/entity/src/account.rs +++ b/entity/src/account.rs @@ -56,7 +56,7 @@ impl Entity { #[inline] pub async fn exists( user_id: u64, - account_name: impl Into, + account_name: impl Into + Send, db: &DatabaseConnection, ) -> crate::Result { let count = Self::find_by_id((user_id, account_name.into())) @@ -69,7 +69,7 @@ impl Entity { #[inline] pub async fn get( user_id: u64, - account_name: impl Into, + account_name: impl Into + Send, db: &DatabaseConnection, ) -> crate::Result> { Self::find_by_id((user_id, account_name.into())) diff --git a/entity/src/lib.rs b/entity/src/lib.rs index e40efc2..beea226 100644 --- a/entity/src/lib.rs +++ b/entity/src/lib.rs @@ -1,3 +1,6 @@ +// This is fine, because all errors can only be caused by the database errors and the docs would get repetative very quickly +#![allow(clippy::missing_errors_doc)] + pub mod account; pub mod master_pass; pub mod prelude; diff --git a/migration/Cargo.toml b/migration/Cargo.toml index fbb039a..f0743ba 100644 --- a/migration/Cargo.toml +++ b/migration/Cargo.toml @@ -3,6 +3,9 @@ name = "migration" version = "0.2.0" edition = "2021" +[lints] +workspace = true + [dependencies.sea-orm-migration] version = "0.12" features = ["runtime-tokio-rustls", "sqlx-mysql"] diff --git a/src/callbacks/alter.rs b/src/callbacks/alter.rs index 1bc8330..83f5e4b 100644 --- a/src/callbacks/alter.rs +++ b/src/callbacks/alter.rs @@ -1,6 +1,7 @@ use super::AlterableField::{self, Login, Name, Pass}; use crate::{change_state, prelude::*}; use account::ActiveModel; +use cryptography::account::Cipher; use futures::TryFutureExt; use sea_orm::ActiveValue::Set; use tokio::{task::spawn_blocking, try_join}; diff --git a/src/commands/get_accounts.rs b/src/commands/get_accounts.rs index 436d8eb..c0db6c3 100644 --- a/src/commands/get_accounts.rs +++ b/src/commands/get_accounts.rs @@ -12,15 +12,16 @@ pub async fn get_accounts( ) -> crate::Result<()> { let user_id = msg.from().ok_or(NoUserInfo)?.id.0; let mut account_names = Account::get_names(user_id, &db).await?; - let mut text = match account_names.try_next().await? { - Some(name) => format!("Accounts:\n`{name}`"), - None => { - bot.send_message(msg.chat.id, "No accounts found") - .reply_markup(deletion_markup()) - .await?; - return Ok(()); - } + + let mut text = if let Some(name) = account_names.try_next().await? { + format!("Accounts:\n`{name}`") + } else { + bot.send_message(msg.chat.id, "No accounts found") + .reply_markup(deletion_markup()) + .await?; + return Ok(()); }; + account_names .map_err(crate::Error::from) .try_for_each(|name| { @@ -28,6 +29,7 @@ pub async fn get_accounts( future::ready(result) }) .await?; + bot.send_message(msg.chat.id, text) .parse_mode(ParseMode::MarkdownV2) .reply_markup(deletion_markup()) diff --git a/src/main.rs b/src/main.rs index 68439ac..8e6db41 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,5 @@ -#![warn(clippy::pedantic, clippy::all, clippy::nursery)] -#![allow(clippy::single_match_else)] +// #![warn(clippy::pedantic, clippy::all, clippy::nursery)] +// #![allow(clippy::single_match_else)] mod callbacks; mod commands; diff --git a/src/state/generic.rs b/src/state/generic.rs index 3c46304..9df33f0 100644 --- a/src/state/generic.rs +++ b/src/state/generic.rs @@ -26,15 +26,12 @@ where return Err(HandlerUsed.into()); } - let text = match msg.text() { - Some(text) => text.trim(), - None => { - handler - .previous - .alter_message(&bot, no_text_message, None, None) - .await?; - return Ok(()); - } + let Some(text) = msg.text().map(str::trim) else { + handler + .previous + .alter_message(&bot, no_text_message, None, None) + .await?; + return Ok(()); }; if text == "/cancel" { diff --git a/src/state/get_master_pass.rs b/src/state/get_master_pass.rs index abfbddf..a0c46b2 100644 --- a/src/state/get_master_pass.rs +++ b/src/state/get_master_pass.rs @@ -11,21 +11,17 @@ async fn check_master_pass( master_pass: &str, ) -> crate::Result> { let user_id = msg.from().ok_or(NoUserInfo)?.id.0; - let model = MasterPass::get(user_id, db).await?; + let Some(model) = MasterPass::get(user_id, db).await? else { + error!("User was put into the GetMasterPass state with no master password set"); + return Ok(Some( + "No master password set. Use /cancel and set it by using /set_master_pass".to_owned(), + )); + }; - let is_valid = match model { - Some(model) => { - let hash: HashedBytes<_, _> = model.into(); - let master_pass = master_pass.to_owned(); - spawn_blocking(move || hash.verify(master_pass.as_bytes())).await? - } - None => { - error!("User was put into the GetMasterPass state with no master password set"); - return Ok(Some( - "No master password set. Use /cancel and set it by using /set_master_pass" - .to_owned(), - )); - } + let is_valid = { + let hash = HashedBytes::from(model); + let master_pass = master_pass.to_owned(); + spawn_blocking(move || hash.verify(master_pass.as_bytes())).await? }; if !is_valid {