mod bayou; mod config; mod cryptoblob; mod login; mod mailbox; mod server; mod time; mod uidindex; use std::path::PathBuf; use anyhow::{bail, Result}; use clap::{Parser, Subcommand}; use rand::prelude::*; use rusoto_signature::Region; use config::*; use cryptoblob::*; use login::{static_provider::*, *}; use server::Server; #[derive(Parser, Debug)] #[clap(author, version, about, long_about = None)] struct Args { #[clap(subcommand)] command: Command, } #[derive(Subcommand, Debug)] enum Command { /// Runs the IMAP+LMTP server daemon Server { #[clap(short, long, env = "CONFIG_FILE", default_value = "mailrage.toml")] config_file: PathBuf, }, /// Initializes key pairs for a user and adds a key decryption password FirstLogin { #[clap(flatten)] creds: StorageCredsArgs, #[clap(flatten)] user_secrets: UserSecretsArgs, }, /// Initializes key pairs for a user and dumps keys to stdout for usage with static /// login provider InitializeLocalKeys { #[clap(flatten)] creds: StorageCredsArgs, }, /// Adds a key decryption password for a user AddPassword { #[clap(flatten)] creds: StorageCredsArgs, #[clap(flatten)] user_secrets: UserSecretsArgs, /// Automatically generate password #[clap(short, long)] gen: bool, }, /// Deletes a key decription password for a user DeletePassword { #[clap(flatten)] creds: StorageCredsArgs, #[clap(flatten)] user_secrets: UserSecretsArgs, /// Allow to delete all passwords #[clap(long)] allow_delete_all: bool, }, /// Dumps all encryption keys for user ShowKeys { #[clap(flatten)] creds: StorageCredsArgs, #[clap(flatten)] user_secrets: UserSecretsArgs, }, } #[derive(Parser, Debug)] struct StorageCredsArgs { /// Name of the region to use #[clap(short = 'r', long, env = "AWS_REGION")] region: String, /// Url of the endpoint to connect to for K2V #[clap(short = 'k', long, env = "K2V_ENDPOINT")] k2v_endpoint: String, /// Url of the endpoint to connect to for S3 #[clap(short = 's', long, env = "S3_ENDPOINT")] s3_endpoint: String, /// Access key ID #[clap(short = 'A', long, env = "AWS_ACCESS_KEY_ID")] aws_access_key_id: String, /// Access key ID #[clap(short = 'S', long, env = "AWS_SECRET_ACCESS_KEY")] aws_secret_access_key: String, /// Bucket name #[clap(short = 'b', long, env = "BUCKET")] bucket: String, } #[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, } #[tokio::main] async fn main() -> Result<()> { if std::env::var("RUST_LOG").is_err() { std::env::set_var("RUST_LOG", "main=info,mailrage=info,k2v_client=info") } tracing_subscriber::fmt::init(); let args = Args::parse(); match args.command { Command::Server { config_file } => { let config = read_config(config_file)?; let server = Server::new(config)?; server.run().await?; } Command::FirstLogin { creds, user_secrets, } => { let creds = make_storage_creds(creds); let user_secrets = make_user_secrets(user_secrets); println!("Please enter your password for key decryption."); println!("If you are using LDAP login, this must be your LDAP password."); println!("If you are using the static login provider, enter any password, and this will also become your password for local IMAP access."); let password = rpassword::prompt_password("Enter password: ")?; let password_confirm = rpassword::prompt_password("Confirm password: ")?; if password != password_confirm { bail!("Passwords don't match."); } CryptoKeys::init(&creds, &user_secrets, &password).await?; println!(""); println!("Cryptographic key setup is complete."); println!(""); println!("If you are using the static login provider, add the following section to your .toml configuration file:"); println!(""); dump_config(&password, &creds); } Command::InitializeLocalKeys { creds } => { let creds = make_storage_creds(creds); println!("Please enter a password for local IMAP access."); println!("This password is not used for key decryption, your keys will be printed below (do not lose them!)"); println!( "If you plan on using LDAP login, stop right here and use `first-login` instead" ); let password = rpassword::prompt_password("Enter password: ")?; let password_confirm = rpassword::prompt_password("Confirm password: ")?; if password != password_confirm { bail!("Passwords don't match."); } let master = gen_key(); let (_, secret) = gen_keypair(); let keys = CryptoKeys::init_without_password(&creds, &master, &secret).await?; println!(""); println!("Cryptographic key setup is complete."); println!(""); println!("Add the following section to your .toml configuration file:"); println!(""); dump_config(&password, &creds); dump_keys(&keys); } Command::AddPassword { creds, user_secrets, gen, } => { let creds = make_storage_creds(creds); let user_secrets = make_user_secrets(user_secrets); let existing_password = rpassword::prompt_password("Enter existing password to decrypt keys: ")?; let new_password = if gen { let password = base64::encode_config( &u128::to_be_bytes(thread_rng().gen())[..10], base64::URL_SAFE_NO_PAD, ); println!("Your new password: {}", password); println!("Keep it safe!"); password } else { 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 = CryptoKeys::open(&creds, &user_secrets, &existing_password).await?; keys.add_password(&creds, &user_secrets, &new_password) .await?; println!(""); println!("New password added successfully."); } Command::DeletePassword { creds, user_secrets, allow_delete_all, } => { let creds = make_storage_creds(creds); let user_secrets = make_user_secrets(user_secrets); let existing_password = rpassword::prompt_password("Enter password to delete: ")?; let keys = match allow_delete_all { true => Some(CryptoKeys::open(&creds, &user_secrets, &existing_password).await?), false => None, }; CryptoKeys::delete_password(&creds, &existing_password, allow_delete_all).await?; println!(""); println!("Password was deleted successfully."); if let Some(keys) = keys { println!("As a reminder, here are your cryptographic keys:"); dump_keys(&keys); } } Command::ShowKeys { creds, user_secrets, } => { let creds = make_storage_creds(creds); let user_secrets = make_user_secrets(user_secrets); let existing_password = rpassword::prompt_password("Enter key decryption password: ")?; let keys = CryptoKeys::open(&creds, &user_secrets, &existing_password).await?; dump_keys(&keys); } } Ok(()) } fn make_storage_creds(c: StorageCredsArgs) -> StorageCredentials { let s3_region = Region::Custom { name: c.region.clone(), endpoint: c.s3_endpoint, }; let k2v_region = Region::Custom { name: c.region, endpoint: c.k2v_endpoint, }; StorageCredentials { k2v_region, s3_region, aws_access_key_id: c.aws_access_key_id, aws_secret_access_key: c.aws_secret_access_key, bucket: c.bucket, } } fn make_user_secrets(c: UserSecretsArgs) -> UserSecrets { UserSecrets { user_secret: c.user_secret, alternate_user_secrets: c .alternate_user_secrets .split(",") .map(|x| x.trim()) .filter(|x| !x.is_empty()) .map(|x| x.to_string()) .collect(), } } fn dump_config(password: &str, creds: &StorageCredentials) { println!("[login_static.users.]"); println!( "password = \"{}\"", hash_password(password).expect("unable to hash password") ); println!("aws_access_key_id = \"{}\"", creds.aws_access_key_id); println!( "aws_secret_access_key = \"{}\"", creds.aws_secret_access_key ); } fn dump_keys(keys: &CryptoKeys) { println!("master_key = \"{}\"", base64::encode(&keys.master)); println!("secret_key = \"{}\"", base64::encode(&keys.secret)); }