aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/config.rs33
-rw-r--r--src/login/mod.rs43
-rw-r--r--src/login/static_provider.rs44
-rw-r--r--src/main.rs146
-rw-r--r--src/server.rs40
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);