Implement some crypto
This commit is contained in:
parent
addaf087ab
commit
46145350eb
3 changed files with 281 additions and 12 deletions
|
@ -69,7 +69,6 @@ Operations:
|
||||||
- **Initialize**(`password`):
|
- **Initialize**(`password`):
|
||||||
- if `"salt"` or `"public"` already exist, BAIL
|
- if `"salt"` or `"public"` already exist, BAIL
|
||||||
- generate salt `S` (32 random bytes)
|
- generate salt `S` (32 random bytes)
|
||||||
- write `S` at `"salt"`
|
|
||||||
- generate `public`, `private` (curve25519 keypair)
|
- generate `public`, `private` (curve25519 keypair)
|
||||||
- generate `master` (secretbox secret key)
|
- generate `master` (secretbox secret key)
|
||||||
- calculate `digest = argon2_S(password)`
|
- calculate `digest = argon2_S(password)`
|
||||||
|
@ -77,6 +76,7 @@ Operations:
|
||||||
- calculate `key = argon2_Skey(password)`
|
- calculate `key = argon2_Skey(password)`
|
||||||
- serialize `box_contents = (private, master)`
|
- serialize `box_contents = (private, master)`
|
||||||
- seal box `blob = seal_key(box_contents)`
|
- seal box `blob = seal_key(box_contents)`
|
||||||
|
- write `S` at `"salt"`
|
||||||
- write `concat(Skey, blob)` at `"password:{hex(digest[..16])}"`
|
- write `concat(Skey, blob)` at `"password:{hex(digest[..16])}"`
|
||||||
- write `public` at `"public"`
|
- write `public` at `"public"`
|
||||||
|
|
||||||
|
|
289
src/login/mod.rs
289
src/login/mod.rs
|
@ -1,9 +1,10 @@
|
||||||
pub mod ldap_provider;
|
pub mod ldap_provider;
|
||||||
pub mod static_provider;
|
pub mod static_provider;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::{anyhow, bail, Result};
|
||||||
use async_trait::async_trait;
|
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_core::HttpClient;
|
||||||
use rusoto_credential::{AwsCredentials, StaticProvider};
|
use rusoto_credential::{AwsCredentials, StaticProvider};
|
||||||
use rusoto_s3::S3Client;
|
use rusoto_s3::S3Client;
|
||||||
|
@ -88,27 +89,151 @@ impl StorageCredentials {
|
||||||
|
|
||||||
impl CryptoKeys {
|
impl CryptoKeys {
|
||||||
pub async fn init(storage: &StorageCredentials, password: &str) -> Result<Self> {
|
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(
|
pub async fn init_without_password(
|
||||||
storage: &StorageCredentials,
|
storage: &StorageCredentials,
|
||||||
master_key: &Key,
|
master: &Key,
|
||||||
secret_key: &SecretKey,
|
secret: &SecretKey,
|
||||||
) -> Result<Self> {
|
) -> 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> {
|
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(
|
pub async fn open_without_password(
|
||||||
storage: &StorageCredentials,
|
storage: &StorageCredentials,
|
||||||
master_key: &Key,
|
master: &Key,
|
||||||
secret_key: &SecretKey,
|
secret: &SecretKey,
|
||||||
) -> Result<Self> {
|
) -> 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<()> {
|
pub async fn add_password(&self, storage: &StorageCredentials, password: &str) -> Result<()> {
|
||||||
|
@ -123,4 +248,148 @@ impl CryptoKeys {
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
unimplemented!()
|
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()),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -86,7 +86,7 @@ pub fn hash_password(password: &str) -> Result<String> {
|
||||||
|
|
||||||
pub fn verify_password(password: &str, hash: &str) -> Result<bool> {
|
pub fn verify_password(password: &str, hash: &str) -> Result<bool> {
|
||||||
use argon2::{
|
use argon2::{
|
||||||
password_hash::{rand_core::OsRng, PasswordHash, PasswordVerifier},
|
password_hash::{PasswordHash, PasswordVerifier},
|
||||||
Argon2,
|
Argon2,
|
||||||
};
|
};
|
||||||
let parsed_hash =
|
let parsed_hash =
|
||||||
|
|
Loading…
Reference in a new issue