aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlex Auvolat <alex@adnab.me>2022-05-19 15:14:36 +0200
committerAlex Auvolat <alex@adnab.me>2022-05-19 15:14:36 +0200
commit1dcb11643c783096e1b52bf48d6b76121504e6bd (patch)
tree658f1db34d0e4f91331dabcab85a5898a95d4ad4
parent6be90936a108d971e0cfa3ddaa9c2d54557e30f3 (diff)
downloadaerogramme-1dcb11643c783096e1b52bf48d6b76121504e6bd.tar.gz
aerogramme-1dcb11643c783096e1b52bf48d6b76121504e6bd.zip
CLI skeleton
-rw-r--r--Cargo.lock121
-rw-r--r--Cargo.toml2
-rw-r--r--src/bayou.rs5
-rw-r--r--src/config.rs3
-rw-r--r--src/cryptoblob.rs10
-rw-r--r--src/login/mod.rs32
-rw-r--r--src/login/static_provider.rs17
-rw-r--r--src/mailbox.rs5
-rw-r--r--src/main.rs224
-rw-r--r--src/server.rs14
10 files changed, 380 insertions, 53 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 3f85eea..0925486 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -20,6 +20,17 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -96,6 +107,45 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -350,6 +400,12 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -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",
@@ -709,6 +767,12 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -745,6 +809,30 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -853,6 +941,18 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1101,6 +1201,12 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1132,6 +1238,21 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
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<S: BayouState> {
}
impl<S: BayouState> Bayou<S> {
- pub fn new(
- creds: &Credentials,
- path: String,
- ) -> Result<Self> {
+ pub fn new(creds: &Credentials, path: String) -> Result<Self> {
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<LoginStaticConfig>,
pub login_ldap: Option<LoginLdapConfig>,
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<Vec<u8>> {
- 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<Vec<u8>> {
}
pub fn seal(plainblob: &[u8], key: &Key) -> Result<Vec<u8>> {
- 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<Self> {
+ pub async fn init(storage: &StorageCredentials, password: &str) -> Result<Self> {
unimplemented!()
}
- pub fn init_without_password(storage: &StorageCredentials, master_key: &Key, secret_key: &SecretKey) -> Result<Self> {
+ pub async fn init_without_password(
+ storage: &StorageCredentials,
+ master_key: &Key,
+ secret_key: &SecretKey,
+ ) -> Result<Self> {
unimplemented!()
}
- pub fn open(storage: &StorageCredentials, password: &str) -> Result<Self> {
+ pub async fn open(storage: &StorageCredentials, password: &str) -> Result<Self> {
unimplemented!()
}
- pub fn open_without_password(storage: &StorageCredentials, master_key: &Key, secret_key: &SecretKey) -> Result<Self> {
+ pub async fn open_without_password(
+ storage: &StorageCredentials,
+ master_key: &Key,
+ secret_key: &SecretKey,
+ ) -> Result<Self> {
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<Self> {
+ pub async fn new(creds: &Credentials, name: String) -> Result<Self> {
let uid_index = Bayou::<UidIndex>::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 anyhow::{bail, Result};
+use std::path::PathBuf;
use std::sync::Arc;
+use anyhow::{bail, Result};
+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(())
}
-async fn main2() -> Result<()> {
- let config = read_config("mailrage.toml".into())?;
+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,
+ }
+}
- let server = Server::new(config)?;
- server.run().await
+fn dump_config(password: &str, creds: &StorageCredentials) {
+ println!("[login_static.users.<username>]");
+ 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<Arc<Self>> {
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<dyn LoginProvider> = 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<Self>) -> 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?;