use argon2::{Algorithm, Argon2, Params, Version};
use hkdf::Hkdf;
use rand::RngCore;
use sha2::Sha256;
use zeroize::{Zeroize, ZeroizeOnDrop};
use crate::{Result, VaultConfig, VaultError};
pub const SALT_SIZE: usize = 16;
pub const KEY_SIZE: usize = 32;
#[derive(Zeroize, ZeroizeOnDrop)]
pub struct MasterKey {
bytes: [u8; KEY_SIZE],
}
impl MasterKey {
pub fn derive(input: &[u8], config: &VaultConfig) -> Result<(Self, [u8; SALT_SIZE])> {
match config.salt {
Some(salt) => {
let key = Self::derive_with_salt(input, &salt, config)?;
Ok((key, salt))
},
None => Self::derive_with_random_salt(input, config),
}
}
pub fn derive_with_salt(input: &[u8], salt: &[u8], config: &VaultConfig) -> Result<Self> {
let params = Params::new(
config.argon2_memory_cost,
config.argon2_time_cost,
config.argon2_parallelism,
Some(KEY_SIZE),
)
.map_err(|e| VaultError::KeyDerivationError(format!("Invalid Argon2 params: {e}")))?;
let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
let mut key = [0u8; KEY_SIZE];
argon2
.hash_password_into(input, salt, &mut key)
.map_err(|e| VaultError::KeyDerivationError(format!("Argon2 failed: {e}")))?;
Ok(Self { bytes: key })
}
pub fn derive_with_random_salt(
input: &[u8],
config: &VaultConfig,
) -> Result<(Self, [u8; SALT_SIZE])> {
let mut salt = [0u8; SALT_SIZE];
rand::rng().fill_bytes(&mut salt);
let key = Self::derive_with_salt(input, &salt, config)?;
Ok((key, salt))
}
pub fn from_bytes(bytes: [u8; KEY_SIZE]) -> Self {
Self { bytes }
}
pub fn as_bytes(&self) -> &[u8; KEY_SIZE] {
&self.bytes
}
#[allow(clippy::missing_panics_doc)] pub fn derive_subkey(&self, domain: &[u8]) -> [u8; KEY_SIZE] {
let hk = Hkdf::<Sha256>::new(None, &self.bytes);
let mut output = [0u8; KEY_SIZE];
hk.expand(domain, &mut output)
.expect("HKDF expand should never fail with 32-byte output");
output
}
pub fn encryption_key(&self) -> [u8; KEY_SIZE] {
self.derive_subkey(b"neumann_vault_encryption_v1")
}
pub fn obfuscation_key(&self) -> [u8; KEY_SIZE] {
self.derive_subkey(b"neumann_vault_obfuscation_v1")
}
pub fn metadata_key(&self) -> [u8; KEY_SIZE] {
self.derive_subkey(b"neumann_vault_metadata_v1")
}
pub fn audit_key(&self) -> [u8; KEY_SIZE] {
self.derive_subkey(b"neumann_vault_audit_v1")
}
pub fn transit_key(&self) -> [u8; KEY_SIZE] {
self.derive_subkey(b"neumann_vault_transit_v1")
}
pub fn snapshot_key(&self) -> [u8; KEY_SIZE] {
self.derive_subkey(b"neumann_vault_snapshot_v1")
}
pub fn sync_key(&self) -> [u8; KEY_SIZE] {
self.derive_subkey(b"neumann_vault_sync_v1")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_derive_with_explicit_salt_deterministic() {
let config = VaultConfig::default().with_salt([42u8; SALT_SIZE]);
let (key1, salt1) = MasterKey::derive(b"password123", &config).unwrap();
let (key2, salt2) = MasterKey::derive(b"password123", &config).unwrap();
assert_eq!(key1.as_bytes(), key2.as_bytes());
assert_eq!(salt1, salt2);
}
#[test]
fn test_derive_without_salt_generates_random() {
let config = VaultConfig::default();
let (key1, salt1) = MasterKey::derive(b"password123", &config).unwrap();
let (key2, salt2) = MasterKey::derive(b"password123", &config).unwrap();
assert_ne!(salt1, salt2);
assert_ne!(key1.as_bytes(), key2.as_bytes());
}
#[test]
fn test_different_passwords_different_keys() {
let config = VaultConfig::default().with_salt([42u8; SALT_SIZE]);
let (key1, _) = MasterKey::derive(b"password1", &config).unwrap();
let (key2, _) = MasterKey::derive(b"password2", &config).unwrap();
assert_ne!(key1.as_bytes(), key2.as_bytes());
}
#[test]
fn test_different_salts_different_keys() {
let config1 = VaultConfig {
salt: Some([1u8; 16]),
..VaultConfig::default()
};
let config2 = VaultConfig {
salt: Some([2u8; 16]),
..VaultConfig::default()
};
let (key1, _) = MasterKey::derive(b"password", &config1).unwrap();
let (key2, _) = MasterKey::derive(b"password", &config2).unwrap();
assert_ne!(key1.as_bytes(), key2.as_bytes());
}
#[test]
fn test_empty_password() {
let config = VaultConfig::default().with_salt([42u8; SALT_SIZE]);
let (key, _) = MasterKey::derive(b"", &config).unwrap();
assert_eq!(key.as_bytes().len(), KEY_SIZE);
}
#[test]
fn test_long_password() {
let config = VaultConfig::default().with_salt([42u8; SALT_SIZE]);
let long_password = vec![b'a'; 10000];
let (key, _) = MasterKey::derive(&long_password, &config).unwrap();
assert_eq!(key.as_bytes().len(), KEY_SIZE);
}
#[test]
fn test_key_is_32_bytes() {
let config = VaultConfig::default().with_salt([42u8; SALT_SIZE]);
let (key, _) = MasterKey::derive(b"test", &config).unwrap();
assert_eq!(key.as_bytes().len(), 32);
}
#[test]
fn test_from_bytes() {
let bytes = [42u8; KEY_SIZE];
let key = MasterKey::from_bytes(bytes);
assert_eq!(key.as_bytes(), &bytes);
}
#[test]
fn test_hkdf_subkey_derivation() {
let key = MasterKey::from_bytes([1u8; KEY_SIZE]);
let subkey1 = key.derive_subkey(b"domain1");
let subkey2 = key.derive_subkey(b"domain2");
assert_ne!(subkey1, subkey2);
assert_eq!(subkey1, key.derive_subkey(b"domain1"));
}
#[test]
fn test_hkdf_subkeys_are_independent() {
let key = MasterKey::from_bytes([42u8; KEY_SIZE]);
let encryption = key.encryption_key();
let obfuscation = key.obfuscation_key();
let metadata = key.metadata_key();
assert_ne!(encryption, obfuscation);
assert_ne!(encryption, metadata);
assert_ne!(obfuscation, metadata);
assert_ne!(&encryption, key.as_bytes());
assert_ne!(&obfuscation, key.as_bytes());
assert_ne!(&metadata, key.as_bytes());
}
#[test]
fn test_audit_key_independent() {
let key = MasterKey::from_bytes([42u8; KEY_SIZE]);
let audit = key.audit_key();
let encryption = key.encryption_key();
let obfuscation = key.obfuscation_key();
let metadata = key.metadata_key();
assert_ne!(audit, encryption);
assert_ne!(audit, obfuscation);
assert_ne!(audit, metadata);
assert_ne!(&audit, key.as_bytes());
}
#[test]
fn test_hkdf_subkeys_are_deterministic() {
let key1 = MasterKey::from_bytes([99u8; KEY_SIZE]);
let key2 = MasterKey::from_bytes([99u8; KEY_SIZE]);
assert_eq!(key1.encryption_key(), key2.encryption_key());
assert_eq!(key1.obfuscation_key(), key2.obfuscation_key());
assert_eq!(key1.metadata_key(), key2.metadata_key());
}
#[test]
fn test_snapshot_key_independent() {
let key = MasterKey::from_bytes([42u8; KEY_SIZE]);
let snapshot = key.snapshot_key();
let encryption = key.encryption_key();
let obfuscation = key.obfuscation_key();
let metadata = key.metadata_key();
let audit = key.audit_key();
let transit = key.transit_key();
let sync = key.sync_key();
assert_ne!(snapshot, encryption);
assert_ne!(snapshot, obfuscation);
assert_ne!(snapshot, metadata);
assert_ne!(snapshot, audit);
assert_ne!(snapshot, transit);
assert_ne!(snapshot, sync);
assert_ne!(&snapshot, key.as_bytes());
}
#[test]
fn test_sync_key_independent() {
let key = MasterKey::from_bytes([42u8; KEY_SIZE]);
let sync = key.sync_key();
let encryption = key.encryption_key();
let obfuscation = key.obfuscation_key();
let metadata = key.metadata_key();
let audit = key.audit_key();
let transit = key.transit_key();
let snapshot = key.snapshot_key();
assert_ne!(sync, encryption);
assert_ne!(sync, obfuscation);
assert_ne!(sync, metadata);
assert_ne!(sync, audit);
assert_ne!(sync, transit);
assert_ne!(sync, snapshot);
assert_ne!(&sync, key.as_bytes());
}
#[test]
fn test_derive_with_random_salt() {
let config = VaultConfig::default();
let (key1, salt1) = MasterKey::derive_with_random_salt(b"password", &config).unwrap();
let (key2, salt2) = MasterKey::derive_with_random_salt(b"password", &config).unwrap();
assert_ne!(salt1, salt2);
assert_ne!(key1.as_bytes(), key2.as_bytes());
}
#[test]
fn test_derive_with_salt_reproducible() {
let config = VaultConfig::default();
let salt = [7u8; SALT_SIZE];
let key1 = MasterKey::derive_with_salt(b"password", &salt, &config).unwrap();
let key2 = MasterKey::derive_with_salt(b"password", &salt, &config).unwrap();
assert_eq!(key1.as_bytes(), key2.as_bytes());
}
}