aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlex Auvolat <alex@adnab.me>2022-05-20 12:49:53 +0200
committerAlex Auvolat <alex@adnab.me>2022-05-20 12:49:53 +0200
commit46145350eb0e3bae8ca935d0f0f23eb1d40e6f58 (patch)
treee96976dd2e5e5e8f6e9bf9025ef4eb2446fff318
parentaddaf087abfaa9fd41f75ce0c2181a2df48a972a (diff)
downloadaerogramme-46145350eb0e3bae8ca935d0f0f23eb1d40e6f58.tar.gz
aerogramme-46145350eb0e3bae8ca935d0f0f23eb1d40e6f58.zip
Implement some crypto
-rw-r--r--README.md2
-rw-r--r--src/login/mod.rs289
-rw-r--r--src/login/static_provider.rs2
3 files changed, 281 insertions, 12 deletions
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<Self> {
- 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<Self> {
- 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<Self> {
- 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<Self> {
- 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<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))
+ }
+
+ 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,
+ })
+ }
+}
+
+// ---- 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);
+
+ 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<CausalityToken>,
+ 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<String> {
pub fn verify_password(password: &str, hash: &str) -> Result<bool> {
use argon2::{
- password_hash::{rand_core::OsRng, PasswordHash, PasswordVerifier},
+ password_hash::{PasswordHash, PasswordVerifier},
Argon2,
};
let parsed_hash =