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,
#[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())?;

View file

@ -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(kdf_salt: &[u8], password: &str) -> Result<Key> {
Ok(Key::from_slice(&argon2_kdf(kdf_salt, password.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 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 ----

View file

@ -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");

View file

@ -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,
},
Test,
#[clap(subcommand)]
Companion(CompanionCommand),
#[clap(subcommand)]
Provider(ProviderCommand),
//Test,
}
#[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 CompanionCommand {
/// Runs the IMAP proxy
Daemon,
Reload {
#[clap(short, long, env = "AEROGRAMME_PID")]
pid: Option<u64>,
},
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]
@ -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?;
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::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());
},
(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!();
},
}
}

View file

@ -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);