2022-05-19 10:10:48 +00:00
|
|
|
pub mod ldap_provider;
|
|
|
|
pub mod static_provider;
|
|
|
|
|
2022-05-20 11:05:44 +00:00
|
|
|
use std::collections::BTreeMap;
|
2022-06-17 16:39:36 +00:00
|
|
|
use std::sync::Arc;
|
2022-05-20 11:05:44 +00:00
|
|
|
|
2022-05-20 11:36:45 +00:00
|
|
|
use anyhow::{anyhow, bail, Context, Result};
|
2022-05-19 10:10:48 +00:00
|
|
|
use async_trait::async_trait;
|
2022-05-20 11:05:44 +00:00
|
|
|
use k2v_client::{
|
|
|
|
BatchInsertOp, BatchReadOp, CausalValue, CausalityToken, Filter, K2vClient, K2vValue,
|
|
|
|
};
|
2022-05-20 10:49:53 +00:00
|
|
|
use rand::prelude::*;
|
2022-05-19 11:54:38 +00:00
|
|
|
use rusoto_core::HttpClient;
|
|
|
|
use rusoto_credential::{AwsCredentials, StaticProvider};
|
|
|
|
use rusoto_s3::S3Client;
|
2022-05-19 10:10:48 +00:00
|
|
|
|
2022-05-19 12:33:49 +00:00
|
|
|
use crate::cryptoblob::*;
|
2022-05-19 10:10:48 +00:00
|
|
|
|
2022-05-23 15:31:53 +00:00
|
|
|
/// 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.
|
2022-05-19 11:54:38 +00:00
|
|
|
#[async_trait]
|
|
|
|
pub trait LoginProvider {
|
2022-05-23 15:31:53 +00:00
|
|
|
/// The login method takes an account's password as an input to decypher
|
|
|
|
/// decryption keys and obtain full access to the user's account.
|
2022-05-19 11:54:38 +00:00
|
|
|
async fn login(&self, username: &str, password: &str) -> Result<Credentials>;
|
2022-05-31 13:30:32 +00:00
|
|
|
/// 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>;
|
2022-05-19 11:54:38 +00:00
|
|
|
}
|
|
|
|
|
2022-06-17 16:39:36 +00:00
|
|
|
/// 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>;
|
|
|
|
|
2022-05-23 15:31:53 +00:00
|
|
|
/// The struct Credentials represent all of the necessary information to interact
|
|
|
|
/// with a user account's data after they are logged in.
|
2022-05-19 10:10:48 +00:00
|
|
|
#[derive(Clone, Debug)]
|
|
|
|
pub struct Credentials {
|
2022-05-23 15:31:53 +00:00
|
|
|
/// The storage credentials are used to authenticate access to the underlying storage (S3, K2V)
|
2022-05-19 12:33:49 +00:00
|
|
|
pub storage: StorageCredentials,
|
2022-05-23 15:31:53 +00:00
|
|
|
/// The cryptographic keys are used to encrypt and decrypt data stored in S3 and K2V
|
2022-05-19 12:33:49 +00:00
|
|
|
pub keys: CryptoKeys,
|
|
|
|
}
|
|
|
|
|
2022-05-31 13:30:32 +00:00
|
|
|
#[derive(Clone, Debug)]
|
|
|
|
pub struct PublicCredentials {
|
|
|
|
/// The storage credentials are used to authenticate access to the underlying storage (S3, K2V)
|
|
|
|
pub storage: StorageCredentials,
|
|
|
|
pub public_key: PublicKey,
|
|
|
|
}
|
|
|
|
|
2022-05-23 15:31:53 +00:00
|
|
|
/// The struct StorageCredentials contains access key to an S3 and K2V bucket
|
2022-06-29 13:39:54 +00:00
|
|
|
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
|
2022-05-19 12:33:49 +00:00
|
|
|
pub struct StorageCredentials {
|
|
|
|
pub s3_region: Region,
|
|
|
|
pub k2v_region: Region,
|
|
|
|
|
2022-05-19 10:10:48 +00:00
|
|
|
pub aws_access_key_id: String,
|
|
|
|
pub aws_secret_access_key: String,
|
|
|
|
pub bucket: String,
|
|
|
|
}
|
|
|
|
|
2022-05-23 15:31:53 +00:00
|
|
|
/// 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.
|
2022-05-19 12:33:49 +00:00
|
|
|
#[derive(Clone, Debug)]
|
|
|
|
pub struct CryptoKeys {
|
2022-05-23 15:31:53 +00:00
|
|
|
/// Master key for symmetric encryption of mailbox data
|
2022-05-19 12:33:49 +00:00
|
|
|
pub master: Key,
|
2022-05-23 15:31:53 +00:00
|
|
|
/// Public/private keypair for encryption of incomming emails (secret part)
|
2022-05-19 12:33:49 +00:00
|
|
|
pub secret: SecretKey,
|
2022-05-23 15:31:53 +00:00
|
|
|
/// Public/private keypair for encryption of incomming emails (public part)
|
2022-05-19 12:33:49 +00:00
|
|
|
pub public: PublicKey,
|
|
|
|
}
|
|
|
|
|
2022-06-29 13:39:54 +00:00
|
|
|
/// 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(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-05-19 12:33:49 +00:00
|
|
|
// ----
|
|
|
|
|
2022-05-19 11:54:38 +00:00
|
|
|
impl Credentials {
|
2022-05-19 12:33:49 +00:00
|
|
|
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> {
|
2022-05-19 11:54:38 +00:00
|
|
|
let aws_creds = AwsCredentials::new(
|
|
|
|
self.aws_access_key_id.clone(),
|
|
|
|
self.aws_secret_access_key.clone(),
|
|
|
|
None,
|
|
|
|
None,
|
|
|
|
);
|
|
|
|
|
|
|
|
Ok(K2vClient::new(
|
2022-06-29 13:39:54 +00:00
|
|
|
self.k2v_region.as_rusoto_region(),
|
2022-05-19 11:54:38 +00:00
|
|
|
self.bucket.clone(),
|
|
|
|
aws_creds,
|
|
|
|
None,
|
|
|
|
)?)
|
|
|
|
}
|
|
|
|
|
2022-05-19 12:33:49 +00:00
|
|
|
pub fn s3_client(&self) -> Result<S3Client> {
|
2022-05-19 11:54:38 +00:00
|
|
|
let aws_creds_provider = StaticProvider::new_minimal(
|
|
|
|
self.aws_access_key_id.clone(),
|
|
|
|
self.aws_secret_access_key.clone(),
|
|
|
|
);
|
|
|
|
|
|
|
|
Ok(S3Client::new_with(
|
|
|
|
HttpClient::new()?,
|
|
|
|
aws_creds_provider,
|
2022-06-29 13:39:54 +00:00
|
|
|
self.s3_region.as_rusoto_region(),
|
2022-05-19 11:54:38 +00:00
|
|
|
))
|
|
|
|
}
|
2022-05-19 10:10:48 +00:00
|
|
|
}
|
2022-05-19 12:33:49 +00:00
|
|
|
|
|
|
|
impl CryptoKeys {
|
2022-05-23 15:31:53 +00:00
|
|
|
pub async fn init(
|
|
|
|
storage: &StorageCredentials,
|
|
|
|
user_secrets: &UserSecrets,
|
|
|
|
password: &str,
|
|
|
|
) -> Result<Self> {
|
2022-05-20 10:49:53 +00:00
|
|
|
// Check that salt and public don't exist already
|
|
|
|
let k2v = storage.k2v_client()?;
|
2022-05-20 19:35:50 +00:00
|
|
|
let (salt_ct, public_ct) = Self::check_uninitialized(&k2v).await?;
|
2022-05-20 10:49:53 +00:00
|
|
|
|
|
|
|
// 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
|
|
|
|
let (public, secret) = gen_keypair();
|
|
|
|
let master = gen_key();
|
|
|
|
let keys = 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
|
2022-05-23 15:31:53 +00:00
|
|
|
let password_key = user_secrets.derive_password_key(&kdf_salt, password)?;
|
2022-05-20 10:49:53 +00:00
|
|
|
|
|
|
|
// 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(&[
|
2022-05-20 19:35:50 +00:00
|
|
|
k2v_insert_single_key("keys", "salt", salt_ct, &ident_salt),
|
|
|
|
k2v_insert_single_key("keys", "public", public_ct, &keys.public),
|
2022-05-20 10:49:53 +00:00
|
|
|
k2v_insert_single_key("keys", &password_sortkey, None, &password_blob),
|
|
|
|
])
|
2022-05-20 11:36:45 +00:00
|
|
|
.await
|
|
|
|
.context("InsertBatch for salt, public, and password")?;
|
2022-05-20 10:49:53 +00:00
|
|
|
|
|
|
|
Ok(keys)
|
2022-05-19 12:33:49 +00:00
|
|
|
}
|
|
|
|
|
2022-05-19 13:14:36 +00:00
|
|
|
pub async fn init_without_password(
|
|
|
|
storage: &StorageCredentials,
|
2022-05-20 10:49:53 +00:00
|
|
|
master: &Key,
|
|
|
|
secret: &SecretKey,
|
2022-05-19 13:14:36 +00:00
|
|
|
) -> Result<Self> {
|
2022-05-20 10:49:53 +00:00
|
|
|
// Check that salt and public don't exist already
|
|
|
|
let k2v = storage.k2v_client()?;
|
2022-05-20 19:35:50 +00:00
|
|
|
let (salt_ct, public_ct) = Self::check_uninitialized(&k2v).await?;
|
2022-05-20 10:49:53 +00:00
|
|
|
|
|
|
|
// 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(&[
|
2022-05-20 19:35:50 +00:00
|
|
|
k2v_insert_single_key("keys", "salt", salt_ct, &ident_salt),
|
|
|
|
k2v_insert_single_key("keys", "public", public_ct, &keys.public),
|
2022-05-20 10:49:53 +00:00
|
|
|
])
|
2022-05-20 11:36:45 +00:00
|
|
|
.await
|
|
|
|
.context("InsertBatch for salt and public")?;
|
2022-05-20 10:49:53 +00:00
|
|
|
|
|
|
|
Ok(keys)
|
2022-05-19 12:33:49 +00:00
|
|
|
}
|
|
|
|
|
2022-05-23 15:31:53 +00:00
|
|
|
pub async fn open(
|
|
|
|
storage: &StorageCredentials,
|
|
|
|
user_secrets: &UserSecrets,
|
|
|
|
password: &str,
|
|
|
|
) -> Result<Self> {
|
2022-05-20 10:49:53 +00:00
|
|
|
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 = {
|
2022-05-20 19:02:53 +00:00
|
|
|
let mut val = match k2v.read_item("keys", &password_sortkey).await {
|
|
|
|
Err(k2v_client::Error::NotFound) => {
|
2022-05-20 19:35:50 +00:00
|
|
|
bail!("invalid password")
|
2022-05-20 19:02:53 +00:00
|
|
|
}
|
|
|
|
x => x?,
|
|
|
|
};
|
|
|
|
if val.value.len() != 1 {
|
|
|
|
bail!("multiple values for password in storage");
|
2022-05-20 10:49:53 +00:00
|
|
|
}
|
2022-05-20 19:02:53 +00:00
|
|
|
match val.value.pop().unwrap() {
|
|
|
|
K2vValue::Value(v) => v,
|
2022-05-20 19:35:50 +00:00
|
|
|
K2vValue::Tombstone => bail!("invalid password"),
|
2022-05-20 10:49:53 +00:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
// Try to open blob
|
|
|
|
let kdf_salt = &password_blob[..32];
|
2022-05-23 15:31:53 +00:00
|
|
|
let password_openned =
|
|
|
|
user_secrets.try_open_encrypted_keys(&kdf_salt, password, &password_blob[32..])?;
|
2022-05-20 10:49:53 +00:00
|
|
|
|
|
|
|
let keys = Self::deserialize(&password_openned)?;
|
|
|
|
if keys.public != expected_public {
|
|
|
|
bail!("Password public key doesn't match stored public key");
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(keys)
|
2022-05-19 12:33:49 +00:00
|
|
|
}
|
|
|
|
|
2022-05-19 13:14:36 +00:00
|
|
|
pub async fn open_without_password(
|
|
|
|
storage: &StorageCredentials,
|
2022-05-20 10:49:53 +00:00
|
|
|
master: &Key,
|
|
|
|
secret: &SecretKey,
|
2022-05-19 13:14:36 +00:00
|
|
|
) -> Result<Self> {
|
2022-05-20 10:49:53 +00:00
|
|
|
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)
|
2022-05-19 12:33:49 +00:00
|
|
|
}
|
|
|
|
|
2022-05-23 15:31:53 +00:00
|
|
|
pub async fn add_password(
|
|
|
|
&self,
|
|
|
|
storage: &StorageCredentials,
|
|
|
|
user_secrets: &UserSecrets,
|
|
|
|
password: &str,
|
|
|
|
) -> Result<()> {
|
2022-05-20 11:05:44 +00:00
|
|
|
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
|
2022-05-23 15:31:53 +00:00
|
|
|
let password_key = user_secrets.derive_password_key(&kdf_salt, password)?;
|
2022-05-20 11:05:44 +00:00
|
|
|
|
|
|
|
// 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
|
2022-05-20 19:35:50 +00:00
|
|
|
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");
|
2022-05-20 11:05:44 +00:00
|
|
|
}
|
2022-05-20 19:35:50 +00:00
|
|
|
Some(entry.causality.clone())
|
2022-05-20 11:05:44 +00:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
// Write values to storage
|
|
|
|
k2v.insert_batch(&[k2v_insert_single_key(
|
|
|
|
"keys",
|
|
|
|
&password_sortkey,
|
|
|
|
ct,
|
|
|
|
&password_blob,
|
|
|
|
)])
|
2022-05-20 11:36:45 +00:00
|
|
|
.await
|
|
|
|
.context("InsertBatch for new password")?;
|
2022-05-20 11:05:44 +00:00
|
|
|
|
|
|
|
Ok(())
|
2022-05-19 12:33:49 +00:00
|
|
|
}
|
|
|
|
|
2022-05-19 13:14:36 +00:00
|
|
|
pub async fn delete_password(
|
|
|
|
storage: &StorageCredentials,
|
|
|
|
password: &str,
|
|
|
|
allow_delete_all: bool,
|
|
|
|
) -> Result<()> {
|
2022-05-20 11:05:44 +00:00
|
|
|
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())
|
2022-05-20 11:36:45 +00:00
|
|
|
.await
|
|
|
|
.context("DeleteItem for password")?;
|
2022-05-20 11:05:44 +00:00
|
|
|
|
|
|
|
Ok(())
|
2022-05-19 12:33:49 +00:00
|
|
|
}
|
2022-05-20 10:49:53 +00:00
|
|
|
|
|
|
|
// ---- STORAGE UTIL ----
|
|
|
|
|
2022-05-20 19:35:50 +00:00
|
|
|
async fn check_uninitialized(
|
|
|
|
k2v: &K2vClient,
|
|
|
|
) -> Result<(Option<CausalityToken>, Option<CausalityToken>)> {
|
2022-05-20 10:49:53 +00:00
|
|
|
let params = k2v
|
|
|
|
.read_batch(&[
|
2022-05-20 19:35:50 +00:00
|
|
|
k2v_read_single_key("keys", "salt", true),
|
|
|
|
k2v_read_single_key("keys", "public", true),
|
2022-05-20 10:49:53 +00:00
|
|
|
])
|
2022-05-20 11:36:45 +00:00
|
|
|
.await
|
|
|
|
.context("ReadBatch for salt and public in check_uninitialized")?;
|
2022-05-20 10:49:53 +00:00
|
|
|
if params.len() != 2 {
|
|
|
|
bail!(
|
|
|
|
"Invalid response from k2v storage: {:?} (expected two items)",
|
|
|
|
params
|
|
|
|
);
|
|
|
|
}
|
2022-05-20 19:35:50 +00:00
|
|
|
if params[0].items.len() > 1 || params[1].items.len() > 1 {
|
|
|
|
bail!(
|
|
|
|
"invalid response from k2v storage: {:?} (several items in single_item read)",
|
|
|
|
params
|
|
|
|
);
|
2022-05-20 10:49:53 +00:00
|
|
|
}
|
|
|
|
|
2022-05-20 19:35:50 +00:00
|
|
|
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))
|
2022-05-20 10:49:53 +00:00
|
|
|
}
|
|
|
|
|
2022-05-31 13:30:32 +00:00
|
|
|
pub async fn load_salt_and_public(k2v: &K2vClient) -> Result<([u8; 32], PublicKey)> {
|
2022-05-20 10:49:53 +00:00
|
|
|
let mut params = k2v
|
|
|
|
.read_batch(&[
|
2022-05-20 19:35:50 +00:00
|
|
|
k2v_read_single_key("keys", "salt", false),
|
|
|
|
k2v_read_single_key("keys", "public", false),
|
2022-05-20 10:49:53 +00:00
|
|
|
])
|
2022-05-20 11:36:45 +00:00
|
|
|
.await
|
|
|
|
.context("ReadBatch for salt and public in load_salt_and_public")?;
|
2022-05-20 10:49:53 +00:00
|
|
|
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 {
|
2022-05-20 19:35:50 +00:00
|
|
|
bail!("cryptographic keys not initialized for user");
|
2022-05-20 10:49:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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))
|
|
|
|
}
|
|
|
|
|
2022-05-20 11:05:44 +00:00
|
|
|
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,
|
|
|
|
}])
|
2022-05-20 11:36:45 +00:00
|
|
|
.await
|
|
|
|
.context("ReadBatch for prefix password: in list_existing_passwords")?;
|
2022-05-20 11:05:44 +00:00
|
|
|
if res.len() != 1 {
|
|
|
|
bail!("unexpected k2v result: {:?}, expected one item", res);
|
|
|
|
}
|
|
|
|
Ok(res.pop().unwrap().items)
|
|
|
|
}
|
|
|
|
|
2022-05-20 10:49:53 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
|
|
|
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,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-05-23 15:31:53 +00:00
|
|
|
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.");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-05-20 10:49:53 +00:00
|
|
|
// ---- UTIL ----
|
|
|
|
|
|
|
|
pub fn argon2_kdf(salt: &[u8], password: &[u8], output_len: usize) -> Result<Vec<u8>> {
|
|
|
|
use argon2::{Algorithm, Argon2, ParamsBuilder, PasswordHasher, Version};
|
|
|
|
|
|
|
|
let mut params = ParamsBuilder::new();
|
|
|
|
params
|
|
|
|
.output_len(output_len)
|
|
|
|
.map_err(|e| anyhow!("Invalid output length: {}", e))?;
|
|
|
|
|
|
|
|
let params = params
|
|
|
|
.params()
|
|
|
|
.map_err(|e| anyhow!("Invalid argon2 params: {}", e))?;
|
|
|
|
let argon2 = Argon2::new(Algorithm::default(), Version::default(), params);
|
|
|
|
|
2022-05-20 11:36:45 +00:00
|
|
|
let salt = base64::encode_config(salt, base64::STANDARD_NO_PAD);
|
2022-05-20 10:49:53 +00:00
|
|
|
let hash = argon2
|
|
|
|
.hash_password(password, &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())
|
|
|
|
}
|
|
|
|
|
2022-05-20 19:35:50 +00:00
|
|
|
pub fn k2v_read_single_key<'a>(
|
|
|
|
partition_key: &'a str,
|
|
|
|
sort_key: &'a str,
|
|
|
|
tombstones: bool,
|
|
|
|
) -> BatchReadOp<'a> {
|
2022-05-20 10:49:53 +00:00
|
|
|
BatchReadOp {
|
|
|
|
partition_key: partition_key,
|
|
|
|
filter: Filter {
|
|
|
|
start: Some(sort_key),
|
|
|
|
end: None,
|
|
|
|
prefix: None,
|
|
|
|
limit: None,
|
|
|
|
reverse: false,
|
|
|
|
},
|
|
|
|
conflicts_only: false,
|
2022-05-20 19:35:50 +00:00
|
|
|
tombstones,
|
2022-05-20 10:49:53 +00:00
|
|
|
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()),
|
|
|
|
}
|
2022-05-19 12:33:49 +00:00
|
|
|
}
|