use std::fs;
use std::path::PathBuf;
use argon2::Argon2;
use rand::RngCore;
use crate::config;
use crate::error::{HuddleError, Result};
pub const MASTER_KEY_LEN: usize = 32;
pub const KEYCHAIN_SALT_LEN: usize = 16;
pub fn keychain_salt_path() -> PathBuf {
config::data_dir().join("keychain.salt")
}
pub fn load_or_create_salt() -> Result<[u8; KEYCHAIN_SALT_LEN]> {
let path = keychain_salt_path();
if let Ok(bytes) = fs::read(&path) {
if bytes.len() == KEYCHAIN_SALT_LEN {
let mut out = [0u8; KEYCHAIN_SALT_LEN];
out.copy_from_slice(&bytes);
return Ok(out);
}
}
config::ensure_data_dir()?;
let mut salt = [0u8; KEYCHAIN_SALT_LEN];
rand::thread_rng().fill_bytes(&mut salt);
fs::write(&path, salt).map_err(|e| HuddleError::Other(format!("write salt: {e}")))?;
Ok(salt)
}
pub fn derive_master_key(
passphrase: &str,
salt: &[u8; KEYCHAIN_SALT_LEN],
) -> Result<[u8; MASTER_KEY_LEN]> {
let argon = Argon2::default();
let mut out = [0u8; MASTER_KEY_LEN];
argon
.hash_password_into(passphrase.as_bytes(), salt, &mut out)
.map_err(|e| HuddleError::Other(format!("argon2 derive: {e}")))?;
Ok(out)
}
pub fn derive_subkey(master_key: &[u8; MASTER_KEY_LEN], purpose: &[u8]) -> [u8; 32] {
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update(master_key);
hasher.update(b"|");
hasher.update(purpose);
let h = hasher.finalize();
let mut out = [0u8; 32];
out.copy_from_slice(&h);
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn derive_is_deterministic() {
let salt = [42u8; KEYCHAIN_SALT_LEN];
let k1 = derive_master_key("hunter2", &salt).unwrap();
let k2 = derive_master_key("hunter2", &salt).unwrap();
assert_eq!(k1, k2);
}
#[test]
fn derive_differs_with_passphrase() {
let salt = [42u8; KEYCHAIN_SALT_LEN];
let k1 = derive_master_key("hunter2", &salt).unwrap();
let k2 = derive_master_key("hunter3", &salt).unwrap();
assert_ne!(k1, k2);
}
#[test]
fn subkeys_are_purpose_separated() {
let mk = [9u8; MASTER_KEY_LEN];
let a = derive_subkey(&mk, b"megolm-persist");
let b = derive_subkey(&mk, b"db-encryption");
assert_ne!(a, b);
}
}