use aes_gcm::{
aead::{Aead, KeyInit},
Aes256Gcm, Nonce,
};
use blake2::{digest::consts::U32, Blake2b, Digest};
use rand::RngCore;
use zeroize::Zeroizing;
use crate::{key::MasterKey, Result, VaultError};
pub const METADATA_NONCE_SIZE: usize = 12;
pub const MAX_PLAINTEXT_SIZE: usize = 65531;
const LENGTH_PREFIX_SIZE: usize = 4;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PaddingSize {
Small = 256,
Medium = 1024,
Large = 4096,
ExtraLarge = 16384,
Huge = 32768,
Maximum = 65536,
}
impl PaddingSize {
pub fn for_length(len: usize) -> Option<Self> {
let min_required = len.checked_add(LENGTH_PREFIX_SIZE + 1)?;
if min_required <= Self::Small as usize {
Some(Self::Small)
} else if min_required <= Self::Medium as usize {
Some(Self::Medium)
} else if min_required <= Self::Large as usize {
Some(Self::Large)
} else if min_required <= Self::ExtraLarge as usize {
Some(Self::ExtraLarge)
} else if min_required <= Self::Huge as usize {
Some(Self::Huge)
} else if min_required <= Self::Maximum as usize {
Some(Self::Maximum)
} else {
None }
}
}
pub struct Obfuscator {
obfuscation_key: Zeroizing<[u8; 32]>,
metadata_key: Zeroizing<[u8; 32]>,
}
impl Obfuscator {
pub fn new(master_key: &MasterKey) -> Self {
Self {
obfuscation_key: Zeroizing::new(master_key.obfuscation_key()),
metadata_key: Zeroizing::new(master_key.metadata_key()),
}
}
pub fn from_zeroed() -> Self {
Self {
obfuscation_key: Zeroizing::new([0u8; 32]),
metadata_key: Zeroizing::new([0u8; 32]),
}
}
pub fn obfuscate_key(&self, key: &str) -> String {
let hash = self.hmac_hash(key.as_bytes(), b"key");
hex::encode(&hash[..16]) }
pub fn generate_storage_id(&self, key: &str, nonce: &[u8]) -> String {
let mut input = key.as_bytes().to_vec();
input.extend_from_slice(nonce);
let hash = self.hmac_hash(&input, b"storage_id");
format!("_vs:{}", hex::encode(&hash[..12])) }
pub fn encrypt_metadata(&self, data: &[u8]) -> Result<Vec<u8>> {
let cipher = Aes256Gcm::new_from_slice(&*self.metadata_key)
.map_err(|e| VaultError::CryptoError(format!("Invalid metadata key: {e}")))?;
let mut nonce_bytes = [0u8; METADATA_NONCE_SIZE];
rand::rng().fill_bytes(&mut nonce_bytes);
let nonce = Nonce::from_slice(&nonce_bytes);
let ciphertext = cipher
.encrypt(nonce, data)
.map_err(|e| VaultError::CryptoError(format!("Metadata encryption failed: {e}")))?;
let mut result = Vec::with_capacity(METADATA_NONCE_SIZE + ciphertext.len());
result.extend_from_slice(&nonce_bytes);
result.extend(ciphertext);
Ok(result)
}
pub fn decrypt_metadata(&self, encrypted: &[u8]) -> Result<Vec<u8>> {
if encrypted.len() < METADATA_NONCE_SIZE {
return Err(VaultError::CryptoError(
"Metadata too short (missing nonce)".into(),
));
}
let (nonce_bytes, ciphertext) = encrypted.split_at(METADATA_NONCE_SIZE);
let cipher = Aes256Gcm::new_from_slice(&*self.metadata_key)
.map_err(|e| VaultError::CryptoError(format!("Invalid metadata key: {e}")))?;
let nonce = Nonce::from_slice(nonce_bytes);
cipher
.decrypt(nonce, ciphertext)
.map_err(|e| VaultError::CryptoError(format!("Metadata decryption failed: {e}")))
}
#[deprecated(note = "Use encrypt_metadata for new code")]
pub fn obfuscate_metadata(&self, data: &[u8]) -> Vec<u8> {
let keystream = self.hmac_hash(&[], b"metadata_stream");
data.iter()
.zip(keystream.iter().cycle())
.map(|(d, k)| d ^ k)
.collect()
}
#[deprecated(note = "Use decrypt_metadata for new code")]
pub fn deobfuscate_metadata(&self, obfuscated: &[u8]) -> Vec<u8> {
#[allow(deprecated)] self.obfuscate_metadata(obfuscated)
}
fn hmac_hash(&self, data: &[u8], domain: &[u8]) -> [u8; 32] {
let mut inner_key = *self.obfuscation_key;
for byte in &mut inner_key {
*byte ^= 0x36; }
let mut inner_hasher = Blake2b::<U32>::new();
inner_hasher.update(inner_key);
inner_hasher.update(domain);
inner_hasher.update(data);
let inner_hash = inner_hasher.finalize();
let mut outer_key = *self.obfuscation_key;
for byte in &mut outer_key {
*byte ^= 0x5c; }
let mut outer_hasher = Blake2b::<U32>::new();
outer_hasher.update(outer_key);
outer_hasher.update(inner_hash);
let result = outer_hasher.finalize();
result.into()
}
}
pub fn pad_plaintext(plaintext: &[u8]) -> Result<Vec<u8>> {
if plaintext.len() > MAX_PLAINTEXT_SIZE {
return Err(VaultError::CryptoError(format!(
"Plaintext too large: {} bytes exceeds maximum {}",
plaintext.len(),
MAX_PLAINTEXT_SIZE
)));
}
let target_size = PaddingSize::for_length(plaintext.len()).ok_or_else(|| {
VaultError::CryptoError(format!(
"Cannot determine padding size for {} bytes",
plaintext.len()
))
})? as usize;
let padding_len = target_size
.checked_sub(LENGTH_PREFIX_SIZE)
.and_then(|n| n.checked_sub(plaintext.len()))
.ok_or_else(|| VaultError::CryptoError("Padding calculation overflow".into()))?;
let mut padded = Vec::with_capacity(target_size);
let len_bytes = (plaintext.len() as u32).to_le_bytes();
padded.extend_from_slice(&len_bytes);
padded.extend_from_slice(plaintext);
let mut rng_bytes = vec![0u8; padding_len];
rand::RngCore::fill_bytes(&mut rand::rng(), &mut rng_bytes);
padded.extend_from_slice(&rng_bytes);
Ok(padded)
}
pub fn unpad_plaintext(padded: &[u8]) -> Result<Vec<u8>> {
if padded.len() < LENGTH_PREFIX_SIZE {
return Err(VaultError::CryptoError(
"Padded data too short for length prefix".into(),
));
}
let len = u32::from_le_bytes([padded[0], padded[1], padded[2], padded[3]]) as usize;
let data_start = LENGTH_PREFIX_SIZE;
let data_end = data_start
.checked_add(len)
.ok_or_else(|| VaultError::CryptoError("Length overflow".into()))?;
if data_end > padded.len() {
return Err(VaultError::CryptoError(format!(
"Invalid length prefix: {} exceeds padded data size {}",
len,
padded.len() - LENGTH_PREFIX_SIZE
)));
}
Ok(padded[data_start..data_end].to_vec())
}
mod hex {
pub fn encode(data: &[u8]) -> String {
data.iter().map(|b| format!("{b:02x}")).collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::key::KEY_SIZE;
fn test_key() -> MasterKey {
MasterKey::from_bytes([42u8; KEY_SIZE])
}
#[test]
fn test_obfuscate_key_deterministic() {
let obf = Obfuscator::new(&test_key());
let key1 = obf.obfuscate_key("api_key");
let key2 = obf.obfuscate_key("api_key");
assert_eq!(key1, key2);
assert_eq!(key1.len(), 32); }
#[test]
fn test_obfuscate_key_different_inputs() {
let obf = Obfuscator::new(&test_key());
let key1 = obf.obfuscate_key("api_key");
let key2 = obf.obfuscate_key("db_password");
assert_ne!(key1, key2);
}
#[test]
fn test_obfuscate_key_different_master_keys() {
let obf1 = Obfuscator::new(&MasterKey::from_bytes([1u8; KEY_SIZE]));
let obf2 = Obfuscator::new(&MasterKey::from_bytes([2u8; KEY_SIZE]));
let key1 = obf1.obfuscate_key("api_key");
let key2 = obf2.obfuscate_key("api_key");
assert_ne!(key1, key2);
}
#[test]
fn test_generate_storage_id() {
let obf = Obfuscator::new(&test_key());
let id1 = obf.generate_storage_id("key", &[1, 2, 3]);
let id2 = obf.generate_storage_id("key", &[1, 2, 3]);
let id3 = obf.generate_storage_id("key", &[4, 5, 6]);
assert_eq!(id1, id2);
assert_ne!(id1, id3);
assert!(id1.starts_with("_vs:"));
}
#[test]
#[allow(deprecated)]
fn test_metadata_obfuscation_roundtrip() {
let obf = Obfuscator::new(&test_key());
let original = b"user:alice";
let obfuscated = obf.obfuscate_metadata(original);
let recovered = obf.deobfuscate_metadata(&obfuscated);
assert_ne!(obfuscated.as_slice(), original);
assert_eq!(recovered.as_slice(), original);
}
#[test]
fn test_metadata_aead_roundtrip() {
let obf = Obfuscator::new(&test_key());
let original = b"user:alice:timestamp:1234567890";
let encrypted = obf.encrypt_metadata(original).unwrap();
let decrypted = obf.decrypt_metadata(&encrypted).unwrap();
assert_ne!(encrypted.as_slice(), original.as_slice());
assert!(encrypted.len() > original.len());
assert_eq!(decrypted.as_slice(), original);
}
#[test]
fn test_metadata_aead_unique_nonces() {
let obf = Obfuscator::new(&test_key());
let data = b"same data";
let encrypted1 = obf.encrypt_metadata(data).unwrap();
let encrypted2 = obf.encrypt_metadata(data).unwrap();
assert_ne!(encrypted1, encrypted2);
assert_eq!(
obf.decrypt_metadata(&encrypted1).unwrap(),
obf.decrypt_metadata(&encrypted2).unwrap()
);
}
#[test]
fn test_metadata_aead_tamper_detection() {
let obf = Obfuscator::new(&test_key());
let original = b"sensitive metadata";
let mut encrypted = obf.encrypt_metadata(original).unwrap();
if let Some(last) = encrypted.last_mut() {
*last ^= 0xff;
}
assert!(obf.decrypt_metadata(&encrypted).is_err());
}
#[test]
fn test_metadata_aead_short_input() {
let obf = Obfuscator::new(&test_key());
let result = obf.decrypt_metadata(&[1, 2, 3]);
assert!(result.is_err());
}
#[test]
fn test_padding_small() {
let plaintext = b"short";
let padded = pad_plaintext(plaintext).unwrap();
assert_eq!(padded.len(), PaddingSize::Small as usize);
let unpadded = unpad_plaintext(&padded).unwrap();
assert_eq!(unpadded, plaintext);
}
#[test]
fn test_padding_medium() {
let plaintext = vec![b'x'; 500];
let padded = pad_plaintext(&plaintext).unwrap();
assert_eq!(padded.len(), PaddingSize::Medium as usize);
let unpadded = unpad_plaintext(&padded).unwrap();
assert_eq!(unpadded, plaintext);
}
#[test]
fn test_padding_large() {
let plaintext = vec![b'x'; 2000];
let padded = pad_plaintext(&plaintext).unwrap();
assert_eq!(padded.len(), PaddingSize::Large as usize);
let unpadded = unpad_plaintext(&padded).unwrap();
assert_eq!(unpadded, plaintext);
}
#[test]
fn test_padding_extra_large() {
let plaintext = vec![b'x'; 10000];
let padded = pad_plaintext(&plaintext).unwrap();
assert_eq!(padded.len(), PaddingSize::ExtraLarge as usize);
let unpadded = unpad_plaintext(&padded).unwrap();
assert_eq!(unpadded, plaintext);
}
#[test]
fn test_padding_huge() {
let plaintext = vec![b'x'; 20000];
let padded = pad_plaintext(&plaintext).unwrap();
assert_eq!(padded.len(), PaddingSize::Huge as usize);
let unpadded = unpad_plaintext(&padded).unwrap();
assert_eq!(unpadded, plaintext);
}
#[test]
fn test_padding_maximum() {
let plaintext = vec![b'x'; 50000];
let padded = pad_plaintext(&plaintext).unwrap();
assert_eq!(padded.len(), PaddingSize::Maximum as usize);
let unpadded = unpad_plaintext(&padded).unwrap();
assert_eq!(unpadded, plaintext);
}
#[test]
fn test_padding_at_max_size() {
let plaintext = vec![b'x'; MAX_PLAINTEXT_SIZE];
let padded = pad_plaintext(&plaintext).unwrap();
assert_eq!(padded.len(), PaddingSize::Maximum as usize);
let unpadded = unpad_plaintext(&padded).unwrap();
assert_eq!(unpadded, plaintext);
}
#[test]
fn test_padding_exceeds_max_size() {
let plaintext = vec![b'x'; MAX_PLAINTEXT_SIZE + 1];
let result = pad_plaintext(&plaintext);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("too large"));
}
#[test]
fn test_padding_sizes() {
assert_eq!(PaddingSize::for_length(10), Some(PaddingSize::Small));
assert_eq!(PaddingSize::for_length(251), Some(PaddingSize::Small)); assert_eq!(PaddingSize::for_length(252), Some(PaddingSize::Medium)); assert_eq!(PaddingSize::for_length(1000), Some(PaddingSize::Medium));
assert_eq!(PaddingSize::for_length(1019), Some(PaddingSize::Medium)); assert_eq!(PaddingSize::for_length(1020), Some(PaddingSize::Large)); assert_eq!(PaddingSize::for_length(4000), Some(PaddingSize::Large));
assert_eq!(PaddingSize::for_length(4091), Some(PaddingSize::Large)); assert_eq!(PaddingSize::for_length(4092), Some(PaddingSize::ExtraLarge));
assert_eq!(
PaddingSize::for_length(16379),
Some(PaddingSize::ExtraLarge)
); assert_eq!(PaddingSize::for_length(16380), Some(PaddingSize::Huge));
assert_eq!(PaddingSize::for_length(32763), Some(PaddingSize::Huge)); assert_eq!(PaddingSize::for_length(32764), Some(PaddingSize::Maximum));
assert_eq!(
PaddingSize::for_length(MAX_PLAINTEXT_SIZE),
Some(PaddingSize::Maximum)
);
assert_eq!(PaddingSize::for_length(MAX_PLAINTEXT_SIZE + 1), None);
}
#[test]
fn test_unpad_too_short() {
assert!(unpad_plaintext(&[]).is_err());
assert!(unpad_plaintext(&[1]).is_err());
assert!(unpad_plaintext(&[1, 2]).is_err());
assert!(unpad_plaintext(&[1, 2, 3]).is_err());
}
#[test]
fn test_unpad_invalid_length() {
let mut data = vec![0u8; 10];
data[0..4].copy_from_slice(&100u32.to_le_bytes());
let result = unpad_plaintext(&data);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("exceeds"));
}
#[test]
fn test_u32_length_prefix_roundtrip() {
let plaintext = vec![b'x'; 40000]; let padded = pad_plaintext(&plaintext).unwrap();
let stored_len = u32::from_le_bytes([padded[0], padded[1], padded[2], padded[3]]);
assert_eq!(stored_len as usize, plaintext.len());
let unpadded = unpad_plaintext(&padded).unwrap();
assert_eq!(unpadded, plaintext);
}
#[test]
fn test_hex_encode() {
let data = [0xde, 0xad, 0xbe, 0xef];
let encoded = hex::encode(&data);
assert_eq!(encoded, "deadbeef");
assert_eq!(hex::encode(&[]), "");
assert_eq!(hex::encode(&[0x0f]), "0f");
}
}