diff options
author | Quentin <quentin@dufour.io> | 2023-12-27 16:35:43 +0000 |
---|---|---|
committer | Quentin <quentin@dufour.io> | 2023-12-27 16:35:43 +0000 |
commit | 6ff3c6f71efd802da422a371e6168ae528fb2ddc (patch) | |
tree | 62b5d7d9bc7fd2bf3defd1a85ae1b3f34cd4b8ee /src/login | |
parent | 609dde413972ebeeb8cd658a5ec9f62b34b5c402 (diff) | |
parent | ea4cd48bba96027882a637df08e313af92a3db46 (diff) | |
download | aerogramme-6ff3c6f71efd802da422a371e6168ae528fb2ddc.tar.gz aerogramme-6ff3c6f71efd802da422a371e6168ae528fb2ddc.zip |
Add storage behind a trait
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/aerogramme/pulls/32
Diffstat (limited to 'src/login')
-rw-r--r-- | src/login/ldap_provider.rs | 141 | ||||
-rw-r--r-- | src/login/mod.rs | 640 | ||||
-rw-r--r-- | src/login/static_provider.rs | 181 |
3 files changed, 320 insertions, 642 deletions
diff --git a/src/login/ldap_provider.rs b/src/login/ldap_provider.rs index 2eeb6d9..81e5879 100644 --- a/src/login/ldap_provider.rs +++ b/src/login/ldap_provider.rs @@ -5,10 +5,9 @@ use log::debug; use crate::config::*; use crate::login::*; +use crate::storage; pub struct LdapLoginProvider { - k2v_region: Region, - s3_region: Region, ldap_server: String, pre_bind_on_login: bool, @@ -18,13 +17,10 @@ pub struct LdapLoginProvider { attrs_to_retrieve: Vec<String>, username_attr: String, mail_attr: String, + crypto_root_attr: String, - aws_access_key_id_attr: String, - aws_secret_access_key_attr: String, - user_secret_attr: String, - alternate_user_secrets_attr: Option<String>, - - bucket_source: BucketSource, + storage_specific: StorageSpecific, + in_memory_store: storage::in_memory::MemDb, } enum BucketSource { @@ -32,8 +28,16 @@ enum BucketSource { Attr(String), } +enum StorageSpecific { + InMemory, + Garage { + from_config: LdapGarageConfig, + bucket_source: BucketSource, + }, +} + impl LdapLoginProvider { - pub fn new(config: LoginLdapConfig, k2v_region: Region, s3_region: Region) -> Result<Self> { + pub fn new(config: LoginLdapConfig) -> Result<Self> { let bind_dn_and_pw = match (config.bind_dn, config.bind_password) { (Some(dn), Some(pw)) => Some((dn, pw)), (None, None) => None, @@ -42,12 +46,6 @@ impl LdapLoginProvider { ), }; - let bucket_source = match (config.bucket, config.bucket_attr) { - (Some(b), None) => BucketSource::Constant(b), - (None, Some(a)) => BucketSource::Attr(a), - _ => bail!("Must set `bucket` or `bucket_attr`, but not both"), - }; - if config.pre_bind_on_login && bind_dn_and_pw.is_none() { bail!("Cannot use `pre_bind_on_login` without setting `bind_dn` and `bind_password`"); } @@ -55,20 +53,32 @@ impl LdapLoginProvider { let mut attrs_to_retrieve = vec![ config.username_attr.clone(), config.mail_attr.clone(), - config.aws_access_key_id_attr.clone(), - config.aws_secret_access_key_attr.clone(), - config.user_secret_attr.clone(), + config.crypto_root_attr.clone(), ]; - if let Some(a) = &config.alternate_user_secrets_attr { - attrs_to_retrieve.push(a.clone()); - } - if let BucketSource::Attr(a) = &bucket_source { - attrs_to_retrieve.push(a.clone()); - } + + // storage specific + let specific = match config.storage { + LdapStorage::InMemory => StorageSpecific::InMemory, + LdapStorage::Garage(grgconf) => { + let bucket_source = + match (grgconf.default_bucket.clone(), grgconf.bucket_attr.clone()) { + (Some(b), None) => BucketSource::Constant(b), + (None, Some(a)) => BucketSource::Attr(a), + _ => bail!("Must set `bucket` or `bucket_attr`, but not both"), + }; + + if let BucketSource::Attr(a) = &bucket_source { + attrs_to_retrieve.push(a.clone()); + } + + StorageSpecific::Garage { + from_config: grgconf, + bucket_source, + } + } + }; Ok(Self { - k2v_region, - s3_region, ldap_server: config.ldap_server, pre_bind_on_login: config.pre_bind_on_login, bind_dn_and_pw, @@ -76,29 +86,43 @@ impl LdapLoginProvider { attrs_to_retrieve, username_attr: config.username_attr, mail_attr: config.mail_attr, - aws_access_key_id_attr: config.aws_access_key_id_attr, - aws_secret_access_key_attr: config.aws_secret_access_key_attr, - user_secret_attr: config.user_secret_attr, - alternate_user_secrets_attr: config.alternate_user_secrets_attr, - bucket_source, + crypto_root_attr: config.crypto_root_attr, + storage_specific: specific, + in_memory_store: storage::in_memory::MemDb::new(), }) } - fn storage_creds_from_ldap_user(&self, user: &SearchEntry) -> Result<StorageCredentials> { - let aws_access_key_id = get_attr(user, &self.aws_access_key_id_attr)?; - let aws_secret_access_key = get_attr(user, &self.aws_secret_access_key_attr)?; - let bucket = match &self.bucket_source { - BucketSource::Constant(b) => b.clone(), - BucketSource::Attr(a) => get_attr(user, a)?, + async fn storage_creds_from_ldap_user(&self, user: &SearchEntry) -> Result<Builder> { + let storage: Builder = match &self.storage_specific { + StorageSpecific::InMemory => { + self.in_memory_store + .builder(&get_attr(user, &self.username_attr)?) + .await + } + StorageSpecific::Garage { + from_config, + bucket_source, + } => { + let aws_access_key_id = get_attr(user, &from_config.aws_access_key_id_attr)?; + let aws_secret_access_key = + get_attr(user, &from_config.aws_secret_access_key_attr)?; + let bucket = match bucket_source { + BucketSource::Constant(b) => b.clone(), + BucketSource::Attr(a) => get_attr(user, &a)?, + }; + + storage::garage::GarageBuilder::new(storage::garage::GarageConf { + region: from_config.aws_region.clone(), + s3_endpoint: from_config.s3_endpoint.clone(), + k2v_endpoint: from_config.k2v_endpoint.clone(), + aws_access_key_id, + aws_secret_access_key, + bucket, + })? + } }; - Ok(StorageCredentials { - k2v_region: self.k2v_region.clone(), - s3_region: self.s3_region.clone(), - aws_access_key_id, - aws_secret_access_key, - bucket, - }) + Ok(storage) } } @@ -148,22 +172,16 @@ impl LoginProvider for LdapLoginProvider { .context("Invalid password")?; debug!("Ldap login with user name {} successfull", username); - let storage = self.storage_creds_from_ldap_user(&user)?; + // cryptography + let crstr = get_attr(&user, &self.crypto_root_attr)?; + let cr = CryptoRoot(crstr); + let keys = cr.crypto_keys(password)?; - let user_secret = get_attr(&user, &self.user_secret_attr)?; - let alternate_user_secrets = match &self.alternate_user_secrets_attr { - None => vec![], - Some(a) => user.attrs.get(a).cloned().unwrap_or_default(), - }; - let user_secrets = UserSecrets { - user_secret, - alternate_user_secrets, - }; + // storage + let storage = self.storage_creds_from_ldap_user(&user).await?; drop(ldap); - let keys = CryptoKeys::open(&storage, &user_secrets, password).await?; - Ok(Credentials { storage, keys }) } @@ -201,11 +219,14 @@ impl LoginProvider for LdapLoginProvider { let user = SearchEntry::construct(matches.into_iter().next().unwrap()); debug!("Found matching LDAP user for email {}: {}", email, user.dn); - let storage = self.storage_creds_from_ldap_user(&user)?; - drop(ldap); + // cryptography + let crstr = get_attr(&user, &self.crypto_root_attr)?; + let cr = CryptoRoot(crstr); + let public_key = cr.public_key()?; - let k2v_client = storage.k2v_client()?; - let (_, public_key) = CryptoKeys::load_salt_and_public(&k2v_client).await?; + // storage + let storage = self.storage_creds_from_ldap_user(&user).await?; + drop(ldap); Ok(PublicCredentials { storage, diff --git a/src/login/mod.rs b/src/login/mod.rs index 3fab90a..2926738 100644 --- a/src/login/mod.rs +++ b/src/login/mod.rs @@ -1,20 +1,15 @@ pub mod ldap_provider; pub mod static_provider; -use std::collections::BTreeMap; +use base64::Engine; use std::sync::Arc; use anyhow::{anyhow, bail, Context, Result}; use async_trait::async_trait; -use k2v_client::{ - BatchInsertOp, BatchReadOp, CausalValue, CausalityToken, Filter, K2vClient, K2vValue, -}; use rand::prelude::*; -use rusoto_core::HttpClient; -use rusoto_credential::{AwsCredentials, StaticProvider}; -use rusoto_s3::S3Client; use crate::cryptoblob::*; +use crate::storage::*; /// The trait LoginProvider defines the interface for a login provider that allows /// to retrieve storage and cryptographic credentials for access to a user account @@ -38,7 +33,7 @@ pub type ArcLoginProvider = Arc<dyn LoginProvider + Send + Sync>; #[derive(Clone, Debug)] pub struct Credentials { /// The storage credentials are used to authenticate access to the underlying storage (S3, K2V) - pub storage: StorageCredentials, + pub storage: Builder, /// The cryptographic keys are used to encrypt and decrypt data stored in S3 and K2V pub keys: CryptoKeys, } @@ -46,32 +41,93 @@ pub struct Credentials { #[derive(Clone, Debug)] pub struct PublicCredentials { /// The storage credentials are used to authenticate access to the underlying storage (S3, K2V) - pub storage: StorageCredentials, + pub storage: Builder, pub public_key: PublicKey, } -/// The struct StorageCredentials contains access key to an S3 and K2V bucket -#[derive(Clone, Debug, Hash, PartialEq, Eq)] -pub struct StorageCredentials { - pub s3_region: Region, - pub k2v_region: Region, - - pub aws_access_key_id: String, - pub aws_secret_access_key: String, - pub bucket: String, -} - -/// 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<String>, +use serde::{Deserialize, Serialize}; +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct CryptoRoot(pub String); + +impl CryptoRoot { + pub fn create_pass(password: &str, k: &CryptoKeys) -> Result<Self> { + 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<PublicKey> { + match self.0.splitn(4, ':').collect::<Vec<&str>>()[..] { + ["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<CryptoKeys> { + match self.0.splitn(4, ':').collect::<Vec<&str>>()[..] { + ["aero", "cryptoroot", "pass", b64blob] => { + let blob = base64::engine::general_purpose::STANDARD_NO_PAD.decode(b64blob)?; + CryptoKeys::password_open(password, &blob) + } + ["aero", "cryptoroot", "cleartext", b64blob] => { + let blob = base64::engine::general_purpose::STANDARD_NO_PAD.decode(b64blob)?; + CryptoKeys::deserialize(&blob) + } + ["aero", "cryptoroot", "incoming", _] => { + 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 @@ -86,426 +142,22 @@ pub struct CryptoKeys { pub public: PublicKey, } -/// A custom S3 region, composed of a region name and endpoint. -/// We use this instead of rusoto_signature::Region so that we can -/// derive Hash and Eq -#[derive(Clone, Debug, Hash, PartialEq, Eq)] -pub struct Region { - pub name: String, - pub endpoint: String, -} - -impl Region { - pub fn as_rusoto_region(&self) -> rusoto_signature::Region { - rusoto_signature::Region::Custom { - name: self.name.clone(), - endpoint: self.endpoint.clone(), - } - } -} - // ---- -impl Credentials { - pub fn k2v_client(&self) -> Result<K2vClient> { - self.storage.k2v_client() - } - pub fn s3_client(&self) -> Result<S3Client> { - self.storage.s3_client() - } - pub fn bucket(&self) -> &str { - self.storage.bucket.as_str() - } -} - -impl StorageCredentials { - pub fn k2v_client(&self) -> Result<K2vClient> { - let aws_creds = AwsCredentials::new( - self.aws_access_key_id.clone(), - self.aws_secret_access_key.clone(), - None, - None, - ); - - Ok(K2vClient::new( - self.k2v_region.as_rusoto_region(), - self.bucket.clone(), - aws_creds, - None, - )?) - } - - pub fn s3_client(&self) -> Result<S3Client> { - let aws_creds_provider = StaticProvider::new_minimal( - self.aws_access_key_id.clone(), - self.aws_secret_access_key.clone(), - ); - - let connector = hyper_rustls::HttpsConnectorBuilder::new() - .with_native_roots() - .https_or_http() - .enable_http1() - .enable_http2() - .build(); - let client = HttpClient::from_connector(connector); - - Ok(S3Client::new_with( - client, - aws_creds_provider, - self.s3_region.as_rusoto_region(), - )) - } -} - impl CryptoKeys { - pub async fn init( - storage: &StorageCredentials, - user_secrets: &UserSecrets, - password: &str, - ) -> Result<Self> { - // Check that salt and public don't exist already - let k2v = storage.k2v_client()?; - 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 = user_secrets.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 - k2v.insert_batch(&[ - k2v_insert_single_key("keys", "salt", salt_ct, ident_salt), - k2v_insert_single_key("keys", "public", public_ct, keys.public), - k2v_insert_single_key("keys", &password_sortkey, None, &password_blob), - ]) - .await - .context("InsertBatch for salt, public, and password")?; - - Ok(keys) - } - - pub async fn init_without_password( - storage: &StorageCredentials, - master: &Key, - secret: &SecretKey, - ) -> Result<Self> { - // Check that salt and public don't exist already - let k2v = storage.k2v_client()?; - 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 - k2v.insert_batch(&[ - k2v_insert_single_key("keys", "salt", salt_ct, ident_salt), - k2v_insert_single_key("keys", "public", public_ct, keys.public), - ]) - .await - .context("InsertBatch for salt and public")?; - - Ok(keys) - } - - pub async fn open( - storage: &StorageCredentials, - user_secrets: &UserSecrets, - password: &str, - ) -> Result<Self> { - let k2v = storage.k2v_client()?; - 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_blob = { - let mut val = match k2v.read_item("keys", &password_sortkey).await { - Err(k2v_client::Error::NotFound) => { - bail!("invalid password") - } - x => x?, - }; - if val.value.len() != 1 { - bail!("multiple values for password in storage"); - } - match val.value.pop().unwrap() { - K2vValue::Value(v) => v, - K2vValue::Tombstone => bail!("invalid password"), - } - }; - - // Try to open blob - let kdf_salt = &password_blob[..32]; - let password_openned = - user_secrets.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: &StorageCredentials, - master: &Key, - secret: &SecretKey, - ) -> Result<Self> { - let k2v = storage.k2v_client()?; - 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: &StorageCredentials, - user_secrets: &UserSecrets, - password: &str, - ) -> Result<()> { - let k2v = storage.k2v_client()?; - 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 = user_secrets.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 ct = match k2v.read_item("keys", &password_sortkey).await { - Err(k2v_client::Error::NotFound) => None, - v => { - let entry = v?; - if entry.value.iter().any(|x| matches!(x, K2vValue::Value(_))) { - bail!("password already exists"); - } - Some(entry.causality) - } - }; - - // Write values to storage - k2v.insert_batch(&[k2v_insert_single_key( - "keys", - &password_sortkey, - ct, - &password_blob, - )]) - .await - .context("InsertBatch for new password")?; - - Ok(()) - } - - pub async fn delete_password( - storage: &StorageCredentials, - password: &str, - allow_delete_all: bool, - ) -> Result<()> { - let k2v = storage.k2v_client()?; - 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 - .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."); - } - - k2v.delete_item("keys", &password_sortkey, pw.causality.clone()) - .await - .context("DeleteItem for password")?; - - Ok(()) - } - - // ---- STORAGE UTIL ---- - - async fn check_uninitialized( - k2v: &K2vClient, - ) -> Result<(Option<CausalityToken>, Option<CausalityToken>)> { - let params = k2v - .read_batch(&[ - k2v_read_single_key("keys", "salt", true), - k2v_read_single_key("keys", "public", true), - ]) - .await - .context("ReadBatch for salt and public in check_uninitialized")?; - if params.len() != 2 { - bail!( - "Invalid response from k2v storage: {:?} (expected two items)", - params - ); - } - if params[0].items.len() > 1 || params[1].items.len() > 1 { - bail!( - "invalid response from k2v storage: {:?} (several items in single_item read)", - params - ); - } - - let salt_ct = match params[0].items.iter().next() { - None => None, - Some((_, CausalValue { causality, value })) => { - if value.iter().any(|x| matches!(x, K2vValue::Value(_))) { - bail!("key storage already initialized"); - } - Some(causality.clone()) - } - }; - - let public_ct = match params[1].items.iter().next() { - None => None, - Some((_, CausalValue { causality, value })) => { - if value.iter().any(|x| matches!(x, K2vValue::Value(_))) { - bail!("key storage already initialized"); - } - Some(causality.clone()) - } - }; - - Ok((salt_ct, public_ct)) - } - - pub async fn load_salt_and_public(k2v: &K2vClient) -> Result<([u8; 32], PublicKey)> { - let mut params = k2v - .read_batch(&[ - k2v_read_single_key("keys", "salt", false), - k2v_read_single_key("keys", "public", false), - ]) - .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].items.len() != 1 || params[1].items.len() != 1 { - bail!("cryptographic keys not initialized for user"); - } - - // Retrieve salt from given response - let salt_vals = &mut params[0].items.iter_mut().next().unwrap().1.value; - if salt_vals.len() != 1 { - bail!("Multiple values for `salt`"); - } - let salt: Vec<u8> = match &mut salt_vals[0] { - K2vValue::Value(v) => std::mem::take(v), - K2vValue::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_vals = &mut params[1].items.iter_mut().next().unwrap().1.value; - if public_vals.len() != 1 { - bail!("Multiple values for `public`"); - } - let public: Vec<u8> = match &mut public_vals[0] { - K2vValue::Value(v) => std::mem::take(v), - K2vValue::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: &K2vClient) -> Result<BTreeMap<String, CausalValue>> { - let mut res = k2v - .read_batch(&[BatchReadOp { - partition_key: "keys", - filter: Filter { - start: None, - end: None, - prefix: Some("password:"), - limit: None, - reverse: false, - }, - conflicts_only: false, - tombstones: false, - single_item: false, - }]) - .await - .context("ReadBatch for prefix password: in list_existing_passwords")?; - if res.len() != 1 { - bail!("unexpected k2v result: {:?}, expected one item", res); - } - Ok(res.pop().unwrap().items) } + // 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()); @@ -513,6 +165,7 @@ impl CryptoKeys { res } + /// Deserialize a clear text crypto root without encryption fn deserialize(bytes: &[u8]) -> Result<Self> { if bytes.len() != 64 { bail!("Invalid length: {}, expected 64", bytes.len()); @@ -526,91 +179,66 @@ impl CryptoKeys { public, }) } -} -impl UserSecrets { - fn derive_password_key_with(user_secret: &str, kdf_salt: &[u8], password: &str) -> Result<Key> { - let tmp = format!("{}\n\n{}", user_secret, password); - Ok(Key::from_slice(&argon2_kdf(kdf_salt, tmp.as_bytes(), 32)?).unwrap()) - } + // Password sealed keys serialize/deserialize + pub fn password_open(password: &str, blob: &[u8]) -> Result<Self> { + let _pubkey = &blob[0..32]; + let kdf_salt = &blob[32..64]; + let password_openned = try_open_encrypted_keys(kdf_salt, password, &blob[64..])?; - fn derive_password_key(&self, kdf_salt: &[u8], password: &str) -> Result<Key> { - Self::derive_password_key_with(&self.user_secret, kdf_salt, password) + let keys = Self::deserialize(&password_openned)?; + Ok(keys) } - fn try_open_encrypted_keys( - &self, - kdf_salt: &[u8], - password: &str, - encrypted_keys: &[u8], - ) -> Result<Vec<u8>> { - let secrets_to_try = - std::iter::once(&self.user_secret).chain(self.alternate_user_secrets.iter()); - for user_secret in secrets_to_try { - let password_key = Self::derive_password_key_with(user_secret, kdf_salt, password)?; - if let Ok(res) = open(encrypted_keys, &password_key) { - return Ok(res); - } - } - bail!("Unable to decrypt password blob."); + pub fn password_seal(&self, password: &str) -> Result<Vec<u8>> { + 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<Key> { + Ok(Key::from_slice(&argon2_kdf(kdf_salt, password.as_bytes(), 32)?).unwrap()) +} + +fn try_open_encrypted_keys( + kdf_salt: &[u8], + password: &str, + encrypted_keys: &[u8], +) -> Result<Vec<u8>> { + let password_key = derive_password_key(kdf_salt, password)?; + open(encrypted_keys, &password_key) +} + // ---- UTIL ---- pub fn argon2_kdf(salt: &[u8], password: &[u8], output_len: usize) -> Result<Vec<u8>> { - use argon2::{Algorithm, Argon2, ParamsBuilder, PasswordHasher, Version}; + use argon2::{password_hash, Algorithm, Argon2, ParamsBuilder, PasswordHasher, Version}; - let mut params = ParamsBuilder::new(); - params + let params = ParamsBuilder::new() .output_len(output_len) - .map_err(|e| anyhow!("Invalid output length: {}", e))?; - - let params = params - .params() + .build() .map_err(|e| anyhow!("Invalid argon2 params: {}", e))?; let argon2 = Argon2::new(Algorithm::default(), Version::default(), params); - let salt = base64::encode_config(salt, base64::STANDARD_NO_PAD); + let b64_salt = base64::engine::general_purpose::STANDARD_NO_PAD.encode(salt); + let valid_salt = password_hash::Salt::from_b64(&b64_salt) + .map_err(|e| anyhow!("Invalid salt, error {}", e))?; let hash = argon2 - .hash_password(password, &salt) + .hash_password(password, valid_salt) .map_err(|e| anyhow!("Unable to hash: {}", e))?; let hash = hash.hash.ok_or(anyhow!("Missing output"))?; assert!(hash.len() == output_len); Ok(hash.as_bytes().to_vec()) } - -pub fn k2v_read_single_key<'a>( - partition_key: &'a str, - sort_key: &'a str, - tombstones: bool, -) -> BatchReadOp<'a> { - BatchReadOp { - partition_key, - filter: Filter { - start: Some(sort_key), - end: None, - prefix: None, - limit: None, - reverse: false, - }, - conflicts_only: false, - tombstones, - single_item: true, - } -} - -pub fn k2v_insert_single_key<'a>( - partition_key: &'a str, - sort_key: &'a str, - causality: Option<CausalityToken>, - value: impl AsRef<[u8]>, -) -> BatchInsertOp<'a> { - BatchInsertOp { - partition_key, - sort_key, - causality, - value: K2vValue::Value(value.as_ref().to_vec()), - } -} diff --git a/src/login/static_provider.rs b/src/login/static_provider.rs index b9be5a6..1e1ecbf 100644 --- a/src/login/static_provider.rs +++ b/src/login/static_provider.rs @@ -1,45 +1,89 @@ use std::collections::HashMap; +use std::path::PathBuf; use std::sync::Arc; +use tokio::signal::unix::{signal, SignalKind}; +use tokio::sync::watch; use anyhow::{anyhow, bail, Result}; use async_trait::async_trait; use crate::config::*; -use crate::cryptoblob::{Key, SecretKey}; use crate::login::*; +use crate::storage; -pub struct StaticLoginProvider { - default_bucket: Option<String>, - users: HashMap<String, Arc<LoginStaticUser>>, - users_by_email: HashMap<String, Arc<LoginStaticUser>>, +pub struct ContextualUserEntry { + pub username: String, + pub config: UserEntry, +} - k2v_region: Region, - s3_region: Region, +#[derive(Default)] +pub struct UserDatabase { + users: HashMap<String, Arc<ContextualUserEntry>>, + users_by_email: HashMap<String, Arc<ContextualUserEntry>>, } -impl StaticLoginProvider { - pub fn new(config: LoginStaticConfig, k2v_region: Region, s3_region: Region) -> Result<Self> { - let users = config - .users +pub struct StaticLoginProvider { + user_db: watch::Receiver<UserDatabase>, + in_memory_store: storage::in_memory::MemDb, +} + +pub async fn update_user_list(config: PathBuf, up: watch::Sender<UserDatabase>) -> Result<()> { + let mut stream = signal(SignalKind::user_defined1()) + .expect("failed to install SIGUSR1 signal hander for reload"); + + loop { + let ulist: UserList = match read_config(config.clone()) { + Ok(x) => x, + Err(e) => { + tracing::warn!(path=%config.as_path().to_string_lossy(), error=%e, "Unable to load config"); + stream.recv().await; + continue; + } + }; + + let users = ulist .into_iter() - .map(|(k, v)| (k, Arc::new(v))) + .map(|(username, config)| { + ( + username.clone(), + Arc::new(ContextualUserEntry { username, config }), + ) + }) .collect::<HashMap<_, _>>(); + let mut users_by_email = HashMap::new(); for (_, u) in users.iter() { - for m in u.email_addresses.iter() { + for m in u.config.email_addresses.iter() { if users_by_email.contains_key(m) { - bail!("Several users have same email address: {}", m); + tracing::warn!("Several users have the same email address: {}", m); + stream.recv().await; + continue; } users_by_email.insert(m.clone(), u.clone()); } } - Ok(Self { - default_bucket: config.default_bucket, + tracing::info!("{} users loaded", users.len()); + up.send(UserDatabase { users, users_by_email, - k2v_region, - s3_region, + }) + .context("update user db config")?; + stream.recv().await; + tracing::info!("Received SIGUSR1, reloading"); + } +} + +impl StaticLoginProvider { + pub async fn new(config: LoginStaticConfig) -> Result<Self> { + let (tx, mut rx) = watch::channel(UserDatabase::default()); + + tokio::spawn(update_user_list(config.user_list, tx)); + rx.changed().await?; + + Ok(Self { + user_db: rx, + in_memory_store: storage::in_memory::MemDb::new(), }) } } @@ -48,82 +92,67 @@ impl StaticLoginProvider { impl LoginProvider for StaticLoginProvider { async fn login(&self, username: &str, password: &str) -> Result<Credentials> { tracing::debug!(user=%username, "login"); - let user = match self.users.get(username) { - None => bail!("User {} does not exist", username), - Some(u) => u, + let user = { + let user_db = self.user_db.borrow(); + match user_db.users.get(username) { + None => bail!("User {} does not exist", username), + Some(u) => u.clone(), + } }; tracing::debug!(user=%username, "verify password"); - if !verify_password(password, &user.password)? { + if !verify_password(password, &user.config.password)? { bail!("Wrong password"); } - tracing::debug!(user=%username, "fetch bucket"); - let bucket = user - .bucket - .clone() - .or_else(|| self.default_bucket.clone()) - .ok_or(anyhow!( - "No bucket configured and no default bucket specieid" - ))?; - tracing::debug!(user=%username, "fetch keys"); - let storage = StorageCredentials { - k2v_region: self.k2v_region.clone(), - s3_region: self.s3_region.clone(), - aws_access_key_id: user.aws_access_key_id.clone(), - aws_secret_access_key: user.aws_secret_access_key.clone(), - bucket, - }; - - let keys = match (&user.master_key, &user.secret_key) { - (Some(m), Some(s)) => { - 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? + let storage: storage::Builder = match &user.config.storage { + StaticStorage::InMemory => self.in_memory_store.builder(username).await, + StaticStorage::Garage(grgconf) => { + storage::garage::GarageBuilder::new(storage::garage::GarageConf { + region: grgconf.aws_region.clone(), + k2v_endpoint: grgconf.k2v_endpoint.clone(), + s3_endpoint: grgconf.s3_endpoint.clone(), + aws_access_key_id: grgconf.aws_access_key_id.clone(), + aws_secret_access_key: grgconf.aws_secret_access_key.clone(), + bucket: grgconf.bucket.clone(), + })? } - (None, None) => { - let user_secrets = UserSecrets { - user_secret: user.user_secret.clone(), - alternate_user_secrets: user.alternate_user_secrets.clone(), - }; - CryptoKeys::open(&storage, &user_secrets, password).await? - } - _ => bail!( - "Either both master and secret key or none of them must be specified for user" - ), }; + let cr = CryptoRoot(user.config.crypto_root.clone()); + let keys = cr.crypto_keys(password)?; + tracing::debug!(user=%username, "logged"); Ok(Credentials { storage, keys }) } async fn public_login(&self, email: &str) -> Result<PublicCredentials> { - let user = match self.users_by_email.get(email) { - None => bail!("No user for email address {}", email), - Some(u) => u, + let user = { + let user_db = self.user_db.borrow(); + match user_db.users_by_email.get(email) { + None => bail!("Email {} does not exist", email), + Some(u) => u.clone(), + } }; - - let bucket = user - .bucket - .clone() - .or_else(|| self.default_bucket.clone()) - .ok_or(anyhow!( - "No bucket configured and no default bucket specieid" - ))?; - - let storage = StorageCredentials { - k2v_region: self.k2v_region.clone(), - s3_region: self.s3_region.clone(), - aws_access_key_id: user.aws_access_key_id.clone(), - aws_secret_access_key: user.aws_secret_access_key.clone(), - bucket, + tracing::debug!(user=%user.username, "public_login"); + + let storage: storage::Builder = match &user.config.storage { + StaticStorage::InMemory => self.in_memory_store.builder(&user.username).await, + StaticStorage::Garage(grgconf) => { + storage::garage::GarageBuilder::new(storage::garage::GarageConf { + region: grgconf.aws_region.clone(), + k2v_endpoint: grgconf.k2v_endpoint.clone(), + s3_endpoint: grgconf.s3_endpoint.clone(), + aws_access_key_id: grgconf.aws_access_key_id.clone(), + aws_secret_access_key: grgconf.aws_secret_access_key.clone(), + bucket: grgconf.bucket.clone(), + })? + } }; - let k2v_client = storage.k2v_client()?; - let (_, public_key) = CryptoKeys::load_salt_and_public(&k2v_client).await?; + let cr = CryptoRoot(user.config.crypto_root.clone()); + let public_key = cr.public_key()?; Ok(PublicCredentials { storage, |