diff --git a/Cargo.lock b/Cargo.lock index a5fe09e..0e679da 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -34,7 +34,7 @@ dependencies = [ "argon2", "async-trait", "backtrace", - "base64 0.13.1", + "base64 0.21.2", "boitalettres", "chrono", "clap", diff --git a/Cargo.toml b/Cargo.toml index 7d61962..e7c3c17 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ anyhow = "1.0.28" argon2 = "0.3" async-trait = "0.1" backtrace = "0.3" -base64 = "0.13" +base64 = "0.21" clap = { version = "3.1.18", features = ["derive", "env"] } duplexify = "1.1.0" eml-codec = { git = "https://git.deuxfleurs.fr/Deuxfleurs/eml-codec.git", branch = "main" } diff --git a/src/config.rs b/src/config.rs index cd3bff3..eae50f5 100644 --- a/src/config.rs +++ b/src/config.rs @@ -4,7 +4,7 @@ use std::net::SocketAddr; use std::path::PathBuf; use anyhow::Result; -use serde::{Deserialize, Serialize, Serializer, Deserializer}; +use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, Debug, Clone)] pub struct CompanionConfig { @@ -79,6 +79,8 @@ pub struct LoginLdapConfig { pub username_attr: String, #[serde(default = "default_mail_attr")] pub mail_attr: String, + + // The field that will contain the crypto root thingy pub crypto_root_attr: String, // Storage related thing @@ -108,27 +110,12 @@ pub struct StaticGarageConfig { pub type UserList = HashMap; -#[derive(Serialize, Deserialize, Debug, Clone)] -#[serde(tag = "crypto_root")] -pub enum CryptographyRoot { - PasswordProtected { - root_blob: String, - }, - Keyring, - ClearText { - master_key: String, - secret_key: String, - } -} - #[derive(Serialize, Deserialize, Debug, Clone)] pub struct UserEntry { #[serde(default)] pub email_addresses: Vec, pub password: String, - - #[serde(flatten)] - pub crypto_root: CryptographyRoot, + pub crypto_root: String, #[serde(flatten)] pub storage: StaticStorage, @@ -178,19 +165,3 @@ pub fn write_config(config_file: PathBuf, config: &T) -> Result<() fn default_mail_attr() -> String { "mail".into() } - -fn as_base64(val: &T, serializer: &mut S) -> Result<(), S::Error> - where T: AsRef<[u8]>, - S: Serializer -{ - serializer.serialize_str(&base64::encode(val.as_ref())) -} - -fn from_base64(deserializer: &mut D) -> Result, D::Error> - where D: Deserializer -{ - use serde::de::Error; - String::deserialize(deserializer) - .and_then(|string| base64::decode(&string).map_err(|err| Error::custom(err.to_string()))) -} - diff --git a/src/login/ldap_provider.rs b/src/login/ldap_provider.rs index f72b289..6e94061 100644 --- a/src/login/ldap_provider.rs +++ b/src/login/ldap_provider.rs @@ -17,6 +17,7 @@ pub struct LdapLoginProvider { attrs_to_retrieve: Vec, username_attr: String, mail_attr: String, + crypto_root_attr: String, storage_specific: StorageSpecific, } @@ -48,6 +49,7 @@ impl LdapLoginProvider { let mut attrs_to_retrieve = vec![ config.username_attr.clone(), config.mail_attr.clone(), + config.crypto_root_attr.clone(), ]; // storage specific @@ -78,6 +80,7 @@ impl LdapLoginProvider { attrs_to_retrieve, username_attr: config.username_attr, mail_attr: config.mail_attr, + crypto_root_attr: config.crypto_root_attr, storage_specific: specific, }) } @@ -155,10 +158,16 @@ impl LoginProvider for LdapLoginProvider { .context("Invalid password")?; debug!("Ldap login with user name {} successfull", username); + // cryptography + let crstr = get_attr(&user, &self.crypto_root_attr)?; + let cr = CryptoRoot(crstr); + let keys = cr.crypto_keys(password)?; + + // storage let storage = self.storage_creds_from_ldap_user(&user)?; + drop(ldap); - let keys = CryptoKeys::open(&storage, password).await?; Ok(Credentials { storage, keys }) } @@ -197,12 +206,15 @@ impl LoginProvider for LdapLoginProvider { let user = SearchEntry::construct(matches.into_iter().next().unwrap()); debug!("Found matching LDAP user for email {}: {}", email, user.dn); + // cryptography + let crstr = get_attr(&user, &self.crypto_root_attr)?; + let cr = CryptoRoot(crstr); + let public_key = cr.public_key()?; + + // storage let storage = self.storage_creds_from_ldap_user(&user)?; drop(ldap); - let k2v_client = storage.row_store()?; - let (_, public_key) = CryptoKeys::load_salt_and_public(&k2v_client).await?; - Ok(PublicCredentials { storage, public_key, diff --git a/src/login/mod.rs b/src/login/mod.rs index f7a81c2..3d7a49f 100644 --- a/src/login/mod.rs +++ b/src/login/mod.rs @@ -2,7 +2,7 @@ pub mod ldap_provider; pub mod static_provider; use std::sync::Arc; -use futures::try_join; +use base64::Engine; use anyhow::{anyhow, bail, Context, Result}; use async_trait::async_trait; @@ -37,6 +37,14 @@ pub struct Credentials { /// The cryptographic keys are used to encrypt and decrypt data stored in S3 and K2V pub keys: CryptoKeys, } +impl Credentials { + pub fn row_client(&self) -> Result { + Ok(self.storage.row_store()?) + } + pub fn blob_client(&self) -> Result { + Ok(self.storage.blob_store()?) + } +} #[derive(Clone, Debug)] pub struct PublicCredentials { @@ -45,20 +53,81 @@ pub struct PublicCredentials { pub public_key: PublicKey, } -/* -/// The struct UserSecrets represents intermediary secrets that are mixed in with the user's -/// password when decrypting the cryptographic keys that are stored in their bucket. -/// These secrets should be stored somewhere else (e.g. in the LDAP server or in the -/// local config file), as an additionnal authentification factor so that the password -/// isn't enough just alone to decrypt the content of a user's bucket. -pub struct UserSecrets { - /// The main user secret that will be used to encrypt keys when a new password is added - pub user_secret: String, - /// Alternative user secrets that will be tried when decrypting keys that were encrypted - /// with old passwords - pub alternate_user_secrets: Vec, +use serde::{Serialize, Deserialize}; +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct CryptoRoot(pub String); + +impl CryptoRoot { + pub fn create_pass(password: &str, k: &CryptoKeys) -> Result { + let bytes = k.password_seal(password)?; + let b64 = base64::engine::general_purpose::STANDARD_NO_PAD.encode(bytes); + let cr = format!("aero:cryptoroot:pass:{}", b64); + Ok(Self(cr)) + } + + pub fn create_cleartext(k: &CryptoKeys) -> Self { + let bytes = k.serialize(); + let b64 = base64::engine::general_purpose::STANDARD_NO_PAD.encode(bytes); + let cr = format!("aero:cryptoroot:cleartext:{}", b64); + Self(cr) + } + + pub fn create_incoming(pk: &PublicKey) -> Self { + let bytes: &[u8] = &pk[..]; + let b64 = base64::engine::general_purpose::STANDARD_NO_PAD.encode(bytes); + let cr = format!("aero:cryptoroot:incoming:{}", b64); + Self(cr) + } + + pub fn public_key(&self) -> Result { + match self.0.splitn(4, ':').collect::>()[..] { + [ "aero", "cryptoroot", "pass", b64blob ] => { + let blob = base64::engine::general_purpose::STANDARD_NO_PAD.decode(b64blob)?; + if blob.len() < 32 { + bail!("Decoded data is {} bytes long, expect at least 32 bytes", blob.len()); + } + PublicKey::from_slice(&blob[..32]).context("must be a valid public key") + }, + [ "aero", "cryptoroot", "cleartext", b64blob ] => { + let blob = base64::engine::general_purpose::STANDARD_NO_PAD.decode(b64blob)?; + Ok(CryptoKeys::deserialize(&blob)?.public) + }, + [ "aero", "cryptoroot", "incoming", b64blob ] => { + let blob = base64::engine::general_purpose::STANDARD_NO_PAD.decode(b64blob)?; + if blob.len() < 32 { + bail!("Decoded data is {} bytes long, expect at least 32 bytes", blob.len()); + } + PublicKey::from_slice(&blob[..32]).context("must be a valid public key") + }, + [ "aero", "cryptoroot", "keyring", _ ] => { + bail!("keyring is not yet implemented!") + }, + _ => bail!(format!("passed string '{}' is not a valid cryptoroot", self.0)), + } + } + pub fn crypto_keys(&self, password: &str) -> Result { + match self.0.splitn(4, ':').collect::>()[..] { + [ "aero", "cryptoroot", "pass", b64blob ] => { + let blob = base64::engine::general_purpose::STANDARD_NO_PAD.decode(b64blob)?; + if blob.len() < 32 { + bail!("Decoded data is {} bytes long, expect at least 32 bytes", blob.len()); + } + CryptoKeys::password_open(password, &blob[32..]) + }, + [ "aero", "cryptoroot", "cleartext", b64blob ] => { + let blob = base64::engine::general_purpose::STANDARD_NO_PAD.decode(b64blob)?; + CryptoKeys::deserialize(&blob) + }, + [ "aero", "cryptoroot", "incoming", b64blob ] => { + bail!("incoming cryptoroot does not contain a crypto key!") + }, + [ "aero", "cryptoroot", "keyring", _ ] =>{ + bail!("keyring is not yet implemented!") + }, + _ => bail!(format!("passed string '{}' is not a valid cryptoroot", self.0)), + } + } } -*/ /// The struct CryptoKeys contains the cryptographic keys used to encrypt and decrypt /// data in a user's mailbox. @@ -75,337 +144,22 @@ pub struct CryptoKeys { // ---- -impl Credentials { - pub fn row_client(&self) -> Result { - Ok(self.storage.row_store()?) - } - pub fn blob_client(&self) -> Result { - Ok(self.storage.blob_store()?) - } -} + impl CryptoKeys { - pub async fn init( - storage: &Builders, - password: &str, - ) -> Result { - // Check that salt and public don't exist already - let k2v = storage.row_store()?; - let (salt_ct, public_ct) = Self::check_uninitialized(&k2v).await?; - - // Generate salt for password identifiers - let mut ident_salt = [0u8; 32]; - thread_rng().fill(&mut ident_salt); - - // Generate (public, private) key pair and master key + /// Initialize a new cryptography root + pub fn init() -> Self { let (public, secret) = gen_keypair(); let master = gen_key(); - let keys = CryptoKeys { + CryptoKeys { master, secret, public, - }; - - // Generate short password digest (= password identity) - let ident = argon2_kdf(&ident_salt, password.as_bytes(), 16)?; - - // Generate salt for KDF - let mut kdf_salt = [0u8; 32]; - thread_rng().fill(&mut kdf_salt); - - // Calculate key for password secret box - let password_key = derive_password_key(&kdf_salt, password)?; - - // Seal a secret box that contains our crypto keys - let password_sealed = seal(&keys.serialize(), &password_key)?; - - let password_sortkey = format!("password:{}", hex::encode(&ident)); - let password_blob = [&kdf_salt[..], &password_sealed].concat(); - - // Write values to storage - // @FIXME Implement insert batch in the storage API - let (salt, public, passwd) = ( - salt_ct.set_value(&ident_salt), - public_ct.set_value(keys.public.as_ref()), - k2v.row("keys", &password_sortkey).set_value(&password_blob) - ); - try_join!(salt.push(), public.push(), passwd.push()) - .context("InsertBatch for salt, public, and password")?; - - Ok(keys) - } - - pub async fn init_without_password( - storage: &Builders, - master: &Key, - secret: &SecretKey, - ) -> Result { - // Check that salt and public don't exist already - let k2v = storage.row_store()?; - let (salt_ct, public_ct) = Self::check_uninitialized(&k2v).await?; - - // Generate salt for password identifiers - let mut ident_salt = [0u8; 32]; - thread_rng().fill(&mut ident_salt); - - // Create CryptoKeys struct from given keys - let public = secret.public_key(); - let keys = CryptoKeys { - master: master.clone(), - secret: secret.clone(), - public, - }; - - // Write values to storage - // @FIXME implement insert batch in the storage API - let (salt, public) = ( - salt_ct.set_value(&ident_salt), - public_ct.set_value(keys.public.as_ref()), - ); - - try_join!(salt.push(), public.push()).context("InsertBatch for salt and public")?; - - Ok(keys) - } - - pub async fn open( - password: &str, - root_blob: &str, - ) -> Result { - let kdf_salt = &password_blob[..32]; - let password_openned = try_open_encrypted_keys(kdf_salt, password, &password_blob[32..])?; - - let keys = Self::deserialize(&password_openned)?; - if keys.public != expected_public { - bail!("Password public key doesn't match stored public key"); - } - - Ok(keys) - - /* - let k2v = storage.row_store()?; - let (ident_salt, expected_public) = Self::load_salt_and_public(&k2v).await?; - - // Generate short password digest (= password identity) - let ident = argon2_kdf(&ident_salt, password.as_bytes(), 16)?; - - // Lookup password blob - let password_sortkey = format!("password:{}", hex::encode(&ident)); - let password_ref = k2v.row("keys", &password_sortkey); - - let password_blob = { - let val = match password_ref.fetch().await { - Err(StorageError::NotFound) => { - bail!("invalid password") - } - x => x?, - }; - if val.content().len() != 1 { - bail!("multiple values for password in storage"); - } - match val.content().pop().unwrap() { - Alternative::Value(v) => v, - Alternative::Tombstone => bail!("invalid password"), - } - }; - - // Try to open blob - let kdf_salt = &password_blob[..32]; - let password_openned = try_open_encrypted_keys(kdf_salt, password, &password_blob[32..])?; - - let keys = Self::deserialize(&password_openned)?; - if keys.public != expected_public { - bail!("Password public key doesn't match stored public key"); - } - - Ok(keys) - */ - } - - pub async fn open_without_password( - storage: &Builders, - master: &Key, - secret: &SecretKey, - ) -> Result { - let k2v = storage.row_store()?; - let (_ident_salt, expected_public) = Self::load_salt_and_public(&k2v).await?; - - // Create CryptoKeys struct from given keys - let public = secret.public_key(); - let keys = CryptoKeys { - master: master.clone(), - secret: secret.clone(), - public, - }; - - // Check public key matches - if keys.public != expected_public { - bail!("Given public key doesn't match stored public key"); - } - - Ok(keys) - } - - pub async fn add_password( - &self, - storage: &Builders, - password: &str, - ) -> Result<()> { - let k2v = storage.row_store()?; - let (ident_salt, _public) = Self::load_salt_and_public(&k2v).await?; - - // Generate short password digest (= password identity) - let ident = argon2_kdf(&ident_salt, password.as_bytes(), 16)?; - - // Generate salt for KDF - let mut kdf_salt = [0u8; 32]; - thread_rng().fill(&mut kdf_salt); - - // Calculate key for password secret box - let password_key = derive_password_key(&kdf_salt, password)?; - - // Seal a secret box that contains our crypto keys - let password_sealed = seal(&self.serialize(), &password_key)?; - - let password_sortkey = format!("password:{}", hex::encode(&ident)); - let password_blob = [&kdf_salt[..], &password_sealed].concat(); - - // List existing passwords to overwrite existing entry if necessary - let pass_key = k2v.row("keys", &password_sortkey); - let passwd = match pass_key.fetch().await { - Err(StorageError::NotFound) => pass_key, - v => { - let entry = v?; - if entry.content().iter().any(|x| matches!(x, Alternative::Value(_))) { - bail!("password already exists"); - } - entry.to_ref() - } - }; - - // Write values to storage - passwd - .set_value(&password_blob) - .push() - .await - .context("InsertBatch for new password")?; - - Ok(()) - } - - pub async fn delete_password( - storage: &Builders, - password: &str, - allow_delete_all: bool, - ) -> Result<()> { - let k2v = storage.row_store()?; - let (ident_salt, _public) = Self::load_salt_and_public(&k2v).await?; - - // Generate short password digest (= password identity) - let ident = argon2_kdf(&ident_salt, password.as_bytes(), 16)?; - let password_sortkey = format!("password:{}", hex::encode(&ident)); - - // List existing passwords - let existing_passwords = Self::list_existing_passwords(&k2v).await?; - - // Check password is there - let pw = existing_passwords - .iter() - .map(|x| x.to_ref()) - .find(|x| x.key().1 == &password_sortkey) - //.get(&password_sortkey) - .ok_or(anyhow!("password does not exist"))?; - - if !allow_delete_all && existing_passwords.len() < 2 { - bail!("No other password exists, not deleting last password."); - } - - pw.rm().await.context("DeleteItem for password")?; - - Ok(()) - } - - // ---- STORAGE UTIL ---- - // - async fn check_uninitialized( - k2v: &RowStore, - ) -> Result<(RowRef, RowRef)> { - let params = k2v - .select(Selector::List(vec![ - ("keys", "salt"), - ("keys", "public"), - ])) - .await - .context("ReadBatch for salt and public in check_uninitialized")?; - - if params.len() != 2 { - bail!( - "Invalid response from k2v storage: {:?} (expected two items)", - params - ); - } - - let salt_ct = params[0].to_ref(); - if params[0].content().iter().any(|x| matches!(x, Alternative::Value(_))) { - bail!("key storage already initialized"); - } - - let public_ct = params[1].to_ref(); - if params[1].content().iter().any(|x| matches!(x, Alternative::Value(_))) { - bail!("key storage already initialized"); - } - - Ok((salt_ct, public_ct)) - } - - pub async fn load_salt_and_public(k2v: &RowStore) -> Result<([u8; 32], PublicKey)> { - let params = k2v - .select(Selector::List(vec![ - ("keys", "salt"), - ("keys", "public"), - ])) - .await - .context("ReadBatch for salt and public in load_salt_and_public")?; - - if params.len() != 2 { - bail!( - "Invalid response from k2v storage: {:?} (expected two items)", - params - ); - } - if params[0].content().len() != 1 || params[1].content().len() != 1 { - bail!("cryptographic keys not initialized for user"); - } - - // Retrieve salt from given response - let salt: Vec = match &mut params[0].content().iter_mut().next().unwrap() { - Alternative::Value(v) => std::mem::take(v), - Alternative::Tombstone => bail!("salt is a tombstone"), - }; - if salt.len() != 32 { - bail!("`salt` is not 32 bytes long"); - } - let mut salt_constlen = [0u8; 32]; - salt_constlen.copy_from_slice(&salt); - - // Retrieve public from given response - let public: Vec = match &mut params[1].content().iter_mut().next().unwrap() { - Alternative::Value(v) => std::mem::take(v), - Alternative::Tombstone => bail!("public is a tombstone"), - }; - let public = PublicKey::from_slice(&public).ok_or(anyhow!("Invalid public key length"))?; - - Ok((salt_constlen, public)) - } - - async fn list_existing_passwords(k2v: &RowStore) -> Result> { - let res = k2v.select(Selector::Prefix { shard_key: "keys", prefix: "password:" }) - .await - .context("ReadBatch for prefix password: in list_existing_passwords")?; - - Ok(res) + } } + // Clear text serialize/deserialize + /// Serialize the root as bytes without encryption fn serialize(&self) -> [u8; 64] { let mut res = [0u8; 64]; res[..32].copy_from_slice(self.master.as_ref()); @@ -413,6 +167,7 @@ impl CryptoKeys { res } + /// Deserialize a clear text crypto root without encryption fn deserialize(bytes: &[u8]) -> Result { if bytes.len() != 64 { bail!("Invalid length: {}, expected 64", bytes.len()); @@ -426,6 +181,31 @@ impl CryptoKeys { public, }) } + + // Password sealed keys serialize/deserialize + pub fn password_open(password: &str, blob: &[u8]) -> Result { + let kdf_salt = &blob[0..32]; + let password_openned = try_open_encrypted_keys(kdf_salt, password, &blob[32..])?; + + let keys = Self::deserialize(&password_openned)?; + Ok(keys) + } + + pub fn password_seal(&self, password: &str) -> Result> { + let mut kdf_salt = [0u8; 32]; + thread_rng().fill(&mut kdf_salt); + + // Calculate key for password secret box + let password_key = derive_password_key(&kdf_salt, password)?; + + // Seal a secret box that contains our crypto keys + let password_sealed = seal(&self.serialize(), &password_key)?; + + // Create blob + let password_blob = [&self.public[..], &kdf_salt[..], &password_sealed].concat(); + + Ok(password_blob) + } } fn derive_password_key(kdf_salt: &[u8], password: &str) -> Result { @@ -452,7 +232,7 @@ pub fn argon2_kdf(salt: &[u8], password: &[u8], output_len: usize) -> Result { - let master_key = - Key::from_slice(&base64::decode(m)?).ok_or(anyhow!("Invalid master key"))?; - let secret_key = SecretKey::from_slice(&base64::decode(s)?) - .ok_or(anyhow!("Invalid secret key"))?; - CryptoKeys::open_without_password(&storage, &master_key, &secret_key).await? - } - CryptographyRoot::PasswordProtected { root_blob } => { - CryptoKeys::open(password, root_blob).await? - } - CryptographyRoot::Keyring => unimplemented!(), - }; + let cr = CryptoRoot(user.crypto_root); + let keys = cr.crypto_keys(password)?; tracing::debug!(user=%username, "logged"); Ok(Credentials { storage, keys }) @@ -118,8 +106,8 @@ impl LoginProvider for StaticLoginProvider { }), }; - let k2v_client = storage.row_store()?; - let (_, public_key) = CryptoKeys::load_salt_and_public(&k2v_client).await?; + let cr = CryptoRoot(user.crypto_root); + let public_key = cr.public_key()?; Ok(PublicCredentials { storage, diff --git a/src/main.rs b/src/main.rs index c252623..3b5f474 100644 --- a/src/main.rs +++ b/src/main.rs @@ -156,17 +156,6 @@ fn account_management(root: &Command, cmd: &AccountManagement, users: PathBuf) - tracing::debug!(user=login, "will-create"); let stp: SetupEntry = read_config(setup.clone())?; tracing::debug!(user=login, "loaded setup entry"); - let crypto_root = match root { - Command::Provider(_) => CryptographyRoot::PasswordProtected, - Command::Companion(_) => { - // @TODO use keyring by default instead of inplace in the future - // @TODO generate keys - CryptographyRoot::InPlace { - master_key: "".to_string(), - secret_key: "".to_string(), - } - } - }; let password = match stp.clear_password { Some(pwd) => pwd, @@ -179,12 +168,19 @@ fn account_management(root: &Command, cmd: &AccountManagement, users: PathBuf) - password } }; + + 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), + }; + let hash = hash_password(password.as_str()).context("unable to hash password")?; ulist.insert(login.clone(), UserEntry { email_addresses: stp.email_addresses, password: hash, - crypto_root, + crypto_root: crypto_root.0, storage: stp.storage, }); @@ -192,7 +188,7 @@ fn account_management(root: &Command, cmd: &AccountManagement, users: PathBuf) - }, AccountManagement::Delete { login } => { tracing::debug!(user=login, "will-delete"); - ulist.remove(&login); + ulist.remove(login); write_config(users.clone(), &ulist)?; }, AccountManagement::ChangePassword { login } => {