use crate::crypto::{CipherId, Kek, NONCE_LEN, SecretKey, ZERO_NONCE};
use crate::error::{Error, Result};
use crate::util::Writer;
pub const KEYS_SIDECAR: &str = ".omy-keys";
pub const SIDECAR_MAGIC: &[u8; 8] = b"OMYDNAME";
pub const SIDECAR_VERSION: u8 = 1;
pub const DK_LEN: usize = 32;
pub const WRAP_LEN: usize = 48;
pub const SIDECAR_SLOTS: usize = 8;
pub const SIDECAR_SLOT_AREA: usize = SIDECAR_SLOTS * WRAP_LEN;
pub const SIDECAR_LEN: usize = 8 + 1 + 1 + 2 + NONCE_LEN + SIDECAR_SLOT_AREA;
pub const SIDECAR_LEN_MANAGED: usize = SIDECAR_LEN + crate::slotdir::DIRECTORY_LEN + 16;
pub const SIDECAR_FLAG_MANAGED: u8 = 1 << 0;
const INFO_SIDECAR_DIR: &[u8] = b"omy/v1/sidecar-slotdir";
const INFO_SIDECAR_WRAP: &[u8] = b"omy/v1/dirname-wrap";
#[derive(Debug)]
pub struct DirKey(SecretKey);
impl DirKey {
#[must_use]
pub fn generate() -> Self {
let mut raw = [0u8; DK_LEN];
crate::util::fill_random(&mut raw);
Self(SecretKey::from_bytes(raw))
}
#[must_use]
pub const fn from_bytes(raw: [u8; DK_LEN]) -> Self {
Self(SecretKey::from_bytes(raw))
}
#[must_use]
pub const fn as_key(&self) -> &SecretKey {
&self.0
}
#[must_use]
pub fn duplicate(&self) -> Self {
Self(SecretKey::from_bytes(*self.0.as_bytes()))
}
#[must_use]
fn slotdir_key(&self, vault_salt: &[u8; 16]) -> SecretKey {
crate::crypto::derive_from_secret(&self.0, vault_salt, INFO_SIDECAR_DIR)
}
}
#[must_use]
pub fn is_managed(blob: &[u8]) -> bool {
blob.get(..8) == Some(SIDECAR_MAGIC.as_slice())
&& blob.get(9).is_some_and(|f| f & SIDECAR_FLAG_MANAGED != 0)
}
pub fn read_slot_directory(
blob: &[u8],
dk: &DirKey,
vault_salt: &[u8; 16],
cipher: CipherId,
) -> Result<crate::slotdir::SlotDirectory> {
if !is_managed(blob) {
return Err(Error::MalformedTlv {
tlv_type: 0,
reason: "this sidecar has no slot directory (deniable mode)",
});
}
if blob.len() != SIDECAR_LEN_MANAGED {
return Err(Error::MalformedTlv {
tlv_type: 0,
reason: "managed sidecar has wrong length",
});
}
let ct = blob.get(SIDECAR_LEN..).ok_or(Error::MalformedTlv {
tlv_type: 0,
reason: "managed sidecar truncated before slot directory",
})?;
let key = dk.slotdir_key(vault_salt);
let plain = cipher
.decrypt(&key, &ZERO_NONCE, ct, &[])?
.ok_or(Error::MalformedTlv {
tlv_type: 0,
reason: "slot directory failed authentication",
})?;
crate::slotdir::SlotDirectory::decode(&plain)
}
fn append_slot_directory(
base: Vec<u8>,
dir: &crate::slotdir::SlotDirectory,
dk: &DirKey,
vault_salt: &[u8; 16],
cipher: CipherId,
) -> Result<Vec<u8>> {
let key = dk.slotdir_key(vault_salt);
let ct = cipher.encrypt(&key, &ZERO_NONCE, &dir.encode(), &[])?;
let mut out = base;
if let Some(f) = out.get_mut(9) {
*f |= SIDECAR_FLAG_MANAGED;
}
out.extend_from_slice(&ct);
debug_assert_eq!(out.len(), SIDECAR_LEN_MANAGED, "可管理边车必须恰好 {SIDECAR_LEN_MANAGED} 字节");
Ok(out)
}
fn wrap_key(kek: &Kek, vault_salt: &[u8; 16]) -> SecretKey {
kek.derive_dirname_key(vault_salt, INFO_SIDECAR_WRAP)
}
pub fn build_sidecar(
keks: &[Kek],
dk: &DirKey,
vault_salt: &[u8; 16],
nonce: &[u8; NONCE_LEN],
cipher: CipherId,
) -> Result<Vec<u8>> {
if keks.is_empty() {
return Err(Error::MalformedTlv {
tlv_type: 0,
reason: "refusing to build a sidecar with zero keys; the directory name could never be decrypted",
});
}
if keks.len() > SIDECAR_SLOTS {
return Err(Error::TooManySlots { got: keks.len(), max: SIDECAR_SLOTS });
}
let mut w = Writer::with_capacity(SIDECAR_LEN);
w.bytes(SIDECAR_MAGIC);
w.u8(SIDECAR_VERSION);
w.bytes(&[0u8; 3]);
w.bytes(nonce);
for kek in keks {
let wk = wrap_key(kek, vault_salt);
let wrapped = cipher.encrypt(&wk, &ZERO_NONCE, dk.as_key().as_bytes(), &[])?;
debug_assert_eq!(wrapped.len(), WRAP_LEN, "包裹后必须是 48 字节");
w.bytes(&wrapped);
}
let used = keks.len().saturating_mul(WRAP_LEN);
w.random(SIDECAR_SLOT_AREA.saturating_sub(used));
let out = w.into_vec();
debug_assert_eq!(out.len(), SIDECAR_LEN, "边车必须恰好 {SIDECAR_LEN} 字节");
Ok(out)
}
pub fn rebuild_sidecar_carrying(
old: Option<&[u8]>,
keep: &[Kek],
dk: &DirKey,
vault_salt: &[u8; 16],
nonce: &[u8; NONCE_LEN],
cipher: CipherId,
) -> Result<Vec<u8>> {
if keep.is_empty() {
return Err(Error::MalformedTlv {
tlv_type: 0,
reason: "refusing to build a sidecar with zero keys; the directory name could never be decrypted",
});
}
if keep.len() > SIDECAR_SLOTS {
return Err(Error::TooManySlots { got: keep.len(), max: SIDECAR_SLOTS });
}
let mut w = Writer::with_capacity(SIDECAR_LEN);
w.bytes(SIDECAR_MAGIC);
w.u8(SIDECAR_VERSION);
w.bytes(&[0u8; 3]);
w.bytes(nonce);
for kek in keep {
let wk = wrap_key(kek, vault_salt);
let wrapped = cipher.encrypt(&wk, &ZERO_NONCE, dk.as_key().as_bytes(), &[])?;
w.bytes(&wrapped);
}
let mut written = keep.len();
if let Some(prev) = old {
let known_len = prev.len() == SIDECAR_LEN || prev.len() == SIDECAR_LEN_MANAGED;
if known_len && prev.get(..8) == Some(SIDECAR_MAGIC.as_slice()) {
let slots_start = 12 + NONCE_LEN;
for i in 0..SIDECAR_SLOTS {
if written >= SIDECAR_SLOTS {
break;
}
let from = slots_start.saturating_add(i.saturating_mul(WRAP_LEN));
let to = from.saturating_add(WRAP_LEN);
let Some(slot) = prev.get(from..to) else { continue };
let already = keep.iter().any(|k| {
let wk = wrap_key(k, vault_salt);
matches!(cipher.decrypt(&wk, &ZERO_NONCE, slot, &[]), Ok(Some(_)))
});
if already {
continue;
}
w.bytes(slot);
written = written.saturating_add(1);
}
}
}
let used = written.saturating_mul(WRAP_LEN);
w.random(SIDECAR_SLOT_AREA.saturating_sub(used));
let out = w.into_vec();
debug_assert_eq!(out.len(), SIDECAR_LEN, "边车必须恰好 {SIDECAR_LEN} 字节");
Ok(out)
}
pub fn build_sidecar_managed(
keks: &[Kek],
dir: &crate::slotdir::SlotDirectory,
dk: &DirKey,
vault_salt: &[u8; 16],
nonce: &[u8; NONCE_LEN],
cipher: CipherId,
) -> Result<Vec<u8>> {
let base = build_sidecar(keks, dk, vault_salt, nonce, cipher)?;
append_slot_directory(base, dir, dk, vault_salt, cipher)
}
pub fn rewrite_sidecar_managed(
old: &[u8],
plans: &[SidecarPlan],
dir: &crate::slotdir::SlotDirectory,
dk: &DirKey,
vault_salt: &[u8; 16],
nonce: &[u8; NONCE_LEN],
cipher: CipherId,
) -> Result<Vec<u8>> {
if !is_managed(old) {
return Err(Error::MalformedTlv {
tlv_type: 0,
reason: "refusing precise rewrite on a deniable-mode sidecar",
});
}
if old.len() != SIDECAR_LEN_MANAGED {
return Err(Error::MalformedTlv {
tlv_type: 0,
reason: "managed sidecar has wrong length",
});
}
if !plans.iter().any(|p| matches!(p, SidecarPlan::Keep | SidecarPlan::Write(_))) {
return Err(Error::MalformedTlv {
tlv_type: 0,
reason: "refusing to leave the sidecar with zero usable keys",
});
}
let mut w = Writer::with_capacity(SIDECAR_LEN);
w.bytes(SIDECAR_MAGIC);
w.u8(SIDECAR_VERSION);
w.u8(SIDECAR_FLAG_MANAGED);
w.bytes(&[0u8; 2]);
w.bytes(nonce);
let slots_start = 12 + NONCE_LEN;
for (i, plan) in plans.iter().enumerate().take(SIDECAR_SLOTS) {
match plan {
SidecarPlan::Keep => {
let from = slots_start.saturating_add(i.saturating_mul(WRAP_LEN));
let to = from.saturating_add(WRAP_LEN);
match old.get(from..to) {
Some(slot) => {
w.bytes(slot);
}
None => {
w.random(WRAP_LEN);
}
}
}
SidecarPlan::Write(kek) => {
let wk = wrap_key(kek, vault_salt);
let wrapped = cipher.encrypt(&wk, &ZERO_NONCE, dk.as_key().as_bytes(), &[])?;
w.bytes(&wrapped);
}
SidecarPlan::Clear => {
w.random(WRAP_LEN);
}
}
}
let written = plans.len().min(SIDECAR_SLOTS);
w.random(SIDECAR_SLOT_AREA.saturating_sub(written.saturating_mul(WRAP_LEN)));
append_slot_directory(w.into_vec(), dir, dk, vault_salt, cipher)
}
#[derive(Debug)]
pub enum SidecarPlan {
Keep,
Write(Kek),
Clear,
}
pub fn open_sidecar(
blob: &[u8],
keks: &[Kek],
vault_salt: &[u8; 16],
cipher: CipherId,
) -> Result<(DirKey, [u8; NONCE_LEN])> {
open_sidecar_at(blob, keks, vault_salt, cipher).map(|(dk, nonce, _)| (dk, nonce))
}
pub fn open_sidecar_at(
blob: &[u8],
keks: &[Kek],
vault_salt: &[u8; 16],
cipher: CipherId,
) -> Result<(DirKey, [u8; NONCE_LEN], usize)> {
if blob.len() != SIDECAR_LEN && blob.len() != SIDECAR_LEN_MANAGED {
return Err(Error::MalformedTlv {
tlv_type: 0,
reason: "dirname sidecar has wrong length",
});
}
if blob.get(..8) != Some(SIDECAR_MAGIC.as_slice()) {
return Err(Error::MalformedTlv {
tlv_type: 0,
reason: "dirname sidecar magic mismatch",
});
}
if blob.get(8) != Some(&SIDECAR_VERSION) {
return Err(Error::MalformedTlv {
tlv_type: 0,
reason: "unsupported dirname sidecar version",
});
}
let nonce_start = 12;
let slots_start = nonce_start + NONCE_LEN;
let mut nonce = [0u8; NONCE_LEN];
nonce.copy_from_slice(
blob.get(nonce_start..slots_start).ok_or(Error::MalformedTlv {
tlv_type: 0,
reason: "dirname sidecar truncated before nonce",
})?,
);
for kek in keks {
let wk = wrap_key(kek, vault_salt);
for i in 0..SIDECAR_SLOTS {
let from = slots_start.saturating_add(i.saturating_mul(WRAP_LEN));
let to = from.saturating_add(WRAP_LEN);
let Some(slot) = blob.get(from..to) else { continue };
let Ok(Some(plain)) = cipher.decrypt(&wk, &ZERO_NONCE, slot, &[]) else {
continue;
};
if plain.len() != DK_LEN {
continue;
}
let mut raw = [0u8; DK_LEN];
raw.copy_from_slice(&plain);
return Ok((DirKey::from_bytes(raw), nonce, i));
}
}
Err(Error::NoMatchingSlot)
}
#[cfg(test)]
#[expect(clippy::panic, reason = "测试里 panic 就是断言失败的表达方式")]
mod tests {
use super::*;
use crate::crypto::Argon2Params;
fn kek(pw: &str, salt: &[u8; 16]) -> Kek {
Kek::from_password(pw.as_bytes(), salt, Argon2Params::TEST_WEAK).expect("派生 KEK")
}
fn nonce_of(n: u8) -> [u8; NONCE_LEN] {
core::array::from_fn(|i| (i as u8).wrapping_mul(3) ^ n)
}
#[test]
fn every_key_opens_the_same_dir_key() {
let salt = [9u8; 16];
let keks = vec![kek("one", &salt), kek("two", &salt), kek("three", &salt)];
let dk = DirKey::generate();
let n = nonce_of(1);
let blob = build_sidecar(&keks, &dk, &salt, &n, CipherId::ChaCha20Poly1305).expect("构建");
for (i, k) in keks.iter().enumerate() {
let (got, got_n) =
open_sidecar(&blob, std::slice::from_ref(k), &salt, CipherId::ChaCha20Poly1305)
.unwrap_or_else(|e| panic!("第 {i} 把钥匙应能解开边车:{e}"));
assert_eq!(got.as_key().as_bytes(), dk.as_key().as_bytes(), "第 {i} 把解出的 DK 不对");
assert_eq!(got_n, n, "nonce 应原样取回");
}
}
#[test]
fn length_is_constant_regardless_of_key_count() {
let salt = [3u8; 16];
let dk = DirKey::generate();
let n = nonce_of(2);
let all = vec![kek("a", &salt), kek("b", &salt), kek("c", &salt), kek("d", &salt)];
for count in 1..=4 {
let blob = build_sidecar(
all.get(..count).unwrap_or(&all),
&dk,
&salt,
&n,
CipherId::ChaCha20Poly1305,
)
.expect("构建");
assert_eq!(blob.len(), SIDECAR_LEN, "{count} 把钥匙时长度变了");
}
}
#[test]
fn unused_slots_are_not_zero_filled() {
let salt = [4u8; 16];
let dk = DirKey::generate();
let blob = build_sidecar(
&[kek("only", &salt)],
&dk,
&salt,
&nonce_of(3),
CipherId::ChaCha20Poly1305,
)
.expect("构建");
let slots_start = 12 + NONCE_LEN;
let unused = blob.get(slots_start + WRAP_LEN..).expect("应有剩余槽");
assert!(
unused.iter().any(|&b| b != 0),
"未使用的槽被填成了零,可否认性失效"
);
}
#[test]
fn carrying_keeps_unknown_wraps_but_discarding_drops_them() {
let salt = [11u8; 16];
let main_key = kek("main", &salt);
let reco = kek("recovery-code", &salt);
let dk = DirKey::generate();
let cipher = CipherId::ChaCha20Poly1305;
let original = build_sidecar(
&[main_key.duplicate(), reco.duplicate()],
&dk,
&salt,
&nonce_of(20),
cipher,
)
.expect("构建");
let carried = rebuild_sidecar_carrying(
Some(&original),
&[main_key.duplicate()],
&dk,
&salt,
&nonce_of(21),
cipher,
)
.expect("Carry 重建");
assert!(
open_sidecar(&carried, &[reco.duplicate()], &salt, cipher).is_ok(),
"Carry 应当保住恢复码的包裹"
);
assert!(
open_sidecar(&carried, &[main_key.duplicate()], &salt, cipher).is_ok(),
"Carry 之后 main 自己也得还能用"
);
let dropped = rebuild_sidecar_carrying(
None,
&[main_key.duplicate()],
&dk,
&salt,
&nonce_of(22),
cipher,
)
.expect("Discard 重建");
assert!(
open_sidecar(&dropped, &[reco], &salt, cipher).is_err(),
"丢弃时旧包裹必须真的没了——留着等于那把钥匙没被作废"
);
assert!(
open_sidecar(&dropped, &[main_key], &salt, cipher).is_ok(),
"丢弃不该连保留的钥匙一起丢"
);
}
#[test]
fn wrong_key_is_rejected() {
let salt = [5u8; 16];
let dk = DirKey::generate();
let blob = build_sidecar(
&[kek("right", &salt)],
&dk,
&salt,
&nonce_of(4),
CipherId::ChaCha20Poly1305,
)
.expect("构建");
let wrong = kek("wrong", &salt);
assert!(
matches!(
open_sidecar(&blob, &[wrong], &salt, CipherId::ChaCha20Poly1305),
Err(Error::NoMatchingSlot)
),
"不属于这棵树的钥匙必须被拒绝"
);
}
#[test]
fn sidecar_is_bound_to_vault_salt() {
let salt_a = [1u8; 16];
let salt_b = [2u8; 16];
let k = kek("same-password", &salt_a);
let dk = DirKey::generate();
let blob =
build_sidecar(&[k], &dk, &salt_a, &nonce_of(5), CipherId::ChaCha20Poly1305)
.expect("构建");
let k2 = kek("same-password", &salt_a);
assert!(
open_sidecar(&blob, &[k2], &salt_b, CipherId::ChaCha20Poly1305).is_err(),
"边车必须绑定 vault_salt"
);
}
#[test]
fn refuses_to_build_with_no_keys() {
let salt = [6u8; 16];
assert!(
build_sidecar(
&[],
&DirKey::generate(),
&salt,
&nonce_of(6),
CipherId::ChaCha20Poly1305
)
.is_err(),
"零把钥匙必须拒绝"
);
}
#[test]
fn rejects_corrupted_header() {
let salt = [8u8; 16];
let dk = DirKey::generate();
let good = build_sidecar(
&[kek("k", &salt)],
&dk,
&salt,
&nonce_of(7),
CipherId::ChaCha20Poly1305,
)
.expect("构建");
let mut bad_magic = good.clone();
if let Some(b) = bad_magic.get_mut(0) {
*b ^= 0xFF;
}
assert!(open_sidecar(&bad_magic, &[kek("k", &salt)], &salt, CipherId::ChaCha20Poly1305)
.is_err());
let mut bad_ver = good.clone();
if let Some(b) = bad_ver.get_mut(8) {
*b = 99;
}
assert!(open_sidecar(&bad_ver, &[kek("k", &salt)], &salt, CipherId::ChaCha20Poly1305)
.is_err());
let short = good.get(..SIDECAR_LEN - 1).unwrap_or(&good).to_vec();
assert!(open_sidecar(&short, &[kek("k", &salt)], &salt, CipherId::ChaCha20Poly1305)
.is_err());
}
#[test]
fn managed_sidecar_roundtrips_the_directory() {
let salt = [9u8; 16];
let owner = kek("owner", &salt);
let dk = DirKey::generate();
let mut dir = crate::slotdir::SlotDirectory::new();
dir.set(0, crate::slotdir::SlotEntry::of(crate::slotdir::SlotKind::Vault))
.expect("set 0");
let blob = build_sidecar_managed(
&[owner.duplicate()],
&dir,
&dk,
&salt,
&nonce_of(1),
CipherId::ChaCha20Poly1305,
)
.expect("构建");
assert!(is_managed(&blob), "flags 位没置上");
assert_eq!(blob.len(), SIDECAR_LEN_MANAGED);
let (got_dk, _, idx) =
open_sidecar_at(&blob, &[owner], &salt, CipherId::ChaCha20Poly1305).expect("解开");
assert_eq!(idx, 0, "命中的应当是 slot 0");
let got = read_slot_directory(&blob, &got_dk, &salt, CipherId::ChaCha20Poly1305)
.expect("读目录");
assert_eq!(got, dir, "目录内容对不上");
}
#[test]
fn deniable_sidecar_refuses_to_give_a_directory() {
let salt = [9u8; 16];
let owner = kek("owner", &salt);
let dk = DirKey::generate();
let blob = build_sidecar(
&[owner.duplicate()],
&dk,
&salt,
&nonce_of(2),
CipherId::ChaCha20Poly1305,
)
.expect("构建");
assert!(!is_managed(&blob));
assert!(
read_slot_directory(&blob, &dk, &salt, CipherId::ChaCha20Poly1305).is_err(),
"可否认模式不该给出目录"
);
}
#[test]
fn managed_length_is_constant_regardless_of_key_count() {
let salt = [9u8; 16];
let dk = DirKey::generate();
let dir = crate::slotdir::SlotDirectory::new();
let mut lens = Vec::new();
for n in 1..=4usize {
let keks: Vec<Kek> = (0..n).map(|i| kek(&format!("pw{i}"), &salt)).collect();
let blob = build_sidecar_managed(
&keks,
&dir,
&dk,
&salt,
&nonce_of(3),
CipherId::ChaCha20Poly1305,
)
.expect("构建");
lens.push(blob.len());
}
assert!(
lens.iter().all(|&l| l == SIDECAR_LEN_MANAGED),
"长度随钥匙数变化了:{lens:?}"
);
}
#[test]
fn precise_rewrite_removes_one_key_and_keeps_the_rest() {
let salt = [9u8; 16];
let owner = kek("owner", &salt);
let mate = kek("mate", &salt);
let reco = kek("reco", &salt);
let dk = DirKey::generate();
let mut dir = crate::slotdir::SlotDirectory::new();
dir.set(0, crate::slotdir::SlotEntry::of(crate::slotdir::SlotKind::Vault))
.expect("0");
dir.set(1, crate::slotdir::SlotEntry::of(crate::slotdir::SlotKind::Vault))
.expect("1");
dir.set(2, crate::slotdir::SlotEntry::of(crate::slotdir::SlotKind::Recovery))
.expect("2");
let blob = build_sidecar_managed(
&[owner.duplicate(), mate.duplicate(), reco.duplicate()],
&dir,
&dk,
&salt,
&nonce_of(4),
CipherId::ChaCha20Poly1305,
)
.expect("构建");
for (name, k) in [("owner", &owner), ("mate", &mate), ("reco", &reco)] {
assert!(
open_sidecar(&blob, &[k.duplicate()], &salt, CipherId::ChaCha20Poly1305).is_ok(),
"{name} 一开始就该能解开"
);
}
let mut after = dir;
after.set(1, crate::slotdir::SlotEntry::empty()).expect("清空");
let mut plans = vec![SidecarPlan::Keep, SidecarPlan::Clear, SidecarPlan::Keep];
plans.resize_with(SIDECAR_SLOTS, || SidecarPlan::Clear);
let out = rewrite_sidecar_managed(
&blob,
&plans,
&after,
&dk,
&salt,
&nonce_of(4),
CipherId::ChaCha20Poly1305,
)
.expect("改写");
assert!(
open_sidecar(&out, &[owner], &salt, CipherId::ChaCha20Poly1305).is_ok(),
"主密码不该受影响"
);
assert!(
open_sidecar(&out, &[reco], &salt, CipherId::ChaCha20Poly1305).is_ok(),
"恢复码必须保住——这正是可管理模式的全部意义"
);
assert!(
open_sidecar(&out, &[mate], &salt, CipherId::ChaCha20Poly1305).is_err(),
"被踢掉的协作者必须再也解不开"
);
}
#[test]
fn precise_rewrite_refuses_to_orphan_the_directory_name() {
let salt = [9u8; 16];
let owner = kek("owner", &salt);
let dk = DirKey::generate();
let dir = crate::slotdir::SlotDirectory::new();
let blob = build_sidecar_managed(
&[owner],
&dir,
&dk,
&salt,
&nonce_of(5),
CipherId::ChaCha20Poly1305,
)
.expect("构建");
let plans: Vec<SidecarPlan> = (0..SIDECAR_SLOTS).map(|_| SidecarPlan::Clear).collect();
assert!(
rewrite_sidecar_managed(
&blob,
&plans,
&dir,
&dk,
&salt,
&nonce_of(5),
CipherId::ChaCha20Poly1305,
)
.is_err(),
"一个槽都不留必须报错"
);
}
#[test]
fn precise_rewrite_rejects_a_deniable_sidecar() {
let salt = [9u8; 16];
let owner = kek("owner", &salt);
let dk = DirKey::generate();
let blob = build_sidecar(
&[owner.duplicate()],
&dk,
&salt,
&nonce_of(6),
CipherId::ChaCha20Poly1305,
)
.expect("构建");
let mut plans = vec![SidecarPlan::Keep];
plans.resize_with(SIDECAR_SLOTS, || SidecarPlan::Clear);
assert!(
rewrite_sidecar_managed(
&blob,
&plans,
&crate::slotdir::SlotDirectory::new(),
&dk,
&salt,
&nonce_of(6),
CipherId::ChaCha20Poly1305,
)
.is_err(),
"对可否认模式的边车应当拒绝"
);
}
#[test]
fn precise_rewrite_fills_cleared_slots_with_random() {
let salt = [9u8; 16];
let owner = kek("owner", &salt);
let mate = kek("mate", &salt);
let dk = DirKey::generate();
let mut dir = crate::slotdir::SlotDirectory::new();
dir.set(0, crate::slotdir::SlotEntry::of(crate::slotdir::SlotKind::Vault))
.expect("0");
dir.set(1, crate::slotdir::SlotEntry::of(crate::slotdir::SlotKind::Vault))
.expect("1");
let blob = build_sidecar_managed(
&[owner, mate],
&dir,
&dk,
&salt,
&nonce_of(8),
CipherId::ChaCha20Poly1305,
)
.expect("构建");
let mut after = dir;
after.set(1, crate::slotdir::SlotEntry::empty()).expect("清空");
let mut plans = vec![SidecarPlan::Keep, SidecarPlan::Clear];
plans.resize_with(SIDECAR_SLOTS, || SidecarPlan::Clear);
let out = rewrite_sidecar_managed(
&blob,
&plans,
&after,
&dk,
&salt,
&nonce_of(8),
CipherId::ChaCha20Poly1305,
)
.expect("改写");
let slots_start = 12 + NONCE_LEN;
let area = out
.get(slots_start..slots_start + SIDECAR_SLOT_AREA)
.expect("槽位区");
let zeros = vec![0u8; WRAP_LEN];
for i in 0..SIDECAR_SLOTS {
let from = i * WRAP_LEN;
let slot = area.get(from..from + WRAP_LEN).expect("槽");
assert_ne!(slot, zeros.as_slice(), "slot {i} 被填成了零");
}
}
#[test]
fn wrong_key_cannot_read_the_directory() {
let salt = [9u8; 16];
let owner = kek("owner", &salt);
let outsider = DirKey::generate();
let dk = DirKey::generate();
let mut dir = crate::slotdir::SlotDirectory::new();
dir.set(0, crate::slotdir::SlotEntry::of(crate::slotdir::SlotKind::Vault))
.expect("set");
let blob = build_sidecar_managed(
&[owner],
&dir,
&dk,
&salt,
&nonce_of(7),
CipherId::ChaCha20Poly1305,
)
.expect("构建");
assert!(
read_slot_directory(&blob, &outsider, &salt, CipherId::ChaCha20Poly1305).is_err(),
"用别的 DK 不该读出目录"
);
}
}