2023-11-01 08:20:36 +00:00
|
|
|
#![feature(async_fn_in_trait)]
|
|
|
|
|
2023-11-15 14:56:43 +00:00
|
|
|
mod timestamp;
|
2022-05-18 10:24:37 +00:00
|
|
|
mod bayou;
|
2022-05-19 10:10:48 +00:00
|
|
|
mod config;
|
2022-05-18 10:24:37 +00:00
|
|
|
mod cryptoblob;
|
2022-06-17 16:39:36 +00:00
|
|
|
mod imap;
|
2022-07-13 10:30:35 +00:00
|
|
|
mod k2v_util;
|
2022-05-31 15:07:34 +00:00
|
|
|
mod lmtp;
|
2022-05-19 10:10:48 +00:00
|
|
|
mod login;
|
2022-06-27 14:56:20 +00:00
|
|
|
mod mail;
|
2022-05-19 13:14:36 +00:00
|
|
|
mod server;
|
2023-10-30 17:07:40 +00:00
|
|
|
mod storage;
|
2022-05-18 10:24:37 +00:00
|
|
|
|
2022-05-19 13:14:36 +00:00
|
|
|
use std::path::PathBuf;
|
2022-05-18 12:54:48 +00:00
|
|
|
|
2023-12-08 18:06:12 +00:00
|
|
|
use anyhow::{bail, Result, Context};
|
2022-05-19 13:14:36 +00:00
|
|
|
use clap::{Parser, Subcommand};
|
|
|
|
|
2022-05-19 10:10:48 +00:00
|
|
|
use config::*;
|
2022-05-19 12:33:49 +00:00
|
|
|
use server::Server;
|
2023-12-08 18:06:12 +00:00
|
|
|
use login::{static_provider::*, *};
|
2022-05-18 10:24:37 +00:00
|
|
|
|
2022-05-19 13:14:36 +00:00
|
|
|
#[derive(Parser, Debug)]
|
|
|
|
#[clap(author, version, about, long_about = None)]
|
|
|
|
struct Args {
|
|
|
|
#[clap(subcommand)]
|
|
|
|
command: Command,
|
2023-12-06 19:57:25 +00:00
|
|
|
|
|
|
|
#[clap(short, long, env = "CONFIG_FILE", default_value = "aerogramme.toml")]
|
|
|
|
config_file: PathBuf,
|
2022-05-19 13:14:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Subcommand, Debug)]
|
|
|
|
enum Command {
|
2023-12-06 19:57:25 +00:00
|
|
|
#[clap(subcommand)]
|
2023-12-13 17:04:04 +00:00
|
|
|
/// A daemon to be run by the end user, on a personal device
|
2023-12-06 19:57:25 +00:00
|
|
|
Companion(CompanionCommand),
|
|
|
|
|
|
|
|
#[clap(subcommand)]
|
2023-12-13 17:04:04 +00:00
|
|
|
/// A daemon to be run by the service provider, on a server
|
2023-12-06 19:57:25 +00:00
|
|
|
Provider(ProviderCommand),
|
2023-12-13 17:04:04 +00:00
|
|
|
|
|
|
|
#[clap(subcommand)]
|
2023-12-14 12:03:04 +00:00
|
|
|
/// Specific tooling, should not be part of a normal workflow, for debug & experimentation only
|
2023-12-13 17:04:04 +00:00
|
|
|
Tools(ToolsCommand),
|
2023-12-06 19:57:25 +00:00
|
|
|
//Test,
|
|
|
|
}
|
|
|
|
|
2023-12-13 17:04:04 +00:00
|
|
|
#[derive(Subcommand, Debug)]
|
|
|
|
enum ToolsCommand {
|
|
|
|
/// Manage crypto roots
|
|
|
|
#[clap(subcommand)]
|
|
|
|
CryptoRoot(CryptoRootCommand),
|
2023-12-14 10:30:11 +00:00
|
|
|
|
|
|
|
PasswordHash {
|
|
|
|
#[clap(env = "AEROGRAMME_PASSWORD")]
|
|
|
|
maybe_password: Option<String>,
|
|
|
|
}
|
2023-12-13 17:04:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Subcommand, Debug)]
|
|
|
|
enum CryptoRootCommand {
|
|
|
|
/// Generate a new crypto-root protected with a password
|
|
|
|
New {
|
|
|
|
#[clap(env = "AEROGRAMME_PASSWORD")]
|
|
|
|
maybe_password: Option<String>,
|
|
|
|
},
|
|
|
|
/// Generate a new clear text crypto-root, store it securely!
|
|
|
|
NewClearText,
|
|
|
|
/// Change the password of a crypto key
|
|
|
|
ChangePassword {
|
|
|
|
#[clap(env = "AEROGRAMME_OLD_PASSWORD")]
|
|
|
|
maybe_old_password: Option<String>,
|
|
|
|
|
|
|
|
#[clap(env = "AEROGRAMME_NEW_PASSWORD")]
|
|
|
|
maybe_new_password: Option<String>,
|
|
|
|
|
|
|
|
#[clap(short, long, env = "AEROGRAMME_CRYPTO_ROOT")]
|
|
|
|
crypto_root: String,
|
|
|
|
},
|
|
|
|
/// From a given crypto-key, derive one containing only the public key
|
|
|
|
DeriveIncoming {
|
|
|
|
#[clap(short, long, env = "AEROGRAMME_CRYPTO_ROOT")]
|
|
|
|
crypto_root: String,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
2023-12-06 19:57:25 +00:00
|
|
|
#[derive(Subcommand, Debug)]
|
|
|
|
enum CompanionCommand {
|
|
|
|
/// Runs the IMAP proxy
|
|
|
|
Daemon,
|
|
|
|
Reload {
|
|
|
|
#[clap(short, long, env = "AEROGRAMME_PID")]
|
|
|
|
pid: Option<u64>,
|
2023-11-24 10:44:42 +00:00
|
|
|
},
|
2023-12-06 19:57:25 +00:00
|
|
|
Wizard,
|
|
|
|
#[clap(subcommand)]
|
|
|
|
Account(AccountManagement),
|
2022-05-19 13:14:36 +00:00
|
|
|
}
|
|
|
|
|
2023-12-06 19:57:25 +00:00
|
|
|
#[derive(Subcommand, Debug)]
|
|
|
|
enum ProviderCommand {
|
|
|
|
/// Runs the IMAP+LMTP server daemon
|
|
|
|
Daemon,
|
2023-12-08 18:06:12 +00:00
|
|
|
/// Reload the daemon
|
2023-12-06 19:57:25 +00:00
|
|
|
Reload,
|
2023-12-08 18:06:12 +00:00
|
|
|
/// Manage static accounts
|
2023-12-06 19:57:25 +00:00
|
|
|
#[clap(subcommand)]
|
|
|
|
Account(AccountManagement),
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Subcommand, Debug)]
|
|
|
|
enum AccountManagement {
|
2023-12-08 18:06:12 +00:00
|
|
|
/// Add an account
|
2023-12-06 19:57:25 +00:00
|
|
|
Add {
|
|
|
|
#[clap(short, long)]
|
|
|
|
login: String,
|
|
|
|
#[clap(short, long)]
|
|
|
|
setup: PathBuf,
|
|
|
|
},
|
2023-12-08 18:06:12 +00:00
|
|
|
/// Delete an account
|
2023-12-06 19:57:25 +00:00
|
|
|
Delete {
|
|
|
|
#[clap(short, long)]
|
|
|
|
login: String,
|
|
|
|
},
|
2023-12-08 18:06:12 +00:00
|
|
|
/// Change password for a given account
|
2023-12-06 19:57:25 +00:00
|
|
|
ChangePassword {
|
2023-12-13 17:04:04 +00:00
|
|
|
#[clap(env = "AEROGRAMME_OLD_PASSWORD")]
|
|
|
|
maybe_old_password: Option<String>,
|
|
|
|
|
|
|
|
#[clap(env = "AEROGRAMME_NEW_PASSWORD")]
|
|
|
|
maybe_new_password: Option<String>,
|
|
|
|
|
2023-12-06 19:57:25 +00:00
|
|
|
#[clap(short, long)]
|
|
|
|
login: String
|
|
|
|
},
|
2022-05-23 15:31:53 +00:00
|
|
|
}
|
|
|
|
|
2022-05-18 10:24:37 +00:00
|
|
|
#[tokio::main]
|
2022-05-19 13:14:36 +00:00
|
|
|
async fn main() -> Result<()> {
|
2022-05-20 11:36:45 +00:00
|
|
|
if std::env::var("RUST_LOG").is_err() {
|
2022-06-30 08:50:03 +00:00
|
|
|
std::env::set_var("RUST_LOG", "main=info,aerogramme=info,k2v_client=info")
|
2022-05-20 11:36:45 +00:00
|
|
|
}
|
2022-06-02 15:59:29 +00:00
|
|
|
|
2022-06-29 18:00:38 +00:00
|
|
|
// Abort on panic (same behavior as in Go)
|
|
|
|
std::panic::set_hook(Box::new(|panic_info| {
|
2023-05-15 16:23:23 +00:00
|
|
|
eprintln!("{}", panic_info);
|
2022-07-15 14:15:48 +00:00
|
|
|
eprintln!("{:?}", backtrace::Backtrace::new());
|
2022-06-29 18:00:38 +00:00
|
|
|
std::process::abort();
|
|
|
|
}));
|
|
|
|
|
2022-06-02 15:59:29 +00:00
|
|
|
tracing_subscriber::fmt::init();
|
2022-05-20 11:36:45 +00:00
|
|
|
|
2022-05-19 13:14:36 +00:00
|
|
|
let args = Args::parse();
|
2023-12-06 19:57:25 +00:00
|
|
|
let any_config = read_config(args.config_file)?;
|
2022-05-19 13:14:36 +00:00
|
|
|
|
2023-12-08 18:06:12 +00:00
|
|
|
match (&args.command, any_config) {
|
2023-12-06 19:57:25 +00:00
|
|
|
(Command::Companion(subcommand), AnyConfig::Companion(config)) => match subcommand {
|
|
|
|
CompanionCommand::Daemon => {
|
|
|
|
let server = Server::from_companion_config(config).await?;
|
|
|
|
server.run().await?;
|
|
|
|
},
|
2023-12-13 17:04:04 +00:00
|
|
|
CompanionCommand::Reload { pid: _pid } => {
|
2023-12-06 19:57:25 +00:00
|
|
|
unimplemented!();
|
|
|
|
},
|
|
|
|
CompanionCommand::Wizard => {
|
|
|
|
unimplemented!();
|
|
|
|
},
|
|
|
|
CompanionCommand::Account(cmd) => {
|
|
|
|
let user_file = config.users.user_list;
|
2023-12-08 18:06:12 +00:00
|
|
|
account_management(&args.command, cmd, user_file)?;
|
2023-12-06 19:57:25 +00:00
|
|
|
}
|
|
|
|
},
|
|
|
|
(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.")
|
|
|
|
};
|
2023-12-08 18:06:12 +00:00
|
|
|
account_management(&args.command, cmd, user_file)?;
|
2023-12-06 19:57:25 +00:00
|
|
|
}
|
|
|
|
},
|
|
|
|
(Command::Provider(_), AnyConfig::Companion(_)) => {
|
2023-12-13 17:06:18 +00:00
|
|
|
bail!("Your want to run a 'Provider' command but your configuration file has role 'Companion'.");
|
2023-12-06 19:57:25 +00:00
|
|
|
},
|
|
|
|
(Command::Companion(_), AnyConfig::Provider(_)) => {
|
2023-12-13 17:06:18 +00:00
|
|
|
bail!("Your want to run a 'Companion' command but your configuration file has role 'Provider'.");
|
2023-12-06 19:57:25 +00:00
|
|
|
},
|
2023-12-13 17:04:04 +00:00
|
|
|
(Command::Tools(subcommand), _) => match subcommand {
|
2023-12-14 10:30:11 +00:00
|
|
|
ToolsCommand::PasswordHash { maybe_password } => {
|
|
|
|
let password = match maybe_password {
|
|
|
|
Some(pwd) => pwd.clone(),
|
|
|
|
None => rpassword::prompt_password("Enter password: ")?,
|
|
|
|
};
|
|
|
|
println!("{}", hash_password(&password)?);
|
|
|
|
},
|
2023-12-13 17:04:04 +00:00
|
|
|
ToolsCommand::CryptoRoot(crcommand) => {
|
|
|
|
match crcommand {
|
|
|
|
CryptoRootCommand::New { maybe_password } => {
|
|
|
|
let password = match maybe_password {
|
|
|
|
Some(pwd) => pwd.clone(),
|
|
|
|
None => {
|
|
|
|
let password = rpassword::prompt_password("Enter password: ")?;
|
|
|
|
let password_confirm = rpassword::prompt_password("Confirm password: ")?;
|
|
|
|
if password != password_confirm {
|
|
|
|
bail!("Passwords don't match.");
|
|
|
|
}
|
|
|
|
password
|
|
|
|
}
|
|
|
|
};
|
|
|
|
let crypto_keys = CryptoKeys::init();
|
|
|
|
let cr = CryptoRoot::create_pass(&password, &crypto_keys)?;
|
|
|
|
println!("{}", cr.0);
|
|
|
|
},
|
|
|
|
CryptoRootCommand::NewClearText => {
|
|
|
|
let crypto_keys = CryptoKeys::init();
|
|
|
|
let cr = CryptoRoot::create_cleartext(&crypto_keys);
|
|
|
|
println!("{}", cr.0);
|
|
|
|
},
|
|
|
|
CryptoRootCommand::ChangePassword { maybe_old_password, maybe_new_password, crypto_root } => {
|
|
|
|
let old_password = match maybe_old_password {
|
|
|
|
Some(pwd) => pwd.to_string(),
|
|
|
|
None => rpassword::prompt_password("Enter old password: ")?,
|
|
|
|
};
|
|
|
|
|
|
|
|
let new_password = match maybe_new_password {
|
|
|
|
Some(pwd) => pwd.to_string(),
|
|
|
|
None => {
|
|
|
|
let password = rpassword::prompt_password("Enter new password: ")?;
|
|
|
|
let password_confirm = rpassword::prompt_password("Confirm new password: ")?;
|
|
|
|
if password != password_confirm {
|
|
|
|
bail!("Passwords don't match.");
|
|
|
|
}
|
|
|
|
password
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
let keys = CryptoRoot(crypto_root.to_string()).crypto_keys(&old_password)?;
|
|
|
|
let cr = CryptoRoot::create_pass(&new_password, &keys)?;
|
|
|
|
println!("{}", cr.0);
|
|
|
|
},
|
|
|
|
CryptoRootCommand::DeriveIncoming { crypto_root } => {
|
|
|
|
let pubkey = CryptoRoot(crypto_root.to_string()).public_key()?;
|
|
|
|
let cr = CryptoRoot::create_incoming(&pubkey);
|
|
|
|
println!("{}", cr.0);
|
|
|
|
},
|
|
|
|
}
|
|
|
|
},
|
|
|
|
}
|
2022-05-19 10:10:48 +00:00
|
|
|
}
|
2022-05-19 13:14:36 +00:00
|
|
|
|
|
|
|
Ok(())
|
2022-05-18 10:24:37 +00:00
|
|
|
}
|
|
|
|
|
2023-12-08 18:06:12 +00:00
|
|
|
fn account_management(root: &Command, cmd: &AccountManagement, users: PathBuf) -> Result<()> {
|
2023-12-13 17:04:04 +00:00
|
|
|
let mut ulist: UserList = read_config(users.clone()).context(format!("'{:?}' must be a user database", users))?;
|
2023-12-08 18:06:12 +00:00
|
|
|
|
2023-12-06 19:57:25 +00:00
|
|
|
match cmd {
|
2023-12-08 18:06:12 +00:00
|
|
|
AccountManagement::Add { login, setup } => {
|
|
|
|
tracing::debug!(user=login, "will-create");
|
2023-12-13 17:04:04 +00:00
|
|
|
let stp: SetupEntry = read_config(setup.clone()).context(format!("'{:?}' must be a setup file", setup))?;
|
2023-12-08 18:06:12 +00:00
|
|
|
tracing::debug!(user=login, "loaded setup entry");
|
|
|
|
|
|
|
|
let password = match stp.clear_password {
|
|
|
|
Some(pwd) => pwd,
|
|
|
|
None => {
|
|
|
|
let password = rpassword::prompt_password("Enter password: ")?;
|
|
|
|
let password_confirm = rpassword::prompt_password("Confirm password: ")?;
|
|
|
|
if password != password_confirm {
|
|
|
|
bail!("Passwords don't match.");
|
|
|
|
}
|
|
|
|
password
|
|
|
|
}
|
|
|
|
};
|
2023-12-13 15:09:01 +00:00
|
|
|
|
|
|
|
let crypto_keys = CryptoKeys::init();
|
|
|
|
let crypto_root = match root {
|
|
|
|
Command::Provider(_) => CryptoRoot::create_pass(&password, &crypto_keys)?,
|
|
|
|
Command::Companion(_) => CryptoRoot::create_cleartext(&crypto_keys),
|
2023-12-13 17:04:04 +00:00
|
|
|
_ => unreachable!(),
|
2023-12-13 15:09:01 +00:00
|
|
|
};
|
|
|
|
|
2023-12-08 18:06:12 +00:00
|
|
|
let hash = hash_password(password.as_str()).context("unable to hash password")?;
|
|
|
|
|
|
|
|
ulist.insert(login.clone(), UserEntry {
|
|
|
|
email_addresses: stp.email_addresses,
|
|
|
|
password: hash,
|
2023-12-13 15:09:01 +00:00
|
|
|
crypto_root: crypto_root.0,
|
2023-12-08 18:06:12 +00:00
|
|
|
storage: stp.storage,
|
|
|
|
});
|
|
|
|
|
|
|
|
write_config(users.clone(), &ulist)?;
|
2023-12-06 19:57:25 +00:00
|
|
|
},
|
2023-12-08 18:06:12 +00:00
|
|
|
AccountManagement::Delete { login } => {
|
2023-12-12 08:17:59 +00:00
|
|
|
tracing::debug!(user=login, "will-delete");
|
2023-12-13 15:09:01 +00:00
|
|
|
ulist.remove(login);
|
2023-12-12 08:17:59 +00:00
|
|
|
write_config(users.clone(), &ulist)?;
|
2023-12-06 19:57:25 +00:00
|
|
|
},
|
2023-12-13 17:04:04 +00:00
|
|
|
AccountManagement::ChangePassword { maybe_old_password, maybe_new_password, login } => {
|
|
|
|
let mut user = ulist.remove(login).context("user must exist first")?;
|
|
|
|
|
|
|
|
let old_password = match maybe_old_password {
|
|
|
|
Some(pwd) => pwd.to_string(),
|
|
|
|
None => rpassword::prompt_password("Enter old password: ")?,
|
|
|
|
};
|
|
|
|
|
|
|
|
if !verify_password(&old_password, &user.password)? {
|
|
|
|
bail!(format!("invalid password for login {}", login));
|
|
|
|
}
|
|
|
|
|
|
|
|
let crypto_keys = CryptoRoot(user.crypto_root).crypto_keys(&old_password)?;
|
|
|
|
|
|
|
|
let new_password = match maybe_new_password {
|
|
|
|
Some(pwd) => pwd.to_string(),
|
|
|
|
None => {
|
|
|
|
let password = rpassword::prompt_password("Enter new password: ")?;
|
|
|
|
let password_confirm = rpassword::prompt_password("Confirm new password: ")?;
|
|
|
|
if password != password_confirm {
|
|
|
|
bail!("Passwords don't match.");
|
|
|
|
}
|
|
|
|
password
|
|
|
|
}
|
|
|
|
};
|
|
|
|
let new_hash = hash_password(&new_password)?;
|
|
|
|
let new_crypto_root = CryptoRoot::create_pass(&new_password, &crypto_keys)?;
|
|
|
|
|
|
|
|
user.password = new_hash;
|
|
|
|
user.crypto_root = new_crypto_root.0;
|
|
|
|
|
|
|
|
ulist.insert(login.clone(), user);
|
|
|
|
write_config(users.clone(), &ulist)?;
|
2023-12-06 19:57:25 +00:00
|
|
|
},
|
2023-12-08 18:06:12 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
Ok(())
|
2023-12-06 19:57:25 +00:00
|
|
|
}
|