use omy_core::crypto::{Argon2Params, Kek, SecretKey};
use omy_core::file::{EncryptOptions, RandomMaterial, encrypt};
use omy_core::keyslot::{SlotPlan, rewrite_slots_managed};
use omy_core::slotdir::{SlotDirectory, SlotEntry, SlotKind};
fn kek(seed: u8) -> Kek {
Kek::from_key(SecretKey::from_bytes(core::array::from_fn(|i| {
u8::try_from(i & 0xFF).unwrap_or(0).wrapping_mul(31) ^ seed
})))
}
fn opts_managed(dir: &SlotDirectory) -> EncryptOptions {
EncryptOptions {
argon2: Argon2Params { m_kib: 8, t: 1, p: 1 },
slot_directory: Some(dir.encode()),
..EncryptOptions::default()
}
}
fn three_key_file() -> (Vec<u8>, Kek, Kek, Kek, SlotDirectory) {
let owner = kek(1);
let mate = kek(2);
let reco = kek(3);
let mut dir = SlotDirectory::new();
dir.set(0, SlotEntry::of(SlotKind::Vault)).expect("set 0");
dir.set(1, SlotEntry::of(SlotKind::Vault)).expect("set 1");
dir.set(2, SlotEntry::of(SlotKind::Recovery)).expect("set 2");
let plain: Vec<u8> = (0..5000u32).map(|i| u8::try_from(i % 251).unwrap_or(0)).collect();
let bytes = encrypt(
&plain,
&[owner.duplicate(), mate.duplicate(), reco.duplicate()],
&[7u8; 16],
&opts_managed(&dir),
&RandomMaterial::generate(),
)
.expect("加密")
.bytes;
(bytes, owner, mate, reco, dir)
}
#[test]
fn managed_mode_removes_one_password_and_keeps_the_recovery_code() {
let (bytes, owner, mate, reco, dir) = three_key_file();
for (name, k) in [("主密码", &owner), ("协作者", &mate), ("恢复码", &reco)] {
assert!(
omy_core::file::open(&bytes, &[k.duplicate()]).is_ok(),
"{name}一开始就该能打开"
);
}
let mut after = dir;
after.set(1, SlotEntry::empty()).expect("清空 slot 1");
let plans = vec![
SlotPlan::Keep, SlotPlan::Clear, SlotPlan::Keep, SlotPlan::Clear,
SlotPlan::Clear,
SlotPlan::Clear,
SlotPlan::Clear,
SlotPlan::Clear,
];
let out = rewrite_slots_managed(&bytes, &[owner.duplicate()], &plans, &after).expect("改写");
assert!(
omy_core::file::open(&out.bytes, &[owner.duplicate()]).is_ok(),
"主密码不该受影响"
);
assert!(
omy_core::file::open(&out.bytes, &[reco.duplicate()]).is_ok(),
"恢复码必须保住——这正是可管理模式的全部意义"
);
assert!(
omy_core::file::open(&out.bytes, &[mate.duplicate()]).is_err(),
"被删的协作者必须再也打不开"
);
let opened = omy_core::file::open(&out.bytes, &[owner]).expect("打开");
let got = opened.slot_directory().expect("读目录");
assert_eq!(got.get(1).expect("slot 1").kind, SlotKind::Empty);
assert_eq!(got.get(2).expect("slot 2").kind, SlotKind::Recovery);
assert_eq!(got.used(), 2, "应当只剩主密码与恢复码");
}
#[test]
fn managed_mode_reuses_the_hole_left_by_a_removal() {
let (bytes, owner, mate, reco, dir) = three_key_file();
let mut after = dir;
after.set(1, SlotEntry::empty()).expect("清空");
let plans = vec![
SlotPlan::Keep,
SlotPlan::Clear,
SlotPlan::Keep,
SlotPlan::Clear,
SlotPlan::Clear,
SlotPlan::Clear,
SlotPlan::Clear,
SlotPlan::Clear,
];
let removed =
rewrite_slots_managed(&bytes, &[owner.duplicate()], &plans, &after).expect("删除");
let opened = omy_core::file::open(&removed.bytes, &[owner.duplicate()]).expect("打开");
let mut dir2 = opened.slot_directory().expect("读目录");
let free = dir2.first_free().expect("应当有空洞");
assert_eq!(free, 1, "空洞应当是被删掉的那个下标,而不是末尾");
let newcomer = kek(4);
dir2.set(free, SlotEntry::of(SlotKind::Vault)).expect("记入目录");
let mut plans2 = Vec::new();
for i in 0..8 {
plans2.push(if i == free { SlotPlan::Write(newcomer.duplicate()) } else { SlotPlan::Keep });
}
let added =
rewrite_slots_managed(&removed.bytes, &[owner.duplicate()], &plans2, &dir2).expect("添加");
assert!(
omy_core::file::open(&added.bytes, &[newcomer]).is_ok(),
"新密码应当能打开"
);
assert!(
omy_core::file::open(&added.bytes, &[reco]).is_ok(),
"恢复码不该被 add 顶掉——这正是可否认模式绕不开的那个边界"
);
assert!(
omy_core::file::open(&added.bytes, &[mate]).is_err(),
"已删除的协作者不该复活"
);
assert!(!added.may_have_evicted, "可管理模式不该报告「可能顶掉」");
}
#[test]
fn rewrite_does_not_change_file_length_or_payload() {
let (bytes, owner, _mate, _reco, dir) = three_key_file();
let mut after = dir;
after.set(1, SlotEntry::empty()).expect("清空");
let mut plans = vec![SlotPlan::Keep, SlotPlan::Clear];
plans.resize_with(8, || SlotPlan::Clear);
let out = rewrite_slots_managed(&bytes, &[owner.duplicate()], &plans, &after).expect("改写");
assert_eq!(out.bytes.len(), bytes.len(), "文件长度变了");
let h = omy_core::header::FixedHeader::parse(&out.bytes).expect("解析头部");
let start = h.header_len as usize;
assert_eq!(
out.bytes.get(start..),
bytes.get(start..),
"载荷被改写了——说明退化成了重新加密"
);
}
#[test]
fn refuses_to_leave_the_file_unopenable() {
let (bytes, owner, _m, _r, _d) = three_key_file();
let plans: Vec<SlotPlan> = (0..8).map(|_| SlotPlan::Clear).collect();
assert!(
rewrite_slots_managed(&bytes, &[owner], &plans, &SlotDirectory::new()).is_err(),
"一个槽都不留必须报错"
);
}
#[test]
fn deniable_file_is_rejected_by_the_managed_path() {
let owner = kek(1);
let plain = b"hello".to_vec();
let bytes = encrypt(
&plain,
&[owner.duplicate()],
&[7u8; 16],
&EncryptOptions { argon2: Argon2Params { m_kib: 8, t: 1, p: 1 }, ..Default::default() },
&RandomMaterial::generate(),
)
.expect("加密")
.bytes;
let mut plans = vec![SlotPlan::Keep];
plans.resize_with(8, || SlotPlan::Clear);
assert!(
rewrite_slots_managed(&bytes, &[owner], &plans, &SlotDirectory::new()).is_err(),
"对可否认模式的文件应当拒绝"
);
}
#[test]
fn managed_flag_and_directory_survive_encryption_roundtrip() {
let (bytes, owner, _m, _r, dir) = three_key_file();
let opened = omy_core::file::open(&bytes, &[owner]).expect("打开");
assert!(opened.is_slot_managed(), "SLOT_DIRECTORY flag 没置上");
assert_eq!(opened.slot_directory().expect("读目录"), dir, "目录内容对不上");
}
#[test]
fn deniable_file_reports_no_directory() {
let owner = kek(1);
let bytes = encrypt(
b"x",
&[owner.duplicate()],
&[7u8; 16],
&EncryptOptions { argon2: Argon2Params { m_kib: 8, t: 1, p: 1 }, ..Default::default() },
&RandomMaterial::generate(),
)
.expect("加密")
.bytes;
let opened = omy_core::file::open(&bytes, &[owner]).expect("打开");
assert!(!opened.is_slot_managed());
assert!(opened.slot_directory().is_err(), "可否认模式不该给出目录");
}