diff --git a/cryptography/src/hashing.rs b/cryptography/src/hashing.rs new file mode 100644 index 0000000..8bcce8d --- /dev/null +++ b/cryptography/src/hashing.rs @@ -0,0 +1,71 @@ +use entity::master_pass; +use once_cell::sync::Lazy; +use rand::{rngs::OsRng, RngCore}; +use scrypt::{scrypt, Params}; +use subtle::ConstantTimeEq; + +pub const HASH_LENGTH: usize = 64; +pub const SALT_LENGTH: usize = 64; + +static PARAMS: Lazy = Lazy::new(|| Params::new(14, 8, 1, HASH_LENGTH).unwrap()); + +/// Hashes the bytes with Scrypt with the given salt +#[inline] +pub fn hash_scrypt(bytes: &[u8], salt: &[u8]) -> [u8; HASH_LENGTH] { + let mut hash = [0; HASH_LENGTH]; + scrypt(bytes, salt, &PARAMS, &mut hash).unwrap(); + hash +} + +/// Verifieble scrypt hashed bytes +pub struct HashedBytes +where + T: AsRef<[u8]>, + U: AsRef<[u8]>, +{ + pub hash: T, + pub salt: U, +} + +impl HashedBytes<[u8; HASH_LENGTH], [u8; SALT_LENGTH]> { + #[inline] + pub fn new(bytes: &[u8]) -> Self { + let mut salt = [0; 64]; + OsRng.fill_bytes(&mut salt); + Self { + hash: hash_scrypt(bytes, &salt), + salt, + } + } +} + +impl HashedBytes +where + T: AsRef<[u8]>, + U: AsRef<[u8]>, +{ + #[inline] + pub fn verify(&self, bytes: &[u8]) -> bool { + let hash = hash_scrypt(bytes, self.salt.as_ref()); + hash.ct_eq(self.hash.as_ref()).into() + } +} + +impl<'a> From<&'a master_pass::Model> for HashedBytes<&'a [u8], &'a [u8]> { + #[inline] + fn from(value: &'a master_pass::Model) -> Self { + HashedBytes { + hash: &value.password_hash, + salt: &value.salt, + } + } +} + +impl From for HashedBytes, Vec> { + fn from(value: master_pass::Model) -> Self { + Self { + hash: value.password_hash, + salt: value.salt, + } + } +} diff --git a/cryptography/src/lib.rs b/cryptography/src/lib.rs index 113d516..0eae6d0 100644 --- a/cryptography/src/lib.rs +++ b/cryptography/src/lib.rs @@ -1,6 +1,7 @@ //! Functions to encrypt the database models pub mod account; +pub mod hashing; pub mod master_pass; pub mod passwords; pub mod prelude; diff --git a/cryptography/src/master_pass.rs b/cryptography/src/master_pass.rs index 4d5f5f1..f6f8eb0 100644 --- a/cryptography/src/master_pass.rs +++ b/cryptography/src/master_pass.rs @@ -1,35 +1,6 @@ +use super::hashing::HashedBytes; use entity::master_pass; -use once_cell::sync::Lazy; -use rand::{rngs::OsRng, RngCore}; -use scrypt::{scrypt, Params}; use sea_orm::ActiveValue::Set; -use subtle::ConstantTimeEq; - -const HASH_LENGTH: usize = 64; -const SALT_LENGTH: usize = 64; - -static PARAMS: Lazy = Lazy::new(|| Params::new(14, 8, 1, HASH_LENGTH).unwrap()); - -/// Hashes the password with Scrypt with the given salt -#[inline] -fn hash_password(password: &[u8], salt: &[u8]) -> [u8; HASH_LENGTH] { - let mut password_hash = [0; HASH_LENGTH]; - 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.ct_eq(&self.password_hash).into() - } -} pub trait MasterPassFromUnencryptedExt { fn from_unencrypted(user_id: u64, password: &str) -> master_pass::ActiveModel; @@ -39,13 +10,11 @@ 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 = [0; SALT_LENGTH]; - OsRng.fill_bytes(&mut salt); - let password_hash = hash_password(password.as_bytes(), &salt); + let hash = HashedBytes::new(password.as_bytes()); Self { user_id: Set(user_id), - salt: Set(salt.into()), - password_hash: Set(password_hash.into()), + password_hash: Set(hash.hash.to_vec()), + salt: Set(hash.salt.to_vec()), } } } diff --git a/src/commands/set_master_pass.rs b/src/commands/set_master_pass.rs index 3797041..d344f73 100644 --- a/src/commands/set_master_pass.rs +++ b/src/commands/set_master_pass.rs @@ -1,24 +1,67 @@ use crate::{change_state, prelude::*}; +use cryptography::hashing::HashedBytes; +use sea_orm::ActiveValue::Set; use tokio::task::spawn_blocking; -/// Actually sets the master password -async fn get_master_pass( +async fn get_master_pass2( bot: Throttle, msg: Message, db: DatabaseConnection, dialogue: MainDialogue, mut ids: MessageIds, + hash: HashedBytes<[u8; 64], [u8; 64]>, master_pass: String, ) -> crate::Result<()> { dialogue.exit().await?; + let user_id = Set(msg.from().ok_or(NoUserInfo)?.id.0); - let user_id = msg.from().ok_or(NoUserInfo)?.id.0; - let model = - spawn_blocking(move || master_pass::ActiveModel::from_unencrypted(user_id, &master_pass)) - .await?; + if !hash.verify(master_pass.as_bytes()) { + ids.alter_message( + &bot, + "The passwords didn't match. Use the command again", + deletion_markup(), + None, + ) + .await?; + + return Ok(()); + } + + let model = master_pass::ActiveModel { + user_id, + password_hash: Set(hash.hash.to_vec()), + salt: Set(hash.salt.to_vec()), + }; model.insert(&db).await?; + ids.alter_message(&bot, "Success", deletion_markup(), None) .await?; + + Ok(()) +} + +/// Actually sets the master password +#[inline] +async fn get_master_pass( + bot: Throttle, + _: Message, + _: DatabaseConnection, + dialogue: MainDialogue, + mut ids: MessageIds, + master_pass: String, +) -> crate::Result<()> { + let hash = spawn_blocking(move || HashedBytes::new(master_pass.as_bytes())).await?; + + ids.alter_message(&bot, "Send it again", None, None).await?; + + change_state!( + dialogue, + ids, + (hash), + State::GetNewMasterPass, + get_master_pass2 + ); + Ok(()) } diff --git a/src/state/get_master_pass.rs b/src/state/get_master_pass.rs index 8b4967c..abfbddf 100644 --- a/src/state/get_master_pass.rs +++ b/src/state/get_master_pass.rs @@ -1,4 +1,5 @@ use crate::prelude::*; +use cryptography::hashing::HashedBytes; use log::error; use tokio::task::spawn_blocking; @@ -14,8 +15,9 @@ async fn check_master_pass( let is_valid = match model { Some(model) => { + let hash: HashedBytes<_, _> = model.into(); let master_pass = master_pass.to_owned(); - spawn_blocking(move || model.verify(&master_pass)).await? + spawn_blocking(move || hash.verify(master_pass.as_bytes())).await? } None => { error!("User was put into the GetMasterPass state with no master password set");