diff --git a/.gitignore b/.gitignore index a04ef7f..c21e41a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ /target .vimrc -test.sh +env.sh mailrage.toml diff --git a/README.md b/README.md index 8880183..48d1088 100644 --- a/README.md +++ b/README.md @@ -61,19 +61,25 @@ Keys that are stored in K2V under PK `keys`: - if a password is used, `password:`: - a 32-byte salt `Skey` - followed a secret box - - that is encrypted with a strong argon2 digest of the password (using the salt `Skey`) + - that is encrypted with a strong argon2 digest of the password (using the salt `Skey`) and a user secret (see below) - that contains the master secret key and the curve25519 private key +User secret: an additionnal secret that is added to the password when deriving the encryption key for the secret box. +This additionnal secret should not be stored in K2V/S3, so that just knowing a user's password isn't enough to be able +to decrypt their mailbox (supposing the attacker has a dump of their K2V/S3 bucket). +This user secret should typically be stored in the LDAP database or just in the configuration file when using +the static login provider. + Operations: -- **Initialize**(`password`): +- **Initialize**(`user_secret`, `password`): - if `"salt"` or `"public"` already exist, BAIL - generate salt `S` (32 random bytes) - generate `public`, `private` (curve25519 keypair) - generate `master` (secretbox secret key) - calculate `digest = argon2_S(password)` - generate salt `Skey` (32 random bytes) - - calculate `key = argon2_Skey(password)` + - calculate `key = argon2_Skey(user_secret + password)` - serialize `box_contents = (private, master)` - seal box `blob = seal_key(box_contents)` - write `S` at `"salt"` @@ -87,12 +93,12 @@ Operations: - calculate `public` the public key associated with `private` - write `public` at `"public"` -- **Open**(`password`): +- **Open**(`user_secret`, `password`): - load `S = read("salt")` - calculate `digest = argon2_S(password)` - load `blob = read("password:{hex(digest[..16])}") - set `Skey = blob[..32]` - - calculate `key = argon2_Skey(password)` + - calculate `key = argon2_Skey(user_secret + password)` - open secret box `box_contents = open_key(blob[32..])` - retrieve `master` and `private` from `box_contents` - retrieve `public = read("public")` @@ -101,18 +107,18 @@ Operations: - load `public = read("public")` - check that `public` is the correct public key associated with `private` -- **AddPassword**(`existing_password`, `new_password`): +- **AddPassword**(`user_secret`, `existing_password`, `new_password`): - load `S = read("salt")` - calculate `digest = argon2_S(existing_password)` - load `blob = read("existing_password:{hex(digest[..16])}") - set `Skey = blob[..32]` - - calculate `key = argon2_Skey(existing_password)` + - calculate `key = argon2_Skey(user_secret + existing_password)` - open secret box `box_contents = open_key(blob[32..])` - retrieve `master` and `private` from `box_contents` - calculate `digest_new = argon2_S(new_password)` - generate salt `Skeynew` (32 random bytes) - - calculate `key_new = argon2_Skeynew(new_password)` + - calculate `key_new = argon2_Skeynew(user_secret + new_password)` - serialize `box_contents_new = (private, master)` - seal box `blob_new = seal_key_new(box_contents_new)` - write `concat(Skeynew, blob_new)` at `"new_password:{hex(digest_new[..16])}"` diff --git a/src/config.rs b/src/config.rs index 8abbce6..ab40824 100644 --- a/src/config.rs +++ b/src/config.rs @@ -29,6 +29,10 @@ pub struct LoginStaticUser { pub aws_secret_access_key: String, pub bucket: Option, + pub user_secret: String, + #[serde(default)] + pub alternate_user_secrets: Vec, + pub master_key: Option, pub secret_key: Option, } @@ -41,6 +45,8 @@ pub struct LoginLdapConfig { pub username_attr: String, pub aws_access_key_id_attr: String, pub aws_secret_access_key_attr: String, + pub user_secret_attr: String, + pub alternate_user_secrets_attr: Option, pub bucket: Option, pub bucket_attr: Option, diff --git a/src/login/mod.rs b/src/login/mod.rs index 75a39f8..1ee0007 100644 --- a/src/login/mod.rs +++ b/src/login/mod.rs @@ -16,17 +16,27 @@ use rusoto_signature::Region; use crate::cryptoblob::*; +/// 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; } +/// 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: StorageCredentials, + /// The cryptographic keys are used to encrypt and decrypt data stored in S3 and K2V pub keys: CryptoKeys, } +/// The struct StorageCredentials contains access key to an S3 and K2V bucket #[derive(Clone, Debug)] pub struct StorageCredentials { pub s3_region: Region, @@ -37,12 +47,28 @@ pub struct StorageCredentials { 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, +} + +/// 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 + /// Master key for symmetric encryption of mailbox data pub master: Key, - // Public/private keypair for encryption of incomming emails + /// 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, } @@ -92,7 +118,11 @@ impl StorageCredentials { } impl CryptoKeys { - pub async fn init(storage: &StorageCredentials, password: &str) -> Result { + pub async fn init( + storage: &StorageCredentials, + user_secrets: &UserSecrets, + password: &str, + ) -> Result { // Check that salt and public don't exist already let k2v = storage.k2v_client()?; let (salt_ct, public_ct) = Self::check_uninitialized(&k2v).await?; @@ -118,8 +148,7 @@ impl CryptoKeys { thread_rng().fill(&mut kdf_salt); // Calculate key for password secret box - let password_key = - Key::from_slice(&argon2_kdf(&kdf_salt, password.as_bytes(), 32)?).unwrap(); + 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)?; @@ -171,7 +200,11 @@ impl CryptoKeys { Ok(keys) } - pub async fn open(storage: &StorageCredentials, password: &str) -> Result { + pub async fn open( + storage: &StorageCredentials, + user_secrets: &UserSecrets, + password: &str, + ) -> Result { let k2v = storage.k2v_client()?; let (ident_salt, expected_public) = Self::load_salt_and_public(&k2v).await?; @@ -199,9 +232,8 @@ impl CryptoKeys { // Try to open blob let kdf_salt = &password_blob[..32]; - let password_key = - Key::from_slice(&argon2_kdf(kdf_salt, password.as_bytes(), 32)?).unwrap(); - let password_openned = open(&password_blob[32..], &password_key)?; + 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 { @@ -235,7 +267,12 @@ impl CryptoKeys { Ok(keys) } - pub async fn add_password(&self, storage: &StorageCredentials, password: &str) -> Result<()> { + 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?; @@ -247,8 +284,7 @@ impl CryptoKeys { thread_rng().fill(&mut kdf_salt); // Calculate key for password secret box - let password_key = - Key::from_slice(&argon2_kdf(&kdf_salt, password.as_bytes(), 32)?).unwrap(); + 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)?; @@ -453,6 +489,34 @@ impl CryptoKeys { } } +impl UserSecrets { + fn derive_password_key_with(user_secret: &str, kdf_salt: &[u8], password: &str) -> Result { + let tmp = format!("{}\n\n{}", user_secret, password); + Ok(Key::from_slice(&argon2_kdf(&kdf_salt, tmp.as_bytes(), 32)?).unwrap()) + } + + fn derive_password_key(&self, kdf_salt: &[u8], password: &str) -> Result { + Self::derive_password_key_with(&self.user_secret, kdf_salt, password) + } + + fn try_open_encrypted_keys( + &self, + kdf_salt: &[u8], + password: &str, + encrypted_keys: &[u8], + ) -> Result> { + 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."); + } +} + // ---- UTIL ---- pub fn argon2_kdf(salt: &[u8], password: &[u8], output_len: usize) -> Result> { diff --git a/src/login/static_provider.rs b/src/login/static_provider.rs index fb8ec68..cc6ffb6 100644 --- a/src/login/static_provider.rs +++ b/src/login/static_provider.rs @@ -60,7 +60,11 @@ impl LoginProvider for StaticLoginProvider { CryptoKeys::open_without_password(&storage, &master_key, &secret_key).await? } (None, None) => { - CryptoKeys::open(&storage, password).await? + let user_secrets = UserSecrets { + user_secret: u.user_secret.clone(), + alternate_user_secrets: u.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"), }; diff --git a/src/main.rs b/src/main.rs index fe426b4..088c86f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -38,6 +38,8 @@ enum Command { FirstLogin { #[clap(flatten)] creds: StorageCredsArgs, + #[clap(flatten)] + user_secrets: UserSecretsArgs, }, /// Initializes key pairs for a user and dumps keys to stdout for usage with static /// login provider @@ -49,6 +51,8 @@ enum Command { AddPassword { #[clap(flatten)] creds: StorageCredsArgs, + #[clap(flatten)] + user_secrets: UserSecretsArgs, /// Automatically generate password #[clap(short, long)] gen: bool, @@ -57,6 +61,8 @@ enum Command { DeletePassword { #[clap(flatten)] creds: StorageCredsArgs, + #[clap(flatten)] + user_secrets: UserSecretsArgs, /// Allow to delete all passwords #[clap(long)] allow_delete_all: bool, @@ -65,6 +71,8 @@ enum Command { ShowKeys { #[clap(flatten)] creds: StorageCredsArgs, + #[clap(flatten)] + user_secrets: UserSecretsArgs, }, } @@ -90,6 +98,16 @@ struct StorageCredsArgs { bucket: String, } +#[derive(Parser, Debug)] +struct UserSecretsArgs { + /// User secret + #[clap(short = 'U', long, env = "USER_SECRET")] + user_secret: String, + /// Alternate user secrets (comma-separated list of strings) + #[clap(long, env = "ALTERNATE_USER_SECRETS", default_value = "")] + alternate_user_secrets: String, +} + #[tokio::main] async fn main() -> Result<()> { if std::env::var("RUST_LOG").is_err() { @@ -106,8 +124,12 @@ async fn main() -> Result<()> { let server = Server::new(config)?; server.run().await?; } - Command::FirstLogin { creds } => { + Command::FirstLogin { + creds, + user_secrets, + } => { let creds = make_storage_creds(creds); + let user_secrets = make_user_secrets(user_secrets); println!("Please enter your password for key decryption."); println!("If you are using LDAP login, this must be your LDAP password."); @@ -118,7 +140,7 @@ async fn main() -> Result<()> { bail!("Passwords don't match."); } - CryptoKeys::init(&creds, &password).await?; + CryptoKeys::init(&creds, &user_secrets, &password).await?; println!(""); println!("Cryptographic key setup is complete."); @@ -153,8 +175,14 @@ async fn main() -> Result<()> { dump_config(&password, &creds); dump_keys(&keys); } - Command::AddPassword { creds, gen } => { + Command::AddPassword { + creds, + user_secrets, + gen, + } => { let creds = make_storage_creds(creds); + let user_secrets = make_user_secrets(user_secrets); + let existing_password = rpassword::prompt_password("Enter existing password to decrypt keys: ")?; let new_password = if gen { @@ -174,19 +202,23 @@ async fn main() -> Result<()> { password }; - let keys = CryptoKeys::open(&creds, &existing_password).await?; - keys.add_password(&creds, &new_password).await?; + let keys = CryptoKeys::open(&creds, &user_secrets, &existing_password).await?; + keys.add_password(&creds, &user_secrets, &new_password) + .await?; println!(""); println!("New password added successfully."); } Command::DeletePassword { creds, + user_secrets, allow_delete_all, } => { let creds = make_storage_creds(creds); + let user_secrets = make_user_secrets(user_secrets); + let existing_password = rpassword::prompt_password("Enter password to delete: ")?; - let keys = CryptoKeys::open(&creds, &existing_password).await?; + let keys = CryptoKeys::open(&creds, &user_secrets, &existing_password).await?; keys.delete_password(&creds, &existing_password, allow_delete_all) .await?; @@ -198,11 +230,16 @@ async fn main() -> Result<()> { dump_keys(&keys); } } - Command::ShowKeys { creds } => { + Command::ShowKeys { + creds, + user_secrets, + } => { let creds = make_storage_creds(creds); + let user_secrets = make_user_secrets(user_secrets); + let existing_password = rpassword::prompt_password("Enter key decryption password: ")?; - let keys = CryptoKeys::open(&creds, &existing_password).await?; + let keys = CryptoKeys::open(&creds, &user_secrets, &existing_password).await?; dump_keys(&keys); } } @@ -228,6 +265,19 @@ fn make_storage_creds(c: StorageCredsArgs) -> StorageCredentials { } } +fn make_user_secrets(c: UserSecretsArgs) -> UserSecrets { + UserSecrets { + user_secret: c.user_secret, + alternate_user_secrets: c + .alternate_user_secrets + .split(",") + .map(|x| x.trim()) + .filter(|x| !x.is_empty()) + .map(|x| x.to_string()) + .collect(), + } +} + fn dump_config(password: &str, creds: &StorageCredentials) { println!("[login_static.users.]"); println!(