From 1dcb11643c783096e1b52bf48d6b76121504e6bd Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Thu, 19 May 2022 15:14:36 +0200 Subject: [PATCH] CLI skeleton --- Cargo.lock | 121 +++++++++++++++++++ Cargo.toml | 2 + src/bayou.rs | 5 +- src/config.rs | 3 +- src/cryptoblob.rs | 10 +- src/login/mod.rs | 32 +++-- src/login/static_provider.rs | 17 ++- src/mailbox.rs | 5 +- src/main.rs | 226 +++++++++++++++++++++++++++++++++-- src/server.rs | 14 +-- 10 files changed, 381 insertions(+), 54 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3f85eea..0925486 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,6 +19,17 @@ dependencies = [ "syn", ] +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + [[package]] name = "autocfg" version = "1.1.0" @@ -95,6 +106,45 @@ dependencies = [ "winapi", ] +[[package]] +name = "clap" +version = "3.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2dbdf4bdacb33466e854ce889eee8dfd5729abf7ccd7664d0a2d60cd384440b" +dependencies = [ + "atty", + "bitflags", + "clap_derive", + "clap_lex", + "indexmap", + "lazy_static", + "strsim", + "termcolor", + "textwrap", +] + +[[package]] +name = "clap_derive" +version = "3.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25320346e922cffe59c0bbc5410c8d8784509efb321488971081313cb1e1a33c" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a37c35f1112dad5e6e0b1adaff798507497a18fceeb30cceb3bae7d1427b9213" +dependencies = [ + "os_str_bytes", +] + [[package]] name = "core-foundation" version = "0.9.3" @@ -349,6 +399,12 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +[[package]] +name = "heck" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" + [[package]] name = "hermit-abi" version = "0.1.19" @@ -558,12 +614,14 @@ dependencies = [ "anyhow", "async-trait", "base64", + "clap", "hex", "im", "itertools", "k2v-client", "rand", "rmp-serde", + "rpassword", "rusoto_core", "rusoto_credential", "rusoto_s3", @@ -708,6 +766,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "os_str_bytes" +version = "6.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "029d8d0b2f198229de29dca79676f2738ff952edf3fde542eb8bf94d8c21b435" + [[package]] name = "paste" version = "1.0.7" @@ -744,6 +808,30 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + [[package]] name = "proc-macro2" version = "1.0.39" @@ -852,6 +940,18 @@ dependencies = [ "serde", ] +[[package]] +name = "rpassword" +version = "6.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf099a1888612545b683d2661a1940089f6c2e5a8e38979b2159da876bfd956" +dependencies = [ + "libc", + "serde", + "serde_json", + "winapi", +] + [[package]] name = "rusoto_core" version = "0.48.0" @@ -1100,6 +1200,12 @@ dependencies = [ "serde", ] +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + [[package]] name = "subtle" version = "2.4.1" @@ -1131,6 +1237,21 @@ dependencies = [ "winapi", ] +[[package]] +name = "termcolor" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "textwrap" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" + [[package]] name = "thiserror" version = "1.0.31" diff --git a/Cargo.toml b/Cargo.toml index f0c5d7a..16f619b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ description = "Encrypted mail storage over Garage" anyhow = "1.0.28" async-trait = "0.1" base64 = "0.13" +clap = { version = "3.1.18", features = ["derive", "env"] } hex = "0.4" im = "15" itertools = "0.10" @@ -20,6 +21,7 @@ rusoto_signature = "0.48.0" serde = "1.0.137" rand = "0.8.5" rmp-serde = "0.15" +rpassword = "6.0" sodiumoxide = "0.2" tokio = "1.17.0" toml = "0.5" diff --git a/src/bayou.rs b/src/bayou.rs index acca1e5..fe05e13 100644 --- a/src/bayou.rs +++ b/src/bayou.rs @@ -56,10 +56,7 @@ pub struct Bayou { } impl Bayou { - pub fn new( - creds: &Credentials, - path: String, - ) -> Result { + pub fn new(creds: &Credentials, path: String) -> Result { let k2v_client = creds.k2v_client()?; let s3_client = creds.s3_client()?; diff --git a/src/config.rs b/src/config.rs index d756d6e..8abbce6 100644 --- a/src/config.rs +++ b/src/config.rs @@ -8,9 +8,8 @@ use serde::Deserialize; #[derive(Deserialize, Debug, Clone)] pub struct Config { pub s3_endpoint: String, - pub s3_region: String, pub k2v_endpoint: String, - pub k2v_region: String, + pub aws_region: String, pub login_static: Option, pub login_ldap: Option, diff --git a/src/cryptoblob.rs b/src/cryptoblob.rs index ad05521..5b22ac1 100644 --- a/src/cryptoblob.rs +++ b/src/cryptoblob.rs @@ -5,14 +5,16 @@ use anyhow::{anyhow, Result}; use serde::{Deserialize, Serialize}; use zstd::stream::{decode_all as zstd_decode, encode_all as zstd_encode}; -use sodiumoxide::crypto::secretbox::xsalsa20poly1305 as secretbox; use sodiumoxide::crypto::box_ as publicbox; +use sodiumoxide::crypto::secretbox::xsalsa20poly1305 as secretbox; +pub use sodiumoxide::crypto::box_::{ + gen_keypair, PublicKey, SecretKey, PUBLICKEYBYTES, SECRETKEYBYTES, +}; pub use sodiumoxide::crypto::secretbox::xsalsa20poly1305::{gen_key, Key, KEYBYTES}; -pub use sodiumoxide::crypto::box_::{gen_keypair, PublicKey, SecretKey, PUBLICKEYBYTES, SECRETKEYBYTES}; pub fn open(cryptoblob: &[u8], key: &Key) -> Result> { - use secretbox::{NONCEBYTES, Nonce}; + use secretbox::{Nonce, NONCEBYTES}; if cryptoblob.len() < NONCEBYTES { return Err(anyhow!("Cyphertext too short")); @@ -31,7 +33,7 @@ pub fn open(cryptoblob: &[u8], key: &Key) -> Result> { } pub fn seal(plainblob: &[u8], key: &Key) -> Result> { - use secretbox::{NONCEBYTES, gen_nonce}; + use secretbox::{gen_nonce, NONCEBYTES}; // Compress data using zstd let mut reader = &plainblob[..]; diff --git a/src/login/mod.rs b/src/login/mod.rs index 4022962..4130496 100644 --- a/src/login/mod.rs +++ b/src/login/mod.rs @@ -53,12 +53,6 @@ impl Credentials { pub fn bucket(&self) -> &str { self.storage.bucket.as_str() } - pub fn dump_config(&self) { - println!("aws_access_key_id = \"{}\"", self.storage.aws_access_key_id); - println!("aws_secret_access_key = \"{}\"", self.storage.aws_secret_access_key); - println!("master_key = \"{}\"", base64::encode(&self.keys.master)); - println!("secret_key = \"{}\"", base64::encode(&self.keys.secret)); - } } impl StorageCredentials { @@ -93,28 +87,40 @@ impl StorageCredentials { } impl CryptoKeys { - pub fn init(storage: &StorageCredentials) -> Result { + pub async fn init(storage: &StorageCredentials, password: &str) -> Result { unimplemented!() } - pub fn init_without_password(storage: &StorageCredentials, master_key: &Key, secret_key: &SecretKey) -> Result { + pub async fn init_without_password( + storage: &StorageCredentials, + master_key: &Key, + secret_key: &SecretKey, + ) -> Result { unimplemented!() } - pub fn open(storage: &StorageCredentials, password: &str) -> Result { + pub async fn open(storage: &StorageCredentials, password: &str) -> Result { unimplemented!() } - pub fn open_without_password(storage: &StorageCredentials, master_key: &Key, secret_key: &SecretKey) -> Result { + pub async fn open_without_password( + storage: &StorageCredentials, + master_key: &Key, + secret_key: &SecretKey, + ) -> Result { unimplemented!() } - pub fn add_password(&self, storage: &StorageCredentials, password: &str) -> Result<()> { + pub async fn add_password(&self, storage: &StorageCredentials, password: &str) -> Result<()> { unimplemented!() } - pub fn remove_password(&self, storage: &StorageCredentials, password: &str, allow_remove_all: bool) -> Result<()> { + pub async fn delete_password( + &self, + storage: &StorageCredentials, + password: &str, + allow_delete_all: bool, + ) -> Result<()> { unimplemented!() } } - diff --git a/src/login/static_provider.rs b/src/login/static_provider.rs index d7d791a..3ef8d89 100644 --- a/src/login/static_provider.rs +++ b/src/login/static_provider.rs @@ -58,19 +58,24 @@ impl LoginProvider for StaticLoginProvider { .ok_or(anyhow!("Invalid master key"))?; let secret_key = SecretKey::from_slice(&base64::decode(m)?) .ok_or(anyhow!("Invalid secret key"))?; - CryptoKeys::open_without_password(&storage, &master_key, &secret_key)? + CryptoKeys::open_without_password(&storage, &master_key, &secret_key).await? } (None, None) => { - CryptoKeys::open(&storage, password)? + CryptoKeys::open(&storage, password).await? } _ => bail!("Either both master and secret key or none of them must be specified for user"), }; - Ok(Credentials { - storage, - keys, - }) + Ok(Credentials { storage, keys }) } } } } + +pub fn hash_password(password: &str) -> String { + unimplemented!() +} + +pub fn verify_password(password: &str, hash: &str) -> bool { + unimplemented!() +} diff --git a/src/mailbox.rs b/src/mailbox.rs index 44b9f95..8a90eb1 100644 --- a/src/mailbox.rs +++ b/src/mailbox.rs @@ -21,10 +21,7 @@ pub struct Mailbox { } impl Mailbox { - pub async fn new( - creds: &Credentials, - name: String, - ) -> Result { + pub async fn new(creds: &Credentials, name: String) -> Result { let uid_index = Bayou::::new(creds, name.clone())?; Ok(Self { diff --git a/src/main.rs b/src/main.rs index ca66137..cf26ae1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,32 +3,236 @@ mod config; mod cryptoblob; mod login; mod mailbox; +mod server; mod time; mod uidindex; -mod server; + +use std::path::PathBuf; +use std::sync::Arc; use anyhow::{bail, Result}; -use std::sync::Arc; +use clap::{Parser, Subcommand}; +use rand::prelude::*; use rusoto_signature::Region; use config::*; +use cryptoblob::*; use login::{ldap_provider::*, static_provider::*, *}; use mailbox::Mailbox; 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, + }, + /// 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, + /// Automatically generate password + #[clap(short, long)] + gen: bool, + }, + /// Deletes a key decription password for a user + DeletePassword { + #[clap(flatten)] + creds: StorageCredsArgs, + /// Allow to delete all passwords + #[clap(long)] + allow_delete_all: bool, + }, + /// Dumps all encryption keys for user + ShowKeys { + #[clap(flatten)] + creds: StorageCredsArgs, + }, +} + +#[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, +} + #[tokio::main] -async fn main() { - if let Err(e) = main2().await { - eprintln!("Error: {}", e); - std::process::exit(1); +async fn main() -> Result<()> { + 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 } => { + let creds = make_storage_creds(creds); + + 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, &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, gen } => { + let creds = make_storage_creds(creds); + let existing_password = + rpassword::prompt_password("Enter existing password to decrypt keys: ")?; + let new_password = if gen { + let password = base64::encode(&u128::to_be_bytes(thread_rng().gen())[..10]); + 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, &existing_password).await?; + keys.add_password(&creds, &new_password).await?; + println!(""); + println!("New password added successfully."); + } + Command::DeletePassword { + creds, + allow_delete_all, + } => { + let creds = make_storage_creds(creds); + let existing_password = rpassword::prompt_password("Enter password to delete: ")?; + + let keys = CryptoKeys::open(&creds, &existing_password).await?; + keys.delete_password(&creds, &existing_password, allow_delete_all) + .await?; + + println!(""); + println!("Password was deleted successfully."); + + if allow_delete_all { + println!("As a reminder, here are your cryptographic keys:"); + dump_keys(&keys); + } + } + Command::ShowKeys { creds } => { + let creds = make_storage_creds(creds); + let existing_password = rpassword::prompt_password("Enter key decryption password: ")?; + + let keys = CryptoKeys::open(&creds, &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, } } -async fn main2() -> Result<()> { - let config = read_config("mailrage.toml".into())?; - - let server = Server::new(config)?; - server.run().await +fn dump_config(password: &str, creds: &StorageCredentials) { + println!("[login_static.users.]"); + println!("password = \"{}\"", hash_password(password)); //TODO + 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)); +} diff --git a/src/server.rs b/src/server.rs index 4c628d6..e1ab599 100644 --- a/src/server.rs +++ b/src/server.rs @@ -14,11 +14,11 @@ pub struct Server { impl Server { pub fn new(config: Config) -> Result> { let s3_region = Region::Custom { - name: config.s3_region, + name: config.aws_region.clone(), endpoint: config.s3_endpoint, }; let k2v_region = Region::Custom { - name: config.k2v_region, + name: config.aws_region, endpoint: config.k2v_endpoint, }; let login_provider: Box = match (config.login_static, config.login_ldap) @@ -28,19 +28,13 @@ impl Server { (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(Arc::new(Self { - login_provider, - })) + Ok(Arc::new(Self { login_provider })) } pub async fn run(self: &Arc) -> Result<()> { let creds = self.login_provider.login("lx", "plop").await?; - let mut mailbox = Mailbox::new( - &creds, - "TestMailbox".to_string(), - ) - .await?; + let mut mailbox = Mailbox::new(&creds, "TestMailbox".to_string()).await?; mailbox.test().await?;