use cryptovault::fec::{ConcatenatedFec, ErrorCorrection};
use cryptovault::{CryptoError, CryptoVault};
use zeroize::Zeroizing;
use crate::vault::VaultError;
const LEN_PREFIX: usize = 4;
type Bootstrapped = (Vec<u8>, Vec<u8>, Zeroizing<Vec<u8>>);
fn map_crypto_err(e: CryptoError) -> VaultError {
match e {
CryptoError::Cipher(_) => VaultError::WrongPassphrase,
CryptoError::ErrorCorrection(_)
| CryptoError::Encoding(_)
| CryptoError::InvalidInput(_) => VaultError::VaultMetaCorrupt,
CryptoError::KeyDerivation(m) => VaultError::Crypto(m),
}
}
fn fec_encode(bytes: &[u8]) -> Vec<u8> {
debug_assert!(
bytes.len() <= u32::MAX as usize,
"fec_encode input exceeds the u32 length prefix"
);
let len = u32::try_from(bytes.len()).unwrap_or(u32::MAX);
let encoded = ConcatenatedFec::default().encode(bytes);
let mut out = Vec::with_capacity(LEN_PREFIX + encoded.len());
out.extend_from_slice(&len.to_le_bytes());
out.extend_from_slice(&encoded);
out
}
fn fec_decode(blob: &[u8]) -> Result<Vec<u8>, VaultError> {
let (len_arr, payload) = blob
.split_first_chunk::<LEN_PREFIX>()
.ok_or(VaultError::VaultMetaCorrupt)?;
let pre_len = u32::from_le_bytes(*len_arr) as usize;
ConcatenatedFec::default()
.decode(payload, pre_len)
.map_err(map_crypto_err)
}
pub fn bootstrap_envelope(vault: &CryptoVault, master: &str) -> Result<Bootstrapped, VaultError> {
let salt = cryptovault::generate_salt().map_err(map_crypto_err)?;
let dek = cryptovault::generate_dek().map_err(map_crypto_err)?;
let kek = vault.derive_key(master, &salt).map_err(map_crypto_err)?;
let wrapped = vault.wrap_key(&kek, &salt, &dek).map_err(map_crypto_err)?;
let salt_fec = fec_encode(&salt);
let wrapped_fec = fec_encode(wrapped.as_bytes());
Ok((salt_fec, wrapped_fec, dek))
}
pub fn open_envelope(
vault: &CryptoVault,
master: &str,
salt_fec: &[u8],
wrapped_dek_fec: &[u8],
) -> Result<Zeroizing<Vec<u8>>, VaultError> {
let salt = fec_decode(salt_fec)?;
let wrapped_bytes = fec_decode(wrapped_dek_fec)?;
let wrapped = String::from_utf8(wrapped_bytes).map_err(|_| VaultError::VaultMetaCorrupt)?;
let kek = vault.derive_key(master, &salt).map_err(map_crypto_err)?;
vault
.unwrap_key(&kek, &salt, &wrapped)
.map_err(map_crypto_err)
}
#[doc(hidden)]
pub fn fuzz_open_entrypoint(data: &[u8]) {
const SPLIT_PREFIX: usize = 2;
let Some(prefix) = data.get(0..SPLIT_PREFIX) else {
return;
};
let Ok(len_arr) = <[u8; SPLIT_PREFIX]>::try_from(prefix) else {
return;
};
let salt_len = u16::from_le_bytes(len_arr) as usize;
let rest = data.get(SPLIT_PREFIX..).unwrap_or(&[]);
let split = salt_len.min(rest.len());
let (salt_fec, wrapped_fec) = rest.split_at(split);
let vault = CryptoVault::default();
let _ = open_envelope(&vault, "fuzz-master-key-fixed", salt_fec, wrapped_fec);
}
#[cfg(test)]
mod tests {
use super::{bootstrap_envelope, open_envelope};
use crate::vault::VaultError;
const M: &str = "bWFzdGVyLWtleS0zMi1ieXRlcy1iYXNlNjQtc3RyaW5n";
#[test]
fn test_envelope_bootstrap_then_open_recovers_same_dek() {
let vault = cryptovault::CryptoVault::default();
let (salt_fec, wrapped_fec, dek) = bootstrap_envelope(&vault, M).expect("bootstrap");
let dek2 = open_envelope(&vault, M, &salt_fec, &wrapped_fec).expect("open");
assert_eq!(&dek[..], &dek2[..]);
}
#[test]
fn test_open_with_wrong_master_yields_wrong_passphrase_not_corrupt() {
let vault = cryptovault::CryptoVault::default();
let (salt_fec, wrapped_fec, _) = bootstrap_envelope(&vault, M).expect("bootstrap");
let err = open_envelope(
&vault,
"d3JvbmctbWFzdGVyLWtleS1zdHJpbmc",
&salt_fec,
&wrapped_fec,
)
.expect_err("wrong master must fail");
assert!(matches!(err, VaultError::WrongPassphrase));
}
#[test]
fn test_open_with_fec_uncorrectable_wrapped_dek_yields_corrupt() {
let vault = cryptovault::CryptoVault::default();
let (salt_fec, mut wrapped_fec, _) = bootstrap_envelope(&vault, M).expect("bootstrap");
for b in wrapped_fec.iter_mut() {
*b ^= 0xFF; }
let err = open_envelope(&vault, M, &salt_fec, &wrapped_fec).expect_err("corrupt must fail");
assert!(matches!(err, VaultError::VaultMetaCorrupt));
}
#[test]
fn test_single_bit_flip_in_wrapped_dek_is_corrected_by_fec() {
let vault = cryptovault::CryptoVault::default();
let (salt_fec, mut wrapped_fec, dek) = bootstrap_envelope(&vault, M).expect("bootstrap");
wrapped_fec[super::LEN_PREFIX] ^= 0x01;
let dek2 = open_envelope(&vault, M, &salt_fec, &wrapped_fec).expect("bit-flip corregible");
assert_eq!(&dek[..], &dek2[..]);
}
#[test]
fn test_single_bit_flip_in_salt_is_corrected_by_fec() {
let vault = cryptovault::CryptoVault::default();
let (mut salt_fec, wrapped_fec, dek) = bootstrap_envelope(&vault, M).expect("bootstrap");
salt_fec[super::LEN_PREFIX] ^= 0x01;
let dek2 = open_envelope(&vault, M, &salt_fec, &wrapped_fec).expect("salt bit-flip");
assert_eq!(&dek[..], &dek2[..]);
}
#[test]
fn test_bit_flip_in_length_prefix_fails_safe_as_corrupt() {
let vault = cryptovault::CryptoVault::default();
let (salt_fec, wrapped_fec, _) = bootstrap_envelope(&vault, M).expect("bootstrap");
let mut multi = wrapped_fec.clone();
for b in multi.iter_mut().take(super::LEN_PREFIX) {
*b ^= 0xFF;
}
let err = open_envelope(&vault, M, &salt_fec, &multi).expect_err("prefix corruption fails");
assert!(matches!(err, VaultError::VaultMetaCorrupt));
let mut single = wrapped_fec.clone();
single[super::LEN_PREFIX - 1] ^= 0x80;
let err2 = open_envelope(&vault, M, &salt_fec, &single).expect_err("prefix bit-flip fails");
assert!(matches!(err2, VaultError::VaultMetaCorrupt));
}
#[test]
fn test_fuzz_entrypoint_never_panics_on_arbitrary_input() {
for data in [
&b""[..],
&b"\x00"[..],
&b"\x05\x00abcdefghij"[..],
&[0xFFu8; 300],
] {
super::fuzz_open_entrypoint(data);
}
}
}