WIP refactor

This commit is contained in:
Quentin 2023-12-06 20:57:25 +01:00
parent 2779837a37
commit 3ddbce4529
Signed by: quentin
GPG key ID: E9602264D639FF68
5 changed files with 178 additions and 130 deletions

View file

@ -12,7 +12,7 @@ pub struct CompanionConfig {
pub imap: ImapConfig, pub imap: ImapConfig,
#[serde(flatten)] #[serde(flatten)]
pub users: LoginStaticUser, pub users: LoginStaticConfig,
} }
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone)]
@ -26,7 +26,7 @@ pub struct ProviderConfig {
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(tag = "user_driver")] #[serde(tag = "user_driver")]
pub enum UserManagement { pub enum UserManagement {
Static(LoginStaticUser), Static(LoginStaticConfig),
Ldap(LoginLdapConfig), Ldap(LoginLdapConfig),
} }
@ -42,8 +42,8 @@ pub struct ImapConfig {
} }
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone)]
pub struct LoginStaticUser { pub struct LoginStaticConfig {
pub user_list: String, pub user_list: PathBuf,
} }
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone)]
@ -107,21 +107,40 @@ pub struct StaticGarageConfig {
pub bucket: String, 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)] #[derive(Serialize, Deserialize, Debug, Clone)]
pub struct UserEntry { pub struct UserEntry {
#[serde(default)] #[serde(default)]
pub email_addresses: Vec<String>, pub email_addresses: Vec<String>,
pub password: String, pub password: String,
pub master_key: Option<String>, pub crypto_root: CryptographyRoot,
pub secret_key: Option<String>,
#[serde(flatten)] #[serde(flatten)]
pub storage: StaticStorage, 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() let mut file = std::fs::OpenOptions::new()
.read(true) .read(true)
.open(config_file.as_path())?; .open(config_file.as_path())?;

View file

@ -45,6 +45,7 @@ pub struct PublicCredentials {
pub public_key: PublicKey, pub public_key: PublicKey,
} }
/*
/// The struct UserSecrets represents intermediary secrets that are mixed in with the user's /// 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. /// 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 /// 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 /// with old passwords
pub alternate_user_secrets: Vec<String>, pub alternate_user_secrets: Vec<String>,
} }
*/
/// The struct CryptoKeys contains the cryptographic keys used to encrypt and decrypt /// The struct CryptoKeys contains the cryptographic keys used to encrypt and decrypt
/// data in a user's mailbox. /// data in a user's mailbox.
@ -85,7 +87,6 @@ impl Credentials {
impl CryptoKeys { impl CryptoKeys {
pub async fn init( pub async fn init(
storage: &Builders, storage: &Builders,
user_secrets: &UserSecrets,
password: &str, password: &str,
) -> Result<Self> { ) -> Result<Self> {
// Check that salt and public don't exist already // Check that salt and public don't exist already
@ -113,7 +114,7 @@ impl CryptoKeys {
thread_rng().fill(&mut kdf_salt); thread_rng().fill(&mut kdf_salt);
// Calculate key for password secret box // 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 // Seal a secret box that contains our crypto keys
let password_sealed = seal(&keys.serialize(), &password_key)?; let password_sealed = seal(&keys.serialize(), &password_key)?;
@ -169,7 +170,6 @@ impl CryptoKeys {
pub async fn open( pub async fn open(
storage: &Builders, storage: &Builders,
user_secrets: &UserSecrets,
password: &str, password: &str,
) -> Result<Self> { ) -> Result<Self> {
let k2v = storage.row_store()?; let k2v = storage.row_store()?;
@ -200,8 +200,7 @@ impl CryptoKeys {
// Try to open blob // Try to open blob
let kdf_salt = &password_blob[..32]; let kdf_salt = &password_blob[..32];
let password_openned = let password_openned = try_open_encrypted_keys(kdf_salt, password, &password_blob[32..])?;
user_secrets.try_open_encrypted_keys(kdf_salt, password, &password_blob[32..])?;
let keys = Self::deserialize(&password_openned)?; let keys = Self::deserialize(&password_openned)?;
if keys.public != expected_public { if keys.public != expected_public {
@ -238,7 +237,6 @@ impl CryptoKeys {
pub async fn add_password( pub async fn add_password(
&self, &self,
storage: &Builders, storage: &Builders,
user_secrets: &UserSecrets,
password: &str, password: &str,
) -> Result<()> { ) -> Result<()> {
let k2v = storage.row_store()?; let k2v = storage.row_store()?;
@ -252,7 +250,7 @@ impl CryptoKeys {
thread_rng().fill(&mut kdf_salt); thread_rng().fill(&mut kdf_salt);
// Calculate key for password secret box // 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 // Seal a secret box that contains our crypto keys
let password_sealed = seal(&self.serialize(), &password_key)?; let password_sealed = seal(&self.serialize(), &password_key)?;
@ -418,32 +416,13 @@ impl CryptoKeys {
} }
} }
impl UserSecrets { fn derive_password_key(kdf_salt: &[u8], password: &str) -> Result<Key> {
fn derive_password_key_with(user_secret: &str, kdf_salt: &[u8], password: &str) -> Result<Key> { Ok(Key::from_slice(&argon2_kdf(kdf_salt, password.as_bytes(), 32)?).unwrap())
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> { fn try_open_encrypted_keys(kdf_salt: &[u8], password: &str, encrypted_keys: &[u8]) -> Result<Vec<u8>> {
Self::derive_password_key_with(&self.user_secret, kdf_salt, password) let password_key = derive_password_key(kdf_salt, password)?;
} open(encrypted_keys, &password_key)
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.");
}
} }
// ---- UTIL ---- // ---- UTIL ----

View file

@ -1,5 +1,6 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
use std::path::PathBuf;
use anyhow::{anyhow, bail, Result}; use anyhow::{anyhow, bail, Result};
use async_trait::async_trait; use async_trait::async_trait;
@ -10,13 +11,28 @@ use crate::login::*;
use crate::storage; use crate::storage;
pub struct StaticLoginProvider { pub struct StaticLoginProvider {
users: HashMap<String, Arc<LoginStaticUser>>, user_list: PathBuf,
users_by_email: HashMap<String, Arc<LoginStaticUser>>, users: HashMap<String, Arc<UserEntry>>,
users_by_email: HashMap<String, Arc<UserEntry>>,
} }
impl StaticLoginProvider { impl StaticLoginProvider {
pub fn new(config: LoginStaticConfig) -> Result<Self> { 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() .into_iter()
.map(|(k, v)| (k, Arc::new(v))) .map(|(k, v)| (k, Arc::new(v)))
.collect::<HashMap<_, _>>(); .collect::<HashMap<_, _>>();
@ -29,11 +45,7 @@ impl StaticLoginProvider {
users_by_email.insert(m.clone(), u.clone()); users_by_email.insert(m.clone(), u.clone());
} }
} }
Ok(())
Ok(Self {
users,
users_by_email,
})
} }
} }
@ -64,24 +76,18 @@ impl LoginProvider for StaticLoginProvider {
}), }),
}; };
let keys = match (&user.master_key, &user.secret_key) { let keys = match user.crypto_root { /*(&user.master_key, &user.secret_key) {*/
(Some(m), Some(s)) => { CryptographyRoot::InPlace { master_key: m, secret_key: s } => {
let master_key = let master_key =
Key::from_slice(&base64::decode(m)?).ok_or(anyhow!("Invalid master key"))?; Key::from_slice(&base64::decode(m)?).ok_or(anyhow!("Invalid master key"))?;
let secret_key = SecretKey::from_slice(&base64::decode(s)?) let secret_key = SecretKey::from_slice(&base64::decode(s)?)
.ok_or(anyhow!("Invalid secret key"))?; .ok_or(anyhow!("Invalid secret key"))?;
CryptoKeys::open_without_password(&storage, &master_key, &secret_key).await? CryptoKeys::open_without_password(&storage, &master_key, &secret_key).await?
} }
(None, None) => { CryptographyRoot::PasswordProtected => {
let user_secrets = UserSecrets { CryptoKeys::open(&storage, password).await?
user_secret: user.user_secret.clone(),
alternate_user_secrets: user.alternate_user_secrets.clone(),
};
CryptoKeys::open(&storage, &user_secrets, password).await?
} }
_ => bail!( CryptographyRoot::Keyring => unimplemented!(),
"Either both master and secret key or none of them must be specified for user"
),
}; };
tracing::debug!(user=%username, "logged"); tracing::debug!(user=%username, "logged");

View file

@ -25,26 +25,59 @@ use server::Server;
struct Args { struct Args {
#[clap(subcommand)] #[clap(subcommand)]
command: Command, command: Command,
#[clap(short, long, env = "CONFIG_FILE", default_value = "aerogramme.toml")]
config_file: PathBuf,
} }
#[derive(Subcommand, Debug)] #[derive(Subcommand, Debug)]
enum Command { enum Command {
/// Runs the IMAP+LMTP server daemon #[clap(subcommand)]
Server { Companion(CompanionCommand),
#[clap(short, long, env = "CONFIG_FILE", default_value = "aerogramme.toml")]
config_file: PathBuf, #[clap(subcommand)]
}, Provider(ProviderCommand),
Test, //Test,
} }
#[derive(Parser, Debug)] #[derive(Subcommand, Debug)]
struct UserSecretsArgs { enum CompanionCommand {
/// User secret /// Runs the IMAP proxy
#[clap(short = 'U', long, env = "USER_SECRET")] Daemon,
user_secret: String, Reload {
/// Alternate user secrets (comma-separated list of strings) #[clap(short, long, env = "AEROGRAMME_PID")]
#[clap(long, env = "ALTERNATE_USER_SECRETS", default_value = "")] pid: Option<u64>,
alternate_user_secrets: String, },
Wizard,
#[clap(subcommand)]
Account(AccountManagement),
}
#[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] #[tokio::main]
@ -63,43 +96,62 @@ async fn main() -> Result<()> {
tracing_subscriber::fmt::init(); tracing_subscriber::fmt::init();
let args = Args::parse(); let args = Args::parse();
let any_config = read_config(args.config_file)?;
match args.command { match (args.command, any_config) {
Command::Server { config_file } => { (Command::Companion(subcommand), AnyConfig::Companion(config)) => match subcommand {
let config = read_config(config_file)?; CompanionCommand::Daemon => {
let server = Server::from_companion_config(config).await?;
let server = Server::new(config).await?; server.run().await?;
server.run().await?; },
} CompanionCommand::Reload { pid } => {
Command::Test => { unimplemented!();
use std::collections::HashMap; },
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; CompanionCommand::Wizard => {
println!("--- message pack ---\n{:?}\n--- end ---\n", rmp_serde::to_vec(&Config { unimplemented!();
lmtp: None, },
imap: Some(ImapConfig { bind_addr: SocketAddr::new(IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)), 8080) }), CompanionCommand::Account(cmd) => {
login_ldap: None, let user_file = config.users.user_list;
login_static: Some(HashMap::from([ account_management(cmd, user_file);
("alice".into(), LoginStaticUser { }
password: "hash".into(), },
user_secret: "hello".into(), (Command::Provider(subcommand), AnyConfig::Provider(config)) => match subcommand {
alternate_user_secrets: vec![], ProviderCommand::Daemon => {
email_addresses: vec![], let server = Server::from_provider_config(config).await?;
master_key: None, server.run().await?;
secret_key: None, },
storage: StaticStorage::Garage(StaticGarageConfig { ProviderCommand::Reload => {
s3_endpoint: "http://".into(), unimplemented!();
k2v_endpoint: "http://".into(), },
aws_region: "garage".into(), ProviderCommand::Account(cmd) => {
aws_access_key_id: "GK...".into(), let user_file = match config.users {
aws_secret_access_key: "xxx".into(), UserManagement::Static(conf) => conf.user_list,
bucket: "aerogramme".into(), UserManagement::Ldap(_) => panic!("LDAP account management is not supported from Aerogramme.")
}), };
}) account_management(cmd, user_file);
])), }
}).unwrap()); },
} (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(()) Ok(())
} }
fn account_management(cmd: AccountManagement, users: PathBuf) {
match cmd {
Add => {
unimplemented!();
},
Delete => {
unimplemented!();
},
ChangePassword => {
unimplemented!();
},
}
}

View file

@ -1,6 +1,6 @@
use std::sync::Arc; use std::sync::Arc;
use anyhow::{bail, Result}; use anyhow::Result;
use futures::try_join; use futures::try_join;
use log::*; use log::*;
use tokio::sync::watch; use tokio::sync::watch;
@ -17,19 +17,24 @@ pub struct Server {
} }
impl Server { impl Server {
pub async fn new(config: Config) -> Result<Self> { pub async fn from_companion_config(config: CompanionConfig) -> Result<Self> {
let (login, lmtp_conf, imap_conf) = build(config)?; let login = Arc::new(StaticLoginProvider::new(config.users)?);
let lmtp_server = lmtp_conf.map(|cfg| LmtpServer::new(cfg, login.clone())); let lmtp_server = None;
let imap_server = match imap_conf { let imap_server = Some(imap::new(config.imap, login).await?);
Some(cfg) => Some(imap::new(cfg, login.clone()).await?), Ok(Self { lmtp_server, imap_server })
None => None, }
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 { let lmtp_server = Some(LmtpServer::new(config.lmtp, login.clone()));
lmtp_server, let imap_server = Some(imap::new(config.imap, login).await?);
imap_server,
}) Ok(Self { lmtp_server, imap_server })
} }
pub async fn run(self) -> Result<()> { 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>>) { pub fn watch_ctrl_c() -> (watch::Receiver<bool>, Arc<watch::Sender<bool>>) {
let (send_cancel, watch_cancel) = watch::channel(false); let (send_cancel, watch_cancel) = watch::channel(false);
let send_cancel = Arc::new(send_cancel); let send_cancel = Arc::new(send_cancel);