diff options
-rw-r--r-- | src/config.rs | 33 | ||||
-rw-r--r-- | src/login/mod.rs | 43 | ||||
-rw-r--r-- | src/login/static_provider.rs | 44 | ||||
-rw-r--r-- | src/main.rs | 146 | ||||
-rw-r--r-- | src/server.rs | 40 |
5 files changed, 177 insertions, 129 deletions
diff --git a/src/config.rs b/src/config.rs index 286ef65..5bd7380 100644 --- a/src/config.rs +++ b/src/config.rs @@ -12,7 +12,7 @@ pub struct CompanionConfig { pub imap: ImapConfig, #[serde(flatten)] - pub users: LoginStaticUser, + pub users: LoginStaticConfig, } #[derive(Serialize, Deserialize, Debug, Clone)] @@ -26,7 +26,7 @@ pub struct ProviderConfig { #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(tag = "user_driver")] pub enum UserManagement { - Static(LoginStaticUser), + Static(LoginStaticConfig), Ldap(LoginLdapConfig), } @@ -42,8 +42,8 @@ pub struct ImapConfig { } #[derive(Serialize, Deserialize, Debug, Clone)] -pub struct LoginStaticUser { - pub user_list: String, +pub struct LoginStaticConfig { + pub user_list: PathBuf, } #[derive(Serialize, Deserialize, Debug, Clone)] @@ -107,21 +107,40 @@ pub struct StaticGarageConfig { pub bucket: String, } +pub type UserList = HashMap<String, UserEntry>; + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(tag = "crypto_root")] +pub enum CryptographyRoot { + PasswordProtected, + Keyring, + InPlace { + master_key: String, + secret_key: String, + } +} + #[derive(Serialize, Deserialize, Debug, Clone)] pub struct UserEntry { #[serde(default)] pub email_addresses: Vec<String>, pub password: String, - pub master_key: Option<String>, - pub secret_key: Option<String>, + pub crypto_root: CryptographyRoot, #[serde(flatten)] pub storage: StaticStorage, } +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(tag = "role")] +pub enum AnyConfig { + Companion(CompanionConfig), + Provider(ProviderConfig), +} + // --- -pub fn read_config(config_file: PathBuf) -> Result<Config> { +pub fn read_config<'a, T: Deserialize<'a>>(config_file: PathBuf) -> Result<T> { let mut file = std::fs::OpenOptions::new() .read(true) .open(config_file.as_path())?; diff --git a/src/login/mod.rs b/src/login/mod.rs index 216c340..a9b9efe 100644 --- a/src/login/mod.rs +++ b/src/login/mod.rs @@ -45,6 +45,7 @@ pub struct PublicCredentials { pub public_key: PublicKey, } +/* /// The struct UserSecrets represents intermediary secrets that are mixed in with the user's /// password when decrypting the cryptographic keys that are stored in their bucket. /// These secrets should be stored somewhere else (e.g. in the LDAP server or in the @@ -57,6 +58,7 @@ pub struct UserSecrets { /// with old passwords pub alternate_user_secrets: Vec<String>, } +*/ /// The struct CryptoKeys contains the cryptographic keys used to encrypt and decrypt /// data in a user's mailbox. @@ -85,7 +87,6 @@ impl Credentials { impl CryptoKeys { pub async fn init( storage: &Builders, - user_secrets: &UserSecrets, password: &str, ) -> Result<Self> { // Check that salt and public don't exist already @@ -113,7 +114,7 @@ impl CryptoKeys { thread_rng().fill(&mut kdf_salt); // Calculate key for password secret box - let password_key = user_secrets.derive_password_key(&kdf_salt, password)?; + let password_key = derive_password_key(&kdf_salt, password)?; // Seal a secret box that contains our crypto keys let password_sealed = seal(&keys.serialize(), &password_key)?; @@ -169,7 +170,6 @@ impl CryptoKeys { pub async fn open( storage: &Builders, - user_secrets: &UserSecrets, password: &str, ) -> Result<Self> { let k2v = storage.row_store()?; @@ -200,8 +200,7 @@ impl CryptoKeys { // Try to open blob let kdf_salt = &password_blob[..32]; - let password_openned = - user_secrets.try_open_encrypted_keys(kdf_salt, password, &password_blob[32..])?; + let password_openned = try_open_encrypted_keys(kdf_salt, password, &password_blob[32..])?; let keys = Self::deserialize(&password_openned)?; if keys.public != expected_public { @@ -238,7 +237,6 @@ impl CryptoKeys { pub async fn add_password( &self, storage: &Builders, - user_secrets: &UserSecrets, password: &str, ) -> Result<()> { let k2v = storage.row_store()?; @@ -252,7 +250,7 @@ impl CryptoKeys { thread_rng().fill(&mut kdf_salt); // Calculate key for password secret box - let password_key = user_secrets.derive_password_key(&kdf_salt, password)?; + 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)?; @@ -418,32 +416,13 @@ impl CryptoKeys { } } -impl UserSecrets { - fn derive_password_key_with(user_secret: &str, kdf_salt: &[u8], password: &str) -> Result<Key> { - let tmp = format!("{}\n\n{}", user_secret, password); - Ok(Key::from_slice(&argon2_kdf(kdf_salt, tmp.as_bytes(), 32)?).unwrap()) - } - - fn derive_password_key(&self, kdf_salt: &[u8], password: &str) -> Result<Key> { - Self::derive_password_key_with(&self.user_secret, kdf_salt, password) - } +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( - &self, - kdf_salt: &[u8], - password: &str, - encrypted_keys: &[u8], - ) -> Result<Vec<u8>> { - let secrets_to_try = - std::iter::once(&self.user_secret).chain(self.alternate_user_secrets.iter()); - for user_secret in secrets_to_try { - let password_key = Self::derive_password_key_with(user_secret, kdf_salt, password)?; - if let Ok(res) = open(encrypted_keys, &password_key) { - return Ok(res); - } - } - bail!("Unable to decrypt password blob."); - } +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 ---- diff --git a/src/login/static_provider.rs b/src/login/static_provider.rs index 0b726cb..3f6a840 100644 --- a/src/login/static_provider.rs +++ b/src/login/static_provider.rs @@ -1,5 +1,6 @@ use std::collections::HashMap; use std::sync::Arc; +use std::path::PathBuf; use anyhow::{anyhow, bail, Result}; use async_trait::async_trait; @@ -10,13 +11,28 @@ use crate::login::*; use crate::storage; pub struct StaticLoginProvider { - users: HashMap<String, Arc<LoginStaticUser>>, - users_by_email: HashMap<String, Arc<LoginStaticUser>>, + user_list: PathBuf, + users: HashMap<String, Arc<UserEntry>>, + users_by_email: HashMap<String, Arc<UserEntry>>, } impl StaticLoginProvider { pub fn new(config: LoginStaticConfig) -> Result<Self> { - let users = config + let mut lp = Self { + user_list: config.user_list, + users: HashMap::new(), + users_by_email: HashMap::new(), + }; + + lp.update_user_list(); + + Ok(lp) + } + + pub fn update_user_list(&mut self) -> Result<()> { + let ulist: UserList = read_config(self.user_list)?; + + let users = ulist .into_iter() .map(|(k, v)| (k, Arc::new(v))) .collect::<HashMap<_, _>>(); @@ -29,11 +45,7 @@ impl StaticLoginProvider { users_by_email.insert(m.clone(), u.clone()); } } - - Ok(Self { - users, - users_by_email, - }) + Ok(()) } } @@ -64,24 +76,18 @@ impl LoginProvider for StaticLoginProvider { }), }; - let keys = match (&user.master_key, &user.secret_key) { - (Some(m), Some(s)) => { + let keys = match user.crypto_root { /*(&user.master_key, &user.secret_key) {*/ + CryptographyRoot::InPlace { master_key: m, secret_key: s } => { let master_key = Key::from_slice(&base64::decode(m)?).ok_or(anyhow!("Invalid master key"))?; let secret_key = SecretKey::from_slice(&base64::decode(s)?) .ok_or(anyhow!("Invalid secret key"))?; CryptoKeys::open_without_password(&storage, &master_key, &secret_key).await? } - (None, None) => { - let user_secrets = UserSecrets { - user_secret: user.user_secret.clone(), - alternate_user_secrets: user.alternate_user_secrets.clone(), - }; - CryptoKeys::open(&storage, &user_secrets, password).await? + CryptographyRoot::PasswordProtected => { + CryptoKeys::open(&storage, password).await? } - _ => bail!( - "Either both master and secret key or none of them must be specified for user" - ), + CryptographyRoot::Keyring => unimplemented!(), }; tracing::debug!(user=%username, "logged"); diff --git a/src/main.rs b/src/main.rs index a566ec6..7b567bc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -25,26 +25,59 @@ use server::Server; struct Args { #[clap(subcommand)] command: Command, + + #[clap(short, long, env = "CONFIG_FILE", default_value = "aerogramme.toml")] + config_file: PathBuf, } #[derive(Subcommand, Debug)] enum Command { - /// Runs the IMAP+LMTP server daemon - Server { - #[clap(short, long, env = "CONFIG_FILE", default_value = "aerogramme.toml")] - config_file: PathBuf, + #[clap(subcommand)] + Companion(CompanionCommand), + + #[clap(subcommand)] + Provider(ProviderCommand), + //Test, +} + +#[derive(Subcommand, Debug)] +enum CompanionCommand { + /// Runs the IMAP proxy + Daemon, + Reload { + #[clap(short, long, env = "AEROGRAMME_PID")] + pid: Option<u64>, }, - Test, + Wizard, + #[clap(subcommand)] + Account(AccountManagement), } -#[derive(Parser, Debug)] -struct UserSecretsArgs { - /// User secret - #[clap(short = 'U', long, env = "USER_SECRET")] - user_secret: String, - /// Alternate user secrets (comma-separated list of strings) - #[clap(long, env = "ALTERNATE_USER_SECRETS", default_value = "")] - alternate_user_secrets: String, +#[derive(Subcommand, Debug)] +enum ProviderCommand { + /// Runs the IMAP+LMTP server daemon + Daemon, + Reload, + #[clap(subcommand)] + Account(AccountManagement), +} + +#[derive(Subcommand, Debug)] +enum AccountManagement { + Add { + #[clap(short, long)] + login: String, + #[clap(short, long)] + setup: PathBuf, + }, + Delete { + #[clap(short, long)] + login: String, + }, + ChangePassword { + #[clap(short, long)] + login: String + }, } #[tokio::main] @@ -63,43 +96,62 @@ async fn main() -> Result<()> { tracing_subscriber::fmt::init(); let args = Args::parse(); + let any_config = read_config(args.config_file)?; - match args.command { - Command::Server { config_file } => { - let config = read_config(config_file)?; - - let server = Server::new(config).await?; - server.run().await?; - } - Command::Test => { - use std::collections::HashMap; - use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; - println!("--- message pack ---\n{:?}\n--- end ---\n", rmp_serde::to_vec(&Config { - lmtp: None, - imap: Some(ImapConfig { bind_addr: SocketAddr::new(IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)), 8080) }), - login_ldap: None, - login_static: Some(HashMap::from([ - ("alice".into(), LoginStaticUser { - password: "hash".into(), - user_secret: "hello".into(), - alternate_user_secrets: vec![], - email_addresses: vec![], - master_key: None, - secret_key: None, - storage: StaticStorage::Garage(StaticGarageConfig { - s3_endpoint: "http://".into(), - k2v_endpoint: "http://".into(), - aws_region: "garage".into(), - aws_access_key_id: "GK...".into(), - aws_secret_access_key: "xxx".into(), - bucket: "aerogramme".into(), - }), - }) - ])), - }).unwrap()); - } + match (args.command, any_config) { + (Command::Companion(subcommand), AnyConfig::Companion(config)) => match subcommand { + CompanionCommand::Daemon => { + let server = Server::from_companion_config(config).await?; + server.run().await?; + }, + CompanionCommand::Reload { pid } => { + unimplemented!(); + }, + CompanionCommand::Wizard => { + unimplemented!(); + }, + CompanionCommand::Account(cmd) => { + let user_file = config.users.user_list; + account_management(cmd, user_file); + } + }, + (Command::Provider(subcommand), AnyConfig::Provider(config)) => match subcommand { + ProviderCommand::Daemon => { + let server = Server::from_provider_config(config).await?; + server.run().await?; + }, + ProviderCommand::Reload => { + unimplemented!(); + }, + ProviderCommand::Account(cmd) => { + let user_file = match config.users { + UserManagement::Static(conf) => conf.user_list, + UserManagement::Ldap(_) => panic!("LDAP account management is not supported from Aerogramme.") + }; + account_management(cmd, user_file); + } + }, + (Command::Provider(_), AnyConfig::Companion(_)) => { + panic!("Your want to run a 'Provider' command but your configuration file has role 'Companion'."); + }, + (Command::Companion(_), AnyConfig::Provider(_)) => { + panic!("Your want to run a 'Companion' command but your configuration file has role 'Provider'."); + }, } Ok(()) } +fn account_management(cmd: AccountManagement, users: PathBuf) { + match cmd { + Add => { + unimplemented!(); + }, + Delete => { + unimplemented!(); + }, + ChangePassword => { + unimplemented!(); + }, + } +} diff --git a/src/server.rs b/src/server.rs index eca11ad..2321da8 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use anyhow::{bail, Result}; +use anyhow::Result; use futures::try_join; use log::*; use tokio::sync::watch; @@ -17,19 +17,24 @@ pub struct Server { } impl Server { - pub async fn new(config: Config) -> Result<Self> { - let (login, lmtp_conf, imap_conf) = build(config)?; + pub async fn from_companion_config(config: CompanionConfig) -> Result<Self> { + let login = Arc::new(StaticLoginProvider::new(config.users)?); - let lmtp_server = lmtp_conf.map(|cfg| LmtpServer::new(cfg, login.clone())); - let imap_server = match imap_conf { - Some(cfg) => Some(imap::new(cfg, login.clone()).await?), - None => None, + let lmtp_server = None; + let imap_server = Some(imap::new(config.imap, login).await?); + Ok(Self { lmtp_server, imap_server }) + } + + pub async fn from_provider_config(config: ProviderConfig) -> Result<Self> { + let login: ArcLoginProvider = match config.users { + UserManagement::Static(x) => Arc::new(StaticLoginProvider::new(x)?), + UserManagement::Ldap(x) => Arc::new(LdapLoginProvider::new(x)?), }; - Ok(Self { - lmtp_server, - imap_server, - }) + let lmtp_server = Some(LmtpServer::new(config.lmtp, login.clone())); + let imap_server = Some(imap::new(config.imap, login).await?); + + Ok(Self { lmtp_server, imap_server }) } pub async fn run(self) -> Result<()> { @@ -60,19 +65,6 @@ impl Server { } } -fn build(config: Config) -> Result<(ArcLoginProvider, Option<LmtpConfig>, Option<ImapConfig>)> { - let lp: ArcLoginProvider = match (config.login_static, config.login_ldap) { - (Some(st), None) => Arc::new(StaticLoginProvider::new(st)?), - (None, Some(ld)) => Arc::new(LdapLoginProvider::new(ld)?), - (Some(_), Some(_)) => { - bail!("A single login provider must be set up in config file") - } - (None, None) => bail!("No login provider is set up in config file"), - }; - - Ok((lp, config.lmtp, config.imap)) -} - pub fn watch_ctrl_c() -> (watch::Receiver<bool>, Arc<watch::Sender<bool>>) { let (send_cancel, watch_cancel) = watch::channel(false); let send_cancel = Arc::new(send_cancel); |