Add user secret in mix to encrypt keys with password
This commit is contained in:
parent
378cbd76d0
commit
cb9b64a184
6 changed files with 160 additions and 30 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,4 +1,4 @@
|
|||
/target
|
||||
.vimrc
|
||||
test.sh
|
||||
env.sh
|
||||
mailrage.toml
|
||||
|
|
22
README.md
22
README.md
|
@ -61,19 +61,25 @@ Keys that are stored in K2V under PK `keys`:
|
|||
- if a password is used, `password:<truncated(128bit) argon2 digest of password using salt S>`:
|
||||
- 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])}"`
|
||||
|
|
|
@ -29,6 +29,10 @@ pub struct LoginStaticUser {
|
|||
pub aws_secret_access_key: String,
|
||||
pub bucket: Option<String>,
|
||||
|
||||
pub user_secret: String,
|
||||
#[serde(default)]
|
||||
pub alternate_user_secrets: Vec<String>,
|
||||
|
||||
pub master_key: Option<String>,
|
||||
pub secret_key: Option<String>,
|
||||
}
|
||||
|
@ -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<String>,
|
||||
|
||||
pub bucket: Option<String>,
|
||||
pub bucket_attr: Option<String>,
|
||||
|
|
|
@ -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<Credentials>;
|
||||
}
|
||||
|
||||
/// 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<String>,
|
||||
}
|
||||
|
||||
/// 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<Self> {
|
||||
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?;
|
||||
|
@ -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<Self> {
|
||||
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?;
|
||||
|
||||
|
@ -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<Key> {
|
||||
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<Key> {
|
||||
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<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.");
|
||||
}
|
||||
}
|
||||
|
||||
// ---- UTIL ----
|
||||
|
||||
pub fn argon2_kdf(salt: &[u8], password: &[u8], output_len: usize) -> Result<Vec<u8>> {
|
||||
|
|
|
@ -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"),
|
||||
};
|
||||
|
|
66
src/main.rs
66
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.<username>]");
|
||||
println!(
|
||||
|
|
Loading…
Reference in a new issue