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 file_with_device() -> (Vec<u8>, Kek, Kek, SlotDirectory) {
let pw = kek(1);
let dev = kek(2);
let mut dir = SlotDirectory::new();
dir.set(0, SlotEntry::of(SlotKind::Vault)).expect("set 0");
dir.set(1, SlotEntry::of(SlotKind::Device)).expect("set 1");
let bytes = encrypt(
b"payload",
&[pw.duplicate(), dev.duplicate()],
&[7u8; 16],
&EncryptOptions {
argon2: Argon2Params { m_kib: 8, t: 1, p: 1 },
slot_directory: Some(dir.encode()),
..EncryptOptions::default()
},
&RandomMaterial::generate(),
)
.expect("加密")
.bytes;
(bytes, pw, dev, dir)
}
fn plans_keeping(indices: &[usize]) -> Vec<SlotPlan> {
(0..omy_core::header::SLOT_COUNT)
.map(|i| {
if indices.contains(&i) {
SlotPlan::Keep
} else {
SlotPlan::Clear
}
})
.collect()
}
#[test]
fn refuses_to_leave_only_a_device_key() {
let (bytes, pw, _dev, dir) = file_with_device();
assert!(
omy_core::file::open(&bytes, &[pw.duplicate()]).is_ok(),
"密码一开始就该能开"
);
let mut after = dir;
after.set(0, SlotEntry::empty()).expect("清掉密码那一项");
let err = rewrite_slots_managed(&bytes, &[pw], &plans_keeping(&[1]), &after);
assert!(err.is_err(), "只剩设备密钥必须被拒绝");
}
#[test]
fn allows_device_key_alongside_a_password() {
let (bytes, pw, dev, dir) = file_with_device();
let out = rewrite_slots_managed(&bytes, &[pw.duplicate()], &plans_keeping(&[0, 1]), &dir)
.expect("密码 + 设备密钥应当放行");
assert!(
omy_core::file::open(&out.bytes, &[pw]).is_ok(),
"改写后密码还该能开"
);
assert!(
omy_core::file::open(&out.bytes, &[dev]).is_ok(),
"改写后设备密钥还该能开"
);
}
#[test]
fn allows_device_key_alongside_a_recovery_code() {
let pw = kek(1);
let dev = kek(2);
let reco = kek(3);
let mut dir = SlotDirectory::new();
dir.set(0, SlotEntry::of(SlotKind::Vault)).expect("0");
dir.set(1, SlotEntry::of(SlotKind::Device)).expect("1");
dir.set(2, SlotEntry::of(SlotKind::Recovery)).expect("2");
let bytes = encrypt(
b"payload",
&[pw.duplicate(), dev.duplicate(), reco.duplicate()],
&[7u8; 16],
&EncryptOptions {
argon2: Argon2Params { m_kib: 8, t: 1, p: 1 },
slot_directory: Some(dir.encode()),
..EncryptOptions::default()
},
&RandomMaterial::generate(),
)
.expect("加密")
.bytes;
let mut after = dir;
after.set(0, SlotEntry::empty()).expect("清掉密码");
let out = rewrite_slots_managed(&bytes, &[pw], &plans_keeping(&[1, 2]), &after)
.expect("设备密钥 + 恢复码应当放行");
assert!(
omy_core::file::open(&out.bytes, &[reco]).is_ok(),
"恢复码还该能开"
);
assert!(
omy_core::file::open(&out.bytes, &[dev]).is_ok(),
"设备密钥还该能开"
);
}
#[test]
fn still_refuses_to_leave_nothing() {
let (bytes, pw, _dev, _dir) = file_with_device();
let all_clear: Vec<SlotPlan> = (0..omy_core::header::SLOT_COUNT)
.map(|_| SlotPlan::Clear)
.collect();
assert!(
rewrite_slots_managed(&bytes, &[pw], &all_clear, &SlotDirectory::new()).is_err(),
"一个槽都不留必须仍然被拒"
);
}
#[test]
fn two_device_keys_are_still_refused() {
let pw = kek(1);
let d1 = kek(2);
let d2 = kek(3);
let mut dir = SlotDirectory::new();
dir.set(0, SlotEntry::of(SlotKind::Vault)).expect("0");
dir.set(1, SlotEntry::of(SlotKind::Device)).expect("1");
dir.set(2, SlotEntry::of(SlotKind::Device)).expect("2");
let bytes = encrypt(
b"payload",
&[pw.duplicate(), d1, d2],
&[7u8; 16],
&EncryptOptions {
argon2: Argon2Params { m_kib: 8, t: 1, p: 1 },
slot_directory: Some(dir.encode()),
..EncryptOptions::default()
},
&RandomMaterial::generate(),
)
.expect("加密")
.bytes;
let mut after = dir;
after.set(0, SlotEntry::empty()).expect("清掉密码");
assert!(
rewrite_slots_managed(&bytes, &[pw], &plans_keeping(&[1, 2]), &after).is_err(),
"只剩两把设备密钥仍必须被拒"
);
}
#[test]
fn checks_plans_not_just_the_directory() {
let (bytes, pw, _dev, dir) = file_with_device();
let err = rewrite_slots_managed(&bytes, &[pw], &plans_keeping(&[1]), &dir);
assert!(
err.is_err(),
"plans 清掉了密码槽,即使目录还记着它也必须拒绝"
);
}