aboutsummaryrefslogtreecommitdiff
path: root/src/login
diff options
context:
space:
mode:
authorQuentin <quentin@dufour.io>2023-12-27 16:35:43 +0000
committerQuentin <quentin@dufour.io>2023-12-27 16:35:43 +0000
commit6ff3c6f71efd802da422a371e6168ae528fb2ddc (patch)
tree62b5d7d9bc7fd2bf3defd1a85ae1b3f34cd4b8ee /src/login
parent609dde413972ebeeb8cd658a5ec9f62b34b5c402 (diff)
parentea4cd48bba96027882a637df08e313af92a3db46 (diff)
downloadaerogramme-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.rs141
-rw-r--r--src/login/mod.rs640
-rw-r--r--src/login/static_provider.rs181
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,