From 1a43ce5ac7033c148f64a033f2b1d335e95e11d5 Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Fri, 8 Mar 2024 08:17:03 +0100 Subject: WIP refactor --- src/login/demo_provider.rs | 51 --------- src/login/ldap_provider.rs | 265 ------------------------------------------- src/login/mod.rs | 245 --------------------------------------- src/login/static_provider.rs | 189 ------------------------------ 4 files changed, 750 deletions(-) delete mode 100644 src/login/demo_provider.rs delete mode 100644 src/login/ldap_provider.rs delete mode 100644 src/login/mod.rs delete mode 100644 src/login/static_provider.rs (limited to 'src/login') diff --git a/src/login/demo_provider.rs b/src/login/demo_provider.rs deleted file mode 100644 index 11c7d54..0000000 --- a/src/login/demo_provider.rs +++ /dev/null @@ -1,51 +0,0 @@ -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 { - 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 { - 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/src/login/ldap_provider.rs b/src/login/ldap_provider.rs deleted file mode 100644 index 0af5676..0000000 --- a/src/login/ldap_provider.rs +++ /dev/null @@ -1,265 +0,0 @@ -use anyhow::Result; -use async_trait::async_trait; -use ldap3::{LdapConnAsync, Scope, SearchEntry}; -use log::debug; - -use crate::config::*; -use crate::login::*; -use crate::storage; - -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, - 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 { - 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 { - 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 { - 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 { - 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 { - 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/src/login/mod.rs b/src/login/mod.rs deleted file mode 100644 index 4a1dee1..0000000 --- a/src/login/mod.rs +++ /dev/null @@ -1,245 +0,0 @@ -pub mod demo_provider; -pub mod ldap_provider; -pub mod static_provider; - -use base64::Engine; -use std::sync::Arc; - -use anyhow::{anyhow, bail, Context, Result}; -use async_trait::async_trait; -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; - /// 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; -} - -/// ArcLoginProvider is simply an alias on a structure that is used -/// in many places in the code -pub type ArcLoginProvider = Arc; - -/// 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 { - 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 { - match self.0.splitn(4, ':').collect::>()[..] { - ["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 { - match self.0.splitn(4, ':').collect::>()[..] { - ["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 { - 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 { - 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> { - 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 { - 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> { - 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> { - 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/src/login/static_provider.rs b/src/login/static_provider.rs deleted file mode 100644 index 79626df..0000000 --- a/src/login/static_provider.rs +++ /dev/null @@ -1,189 +0,0 @@ -use std::collections::HashMap; -use std::path::PathBuf; -use std::sync::Arc; -use tokio::signal::unix::{signal, SignalKind}; -use tokio::sync::watch; - -use anyhow::{anyhow, bail, Result}; -use async_trait::async_trait; - -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>, - users_by_email: HashMap>, -} - -pub struct StaticLoginProvider { - user_db: watch::Receiver, - in_memory_store: storage::in_memory::MemDb, - garage_store: storage::garage::GarageRoot, -} - -pub async fn update_user_list(config: PathBuf, up: watch::Sender) -> 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::>(); - - 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 { - 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 { - 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 { - 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 { - 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 { - 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()) -} -- cgit v1.2.3