WIP refactor
This commit is contained in:
parent
2779837a37
commit
3ddbce4529
5 changed files with 178 additions and 130 deletions
|
@ -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())?;
|
||||
|
|
|
@ -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 ----
|
||||
|
|
|
@ -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");
|
||||
|
|
142
src/main.rs
142
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,
|
||||
},
|
||||
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!();
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in a new issue