use std::sync::{Arc, Mutex};
use cryptovault::fec::{ConcatenatedFec, ErrorCorrection};
use cryptovault::{CryptoError, CryptoVault};
use rusqlite::{Connection, OptionalExtension};
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]) -> Result<Vec<u8>, VaultError> {
let len = u32::try_from(bytes.len()).map_err(|_| {
VaultError::Crypto("vault_meta entry exceeds the u32 length prefix".to_string())
})?;
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);
Ok(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(|e| VaultError::Crypto(format!("salt generation failed: {e}")))?;
let dek = cryptovault::generate_dek()
.map_err(|e| VaultError::Crypto(format!("DEK generation failed: {e}")))?;
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)
}
fn read_meta(guard: &Connection) -> Result<(Vec<u8>, Vec<u8>), VaultError> {
let salt: Option<Vec<u8>> = guard
.query_row("SELECT value FROM vault_meta WHERE key = 'salt'", [], |r| {
r.get(0)
})
.optional()
.map_err(|e| VaultError::Storage(e.to_string()))?;
let wrapped: Option<Vec<u8>> = guard
.query_row(
"SELECT value FROM vault_meta WHERE key = 'wrapped_dek'",
[],
|r| r.get(0),
)
.optional()
.map_err(|e| VaultError::Storage(e.to_string()))?;
match (salt, wrapped) {
(Some(s), Some(w)) => Ok((s, w)),
_ => Err(VaultError::VaultMetaCorrupt),
}
}
pub fn rekey_envelope(
vault: &CryptoVault,
conn: &Arc<Mutex<Connection>>,
current: &str,
new: &str,
) -> Result<(), VaultError> {
rekey_envelope_inner(vault, conn, current, new, || {})
}
fn rekey_envelope_inner(
vault: &CryptoVault,
conn: &Arc<Mutex<Connection>>,
current: &str,
new: &str,
between_read_and_tx: impl FnOnce(),
) -> Result<(), VaultError> {
let (salt_fec, wrapped_fec) = {
let guard = conn.lock().unwrap_or_else(|p| p.into_inner());
read_meta(&guard)?
};
let dek = open_envelope(vault, current, &salt_fec, &wrapped_fec)?;
between_read_and_tx();
let new_salt = cryptovault::generate_salt().map_err(map_crypto_err)?;
let kek_new = vault.derive_key(new, &new_salt).map_err(map_crypto_err)?;
let wrapped_new = vault
.wrap_key(&kek_new, &new_salt, &dek)
.map_err(map_crypto_err)?;
let new_salt_fec = fec_encode(&new_salt)?;
let new_wrapped_fec = fec_encode(wrapped_new.as_bytes())?;
let mut guard = conn.lock().unwrap_or_else(|p| p.into_inner());
let tx = guard
.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)
.map_err(|e| VaultError::Storage(e.to_string()))?;
let (_, current_wrapped) = read_meta(&tx)?;
if current_wrapped != wrapped_fec {
return Err(VaultError::Storage(
"concurrent rekey detected; aborted".to_string(),
));
}
tx.execute(
"INSERT OR REPLACE INTO vault_meta (key, value) VALUES ('salt', ?1)",
[&new_salt_fec],
)
.map_err(|e| VaultError::Storage(e.to_string()))?;
tx.execute(
"INSERT OR REPLACE INTO vault_meta (key, value) VALUES ('wrapped_dek', ?1)",
[&new_wrapped_fec],
)
.map_err(|e| VaultError::Storage(e.to_string()))?;
tx.commit()
.map_err(|e| VaultError::Storage(e.to_string()))?;
Ok(())
}
#[cfg(test)]
pub(crate) fn rekey_envelope_with_hook(
vault: &CryptoVault,
conn: &Arc<Mutex<Connection>>,
current: &str,
new: &str,
between_read_and_tx: impl FnOnce(),
) -> Result<(), VaultError> {
rekey_envelope_inner(vault, conn, current, new, between_read_and_tx)
}
#[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, rekey_envelope, rekey_envelope_with_hook};
use crate::vault::VaultError;
use rusqlite::Connection;
use std::sync::{Arc, Mutex};
fn meta_conn(master: &str) -> Arc<Mutex<Connection>> {
let conn = Connection::open_in_memory().expect("mem");
conn.execute(
"CREATE TABLE vault_meta (key TEXT PRIMARY KEY, value BLOB NOT NULL)",
[],
)
.expect("ddl");
let vault = cryptovault::CryptoVault::default();
let (salt_fec, wrapped_fec, _dek) = bootstrap_envelope(&vault, master).expect("boot");
conn.execute(
"INSERT INTO vault_meta (key, value) VALUES ('salt', ?1)",
[&salt_fec],
)
.expect("s");
conn.execute(
"INSERT INTO vault_meta (key, value) VALUES ('wrapped_dek', ?1)",
[&wrapped_fec],
)
.expect("w");
Arc::new(Mutex::new(conn))
}
fn read_back(conn: &Arc<Mutex<Connection>>) -> (Vec<u8>, Vec<u8>) {
let g = conn.lock().expect("lock");
let s: Vec<u8> = g
.query_row("SELECT value FROM vault_meta WHERE key='salt'", [], |r| {
r.get(0)
})
.expect("s");
let w: Vec<u8> = g
.query_row(
"SELECT value FROM vault_meta WHERE key='wrapped_dek'",
[],
|r| r.get(0),
)
.expect("w");
(s, w)
}
#[test]
fn test_rekey_opens_with_new_passphrase_and_same_dek_not_with_old() {
let vault = cryptovault::CryptoVault::default();
let conn = meta_conn("old-passphrase-long-enough");
let (s0, w0) = read_back(&conn);
let dek0 = open_envelope(&vault, "old-passphrase-long-enough", &s0, &w0).expect("open");
rekey_envelope(
&vault,
&conn,
"old-passphrase-long-enough",
"new-passphrase-long-enough",
)
.expect("rekey");
let (s1, w1) = read_back(&conn);
let dek1 =
open_envelope(&vault, "new-passphrase-long-enough", &s1, &w1).expect("new opens");
assert_eq!(dek0.to_vec(), dek1.to_vec()); assert!(matches!(
open_envelope(&vault, "old-passphrase-long-enough", &s1, &w1),
Err(VaultError::WrongPassphrase)
));
}
#[test]
fn test_rekey_with_wrong_current_changes_nothing() {
let vault = cryptovault::CryptoVault::default();
let conn = meta_conn("old-passphrase-long-enough");
let before = read_back(&conn);
let e =
rekey_envelope(&vault, &conn, "WRONG", "new-passphrase-long-enough").expect_err("lock");
assert!(matches!(e, VaultError::WrongPassphrase));
assert_eq!(read_back(&conn), before); }
#[test]
fn test_rekey_detects_concurrent_rewrap_and_aborts_without_writing() {
let vault = cryptovault::CryptoVault::default();
let conn = meta_conn("old-passphrase-long-enough");
let conn2 = conn.clone();
let err = rekey_envelope_with_hook(
&vault,
&conn,
"old-passphrase-long-enough",
"our-new-passphrase-xyz",
move || {
let v = cryptovault::CryptoVault::default();
rekey_envelope(
&v,
&conn2,
"old-passphrase-long-enough",
"winner-passphrase-xyz",
)
.expect("competitor rekey");
},
)
.expect_err("must detect the concurrent rewrap");
assert!(matches!(err, VaultError::Storage(_)));
let (s, w) = read_back(&conn);
open_envelope(&vault, "winner-passphrase-xyz", &s, &w).expect("winner opens");
}
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);
}
}
}