aboutsummaryrefslogtreecommitdiff
path: root/aero-user/src/login/mod.rs
diff options
context:
space:
mode:
authorQuentin Dufour <quentin@deuxfleurs.fr>2024-05-29 10:14:51 +0200
committerQuentin Dufour <quentin@deuxfleurs.fr>2024-05-29 10:14:51 +0200
commitb9ce5886033677f6c65a4b873e17574fdb8df31d (patch)
tree9ed1d721361027d7d6fef0ecad65d7e1b74a7ddb /aero-user/src/login/mod.rs
parent0dcf69f180f5a7b71b6ad2ac67e4cdd81e5154f1 (diff)
parent5954de6efbb040b8b47daf0c7663a60f3db1da6e (diff)
downloadaerogramme-b9ce5886033677f6c65a4b873e17574fdb8df31d.tar.gz
aerogramme-b9ce5886033677f6c65a4b873e17574fdb8df31d.zip
Merge branch 'caldav'
Diffstat (limited to 'aero-user/src/login/mod.rs')
-rw-r--r--aero-user/src/login/mod.rs245
1 files changed, 245 insertions, 0 deletions
diff --git a/aero-user/src/login/mod.rs b/aero-user/src/login/mod.rs
new file mode 100644
index 0000000..5e54b4a
--- /dev/null
+++ b/aero-user/src/login/mod.rs
@@ -0,0 +1,245 @@
+pub mod demo_provider;
+pub mod ldap_provider;
+pub mod static_provider;
+
+use std::sync::Arc;
+
+use anyhow::{anyhow, bail, Context, Result};
+use async_trait::async_trait;
+use base64::Engine;
+use rand::prelude::*;
+
+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
+/// from their username and password.
+#[async_trait]
+pub trait LoginProvider {
+ /// The login method takes an account's password as an input to decypher
+ /// decryption keys and obtain full access to the user's account.
+ async fn login(&self, username: &str, password: &str) -> Result<Credentials>;
+ /// The public_login method takes an account's email address and returns
+ /// public credentials for adding mails to the user's inbox.
+ async fn public_login(&self, email: &str) -> Result<PublicCredentials>;
+}
+
+/// ArcLoginProvider is simply an alias on a structure that is used
+/// in many places in the code
+pub type ArcLoginProvider = Arc<dyn LoginProvider + Send + Sync>;
+
+/// The struct Credentials represent all of the necessary information to interact
+/// with a user account's data after they are logged in.
+#[derive(Clone, Debug)]
+pub struct Credentials {
+ /// The storage credentials are used to authenticate access to the underlying storage (S3, K2V)
+ pub storage: Builder,
+ /// The cryptographic keys are used to encrypt and decrypt data stored in S3 and K2V
+ pub keys: CryptoKeys,
+}
+
+#[derive(Clone, Debug)]
+pub struct PublicCredentials {
+ /// The storage credentials are used to authenticate access to the underlying storage (S3, K2V)
+ pub storage: Builder,
+ pub public_key: PublicKey,
+}
+
+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
+/// data in a user's mailbox.
+#[derive(Clone, Debug)]
+pub struct CryptoKeys {
+ /// Master key for symmetric encryption of mailbox data
+ pub master: Key,
+ /// Public/private keypair for encryption of incomming emails (secret part)
+ pub secret: SecretKey,
+ /// Public/private keypair for encryption of incomming emails (public part)
+ pub public: PublicKey,
+}
+
+// ----
+
+impl CryptoKeys {
+ /// Initialize a new cryptography root
+ pub fn init() -> Self {
+ let (public, secret) = gen_keypair();
+ let master = gen_key();
+ CryptoKeys {
+ master,
+ secret,
+ public,
+ }
+ }
+
+ // 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());
+ res[32..].copy_from_slice(self.secret.as_ref());
+ 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());
+ }
+ let master = Key::from_slice(&bytes[..32]).unwrap();
+ let secret = SecretKey::from_slice(&bytes[32..]).unwrap();
+ let public = secret.public_key();
+ Ok(Self {
+ master,
+ secret,
+ public,
+ })
+ }
+
+ // 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..])?;
+
+ let keys = Self::deserialize(&password_openned)?;
+ Ok(keys)
+ }
+
+ 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::{password_hash, Algorithm, Argon2, ParamsBuilder, PasswordHasher, Version};
+
+ let params = ParamsBuilder::new()
+ .output_len(output_len)
+ .build()
+ .map_err(|e| anyhow!("Invalid argon2 params: {}", e))?;
+ let argon2 = Argon2::new(Algorithm::default(), Version::default(), params);
+
+ 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, 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())
+}