1use crate::crypto::{CipherId, Kek, NONCE_LEN, SecretKey, ZERO_NONCE};
56use crate::error::{Error, Result};
57use crate::util::Writer;
58
59pub const KEYS_SIDECAR: &str = ".omy-keys";
76
77pub const SIDECAR_MAGIC: &[u8; 8] = b"OMYDNAME";
79
80pub const SIDECAR_VERSION: u8 = 1;
82
83pub const DK_LEN: usize = 32;
85
86pub const WRAP_LEN: usize = 48;
88
89pub const SIDECAR_SLOTS: usize = 8;
95
96pub const SIDECAR_SLOT_AREA: usize = SIDECAR_SLOTS * WRAP_LEN;
98
99pub const SIDECAR_LEN: usize = 8 + 1 + 1 + 2 + NONCE_LEN + SIDECAR_SLOT_AREA;
105
106pub const SIDECAR_LEN_MANAGED: usize = SIDECAR_LEN + crate::slotdir::DIRECTORY_LEN + 16;
111
112pub const SIDECAR_FLAG_MANAGED: u8 = 1 << 0;
118
119const INFO_SIDECAR_DIR: &[u8] = b"omy/v1/sidecar-slotdir";
124
125const INFO_SIDECAR_WRAP: &[u8] = b"omy/v1/dirname-wrap";
130
131#[derive(Debug)]
136pub struct DirKey(SecretKey);
137
138impl DirKey {
139 #[must_use]
141 pub fn generate() -> Self {
142 let mut raw = [0u8; DK_LEN];
143 crate::util::fill_random(&mut raw);
144 Self(SecretKey::from_bytes(raw))
145 }
146
147 #[must_use]
149 pub const fn from_bytes(raw: [u8; DK_LEN]) -> Self {
150 Self(SecretKey::from_bytes(raw))
151 }
152
153 #[must_use]
155 pub const fn as_key(&self) -> &SecretKey {
156 &self.0
157 }
158
159 #[must_use]
161 pub fn duplicate(&self) -> Self {
162 Self(SecretKey::from_bytes(*self.0.as_bytes()))
163 }
164
165 #[must_use]
170 fn slotdir_key(&self, vault_salt: &[u8; 16]) -> SecretKey {
171 crate::crypto::derive_from_secret(&self.0, vault_salt, INFO_SIDECAR_DIR)
172 }
173}
174
175#[must_use]
180pub fn is_managed(blob: &[u8]) -> bool {
181 blob.get(..8) == Some(SIDECAR_MAGIC.as_slice())
182 && blob.get(9).is_some_and(|f| f & SIDECAR_FLAG_MANAGED != 0)
183}
184
185pub fn read_slot_directory(
192 blob: &[u8],
193 dk: &DirKey,
194 vault_salt: &[u8; 16],
195 cipher: CipherId,
196) -> Result<crate::slotdir::SlotDirectory> {
197 if !is_managed(blob) {
198 return Err(Error::MalformedTlv {
199 tlv_type: 0,
200 reason: "this sidecar has no slot directory (deniable mode)",
201 });
202 }
203 if blob.len() != SIDECAR_LEN_MANAGED {
204 return Err(Error::MalformedTlv {
205 tlv_type: 0,
206 reason: "managed sidecar has wrong length",
207 });
208 }
209 let ct = blob.get(SIDECAR_LEN..).ok_or(Error::MalformedTlv {
210 tlv_type: 0,
211 reason: "managed sidecar truncated before slot directory",
212 })?;
213 let key = dk.slotdir_key(vault_salt);
214 let plain = cipher
216 .decrypt(&key, &ZERO_NONCE, ct, &[])?
217 .ok_or(Error::MalformedTlv {
218 tlv_type: 0,
219 reason: "slot directory failed authentication",
220 })?;
221 crate::slotdir::SlotDirectory::decode(&plain)
222}
223
224fn append_slot_directory(
226 base: Vec<u8>,
227 dir: &crate::slotdir::SlotDirectory,
228 dk: &DirKey,
229 vault_salt: &[u8; 16],
230 cipher: CipherId,
231) -> Result<Vec<u8>> {
232 let key = dk.slotdir_key(vault_salt);
233 let ct = cipher.encrypt(&key, &ZERO_NONCE, &dir.encode(), &[])?;
234 let mut out = base;
235 if let Some(f) = out.get_mut(9) {
237 *f |= SIDECAR_FLAG_MANAGED;
238 }
239 out.extend_from_slice(&ct);
240 debug_assert_eq!(out.len(), SIDECAR_LEN_MANAGED, "可管理边车必须恰好 {SIDECAR_LEN_MANAGED} 字节");
241 Ok(out)
242}
243
244fn wrap_key(kek: &Kek, vault_salt: &[u8; 16]) -> SecretKey {
249 kek.derive_dirname_key(vault_salt, INFO_SIDECAR_WRAP)
250}
251
252pub fn build_sidecar(
261 keks: &[Kek],
262 dk: &DirKey,
263 vault_salt: &[u8; 16],
264 nonce: &[u8; NONCE_LEN],
265 cipher: CipherId,
266) -> Result<Vec<u8>> {
267 if keks.is_empty() {
268 return Err(Error::MalformedTlv {
269 tlv_type: 0,
270 reason: "refusing to build a sidecar with zero keys; the directory name could never be decrypted",
271 });
272 }
273 if keks.len() > SIDECAR_SLOTS {
274 return Err(Error::TooManySlots { got: keks.len(), max: SIDECAR_SLOTS });
275 }
276
277 let mut w = Writer::with_capacity(SIDECAR_LEN);
278 w.bytes(SIDECAR_MAGIC);
279 w.u8(SIDECAR_VERSION);
280 w.bytes(&[0u8; 3]);
281 w.bytes(nonce);
282
283 for kek in keks {
284 let wk = wrap_key(kek, vault_salt);
285 let wrapped = cipher.encrypt(&wk, &ZERO_NONCE, dk.as_key().as_bytes(), &[])?;
288 debug_assert_eq!(wrapped.len(), WRAP_LEN, "包裹后必须是 48 字节");
289 w.bytes(&wrapped);
290 }
291
292 let used = keks.len().saturating_mul(WRAP_LEN);
294 w.random(SIDECAR_SLOT_AREA.saturating_sub(used));
295
296 let out = w.into_vec();
297 debug_assert_eq!(out.len(), SIDECAR_LEN, "边车必须恰好 {SIDECAR_LEN} 字节");
298 Ok(out)
299}
300
301pub fn rebuild_sidecar_carrying(
320 old: Option<&[u8]>,
321 keep: &[Kek],
322 dk: &DirKey,
323 vault_salt: &[u8; 16],
324 nonce: &[u8; NONCE_LEN],
325 cipher: CipherId,
326) -> Result<Vec<u8>> {
327 if keep.is_empty() {
328 return Err(Error::MalformedTlv {
329 tlv_type: 0,
330 reason: "refusing to build a sidecar with zero keys; the directory name could never be decrypted",
331 });
332 }
333 if keep.len() > SIDECAR_SLOTS {
334 return Err(Error::TooManySlots { got: keep.len(), max: SIDECAR_SLOTS });
335 }
336
337 let mut w = Writer::with_capacity(SIDECAR_LEN);
339 w.bytes(SIDECAR_MAGIC);
340 w.u8(SIDECAR_VERSION);
341 w.bytes(&[0u8; 3]);
342 w.bytes(nonce);
343
344 for kek in keep {
345 let wk = wrap_key(kek, vault_salt);
346 let wrapped = cipher.encrypt(&wk, &ZERO_NONCE, dk.as_key().as_bytes(), &[])?;
347 w.bytes(&wrapped);
348 }
349
350 let mut written = keep.len();
356 if let Some(prev) = old {
357 let known_len = prev.len() == SIDECAR_LEN || prev.len() == SIDECAR_LEN_MANAGED;
361 if known_len && prev.get(..8) == Some(SIDECAR_MAGIC.as_slice()) {
362 let slots_start = 12 + NONCE_LEN;
363 for i in 0..SIDECAR_SLOTS {
364 if written >= SIDECAR_SLOTS {
365 break;
366 }
367 let from = slots_start.saturating_add(i.saturating_mul(WRAP_LEN));
368 let to = from.saturating_add(WRAP_LEN);
369 let Some(slot) = prev.get(from..to) else { continue };
370 let already = keep.iter().any(|k| {
373 let wk = wrap_key(k, vault_salt);
374 matches!(cipher.decrypt(&wk, &ZERO_NONCE, slot, &[]), Ok(Some(_)))
375 });
376 if already {
377 continue;
378 }
379 w.bytes(slot);
380 written = written.saturating_add(1);
381 }
382 }
383 }
384
385 let used = written.saturating_mul(WRAP_LEN);
386 w.random(SIDECAR_SLOT_AREA.saturating_sub(used));
387
388 let out = w.into_vec();
389 debug_assert_eq!(out.len(), SIDECAR_LEN, "边车必须恰好 {SIDECAR_LEN} 字节");
390 Ok(out)
391}
392
393pub fn build_sidecar_managed(
403 keks: &[Kek],
404 dir: &crate::slotdir::SlotDirectory,
405 dk: &DirKey,
406 vault_salt: &[u8; 16],
407 nonce: &[u8; NONCE_LEN],
408 cipher: CipherId,
409) -> Result<Vec<u8>> {
410 let base = build_sidecar(keks, dk, vault_salt, nonce, cipher)?;
411 append_slot_directory(base, dir, dk, vault_salt, cipher)
412}
413
414pub fn rewrite_sidecar_managed(
426 old: &[u8],
427 plans: &[SidecarPlan],
428 dir: &crate::slotdir::SlotDirectory,
429 dk: &DirKey,
430 vault_salt: &[u8; 16],
431 nonce: &[u8; NONCE_LEN],
432 cipher: CipherId,
433) -> Result<Vec<u8>> {
434 if !is_managed(old) {
435 return Err(Error::MalformedTlv {
436 tlv_type: 0,
437 reason: "refusing precise rewrite on a deniable-mode sidecar",
438 });
439 }
440 if old.len() != SIDECAR_LEN_MANAGED {
441 return Err(Error::MalformedTlv {
442 tlv_type: 0,
443 reason: "managed sidecar has wrong length",
444 });
445 }
446 if !plans.iter().any(|p| matches!(p, SidecarPlan::Keep | SidecarPlan::Write(_))) {
449 return Err(Error::MalformedTlv {
450 tlv_type: 0,
451 reason: "refusing to leave the sidecar with zero usable keys",
452 });
453 }
454
455 let mut w = Writer::with_capacity(SIDECAR_LEN);
456 w.bytes(SIDECAR_MAGIC);
457 w.u8(SIDECAR_VERSION);
458 w.u8(SIDECAR_FLAG_MANAGED);
459 w.bytes(&[0u8; 2]);
460 w.bytes(nonce);
461
462 let slots_start = 12 + NONCE_LEN;
463 for (i, plan) in plans.iter().enumerate().take(SIDECAR_SLOTS) {
464 match plan {
465 SidecarPlan::Keep => {
466 let from = slots_start.saturating_add(i.saturating_mul(WRAP_LEN));
467 let to = from.saturating_add(WRAP_LEN);
468 match old.get(from..to) {
469 Some(slot) => {
470 w.bytes(slot);
471 }
472 None => {
473 w.random(WRAP_LEN);
474 }
475 }
476 }
477 SidecarPlan::Write(kek) => {
478 let wk = wrap_key(kek, vault_salt);
479 let wrapped = cipher.encrypt(&wk, &ZERO_NONCE, dk.as_key().as_bytes(), &[])?;
480 w.bytes(&wrapped);
481 }
482 SidecarPlan::Clear => {
485 w.random(WRAP_LEN);
486 }
487 }
488 }
489 let written = plans.len().min(SIDECAR_SLOTS);
491 w.random(SIDECAR_SLOT_AREA.saturating_sub(written.saturating_mul(WRAP_LEN)));
492
493 append_slot_directory(w.into_vec(), dir, dk, vault_salt, cipher)
494}
495
496#[derive(Debug)]
500pub enum SidecarPlan {
501 Keep,
503 Write(Kek),
505 Clear,
507}
508pub fn open_sidecar(
520 blob: &[u8],
521 keks: &[Kek],
522 vault_salt: &[u8; 16],
523 cipher: CipherId,
524) -> Result<(DirKey, [u8; NONCE_LEN])> {
525 open_sidecar_at(blob, keks, vault_salt, cipher).map(|(dk, nonce, _)| (dk, nonce))
526}
527
528pub fn open_sidecar_at(
538 blob: &[u8],
539 keks: &[Kek],
540 vault_salt: &[u8; 16],
541 cipher: CipherId,
542) -> Result<(DirKey, [u8; NONCE_LEN], usize)> {
543 if blob.len() != SIDECAR_LEN && blob.len() != SIDECAR_LEN_MANAGED {
545 return Err(Error::MalformedTlv {
546 tlv_type: 0,
547 reason: "dirname sidecar has wrong length",
548 });
549 }
550 if blob.get(..8) != Some(SIDECAR_MAGIC.as_slice()) {
551 return Err(Error::MalformedTlv {
552 tlv_type: 0,
553 reason: "dirname sidecar magic mismatch",
554 });
555 }
556 if blob.get(8) != Some(&SIDECAR_VERSION) {
557 return Err(Error::MalformedTlv {
558 tlv_type: 0,
559 reason: "unsupported dirname sidecar version",
560 });
561 }
562
563 let nonce_start = 12;
564 let slots_start = nonce_start + NONCE_LEN;
565 let mut nonce = [0u8; NONCE_LEN];
566 nonce.copy_from_slice(
567 blob.get(nonce_start..slots_start).ok_or(Error::MalformedTlv {
568 tlv_type: 0,
569 reason: "dirname sidecar truncated before nonce",
570 })?,
571 );
572
573 for kek in keks {
574 let wk = wrap_key(kek, vault_salt);
575 for i in 0..SIDECAR_SLOTS {
576 let from = slots_start.saturating_add(i.saturating_mul(WRAP_LEN));
577 let to = from.saturating_add(WRAP_LEN);
578 let Some(slot) = blob.get(from..to) else { continue };
579 let Ok(Some(plain)) = cipher.decrypt(&wk, &ZERO_NONCE, slot, &[]) else {
583 continue;
584 };
585 if plain.len() != DK_LEN {
586 continue;
587 }
588 let mut raw = [0u8; DK_LEN];
589 raw.copy_from_slice(&plain);
590 return Ok((DirKey::from_bytes(raw), nonce, i));
591 }
592 }
593 Err(Error::NoMatchingSlot)
594}
595
596#[cfg(test)]
597#[expect(clippy::panic, reason = "测试里 panic 就是断言失败的表达方式")]
598mod tests {
599 use super::*;
600 use crate::crypto::Argon2Params;
601
602 fn kek(pw: &str, salt: &[u8; 16]) -> Kek {
603 Kek::from_password(pw.as_bytes(), salt, Argon2Params::TEST_WEAK).expect("派生 KEK")
606 }
607
608 fn nonce_of(n: u8) -> [u8; NONCE_LEN] {
609 core::array::from_fn(|i| (i as u8).wrapping_mul(3) ^ n)
610 }
611
612 #[test]
613 fn every_key_opens_the_same_dir_key() {
614 let salt = [9u8; 16];
615 let keks = vec![kek("one", &salt), kek("two", &salt), kek("three", &salt)];
616 let dk = DirKey::generate();
617 let n = nonce_of(1);
618 let blob = build_sidecar(&keks, &dk, &salt, &n, CipherId::ChaCha20Poly1305).expect("构建");
619
620 for (i, k) in keks.iter().enumerate() {
623 let (got, got_n) =
624 open_sidecar(&blob, std::slice::from_ref(k), &salt, CipherId::ChaCha20Poly1305)
625 .unwrap_or_else(|e| panic!("第 {i} 把钥匙应能解开边车:{e}"));
626 assert_eq!(got.as_key().as_bytes(), dk.as_key().as_bytes(), "第 {i} 把解出的 DK 不对");
627 assert_eq!(got_n, n, "nonce 应原样取回");
628 }
629 }
630
631 #[test]
632 fn length_is_constant_regardless_of_key_count() {
633 let salt = [3u8; 16];
634 let dk = DirKey::generate();
635 let n = nonce_of(2);
636 let all = vec![kek("a", &salt), kek("b", &salt), kek("c", &salt), kek("d", &salt)];
637
638 for count in 1..=4 {
640 let blob = build_sidecar(
641 all.get(..count).unwrap_or(&all),
642 &dk,
643 &salt,
644 &n,
645 CipherId::ChaCha20Poly1305,
646 )
647 .expect("构建");
648 assert_eq!(blob.len(), SIDECAR_LEN, "{count} 把钥匙时长度变了");
649 }
650 }
651
652 #[test]
653 fn unused_slots_are_not_zero_filled() {
654 let salt = [4u8; 16];
655 let dk = DirKey::generate();
656 let blob = build_sidecar(
657 &[kek("only", &salt)],
658 &dk,
659 &salt,
660 &nonce_of(3),
661 CipherId::ChaCha20Poly1305,
662 )
663 .expect("构建");
664
665 let slots_start = 12 + NONCE_LEN;
668 let unused = blob.get(slots_start + WRAP_LEN..).expect("应有剩余槽");
669 assert!(
670 unused.iter().any(|&b| b != 0),
671 "未使用的槽被填成了零,可否认性失效"
672 );
673 }
674
675 #[test]
676 fn carrying_keeps_unknown_wraps_but_discarding_drops_them() {
677 let salt = [11u8; 16];
688 let main_key = kek("main", &salt);
689 let reco = kek("recovery-code", &salt);
690 let dk = DirKey::generate();
691 let cipher = CipherId::ChaCha20Poly1305;
692
693 let original = build_sidecar(
694 &[main_key.duplicate(), reco.duplicate()],
695 &dk,
696 &salt,
697 &nonce_of(20),
698 cipher,
699 )
700 .expect("构建");
701
702 let carried = rebuild_sidecar_carrying(
704 Some(&original),
705 &[main_key.duplicate()],
706 &dk,
707 &salt,
708 &nonce_of(21),
709 cipher,
710 )
711 .expect("Carry 重建");
712 assert!(
713 open_sidecar(&carried, &[reco.duplicate()], &salt, cipher).is_ok(),
714 "Carry 应当保住恢复码的包裹"
715 );
716 assert!(
717 open_sidecar(&carried, &[main_key.duplicate()], &salt, cipher).is_ok(),
718 "Carry 之后 main 自己也得还能用"
719 );
720
721 let dropped = rebuild_sidecar_carrying(
723 None,
724 &[main_key.duplicate()],
725 &dk,
726 &salt,
727 &nonce_of(22),
728 cipher,
729 )
730 .expect("Discard 重建");
731 assert!(
732 open_sidecar(&dropped, &[reco], &salt, cipher).is_err(),
733 "丢弃时旧包裹必须真的没了——留着等于那把钥匙没被作废"
734 );
735 assert!(
736 open_sidecar(&dropped, &[main_key], &salt, cipher).is_ok(),
737 "丢弃不该连保留的钥匙一起丢"
738 );
739 }
740
741 #[test]
742 fn wrong_key_is_rejected() {
743 let salt = [5u8; 16];
744 let dk = DirKey::generate();
745 let blob = build_sidecar(
746 &[kek("right", &salt)],
747 &dk,
748 &salt,
749 &nonce_of(4),
750 CipherId::ChaCha20Poly1305,
751 )
752 .expect("构建");
753 let wrong = kek("wrong", &salt);
754 assert!(
755 matches!(
756 open_sidecar(&blob, &[wrong], &salt, CipherId::ChaCha20Poly1305),
757 Err(Error::NoMatchingSlot)
758 ),
759 "不属于这棵树的钥匙必须被拒绝"
760 );
761 }
762
763 #[test]
764 fn sidecar_is_bound_to_vault_salt() {
765 let salt_a = [1u8; 16];
766 let salt_b = [2u8; 16];
767 let k = kek("same-password", &salt_a);
768 let dk = DirKey::generate();
769 let blob =
770 build_sidecar(&[k], &dk, &salt_a, &nonce_of(5), CipherId::ChaCha20Poly1305)
771 .expect("构建");
772
773 let k2 = kek("same-password", &salt_a);
776 assert!(
777 open_sidecar(&blob, &[k2], &salt_b, CipherId::ChaCha20Poly1305).is_err(),
778 "边车必须绑定 vault_salt"
779 );
780 }
781
782 #[test]
783 fn refuses_to_build_with_no_keys() {
784 let salt = [6u8; 16];
787 assert!(
788 build_sidecar(
789 &[],
790 &DirKey::generate(),
791 &salt,
792 &nonce_of(6),
793 CipherId::ChaCha20Poly1305
794 )
795 .is_err(),
796 "零把钥匙必须拒绝"
797 );
798 }
799
800 #[test]
801 fn rejects_corrupted_header() {
802 let salt = [8u8; 16];
803 let dk = DirKey::generate();
804 let good = build_sidecar(
805 &[kek("k", &salt)],
806 &dk,
807 &salt,
808 &nonce_of(7),
809 CipherId::ChaCha20Poly1305,
810 )
811 .expect("构建");
812
813 let mut bad_magic = good.clone();
816 if let Some(b) = bad_magic.get_mut(0) {
817 *b ^= 0xFF;
818 }
819 assert!(open_sidecar(&bad_magic, &[kek("k", &salt)], &salt, CipherId::ChaCha20Poly1305)
820 .is_err());
821
822 let mut bad_ver = good.clone();
823 if let Some(b) = bad_ver.get_mut(8) {
824 *b = 99;
825 }
826 assert!(open_sidecar(&bad_ver, &[kek("k", &salt)], &salt, CipherId::ChaCha20Poly1305)
827 .is_err());
828
829 let short = good.get(..SIDECAR_LEN - 1).unwrap_or(&good).to_vec();
830 assert!(open_sidecar(&short, &[kek("k", &salt)], &salt, CipherId::ChaCha20Poly1305)
831 .is_err());
832 }
833 #[test]
836 fn managed_sidecar_roundtrips_the_directory() {
837 let salt = [9u8; 16];
839 let owner = kek("owner", &salt);
840 let dk = DirKey::generate();
841 let mut dir = crate::slotdir::SlotDirectory::new();
842 dir.set(0, crate::slotdir::SlotEntry::of(crate::slotdir::SlotKind::Vault))
843 .expect("set 0");
844
845 let blob = build_sidecar_managed(
846 &[owner.duplicate()],
847 &dir,
848 &dk,
849 &salt,
850 &nonce_of(1),
851 CipherId::ChaCha20Poly1305,
852 )
853 .expect("构建");
854
855 assert!(is_managed(&blob), "flags 位没置上");
856 assert_eq!(blob.len(), SIDECAR_LEN_MANAGED);
857
858 let (got_dk, _, idx) =
859 open_sidecar_at(&blob, &[owner], &salt, CipherId::ChaCha20Poly1305).expect("解开");
860 assert_eq!(idx, 0, "命中的应当是 slot 0");
861 let got = read_slot_directory(&blob, &got_dk, &salt, CipherId::ChaCha20Poly1305)
862 .expect("读目录");
863 assert_eq!(got, dir, "目录内容对不上");
864 }
865
866 #[test]
867 fn deniable_sidecar_refuses_to_give_a_directory() {
868 let salt = [9u8; 16];
873 let owner = kek("owner", &salt);
874 let dk = DirKey::generate();
875 let blob = build_sidecar(
876 &[owner.duplicate()],
877 &dk,
878 &salt,
879 &nonce_of(2),
880 CipherId::ChaCha20Poly1305,
881 )
882 .expect("构建");
883
884 assert!(!is_managed(&blob));
885 assert!(
886 read_slot_directory(&blob, &dk, &salt, CipherId::ChaCha20Poly1305).is_err(),
887 "可否认模式不该给出目录"
888 );
889 }
890
891 #[test]
892 fn managed_length_is_constant_regardless_of_key_count() {
893 let salt = [9u8; 16];
896 let dk = DirKey::generate();
897 let dir = crate::slotdir::SlotDirectory::new();
898 let mut lens = Vec::new();
899 for n in 1..=4usize {
900 let keks: Vec<Kek> = (0..n).map(|i| kek(&format!("pw{i}"), &salt)).collect();
901 let blob = build_sidecar_managed(
902 &keks,
903 &dir,
904 &dk,
905 &salt,
906 &nonce_of(3),
907 CipherId::ChaCha20Poly1305,
908 )
909 .expect("构建");
910 lens.push(blob.len());
911 }
912 assert!(
913 lens.iter().all(|&l| l == SIDECAR_LEN_MANAGED),
914 "长度随钥匙数变化了:{lens:?}"
915 );
916 }
917
918 #[test]
919 fn precise_rewrite_removes_one_key_and_keeps_the_rest() {
920 let salt = [9u8; 16];
922 let owner = kek("owner", &salt);
923 let mate = kek("mate", &salt);
924 let reco = kek("reco", &salt);
925 let dk = DirKey::generate();
926
927 let mut dir = crate::slotdir::SlotDirectory::new();
928 dir.set(0, crate::slotdir::SlotEntry::of(crate::slotdir::SlotKind::Vault))
929 .expect("0");
930 dir.set(1, crate::slotdir::SlotEntry::of(crate::slotdir::SlotKind::Vault))
931 .expect("1");
932 dir.set(2, crate::slotdir::SlotEntry::of(crate::slotdir::SlotKind::Recovery))
933 .expect("2");
934
935 let blob = build_sidecar_managed(
936 &[owner.duplicate(), mate.duplicate(), reco.duplicate()],
937 &dir,
938 &dk,
939 &salt,
940 &nonce_of(4),
941 CipherId::ChaCha20Poly1305,
942 )
943 .expect("构建");
944
945 for (name, k) in [("owner", &owner), ("mate", &mate), ("reco", &reco)] {
947 assert!(
948 open_sidecar(&blob, &[k.duplicate()], &salt, CipherId::ChaCha20Poly1305).is_ok(),
949 "{name} 一开始就该能解开"
950 );
951 }
952
953 let mut after = dir;
955 after.set(1, crate::slotdir::SlotEntry::empty()).expect("清空");
956 let mut plans = vec![SidecarPlan::Keep, SidecarPlan::Clear, SidecarPlan::Keep];
957 plans.resize_with(SIDECAR_SLOTS, || SidecarPlan::Clear);
958
959 let out = rewrite_sidecar_managed(
960 &blob,
961 &plans,
962 &after,
963 &dk,
964 &salt,
965 &nonce_of(4),
966 CipherId::ChaCha20Poly1305,
967 )
968 .expect("改写");
969
970 assert!(
971 open_sidecar(&out, &[owner], &salt, CipherId::ChaCha20Poly1305).is_ok(),
972 "主密码不该受影响"
973 );
974 assert!(
975 open_sidecar(&out, &[reco], &salt, CipherId::ChaCha20Poly1305).is_ok(),
976 "恢复码必须保住——这正是可管理模式的全部意义"
977 );
978 assert!(
979 open_sidecar(&out, &[mate], &salt, CipherId::ChaCha20Poly1305).is_err(),
980 "被踢掉的协作者必须再也解不开"
981 );
982 }
983
984 #[test]
985 fn precise_rewrite_refuses_to_orphan_the_directory_name() {
986 let salt = [9u8; 16];
989 let owner = kek("owner", &salt);
990 let dk = DirKey::generate();
991 let dir = crate::slotdir::SlotDirectory::new();
992 let blob = build_sidecar_managed(
993 &[owner],
994 &dir,
995 &dk,
996 &salt,
997 &nonce_of(5),
998 CipherId::ChaCha20Poly1305,
999 )
1000 .expect("构建");
1001
1002 let plans: Vec<SidecarPlan> = (0..SIDECAR_SLOTS).map(|_| SidecarPlan::Clear).collect();
1003 assert!(
1004 rewrite_sidecar_managed(
1005 &blob,
1006 &plans,
1007 &dir,
1008 &dk,
1009 &salt,
1010 &nonce_of(5),
1011 CipherId::ChaCha20Poly1305,
1012 )
1013 .is_err(),
1014 "一个槽都不留必须报错"
1015 );
1016 }
1017
1018 #[test]
1019 fn precise_rewrite_rejects_a_deniable_sidecar() {
1020 let salt = [9u8; 16];
1023 let owner = kek("owner", &salt);
1024 let dk = DirKey::generate();
1025 let blob = build_sidecar(
1026 &[owner.duplicate()],
1027 &dk,
1028 &salt,
1029 &nonce_of(6),
1030 CipherId::ChaCha20Poly1305,
1031 )
1032 .expect("构建");
1033
1034 let mut plans = vec![SidecarPlan::Keep];
1035 plans.resize_with(SIDECAR_SLOTS, || SidecarPlan::Clear);
1036 assert!(
1037 rewrite_sidecar_managed(
1038 &blob,
1039 &plans,
1040 &crate::slotdir::SlotDirectory::new(),
1041 &dk,
1042 &salt,
1043 &nonce_of(6),
1044 CipherId::ChaCha20Poly1305,
1045 )
1046 .is_err(),
1047 "对可否认模式的边车应当拒绝"
1048 );
1049 }
1050
1051 #[test]
1052 fn precise_rewrite_fills_cleared_slots_with_random() {
1053 let salt = [9u8; 16];
1059 let owner = kek("owner", &salt);
1060 let mate = kek("mate", &salt);
1061 let dk = DirKey::generate();
1062 let mut dir = crate::slotdir::SlotDirectory::new();
1063 dir.set(0, crate::slotdir::SlotEntry::of(crate::slotdir::SlotKind::Vault))
1064 .expect("0");
1065 dir.set(1, crate::slotdir::SlotEntry::of(crate::slotdir::SlotKind::Vault))
1066 .expect("1");
1067
1068 let blob = build_sidecar_managed(
1069 &[owner, mate],
1070 &dir,
1071 &dk,
1072 &salt,
1073 &nonce_of(8),
1074 CipherId::ChaCha20Poly1305,
1075 )
1076 .expect("构建");
1077
1078 let mut after = dir;
1079 after.set(1, crate::slotdir::SlotEntry::empty()).expect("清空");
1080 let mut plans = vec![SidecarPlan::Keep, SidecarPlan::Clear];
1081 plans.resize_with(SIDECAR_SLOTS, || SidecarPlan::Clear);
1082 let out = rewrite_sidecar_managed(
1083 &blob,
1084 &plans,
1085 &after,
1086 &dk,
1087 &salt,
1088 &nonce_of(8),
1089 CipherId::ChaCha20Poly1305,
1090 )
1091 .expect("改写");
1092
1093 let slots_start = 12 + NONCE_LEN;
1095 let area = out
1096 .get(slots_start..slots_start + SIDECAR_SLOT_AREA)
1097 .expect("槽位区");
1098 let zeros = vec![0u8; WRAP_LEN];
1099 for i in 0..SIDECAR_SLOTS {
1100 let from = i * WRAP_LEN;
1101 let slot = area.get(from..from + WRAP_LEN).expect("槽");
1102 assert_ne!(slot, zeros.as_slice(), "slot {i} 被填成了零");
1103 }
1104 }
1105
1106 #[test]
1107 fn wrong_key_cannot_read_the_directory() {
1108 let salt = [9u8; 16];
1111 let owner = kek("owner", &salt);
1112 let outsider = DirKey::generate();
1113 let dk = DirKey::generate();
1114 let mut dir = crate::slotdir::SlotDirectory::new();
1115 dir.set(0, crate::slotdir::SlotEntry::of(crate::slotdir::SlotKind::Vault))
1116 .expect("set");
1117
1118 let blob = build_sidecar_managed(
1119 &[owner],
1120 &dir,
1121 &dk,
1122 &salt,
1123 &nonce_of(7),
1124 CipherId::ChaCha20Poly1305,
1125 )
1126 .expect("构建");
1127
1128 assert!(
1129 read_slot_directory(&blob, &outsider, &salt, CipherId::ChaCha20Poly1305).is_err(),
1130 "用别的 DK 不该读出目录"
1131 );
1132 }
1133}