diff --git a/README.md b/README.md index e9d506c..8880183 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,6 @@ Operations: - **Initialize**(`password`): - if `"salt"` or `"public"` already exist, BAIL - generate salt `S` (32 random bytes) - - write `S` at `"salt"` - generate `public`, `private` (curve25519 keypair) - generate `master` (secretbox secret key) - calculate `digest = argon2_S(password)` @@ -77,6 +76,7 @@ Operations: - calculate `key = argon2_Skey(password)` - serialize `box_contents = (private, master)` - seal box `blob = seal_key(box_contents)` + - write `S` at `"salt"` - write `concat(Skey, blob)` at `"password:{hex(digest[..16])}"` - write `public` at `"public"` diff --git a/src/login/mod.rs b/src/login/mod.rs index 4130496..90aaede 100644 --- a/src/login/mod.rs +++ b/src/login/mod.rs @@ -1,9 +1,10 @@ pub mod ldap_provider; pub mod static_provider; -use anyhow::Result; +use anyhow::{anyhow, bail, Result}; use async_trait::async_trait; -use k2v_client::K2vClient; +use k2v_client::{BatchInsertOp, BatchReadOp, CausalityToken, Filter, K2vClient, K2vValue}; +use rand::prelude::*; use rusoto_core::HttpClient; use rusoto_credential::{AwsCredentials, StaticProvider}; use rusoto_s3::S3Client; @@ -88,27 +89,151 @@ impl StorageCredentials { impl CryptoKeys { pub async fn init(storage: &StorageCredentials, password: &str) -> Result { - unimplemented!() + // Check that salt and public don't exist already + let k2v = storage.k2v_client()?; + 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 + 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 + let password_key = + Key::from_slice(&argon2_kdf(&kdf_salt, password.as_bytes(), 32)?).unwrap(); + + // 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", None, &ident_salt), + k2v_insert_single_key("keys", "public", None, &keys.public), + k2v_insert_single_key("keys", &password_sortkey, None, &password_blob), + ]) + .await?; + + Ok(keys) } pub async fn init_without_password( storage: &StorageCredentials, - master_key: &Key, - secret_key: &SecretKey, + master: &Key, + secret: &SecretKey, ) -> Result { - unimplemented!() + // Check that salt and public don't exist already + let k2v = storage.k2v_client()?; + 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", None, &ident_salt), + k2v_insert_single_key("keys", "public", None, &keys.public), + ]) + .await?; + + Ok(keys) } pub async fn open(storage: &StorageCredentials, password: &str) -> Result { - unimplemented!() + 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 params = k2v + .read_batch(&[k2v_read_single_key("keys", &password_sortkey)]) + .await?; + if params.len() != 1 { + bail!( + "Invalid response from k2v storage: {:?} (expected one item)", + params + ); + } + if params[0].items.len() != 1 { + bail!("given password does not exist in storage."); + } + let vals = &mut params[0].items.iter_mut().next().unwrap().1.value; + if vals.len() != 1 { + bail!("Multiple values for password in storage"); + } + match &mut vals[0] { + K2vValue::Value(v) => std::mem::take(v), + K2vValue::Tombstone => bail!("password is a tombstone"), + } + }; + + // 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 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: &Key, - secret_key: &SecretKey, + master: &Key, + secret: &SecretKey, ) -> Result { - unimplemented!() + 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, password: &str) -> Result<()> { @@ -123,4 +248,148 @@ impl CryptoKeys { ) -> Result<()> { unimplemented!() } + + // ---- STORAGE UTIL ---- + + async fn check_uninitialized(k2v: &K2vClient) -> Result<()> { + let params = k2v + .read_batch(&[ + k2v_read_single_key("keys", "salt"), + k2v_read_single_key("keys", "public"), + ]) + .await?; + if params.len() != 2 { + bail!( + "Invalid response from k2v storage: {:?} (expected two items)", + params + ); + } + if !params[0].items.is_empty() || !params[1].items.is_empty() { + bail!("`salt` or `public` already exists in keys storage."); + } + + Ok(()) + } + + async fn load_salt_and_public(k2v: &K2vClient) -> Result<([u8; 32], PublicKey)> { + let mut params = k2v + .read_batch(&[ + k2v_read_single_key("keys", "salt"), + k2v_read_single_key("keys", "public"), + ]) + .await?; + 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!("`salt` or `public` do not exist in storage."); + } + + // 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 = 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 = 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)) + } + + 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 { + 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, + }) + } +} + +// ---- UTIL ---- + +pub fn argon2_kdf(salt: &[u8], password: &[u8], output_len: usize) -> Result> { + 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); + + let salt = base64::encode(salt); + 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()) +} + +pub fn k2v_read_single_key<'a>(partition_key: &'a str, sort_key: &'a str) -> BatchReadOp<'a> { + BatchReadOp { + partition_key: partition_key, + filter: Filter { + start: Some(sort_key), + end: None, + prefix: None, + limit: None, + reverse: false, + }, + conflicts_only: false, + tombstones: false, + single_item: true, + } +} + +pub fn k2v_insert_single_key<'a>( + partition_key: &'a str, + sort_key: &'a str, + causality: Option, + 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 74a6c14..fb8ec68 100644 --- a/src/login/static_provider.rs +++ b/src/login/static_provider.rs @@ -86,7 +86,7 @@ pub fn hash_password(password: &str) -> Result { pub fn verify_password(password: &str, hash: &str) -> Result { use argon2::{ - password_hash::{rand_core::OsRng, PasswordHash, PasswordVerifier}, + password_hash::{PasswordHash, PasswordVerifier}, Argon2, }; let parsed_hash =