From c7f767b7d2877cdbbc06893c9cd0fae2c56b96f2 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Thu, 19 May 2022 12:10:48 +0200 Subject: [PATCH] More construction of things --- .gitignore | 1 + Cargo.lock | 11 ++++ Cargo.toml | 2 + src/config.rs | 56 ++++++++++++++++ src/login/ldap_provider.rs | 22 +++++++ src/login/mod.rs | 20 ++++++ src/login/static_provider.rs | 61 +++++++++++++++++ src/mailbox.rs | 112 +++++++++++++++++++++++++++++++ src/main.rs | 124 +++++++++++++++-------------------- 9 files changed, 339 insertions(+), 70 deletions(-) create mode 100644 src/config.rs create mode 100644 src/login/ldap_provider.rs create mode 100644 src/login/mod.rs create mode 100644 src/login/static_provider.rs create mode 100644 src/mailbox.rs diff --git a/.gitignore b/.gitignore index 3151a75..a04ef7f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /target .vimrc test.sh +mailrage.toml diff --git a/Cargo.lock b/Cargo.lock index 5c6e76b..3f85eea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -556,6 +556,7 @@ name = "mailrage" version = "0.0.1" dependencies = [ "anyhow", + "async-trait", "base64", "hex", "im", @@ -570,6 +571,7 @@ dependencies = [ "serde", "sodiumoxide", "tokio", + "toml", "zstd", ] @@ -1203,6 +1205,15 @@ dependencies = [ "tracing", ] +[[package]] +name = "toml" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" +dependencies = [ + "serde", +] + [[package]] name = "tower-service" version = "0.3.1" diff --git a/Cargo.toml b/Cargo.toml index 8f45020..f0c5d7a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ description = "Encrypted mail storage over Garage" [dependencies] anyhow = "1.0.28" +async-trait = "0.1" base64 = "0.13" hex = "0.4" im = "15" @@ -21,6 +22,7 @@ rand = "0.8.5" rmp-serde = "0.15" sodiumoxide = "0.2" tokio = "1.17.0" +toml = "0.5" zstd = { version = "0.9", default-features = false } k2v-client = { git = "https://git.deuxfleurs.fr/Deuxfleurs/garage.git" } diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..47db90d --- /dev/null +++ b/src/config.rs @@ -0,0 +1,56 @@ +use std::collections::HashMap; +use std::io::Read; +use std::path::PathBuf; + +use anyhow::Result; +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 login_static: Option, + pub login_ldap: Option, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct LoginStaticConfig { + pub default_bucket: Option, + pub users: HashMap, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct LoginStaticUser { + pub password: String, + pub aws_access_key_id: String, + pub aws_secret_access_key: String, + pub bucket: Option, + pub master_key: Option, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct LoginLdapConfig { + pub ldap_server: String, + + pub search_dn: String, + pub username_attr: String, + pub aws_access_key_id_attr: String, + pub aws_secret_access_key_attr: String, + + pub bucket: Option, + pub bucket_attr: Option, +} + +pub fn read_config(config_file: PathBuf) -> Result { + let mut file = std::fs::OpenOptions::new() + .read(true) + .open(config_file.as_path())?; + + let mut config = String::new(); + file.read_to_string(&mut config)?; + + Ok(toml::from_str(&config)?) +} diff --git a/src/login/ldap_provider.rs b/src/login/ldap_provider.rs new file mode 100644 index 0000000..ebe2771 --- /dev/null +++ b/src/login/ldap_provider.rs @@ -0,0 +1,22 @@ +use anyhow::Result; +use async_trait::async_trait; + +use crate::config::*; +use crate::login::*; + +pub struct LdapLoginProvider { + // TODO +} + +impl LdapLoginProvider { + pub fn new(config: LoginLdapConfig) -> Result { + unimplemented!() + } +} + +#[async_trait] +impl LoginProvider for LdapLoginProvider { + async fn login(&self, username: &str, password: &str) -> Result { + unimplemented!() + } +} diff --git a/src/login/mod.rs b/src/login/mod.rs new file mode 100644 index 0000000..5637e8a --- /dev/null +++ b/src/login/mod.rs @@ -0,0 +1,20 @@ +pub mod ldap_provider; +pub mod static_provider; + +use anyhow::Result; +use async_trait::async_trait; + +use crate::cryptoblob::Key as SymmetricKey; + +#[derive(Clone, Debug)] +pub struct Credentials { + pub aws_access_key_id: String, + pub aws_secret_access_key: String, + pub bucket: String, + pub master_key: SymmetricKey, +} + +#[async_trait] +pub trait LoginProvider { + async fn login(&self, username: &str, password: &str) -> Result; +} diff --git a/src/login/static_provider.rs b/src/login/static_provider.rs new file mode 100644 index 0000000..037948a --- /dev/null +++ b/src/login/static_provider.rs @@ -0,0 +1,61 @@ +use std::collections::HashMap; + +use anyhow::{anyhow, bail, Result}; +use async_trait::async_trait; +use rusoto_signature::Region; + +use crate::config::*; +use crate::cryptoblob::Key; +use crate::login::*; + +pub struct StaticLoginProvider { + default_bucket: Option, + users: HashMap, + k2v_region: Region, +} + +impl StaticLoginProvider { + pub fn new(config: LoginStaticConfig, k2v_region: Region) -> Result { + Ok(Self { + default_bucket: config.default_bucket, + users: config.users, + k2v_region, + }) + } +} + +#[async_trait] +impl LoginProvider for StaticLoginProvider { + async fn login(&self, username: &str, password: &str) -> Result { + match self.users.get(username) { + None => bail!("User {} does not exist", username), + Some(u) => { + if u.password != password { + // TODO cryptographic password compare + bail!("Wrong password"); + } + let bucket = u + .bucket + .clone() + .or_else(|| self.default_bucket.clone()) + .ok_or(anyhow!( + "No bucket configured and no default bucket specieid" + ))?; + + // TODO if master key is not specified, retrieve it from K2V key storage + let master_key_str = u.master_key.as_ref().ok_or(anyhow!( + "Master key must be specified in config file for now, this will change" + ))?; + let master_key = Key::from_slice(&base64::decode(master_key_str)?) + .ok_or(anyhow!("Invalid master key"))?; + + Ok(Credentials { + aws_access_key_id: u.aws_access_key_id.clone(), + aws_secret_access_key: u.aws_secret_access_key.clone(), + bucket, + master_key, + }) + } + } + } +} diff --git a/src/mailbox.rs b/src/mailbox.rs new file mode 100644 index 0000000..7dcb92a --- /dev/null +++ b/src/mailbox.rs @@ -0,0 +1,112 @@ +use anyhow::{anyhow, bail, Result}; +use k2v_client::{BatchDeleteOp, BatchReadOp, Filter, K2vClient, K2vValue}; +use rusoto_core::HttpClient; +use rusoto_credential::{AwsCredentials, StaticProvider, ProvideAwsCredentials}; +use rusoto_s3::{ + DeleteObjectRequest, GetObjectRequest, ListObjectsV2Request, PutObjectRequest, S3Client, S3, +}; +use rusoto_signature::Region; +use rand::prelude::*; + +use crate::cryptoblob::Key; +use crate::bayou::Bayou; +use crate::login::Credentials; +use crate::uidindex::*; + +pub struct Mailbox { + bucket: String, + name: String, + key: Key, + + k2v: K2vClient, + s3: S3Client, + + uid_index: Bayou, +} + +impl Mailbox { + pub async fn new(k2v_region: Region, s3_region: Region, creds: Credentials,name: String) -> Result { + let aws_creds_provider = StaticProvider::new_minimal( + creds.aws_access_key_id, + creds.aws_secret_access_key, + ); + let aws_creds = aws_creds_provider.credentials().await?; + + let uid_index = Bayou::::new( + aws_creds.clone(), + k2v_region.clone(), + s3_region.clone(), + creds.bucket.clone(), + name.clone(), + creds.master_key.clone(), + )?; + + let k2v_client = K2vClient::new(k2v_region, creds.bucket.clone(), aws_creds, None)?; + let s3_client = S3Client::new_with(HttpClient::new()?, aws_creds_provider, s3_region); + + Ok(Self { + bucket: creds.bucket, + name, + key: creds.master_key, + k2v: k2v_client, + s3: s3_client, + uid_index, + }) + } + + + pub async fn test(&mut self) -> Result<()> { + + self.uid_index.sync().await?; + + dump(&self.uid_index); + + let mut rand_id = [0u8; 24]; + rand_id[..16].copy_from_slice(&u128::to_be_bytes(thread_rng().gen())); + let add_mail_op = self.uid_index + .state() + .op_mail_add(MailUuid(rand_id), vec!["\\Unseen".into()]); + self.uid_index.push(add_mail_op).await?; + + dump(&self.uid_index); + + if self.uid_index.state().mails_by_uid.len() > 6 { + for i in 0..2 { + let (_, uuid) = self.uid_index + .state() + .mails_by_uid + .iter() + .skip(3 + i) + .next() + .unwrap(); + let del_mail_op = self.uid_index.state().op_mail_del(*uuid); + self.uid_index.push(del_mail_op).await?; + + dump(&self.uid_index); + } + } + + Ok(()) + } +} + +fn dump(uid_index: &Bayou) { + let s = uid_index.state(); + println!("---- MAILBOX STATE ----"); + println!("UIDVALIDITY {}", s.uidvalidity); + println!("UIDNEXT {}", s.uidnext); + println!("INTERNALSEQ {}", s.internalseq); + for (uid, uuid) in s.mails_by_uid.iter() { + println!( + "{} {} {}", + uid, + hex::encode(uuid.0), + s.mail_flags + .get(uuid) + .cloned() + .unwrap_or_default() + .join(", ") + ); + } + println!(""); +} diff --git a/src/main.rs b/src/main.rs index 7b46f01..cf1e886 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,96 +1,80 @@ mod bayou; +mod config; mod cryptoblob; +mod login; mod time; mod uidindex; +mod mailbox; -use anyhow::Result; +use anyhow::{bail, Result}; +use std::sync::Arc; use rand::prelude::*; use rusoto_credential::{EnvironmentProvider, ProvideAwsCredentials}; use rusoto_signature::Region; use bayou::*; +use config::*; use cryptoblob::Key; +use login::{ldap_provider::*, static_provider::*, *}; use uidindex::*; +use mailbox::Mailbox; #[tokio::main] async fn main() { - do_stuff().await.expect("Something failed"); + if let Err(e) = main2().await { + eprintln!("Error: {}", e); + std::process::exit(1); + } } -async fn do_stuff() -> Result<()> { - let creds = EnvironmentProvider::default().credentials().await.unwrap(); +async fn main2() -> Result<()> { + let config = read_config("mailrage.toml".into())?; - let k2v_region = Region::Custom { - name: "garage-staging".to_owned(), - endpoint: "https://k2v-staging.home.adnab.me".to_owned(), - }; + let main = Main::new(config)?; + main.run().await +} - let s3_region = Region::Custom { - name: "garage-staging".to_owned(), - endpoint: "https://garage-staging.home.adnab.me".to_owned(), - }; +struct Main { + pub s3_region: Region, + pub k2v_region: Region, + pub login_provider: Box, +} - let key = Key::from_slice(&[0u8; 32]).unwrap(); - - let mut uid_index = Bayou::::new( - creds, - k2v_region, - s3_region, - "mail".into(), - "TestMailbox".into(), - key, - )?; - - uid_index.sync().await?; - - dump(&uid_index); - - let mut rand_id = [0u8; 24]; - rand_id[..16].copy_from_slice(&u128::to_be_bytes(thread_rng().gen())); - let add_mail_op = uid_index - .state() - .op_mail_add(MailUuid(rand_id), vec!["\\Unseen".into()]); - uid_index.push(add_mail_op).await?; - - dump(&uid_index); - - if uid_index.state().mails_by_uid.len() > 6 { - for i in 0..2 { - let (_, uuid) = uid_index - .state() - .mails_by_uid - .iter() - .skip(3 + i) - .next() - .unwrap(); - let del_mail_op = uid_index.state().op_mail_del(*uuid); - uid_index.push(del_mail_op).await?; - - dump(&uid_index); - } +impl Main { + fn new(config: Config) -> Result> { + let s3_region = Region::Custom { + name: config.s3_region, + endpoint: config.s3_endpoint, + }; + let k2v_region = Region::Custom { + name: config.k2v_region, + endpoint: config.k2v_endpoint, + }; + let login_provider: Box = match (config.login_static, config.login_ldap) + { + (Some(st), None) => Box::new(StaticLoginProvider::new(st, k2v_region.clone())?), + (None, Some(ld)) => Box::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(Arc::new(Self { + s3_region, + k2v_region, + login_provider, + })) } - Ok(()) -} + async fn run(self: &Arc) -> Result<()> { + let creds = self.login_provider.login("lx", "plop").await?; -fn dump(uid_index: &Bayou) { - let s = uid_index.state(); - println!("---- MAILBOX STATE ----"); - println!("UIDVALIDITY {}", s.uidvalidity); - println!("UIDNEXT {}", s.uidnext); - println!("INTERNALSEQ {}", s.internalseq); - for (uid, uuid) in s.mails_by_uid.iter() { - println!( - "{} {} {}", - uid, - hex::encode(uuid.0), - s.mail_flags - .get(uuid) - .cloned() - .unwrap_or_default() - .join(", ") - ); + let mut mailbox = Mailbox::new(self.k2v_region.clone(), + self.s3_region.clone(), + creds.clone(), + "TestMailbox".to_string()).await?; + + mailbox.test().await?; + + Ok(()) } - println!(""); }