diff options
author | Quentin Dufour <quentin@deuxfleurs.fr> | 2024-03-08 08:17:03 +0100 |
---|---|---|
committer | Quentin Dufour <quentin@deuxfleurs.fr> | 2024-03-08 08:17:03 +0100 |
commit | 1a43ce5ac7033c148f64a033f2b1d335e95e11d5 (patch) | |
tree | 60b234604170fe207248458a9c4cdd3f4b7c36f2 /aero-user/src/login | |
parent | bb9cb386b65834c44cae86bd100f800883022062 (diff) | |
download | aerogramme-1a43ce5ac7033c148f64a033f2b1d335e95e11d5.tar.gz aerogramme-1a43ce5ac7033c148f64a033f2b1d335e95e11d5.zip |
WIP refactor
Diffstat (limited to 'aero-user/src/login')
-rw-r--r-- | aero-user/src/login/demo_provider.rs | 51 | ||||
-rw-r--r-- | aero-user/src/login/ldap_provider.rs | 264 | ||||
-rw-r--r-- | aero-user/src/login/mod.rs | 245 | ||||
-rw-r--r-- | aero-user/src/login/static_provider.rs | 188 |
4 files changed, 748 insertions, 0 deletions
diff --git a/aero-user/src/login/demo_provider.rs b/aero-user/src/login/demo_provider.rs new file mode 100644 index 0000000..11c7d54 --- /dev/null +++ b/aero-user/src/login/demo_provider.rs @@ -0,0 +1,51 @@ +use crate::login::*; +use crate::storage::*; + +pub struct DemoLoginProvider { + keys: CryptoKeys, + in_memory_store: in_memory::MemDb, +} + +impl DemoLoginProvider { + pub fn new() -> Self { + Self { + keys: CryptoKeys::init(), + in_memory_store: in_memory::MemDb::new(), + } + } +} + +#[async_trait] +impl LoginProvider for DemoLoginProvider { + async fn login(&self, username: &str, password: &str) -> Result<Credentials> { + tracing::debug!(user=%username, "login"); + + if username != "alice" { + bail!("user does not exist"); + } + + if password != "hunter2" { + bail!("wrong password"); + } + + let storage = self.in_memory_store.builder("alice").await; + let keys = self.keys.clone(); + + Ok(Credentials { storage, keys }) + } + + async fn public_login(&self, email: &str) -> Result<PublicCredentials> { + tracing::debug!(user=%email, "public_login"); + if email != "alice@example.tld" { + bail!("invalid email address"); + } + + let storage = self.in_memory_store.builder("alice").await; + let public_key = self.keys.public.clone(); + + Ok(PublicCredentials { + storage, + public_key, + }) + } +} diff --git a/aero-user/src/login/ldap_provider.rs b/aero-user/src/login/ldap_provider.rs new file mode 100644 index 0000000..ca5a356 --- /dev/null +++ b/aero-user/src/login/ldap_provider.rs @@ -0,0 +1,264 @@ +use async_trait::async_trait; +use ldap3::{LdapConnAsync, Scope, SearchEntry}; +use log::debug; + +use crate::config::*; +use crate::storage; +use super::*; + +pub struct LdapLoginProvider { + ldap_server: String, + + pre_bind_on_login: bool, + bind_dn_and_pw: Option<(String, String)>, + + search_base: String, + attrs_to_retrieve: Vec<String>, + username_attr: String, + mail_attr: String, + crypto_root_attr: String, + + storage_specific: StorageSpecific, + in_memory_store: storage::in_memory::MemDb, + garage_store: storage::garage::GarageRoot, +} + +enum BucketSource { + Constant(String), + Attr(String), +} + +enum StorageSpecific { + InMemory, + Garage { + from_config: LdapGarageConfig, + bucket_source: BucketSource, + }, +} + +impl LdapLoginProvider { + pub fn new(config: LoginLdapConfig) -> Result<Self> { + let bind_dn_and_pw = match (config.bind_dn, config.bind_password) { + (Some(dn), Some(pw)) => Some((dn, pw)), + (None, None) => None, + _ => bail!( + "If either of `bind_dn` or `bind_password` is set, the other must be set as well." + ), + }; + + if config.pre_bind_on_login && bind_dn_and_pw.is_none() { + bail!("Cannot use `pre_bind_on_login` without setting `bind_dn` and `bind_password`"); + } + + let mut attrs_to_retrieve = vec![ + config.username_attr.clone(), + config.mail_attr.clone(), + config.crypto_root_attr.clone(), + ]; + + // storage specific + let specific = match config.storage { + LdapStorage::InMemory => StorageSpecific::InMemory, + LdapStorage::Garage(grgconf) => { + attrs_to_retrieve.push(grgconf.aws_access_key_id_attr.clone()); + attrs_to_retrieve.push(grgconf.aws_secret_access_key_attr.clone()); + + let bucket_source = + match (grgconf.default_bucket.clone(), grgconf.bucket_attr.clone()) { + (Some(b), None) => BucketSource::Constant(b), + (None, Some(a)) => BucketSource::Attr(a), + _ => bail!("Must set `bucket` or `bucket_attr`, but not both"), + }; + + if let BucketSource::Attr(a) = &bucket_source { + attrs_to_retrieve.push(a.clone()); + } + + StorageSpecific::Garage { + from_config: grgconf, + bucket_source, + } + } + }; + + Ok(Self { + ldap_server: config.ldap_server, + pre_bind_on_login: config.pre_bind_on_login, + bind_dn_and_pw, + search_base: config.search_base, + attrs_to_retrieve, + username_attr: config.username_attr, + mail_attr: config.mail_attr, + crypto_root_attr: config.crypto_root_attr, + storage_specific: specific, + //@FIXME should be created outside of the login provider + //Login provider should return only a cryptoroot + a storage URI + //storage URI that should be resolved outside... + in_memory_store: storage::in_memory::MemDb::new(), + garage_store: storage::garage::GarageRoot::new()?, + }) + } + + async fn storage_creds_from_ldap_user(&self, user: &SearchEntry) -> Result<Builder> { + let storage: Builder = match &self.storage_specific { + StorageSpecific::InMemory => { + self.in_memory_store + .builder(&get_attr(user, &self.username_attr)?) + .await + } + StorageSpecific::Garage { + from_config, + bucket_source, + } => { + let aws_access_key_id = get_attr(user, &from_config.aws_access_key_id_attr)?; + let aws_secret_access_key = + get_attr(user, &from_config.aws_secret_access_key_attr)?; + let bucket = match bucket_source { + BucketSource::Constant(b) => b.clone(), + BucketSource::Attr(a) => get_attr(user, &a)?, + }; + + self.garage_store.user(storage::garage::GarageConf { + region: from_config.aws_region.clone(), + s3_endpoint: from_config.s3_endpoint.clone(), + k2v_endpoint: from_config.k2v_endpoint.clone(), + aws_access_key_id, + aws_secret_access_key, + bucket, + })? + } + }; + + Ok(storage) + } +} + +#[async_trait] +impl LoginProvider for LdapLoginProvider { + async fn login(&self, username: &str, password: &str) -> Result<Credentials> { + check_identifier(username)?; + + let (conn, mut ldap) = LdapConnAsync::new(&self.ldap_server).await?; + ldap3::drive!(conn); + + if self.pre_bind_on_login { + let (dn, pw) = self.bind_dn_and_pw.as_ref().unwrap(); + ldap.simple_bind(dn, pw).await?.success()?; + } + + let (matches, _res) = ldap + .search( + &self.search_base, + Scope::Subtree, + &format!( + "(&(objectClass=inetOrgPerson)({}={}))", + self.username_attr, username + ), + &self.attrs_to_retrieve, + ) + .await? + .success()?; + + if matches.is_empty() { + bail!("Invalid username"); + } + if matches.len() > 1 { + bail!("Invalid username (multiple matching accounts)"); + } + let user = SearchEntry::construct(matches.into_iter().next().unwrap()); + debug!( + "Found matching LDAP user for username {}: {}", + username, user.dn + ); + + // Try to login against LDAP server with provided password + // to check user's password + ldap.simple_bind(&user.dn, password) + .await? + .success() + .context("Invalid password")?; + debug!("Ldap login with user name {} successfull", username); + + // cryptography + let crstr = get_attr(&user, &self.crypto_root_attr)?; + let cr = CryptoRoot(crstr); + let keys = cr.crypto_keys(password)?; + + // storage + let storage = self.storage_creds_from_ldap_user(&user).await?; + + drop(ldap); + + Ok(Credentials { storage, keys }) + } + + async fn public_login(&self, email: &str) -> Result<PublicCredentials> { + check_identifier(email)?; + + let (dn, pw) = match self.bind_dn_and_pw.as_ref() { + Some(x) => x, + None => bail!("Missing bind_dn and bind_password in LDAP login provider config"), + }; + + let (conn, mut ldap) = LdapConnAsync::new(&self.ldap_server).await?; + ldap3::drive!(conn); + ldap.simple_bind(dn, pw).await?.success()?; + + let (matches, _res) = ldap + .search( + &self.search_base, + Scope::Subtree, + &format!( + "(&(objectClass=inetOrgPerson)({}={}))", + self.mail_attr, email + ), + &self.attrs_to_retrieve, + ) + .await? + .success()?; + + if matches.is_empty() { + bail!("No such user account"); + } + if matches.len() > 1 { + bail!("Multiple matching user accounts"); + } + let user = SearchEntry::construct(matches.into_iter().next().unwrap()); + debug!("Found matching LDAP user for email {}: {}", email, user.dn); + + // cryptography + let crstr = get_attr(&user, &self.crypto_root_attr)?; + let cr = CryptoRoot(crstr); + let public_key = cr.public_key()?; + + // storage + let storage = self.storage_creds_from_ldap_user(&user).await?; + drop(ldap); + + Ok(PublicCredentials { + storage, + public_key, + }) + } +} + +fn get_attr(user: &SearchEntry, attr: &str) -> Result<String> { + Ok(user + .attrs + .get(attr) + .ok_or(anyhow!("Missing attr: {}", attr))? + .iter() + .next() + .ok_or(anyhow!("No value for attr: {}", attr))? + .clone()) +} + +fn check_identifier(id: &str) -> Result<()> { + let is_ok = id + .chars() + .all(|c| c.is_alphanumeric() || "-+_.@".contains(c)); + if !is_ok { + bail!("Invalid username/email address, must contain only a-z A-Z 0-9 - + _ . @"); + } + Ok(()) +} diff --git a/aero-user/src/login/mod.rs b/aero-user/src/login/mod.rs new file mode 100644 index 0000000..5e54b4a --- /dev/null +++ b/aero-user/src/login/mod.rs @@ -0,0 +1,245 @@ +pub mod demo_provider; +pub mod ldap_provider; +pub mod static_provider; + +use std::sync::Arc; + +use anyhow::{anyhow, bail, Context, Result}; +use async_trait::async_trait; +use base64::Engine; +use rand::prelude::*; + +use crate::cryptoblob::*; +use crate::storage::*; + +/// The trait LoginProvider defines the interface for a login provider that allows +/// to retrieve storage and cryptographic credentials for access to a user account +/// from their username and password. +#[async_trait] +pub trait LoginProvider { + /// The login method takes an account's password as an input to decypher + /// decryption keys and obtain full access to the user's account. + async fn login(&self, username: &str, password: &str) -> Result<Credentials>; + /// The public_login method takes an account's email address and returns + /// public credentials for adding mails to the user's inbox. + async fn public_login(&self, email: &str) -> Result<PublicCredentials>; +} + +/// ArcLoginProvider is simply an alias on a structure that is used +/// in many places in the code +pub type ArcLoginProvider = Arc<dyn LoginProvider + Send + Sync>; + +/// The struct Credentials represent all of the necessary information to interact +/// with a user account's data after they are logged in. +#[derive(Clone, Debug)] +pub struct Credentials { + /// The storage credentials are used to authenticate access to the underlying storage (S3, K2V) + pub storage: Builder, + /// The cryptographic keys are used to encrypt and decrypt data stored in S3 and K2V + pub keys: CryptoKeys, +} + +#[derive(Clone, Debug)] +pub struct PublicCredentials { + /// The storage credentials are used to authenticate access to the underlying storage (S3, K2V) + pub storage: Builder, + pub public_key: PublicKey, +} + +use serde::{Deserialize, Serialize}; +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct CryptoRoot(pub String); + +impl CryptoRoot { + pub fn create_pass(password: &str, k: &CryptoKeys) -> Result<Self> { + let bytes = k.password_seal(password)?; + let b64 = base64::engine::general_purpose::STANDARD_NO_PAD.encode(bytes); + let cr = format!("aero:cryptoroot:pass:{}", b64); + Ok(Self(cr)) + } + + pub fn create_cleartext(k: &CryptoKeys) -> Self { + let bytes = k.serialize(); + let b64 = base64::engine::general_purpose::STANDARD_NO_PAD.encode(bytes); + let cr = format!("aero:cryptoroot:cleartext:{}", b64); + Self(cr) + } + + pub fn create_incoming(pk: &PublicKey) -> Self { + let bytes: &[u8] = &pk[..]; + let b64 = base64::engine::general_purpose::STANDARD_NO_PAD.encode(bytes); + let cr = format!("aero:cryptoroot:incoming:{}", b64); + Self(cr) + } + + pub fn public_key(&self) -> Result<PublicKey> { + match self.0.splitn(4, ':').collect::<Vec<&str>>()[..] { + ["aero", "cryptoroot", "pass", b64blob] => { + let blob = base64::engine::general_purpose::STANDARD_NO_PAD.decode(b64blob)?; + if blob.len() < 32 { + bail!( + "Decoded data is {} bytes long, expect at least 32 bytes", + blob.len() + ); + } + PublicKey::from_slice(&blob[..32]).context("must be a valid public key") + } + ["aero", "cryptoroot", "cleartext", b64blob] => { + let blob = base64::engine::general_purpose::STANDARD_NO_PAD.decode(b64blob)?; + Ok(CryptoKeys::deserialize(&blob)?.public) + } + ["aero", "cryptoroot", "incoming", b64blob] => { + let blob = base64::engine::general_purpose::STANDARD_NO_PAD.decode(b64blob)?; + if blob.len() < 32 { + bail!( + "Decoded data is {} bytes long, expect at least 32 bytes", + blob.len() + ); + } + PublicKey::from_slice(&blob[..32]).context("must be a valid public key") + } + ["aero", "cryptoroot", "keyring", _] => { + bail!("keyring is not yet implemented!") + } + _ => bail!(format!( + "passed string '{}' is not a valid cryptoroot", + self.0 + )), + } + } + pub fn crypto_keys(&self, password: &str) -> Result<CryptoKeys> { + match self.0.splitn(4, ':').collect::<Vec<&str>>()[..] { + ["aero", "cryptoroot", "pass", b64blob] => { + let blob = base64::engine::general_purpose::STANDARD_NO_PAD.decode(b64blob)?; + CryptoKeys::password_open(password, &blob) + } + ["aero", "cryptoroot", "cleartext", b64blob] => { + let blob = base64::engine::general_purpose::STANDARD_NO_PAD.decode(b64blob)?; + CryptoKeys::deserialize(&blob) + } + ["aero", "cryptoroot", "incoming", _] => { + bail!("incoming cryptoroot does not contain a crypto key!") + } + ["aero", "cryptoroot", "keyring", _] => { + bail!("keyring is not yet implemented!") + } + _ => bail!(format!( + "passed string '{}' is not a valid cryptoroot", + self.0 + )), + } + } +} + +/// The struct CryptoKeys contains the cryptographic keys used to encrypt and decrypt +/// data in a user's mailbox. +#[derive(Clone, Debug)] +pub struct CryptoKeys { + /// Master key for symmetric encryption of mailbox data + pub master: Key, + /// Public/private keypair for encryption of incomming emails (secret part) + pub secret: SecretKey, + /// Public/private keypair for encryption of incomming emails (public part) + pub public: PublicKey, +} + +// ---- + +impl CryptoKeys { + /// Initialize a new cryptography root + pub fn init() -> Self { + let (public, secret) = gen_keypair(); + let master = gen_key(); + CryptoKeys { + master, + secret, + public, + } + } + + // Clear text serialize/deserialize + /// Serialize the root as bytes without encryption + fn serialize(&self) -> [u8; 64] { + let mut res = [0u8; 64]; + res[..32].copy_from_slice(self.master.as_ref()); + res[32..].copy_from_slice(self.secret.as_ref()); + res + } + + /// Deserialize a clear text crypto root without encryption + fn deserialize(bytes: &[u8]) -> Result<Self> { + if bytes.len() != 64 { + bail!("Invalid length: {}, expected 64", bytes.len()); + } + let master = Key::from_slice(&bytes[..32]).unwrap(); + let secret = SecretKey::from_slice(&bytes[32..]).unwrap(); + let public = secret.public_key(); + Ok(Self { + master, + secret, + public, + }) + } + + // Password sealed keys serialize/deserialize + pub fn password_open(password: &str, blob: &[u8]) -> Result<Self> { + let _pubkey = &blob[0..32]; + let kdf_salt = &blob[32..64]; + let password_openned = try_open_encrypted_keys(kdf_salt, password, &blob[64..])?; + + let keys = Self::deserialize(&password_openned)?; + Ok(keys) + } + + pub fn password_seal(&self, password: &str) -> Result<Vec<u8>> { + let mut kdf_salt = [0u8; 32]; + thread_rng().fill(&mut kdf_salt); + + // Calculate key for password secret box + let password_key = derive_password_key(&kdf_salt, password)?; + + // Seal a secret box that contains our crypto keys + let password_sealed = seal(&self.serialize(), &password_key)?; + + // Create blob + let password_blob = [&self.public[..], &kdf_salt[..], &password_sealed].concat(); + + Ok(password_blob) + } +} + +fn derive_password_key(kdf_salt: &[u8], password: &str) -> Result<Key> { + Ok(Key::from_slice(&argon2_kdf(kdf_salt, password.as_bytes(), 32)?).unwrap()) +} + +fn try_open_encrypted_keys( + kdf_salt: &[u8], + password: &str, + encrypted_keys: &[u8], +) -> Result<Vec<u8>> { + let password_key = derive_password_key(kdf_salt, password)?; + open(encrypted_keys, &password_key) +} + +// ---- UTIL ---- + +pub fn argon2_kdf(salt: &[u8], password: &[u8], output_len: usize) -> Result<Vec<u8>> { + use argon2::{password_hash, Algorithm, Argon2, ParamsBuilder, PasswordHasher, Version}; + + let params = ParamsBuilder::new() + .output_len(output_len) + .build() + .map_err(|e| anyhow!("Invalid argon2 params: {}", e))?; + let argon2 = Argon2::new(Algorithm::default(), Version::default(), params); + + let b64_salt = base64::engine::general_purpose::STANDARD_NO_PAD.encode(salt); + let valid_salt = password_hash::Salt::from_b64(&b64_salt) + .map_err(|e| anyhow!("Invalid salt, error {}", e))?; + let hash = argon2 + .hash_password(password, valid_salt) + .map_err(|e| anyhow!("Unable to hash: {}", e))?; + + let hash = hash.hash.ok_or(anyhow!("Missing output"))?; + assert!(hash.len() == output_len); + Ok(hash.as_bytes().to_vec()) +} diff --git a/aero-user/src/login/static_provider.rs b/aero-user/src/login/static_provider.rs new file mode 100644 index 0000000..ed39343 --- /dev/null +++ b/aero-user/src/login/static_provider.rs @@ -0,0 +1,188 @@ +use std::collections::HashMap; +use std::path::PathBuf; + +use anyhow::{anyhow, bail}; +use async_trait::async_trait; +use tokio::signal::unix::{signal, SignalKind}; +use tokio::sync::watch; + +use crate::config::*; +use crate::login::*; +use crate::storage; + +pub struct ContextualUserEntry { + pub username: String, + pub config: UserEntry, +} + +#[derive(Default)] +pub struct UserDatabase { + users: HashMap<String, Arc<ContextualUserEntry>>, + users_by_email: HashMap<String, Arc<ContextualUserEntry>>, +} + +pub struct StaticLoginProvider { + user_db: watch::Receiver<UserDatabase>, + in_memory_store: storage::in_memory::MemDb, + garage_store: storage::garage::GarageRoot, +} + +pub async fn update_user_list(config: PathBuf, up: watch::Sender<UserDatabase>) -> Result<()> { + let mut stream = signal(SignalKind::user_defined1()) + .expect("failed to install SIGUSR1 signal hander for reload"); + + loop { + let ulist: UserList = match read_config(config.clone()) { + Ok(x) => x, + Err(e) => { + tracing::warn!(path=%config.as_path().to_string_lossy(), error=%e, "Unable to load config"); + stream.recv().await; + continue; + } + }; + + let users = ulist + .into_iter() + .map(|(username, config)| { + ( + username.clone(), + Arc::new(ContextualUserEntry { username, config }), + ) + }) + .collect::<HashMap<_, _>>(); + + let mut users_by_email = HashMap::new(); + for (_, u) in users.iter() { + for m in u.config.email_addresses.iter() { + if users_by_email.contains_key(m) { + tracing::warn!("Several users have the same email address: {}", m); + stream.recv().await; + continue; + } + users_by_email.insert(m.clone(), u.clone()); + } + } + + tracing::info!("{} users loaded", users.len()); + up.send(UserDatabase { + users, + users_by_email, + }) + .context("update user db config")?; + stream.recv().await; + tracing::info!("Received SIGUSR1, reloading"); + } +} + +impl StaticLoginProvider { + pub async fn new(config: LoginStaticConfig) -> Result<Self> { + let (tx, mut rx) = watch::channel(UserDatabase::default()); + + tokio::spawn(update_user_list(config.user_list, tx)); + rx.changed().await?; + + Ok(Self { + user_db: rx, + in_memory_store: storage::in_memory::MemDb::new(), + garage_store: storage::garage::GarageRoot::new()?, + }) + } +} + +#[async_trait] +impl LoginProvider for StaticLoginProvider { + async fn login(&self, username: &str, password: &str) -> Result<Credentials> { + tracing::debug!(user=%username, "login"); + let user = { + let user_db = self.user_db.borrow(); + match user_db.users.get(username) { + None => bail!("User {} does not exist", username), + Some(u) => u.clone(), + } + }; + + tracing::debug!(user=%username, "verify password"); + if !verify_password(password, &user.config.password)? { + bail!("Wrong password"); + } + + tracing::debug!(user=%username, "fetch keys"); + let storage: storage::Builder = match &user.config.storage { + StaticStorage::InMemory => self.in_memory_store.builder(username).await, + StaticStorage::Garage(grgconf) => { + self.garage_store.user(storage::garage::GarageConf { + region: grgconf.aws_region.clone(), + k2v_endpoint: grgconf.k2v_endpoint.clone(), + s3_endpoint: grgconf.s3_endpoint.clone(), + aws_access_key_id: grgconf.aws_access_key_id.clone(), + aws_secret_access_key: grgconf.aws_secret_access_key.clone(), + bucket: grgconf.bucket.clone(), + })? + } + }; + + let cr = CryptoRoot(user.config.crypto_root.clone()); + let keys = cr.crypto_keys(password)?; + + tracing::debug!(user=%username, "logged"); + Ok(Credentials { storage, keys }) + } + + async fn public_login(&self, email: &str) -> Result<PublicCredentials> { + let user = { + let user_db = self.user_db.borrow(); + match user_db.users_by_email.get(email) { + None => bail!("Email {} does not exist", email), + Some(u) => u.clone(), + } + }; + tracing::debug!(user=%user.username, "public_login"); + + let storage: storage::Builder = match &user.config.storage { + StaticStorage::InMemory => self.in_memory_store.builder(&user.username).await, + StaticStorage::Garage(grgconf) => { + self.garage_store.user(storage::garage::GarageConf { + region: grgconf.aws_region.clone(), + k2v_endpoint: grgconf.k2v_endpoint.clone(), + s3_endpoint: grgconf.s3_endpoint.clone(), + aws_access_key_id: grgconf.aws_access_key_id.clone(), + aws_secret_access_key: grgconf.aws_secret_access_key.clone(), + bucket: grgconf.bucket.clone(), + })? + } + }; + + let cr = CryptoRoot(user.config.crypto_root.clone()); + let public_key = cr.public_key()?; + + Ok(PublicCredentials { + storage, + public_key, + }) + } +} + +pub fn hash_password(password: &str) -> Result<String> { + use argon2::{ + password_hash::{rand_core::OsRng, PasswordHasher, SaltString}, + Argon2, + }; + let salt = SaltString::generate(&mut OsRng); + let argon2 = Argon2::default(); + Ok(argon2 + .hash_password(password.as_bytes(), &salt) + .map_err(|e| anyhow!("Argon2 error: {}", e))? + .to_string()) +} + +pub fn verify_password(password: &str, hash: &str) -> Result<bool> { + use argon2::{ + password_hash::{PasswordHash, PasswordVerifier}, + Argon2, + }; + let parsed_hash = + PasswordHash::new(hash).map_err(|e| anyhow!("Invalid hashed password: {}", e))?; + Ok(Argon2::default() + .verify_password(password.as_bytes(), &parsed_hash) + .is_ok()) +} |