1mod adoption;
4mod membership;
5pub mod peer;
6pub mod rotation;
7mod seed;
8mod storage;
9pub use peer::EnrollmentReadiness;
10pub use storage::{FileProtectedStorage, KEYRING_ITEM, ProtectedStorage, ProtectedStorageLock};
11
12use std::collections::{HashMap, HashSet};
13use std::fmt;
14use std::path::Path;
15use std::sync::{Arc, Mutex};
16#[cfg(any(test, feature = "test-support"))]
17use std::{path::PathBuf, sync::atomic::AtomicU64};
18
19use crate::db::Database;
20use crate::sync::{
21 EncryptedLocalSharedStatePackage, LocalSharedStatePackageContext, LocalSharedStatePackageKey,
22 NeverDispatchedLocalSharedCapture,
23};
24use sha2::{Digest, Sha256};
25use zeroize::{Zeroize, Zeroizing};
26
27const KEYRING_MAGIC: &[u8; 8] = b"AVENLKR1";
28const MARKER_MAGIC: &[u8; 8] = b"AVENLKM1";
29const KEYRING_BYTES: usize = 8 + 32 + 32 + 32 + 32;
30const MARKER_BYTES: usize = 8 + 32 + 32;
31const AUTHORITY_RECORD: &str = "authority";
33
34#[derive(Clone, Copy, Debug, PartialEq, Eq)]
38pub enum ProtectedLocalKeyStoreErrorKind {
39 MissingAuthority,
40 Unavailable,
41 Corrupt,
42 WriteFailed,
43 WrongDatabase,
44 SetupMismatch,
45 UnsupportedPlatform,
46}
47
48#[derive(Debug)]
49pub struct ProtectedLocalKeyStoreError {
50 kind: ProtectedLocalKeyStoreErrorKind,
51}
52
53impl ProtectedLocalKeyStoreError {
54 pub fn new(kind: ProtectedLocalKeyStoreErrorKind) -> Self {
55 Self { kind }
56 }
57
58 pub fn kind(&self) -> ProtectedLocalKeyStoreErrorKind {
59 self.kind
60 }
61}
62
63impl fmt::Display for ProtectedLocalKeyStoreError {
64 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
65 let message = match self.kind {
66 ProtectedLocalKeyStoreErrorKind::MissingAuthority => {
67 "protected local key authority is missing"
68 }
69 ProtectedLocalKeyStoreErrorKind::Unavailable => {
70 "protected local key storage is unavailable"
71 }
72 ProtectedLocalKeyStoreErrorKind::Corrupt => {
73 "protected local key storage is corrupt or unsafe"
74 }
75 ProtectedLocalKeyStoreErrorKind::WriteFailed => {
76 "protected local key storage could not be saved"
77 }
78 ProtectedLocalKeyStoreErrorKind::WrongDatabase => {
79 "protected local key store belongs to a different database installation"
80 }
81 ProtectedLocalKeyStoreErrorKind::SetupMismatch => {
82 "protected setup invitation does not match"
83 }
84 ProtectedLocalKeyStoreErrorKind::UnsupportedPlatform => {
85 "protected local key storage is unsupported on this platform"
86 }
87 };
88 formatter.write_str(message)
89 }
90}
91
92impl std::error::Error for ProtectedLocalKeyStoreError {}
93
94pub type StoreResult<T> = Result<T, ProtectedLocalKeyStoreError>;
95
96pub struct ProtectedLocalPackageKey {
100 context: LocalSharedStatePackageContext,
101 key: LocalSharedStatePackageKey,
102}
103
104impl ProtectedLocalPackageKey {
105 pub fn context(&self) -> LocalSharedStatePackageContext {
106 self.context
107 }
108
109 pub fn package_key(&self) -> &LocalSharedStatePackageKey {
110 &self.key
111 }
112}
113
114impl fmt::Debug for ProtectedLocalPackageKey {
115 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
116 formatter
117 .debug_struct("ProtectedLocalPackageKey")
118 .field("context", &self.context)
119 .field("key", &"[REDACTED]")
120 .finish()
121 }
122}
123
124pub struct ProtectedLocalKeyStore {
130 account: String,
131 storage: Arc<dyn ProtectedStorage>,
132 index: Arc<Mutex<Option<Index>>>,
133 #[cfg(any(test, feature = "test-support"))]
135 directory: PathBuf,
136 #[cfg(any(test, feature = "test-support"))]
137 enrollment_clock: Option<Arc<AtomicU64>>,
138}
139
140#[derive(Default)]
146struct Index {
147 listing: Option<Listing>,
148 pins: Option<HashMap<String, Vec<u8>>>,
149 values: HashMap<String, Zeroizing<Vec<u8>>>,
150}
151
152struct Listing {
154 items: HashSet<String>,
155 markers: HashSet<String>,
156}
157
158pub(crate) struct StoreLock {
160 _storage: ProtectedStorageLock,
161 index: Arc<Mutex<Option<Index>>>,
162}
163
164impl Drop for StoreLock {
165 fn drop(&mut self) {
166 *self.index.lock().unwrap_or_else(|e| e.into_inner()) = None;
167 }
168}
169
170#[cfg(any(test, feature = "test-support"))]
171impl ProtectedLocalKeyStore {
172 pub fn set_enrollment_clock(&mut self, clock: Arc<AtomicU64>) {
173 self.enrollment_clock = Some(clock);
174 }
175}
176
177impl ProtectedLocalKeyStore {
178 pub async fn open(
180 database: &Database,
181 storage: Arc<dyn ProtectedStorage>,
182 ) -> StoreResult<Self> {
183 let account = database_account(storage.as_ref(), database).await?;
184 Ok(Self::with_account(account, storage))
185 }
186
187 fn with_account(account: String, storage: Arc<dyn ProtectedStorage>) -> Self {
188 Self {
189 account,
190 storage,
191 index: Arc::default(),
192 #[cfg(any(test, feature = "test-support"))]
193 directory: PathBuf::new(),
194 #[cfg(any(test, feature = "test-support"))]
195 enrollment_clock: None,
196 }
197 }
198
199 pub fn load_or_create(&self) -> StoreResult<ProtectedLocalPackageKey> {
205 self.prepare()?;
206 let _guard = self.lock()?;
207 let marker = self.read_marker()?;
208 match self.load_secret(KEYRING_ITEM, KEYRING_BYTES)? {
209 Some(bytes) => {
210 let key = decode_keyring(bytes)?;
211 match marker {
212 Some(expected) if expected != key.context => {
213 Err(error(ProtectedLocalKeyStoreErrorKind::Corrupt))
214 }
215 Some(_) => Ok(key),
216 None => {
217 self.write_marker(key.context)?;
218 Ok(key)
219 }
220 }
221 }
222 None if marker.is_some() => {
223 Err(error(ProtectedLocalKeyStoreErrorKind::MissingAuthority))
224 }
225 None => {
226 if self.seed_authority_exists()? || self.peer_authority_exists()? {
227 return Err(error(ProtectedLocalKeyStoreErrorKind::MissingAuthority));
228 }
229 let key = generate_keyring()?;
230 let encoded = encode_keyring(&key);
231 self.create_secret(KEYRING_ITEM, &encoded)?;
232 self.write_marker(key.context)?;
233 Ok(key)
234 }
235 }
236 }
237
238 pub fn load_required(&self) -> StoreResult<ProtectedLocalPackageKey> {
243 self.prepare()?;
244 let _guard = self.lock()?;
245 self.load_required_locked()
246 }
247
248 fn load_required_locked(&self) -> StoreResult<ProtectedLocalPackageKey> {
249 let marker = self.read_marker()?;
250 let bytes = self
251 .load_secret(KEYRING_ITEM, KEYRING_BYTES)?
252 .ok_or_else(|| error(ProtectedLocalKeyStoreErrorKind::MissingAuthority))?;
253 let key = decode_keyring(bytes)?;
254 match marker {
255 Some(expected) if expected != key.context => {
256 Err(error(ProtectedLocalKeyStoreErrorKind::Corrupt))
257 }
258 Some(_) => Ok(key),
259 None => {
260 self.write_marker(key.context)?;
261 Ok(key)
262 }
263 }
264 }
265
266 pub async fn package_local_capture(
270 &self,
271 database: &Database,
272 blob_dir: &Path,
273 membership_predecessor: [u8; 32],
274 ) -> Result<EncryptedLocalSharedStatePackage, anyhow::Error> {
275 self.validate_database(database).await?;
276 let protected = if database
277 .has_local_shared_state_package_never_dispatched()
278 .await?
279 || database.local_seed_genesis_commitment().await?.is_some()
280 {
281 self.load_required()?
282 } else {
283 self.load_or_create()?
284 };
285 database
286 .package_local_shared_state_never_dispatched(
287 blob_dir,
288 protected.context(),
289 protected.package_key(),
290 membership_predecessor,
291 )
292 .await
293 }
294
295 pub(crate) async fn package_local_capture_validated(
296 &self,
297 database: &Database,
298 blob_dir: &Path,
299 membership_predecessor: [u8; 32],
300 capture: Option<NeverDispatchedLocalSharedCapture>,
301 ) -> Result<crate::sync::shared_state::validated::ValidatedSeed, anyhow::Error> {
302 self.validate_database(database).await?;
303 let protected = if database
304 .has_local_shared_state_package_never_dispatched()
305 .await?
306 || database.local_seed_genesis_commitment().await?.is_some()
307 {
308 self.load_required()?
309 } else {
310 self.load_or_create()?
311 };
312 database
313 .package_and_validate(
314 blob_dir,
315 protected.context(),
316 protected.package_key(),
317 membership_predecessor,
318 capture,
319 )
320 .await
321 }
322
323 pub fn erase(&self) -> StoreResult<()> {
328 self.prepare()?;
329 let _guard = self.lock()?;
330 for item in self.storage.list_secrets(&self.account)? {
331 self.delete_secret(&item)?;
332 }
333 for name in self.storage.list_records(&self.account)? {
334 self.remove_record(&name)?;
335 }
336 Ok(())
337 }
338
339 async fn validate_database(&self, database: &Database) -> StoreResult<()> {
340 if database_account(self.storage.as_ref(), database).await? != self.account {
341 return Err(error(ProtectedLocalKeyStoreErrorKind::WrongDatabase));
342 }
343 Ok(())
344 }
345
346 fn read_marker(&self) -> StoreResult<Option<LocalSharedStatePackageContext>> {
347 let Some(bytes) = self.read_record(AUTHORITY_RECORD, MARKER_BYTES)? else {
348 return Ok(None);
349 };
350 if &bytes[..8] != MARKER_MAGIC {
351 return Err(error(ProtectedLocalKeyStoreErrorKind::Corrupt));
352 }
353 let mut vault_id = [0_u8; 32];
354 let mut generation_id = [0_u8; 32];
355 vault_id.copy_from_slice(&bytes[8..40]);
356 generation_id.copy_from_slice(&bytes[40..]);
357 Ok(Some(LocalSharedStatePackageContext {
358 vault_id,
359 generation_id,
360 }))
361 }
362
363 fn write_marker(&self, context: LocalSharedStatePackageContext) -> StoreResult<()> {
364 self.write_record(AUTHORITY_RECORD, &marker_bytes(context))
365 }
366
367 fn prepare(&self) -> StoreResult<()> {
368 self.storage.prepare(&self.account)
369 }
370
371 fn lock(&self) -> StoreResult<StoreLock> {
372 let storage = self.storage.lock(&self.account)?;
373 *self.index.lock().unwrap_or_else(|e| e.into_inner()) = Some(Index::default());
374 Ok(StoreLock {
375 _storage: storage,
376 index: self.index.clone(),
377 })
378 }
379
380 fn with_index<T>(&self, f: impl FnOnce(&mut Index) -> T) -> Option<T> {
382 self.index
383 .lock()
384 .unwrap_or_else(|e| e.into_inner())
385 .as_mut()
386 .map(f)
387 }
388
389 fn list_kinds(&self) -> StoreResult<Listing> {
391 let items = self.storage.list_secrets(&self.account)?;
392 let markers = self
393 .storage
394 .list_records(&self.account)?
395 .iter()
396 .filter_map(|name| name.strip_suffix("-authority").map(str::to_string))
397 .collect();
398 Ok(Listing { items, markers })
399 }
400
401 fn load_secret(
403 &self,
404 item: &str,
405 expected_len: usize,
406 ) -> StoreResult<Option<Zeroizing<Vec<u8>>>> {
407 #[cfg(any(test, feature = "test-support"))]
408 test_support::BACKEND_LOADS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
409 let bytes = self
410 .storage
411 .load_secret(&self.account, item, expected_len)?;
412 if bytes
413 .as_ref()
414 .is_some_and(|bytes| bytes.len() != expected_len)
415 {
416 return Err(error(ProtectedLocalKeyStoreErrorKind::Corrupt));
417 }
418 Ok(bytes)
419 }
420
421 fn create_secret(&self, item: &str, bytes: &[u8]) -> StoreResult<()> {
422 self.storage.create_secret(&self.account, item, bytes)
423 }
424
425 fn delete_secret(&self, item: &str) -> StoreResult<()> {
426 self.storage.delete_secret(&self.account, item)
427 }
428
429 fn read_record(&self, name: &str, expected_len: usize) -> StoreResult<Option<Vec<u8>>> {
431 let bytes = self
432 .storage
433 .read_record(&self.account, name, expected_len)?;
434 if bytes
435 .as_ref()
436 .is_some_and(|bytes| bytes.len() != expected_len)
437 {
438 return Err(error(ProtectedLocalKeyStoreErrorKind::Corrupt));
439 }
440 Ok(bytes)
441 }
442
443 fn write_record(&self, name: &str, bytes: &[u8]) -> StoreResult<()> {
444 self.storage.create_record(&self.account, name, bytes)
445 }
446
447 fn remove_record(&self, name: &str) -> StoreResult<()> {
448 self.storage.remove_record(&self.account, name)
449 }
450}
451
452fn error(kind: ProtectedLocalKeyStoreErrorKind) -> ProtectedLocalKeyStoreError {
453 ProtectedLocalKeyStoreError::new(kind)
454}
455
456async fn database_account(
460 storage: &dyn ProtectedStorage,
461 database: &Database,
462) -> StoreResult<String> {
463 let unavailable = || error(ProtectedLocalKeyStoreErrorKind::Unavailable);
464 let location = storage.database_location(database.path())?;
465 let client_id = database
466 .meta("client_id")
467 .await
468 .map_err(|_| unavailable())?
469 .filter(|id| !id.is_empty())
470 .ok_or_else(unavailable)?;
471 Ok(account_for(&location, &client_id))
472}
473
474fn account_for(location: &[u8], client_id: &str) -> String {
475 let mut digest = Sha256::new();
476 digest.update(b"aven-protected-keys-v2\0");
477 digest.update(location);
478 digest.update(b"\0");
479 digest.update(client_id.as_bytes());
480 hex::encode(digest.finalize())
481}
482
483fn generate_keyring() -> StoreResult<ProtectedLocalPackageKey> {
484 let mut bytes = Zeroizing::new([0_u8; 96]);
485 getrandom::fill(bytes.as_mut())
486 .map_err(|_| error(ProtectedLocalKeyStoreErrorKind::Unavailable))?;
487 let mut vault_id = [0_u8; 32];
488 let mut generation_id = [0_u8; 32];
489 let mut key = [0_u8; 32];
490 vault_id.copy_from_slice(&bytes[..32]);
491 generation_id.copy_from_slice(&bytes[32..64]);
492 key.copy_from_slice(&bytes[64..]);
493 Ok(ProtectedLocalPackageKey {
494 context: LocalSharedStatePackageContext {
495 vault_id,
496 generation_id,
497 },
498 key: LocalSharedStatePackageKey::new(key),
499 })
500}
501
502fn encode_keyring(key: &ProtectedLocalPackageKey) -> Zeroizing<Vec<u8>> {
503 let mut encoded = Zeroizing::new(Vec::with_capacity(KEYRING_BYTES));
504 encoded.extend_from_slice(KEYRING_MAGIC);
505 encoded.extend_from_slice(&key.context.vault_id);
506 encoded.extend_from_slice(&key.context.generation_id);
507 encoded.extend_from_slice(key.key.protected_storage_bytes());
510 let checksum = Sha256::digest(encoded.as_slice());
511 encoded.extend_from_slice(&checksum);
512 encoded
513}
514
515fn decode_keyring(mut encoded: Zeroizing<Vec<u8>>) -> StoreResult<ProtectedLocalPackageKey> {
516 if encoded.len() != KEYRING_BYTES || &encoded[..8] != KEYRING_MAGIC {
517 return Err(error(ProtectedLocalKeyStoreErrorKind::Corrupt));
518 }
519 let checksum_offset = KEYRING_BYTES - 32;
520 let expected = Sha256::digest(&encoded[..checksum_offset]);
521 if expected.as_slice() != &encoded[checksum_offset..] {
522 return Err(error(ProtectedLocalKeyStoreErrorKind::Corrupt));
523 }
524 let mut vault_id = [0_u8; 32];
525 let mut generation_id = [0_u8; 32];
526 let mut key = [0_u8; 32];
527 vault_id.copy_from_slice(&encoded[8..40]);
528 generation_id.copy_from_slice(&encoded[40..72]);
529 key.copy_from_slice(&encoded[72..104]);
530 encoded.zeroize();
531 Ok(ProtectedLocalPackageKey {
532 context: LocalSharedStatePackageContext {
533 vault_id,
534 generation_id,
535 },
536 key: LocalSharedStatePackageKey::new(key),
537 })
538}
539
540fn marker_bytes(context: LocalSharedStatePackageContext) -> [u8; MARKER_BYTES] {
541 let mut bytes = [0_u8; MARKER_BYTES];
542 bytes[..8].copy_from_slice(MARKER_MAGIC);
543 bytes[8..40].copy_from_slice(&context.vault_id);
544 bytes[40..].copy_from_slice(&context.generation_id);
545 bytes
546}
547
548#[cfg(any(test, feature = "test-support"))]
550pub mod test_support {
551 use super::storage::canonical_location;
552 use super::*;
553
554 tokio::task_local! {
555 static SCOPED_BACKEND_LOADS: std::sync::atomic::AtomicU64;
556 }
557
558 pub struct BackendLoads(std::sync::atomic::AtomicU64);
559 impl BackendLoads {
560 pub fn fetch_add(&self, value: u64, ordering: std::sync::atomic::Ordering) -> u64 {
561 let previous = self.0.fetch_add(value, ordering);
562 let _ = SCOPED_BACKEND_LOADS.try_with(|loads| {
563 loads.fetch_add(value, ordering);
564 });
565 previous
566 }
567
568 pub fn load(&self, ordering: std::sync::atomic::Ordering) -> u64 {
569 self.0.load(ordering)
570 }
571
572 pub async fn measure<F: std::future::Future>(&self, future: F) -> (F::Output, u64) {
573 SCOPED_BACKEND_LOADS
574 .scope(std::sync::atomic::AtomicU64::new(0), async {
575 let output = future.await;
576 let loads = SCOPED_BACKEND_LOADS
577 .with(|count| count.load(std::sync::atomic::Ordering::Relaxed));
578 (output, loads)
579 })
580 .await
581 }
582 }
583
584 pub static BACKEND_LOADS: BackendLoads = BackendLoads(std::sync::atomic::AtomicU64::new(0));
586 pub async fn isolated_store(database: &Database, root: &Path) -> ProtectedLocalKeyStore {
588 let storage = FileProtectedStorage::new(root.to_path_buf());
589 account_store(database_account(&storage, database).await.unwrap(), root)
590 }
591
592 pub fn raw_account(path: &Path) -> String {
594 account_for(&canonical_location(path).unwrap(), "raw-file")
595 }
596
597 pub fn account_store(account: String, root: &Path) -> ProtectedLocalKeyStore {
599 let storage = Arc::new(FileProtectedStorage::new(root.to_path_buf()));
600 let mut store = ProtectedLocalKeyStore::with_account(account, storage);
601 store.directory = root.to_path_buf();
602 store
603 }
604
605 #[derive(Clone, Copy, Debug)]
607 pub enum Fault {
608 FailWrite,
610 Unavailable,
612 }
613
614 struct FaultyStorage {
615 files: FileProtectedStorage,
616 fault: Fault,
617 }
618
619 impl ProtectedStorage for FaultyStorage {
620 fn prepare(&self, namespace: &str) -> StoreResult<()> {
621 self.files.prepare(namespace)
622 }
623 fn lock(&self, namespace: &str) -> StoreResult<ProtectedStorageLock> {
624 self.files.lock(namespace)
625 }
626 fn load_secret(
627 &self,
628 _: &str,
629 _: &str,
630 _: usize,
631 ) -> StoreResult<Option<Zeroizing<Vec<u8>>>> {
632 match self.fault {
633 Fault::FailWrite => Ok(None),
634 Fault::Unavailable => Err(error(ProtectedLocalKeyStoreErrorKind::Unavailable)),
635 }
636 }
637 fn create_secret(&self, _: &str, _: &str, _: &[u8]) -> StoreResult<()> {
638 Err(error(match self.fault {
639 Fault::FailWrite => ProtectedLocalKeyStoreErrorKind::WriteFailed,
640 Fault::Unavailable => ProtectedLocalKeyStoreErrorKind::Unavailable,
641 }))
642 }
643 fn delete_secret(&self, _: &str, _: &str) -> StoreResult<()> {
644 self.create_secret("", "", &[])
645 }
646 fn read_record(
647 &self,
648 namespace: &str,
649 name: &str,
650 len: usize,
651 ) -> StoreResult<Option<Vec<u8>>> {
652 self.files.read_record(namespace, name, len)
653 }
654 fn create_record(&self, namespace: &str, name: &str, bytes: &[u8]) -> StoreResult<()> {
655 self.files.create_record(namespace, name, bytes)
656 }
657 fn remove_record(&self, namespace: &str, name: &str) -> StoreResult<()> {
658 self.files.remove_record(namespace, name)
659 }
660 fn list_secrets(&self, _: &str) -> StoreResult<HashSet<String>> {
661 match self.fault {
662 Fault::FailWrite => Ok(HashSet::new()),
663 Fault::Unavailable => Err(error(ProtectedLocalKeyStoreErrorKind::Unavailable)),
664 }
665 }
666 fn list_records(&self, namespace: &str) -> StoreResult<HashSet<String>> {
667 self.files.list_records(namespace)
668 }
669 }
670
671 pub fn faulty_store(account: String, root: &Path, fault: Fault) -> ProtectedLocalKeyStore {
674 let storage = Arc::new(FaultyStorage {
675 files: FileProtectedStorage::new(root.to_path_buf()),
676 fault,
677 });
678 let mut store = ProtectedLocalKeyStore::with_account(account, storage);
679 store.directory = root.to_path_buf();
680 store
681 }
682
683 impl ProtectedLocalKeyStore {
684 pub fn account(&self) -> &str {
685 &self.account
686 }
687
688 pub fn directory(&self) -> &Path {
690 &self.directory
691 }
692
693 pub fn file_path(&self, name: &str) -> PathBuf {
695 self.directory.join(format!("{}.{name}", self.account))
696 }
697
698 pub fn marker_path(&self) -> PathBuf {
699 self.file_path(AUTHORITY_RECORD)
700 }
701 }
702}
703
704#[cfg(test)]
705mod tests {
706 use super::test_support::*;
707 use super::*;
708 use crate::api::{CreateTask, Store};
709 use crate::choices::{TaskPriority, TaskStatus};
710 use std::fs::{self, File};
711 use std::io::Read;
712 use std::path::PathBuf;
713
714 pub(super) async fn captured_database(root: &Path) -> Database {
715 let path = root.join("source.sqlite");
716 let store = Store::open(&path).await.unwrap();
717 let workspace = store.list_workspaces().await.unwrap().remove(0);
718 store
719 .create_task(
720 &workspace.id,
721 CreateTask {
722 title: "protected package".into(),
723 description: String::new(),
724 project: "app".into(),
725 status: TaskStatus::Todo,
726 priority: TaskPriority::None,
727 metadata: vec![],
728 available_at: None,
729 due_on: None,
730 },
731 )
732 .await
733 .unwrap();
734 drop(store);
735 let database = Database::open(&path).await.unwrap();
736 database
737 .capture_local_shared_state_never_dispatched()
738 .await
739 .unwrap();
740 database
741 }
742
743 #[tokio::test]
744 async fn protected_store_reopens_and_reuses_the_package_key() {
745 let temp = tempfile::tempdir().unwrap();
746 let database = captured_database(temp.path()).await;
747 let key_root = temp.path().join("authority");
748 let store = isolated_store(&database, &key_root).await;
749 let first = store
750 .package_local_capture(&database, temp.path(), [0x64; 32])
751 .await
752 .unwrap();
753 drop(store);
754 drop(database);
755
756 let database = Database::open(&temp.path().join("source.sqlite"))
757 .await
758 .unwrap();
759 let reopened = isolated_store(&database, &key_root).await;
760 fs::remove_file(reopened.marker_path()).unwrap();
761 let retry = reopened
762 .package_local_capture(&database, temp.path(), [0x64; 32])
763 .await
764 .unwrap();
765 assert_eq!(first, retry);
766 assert!(reopened.marker_path().exists());
767 let key_path = reopened.file_path(KEYRING_ITEM);
768 let encoded = Zeroizing::new(fs::read(key_path).unwrap());
769 let raw_key = &encoded[72..104];
770 for path in [
771 database.path().to_path_buf(),
772 PathBuf::from(format!("{}-wal", database.path().display())),
773 ] {
774 match fs::read(path) {
775 Ok(bytes) => assert!(!bytes.windows(raw_key.len()).any(|window| window == raw_key)),
776 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
777 Err(error) => panic!("cannot inspect database bytes: {error}"),
778 }
779 }
780 let exported = serde_json::to_vec(
781 &database
782 .export_data("2026-09-22T00:00:00Z".into())
783 .await
784 .unwrap(),
785 )
786 .unwrap();
787 assert!(
788 !exported
789 .windows(raw_key.len())
790 .any(|window| window == raw_key)
791 );
792
793 let backup = temp.path().join("backup.tar.zst");
794 database
795 .create_backup_archive(&temp.path().join("blobs"), &backup)
796 .await
797 .unwrap();
798
799 let target_dir = tempfile::tempdir().unwrap();
800 let target = Database::open(&target_dir.path().join("target.sqlite"))
801 .await
802 .unwrap();
803 let protected = reopened.load_required().unwrap();
804 target
805 .decrypt_and_install_local_shared_state_package(&retry, protected.package_key())
806 .await
807 .unwrap();
808 let installed = target
809 .export_data("2026-09-22T00:00:00Z".into())
810 .await
811 .unwrap();
812 assert_eq!(installed.tables.tasks[0].title, "protected package");
813
814 database
815 .cancel_local_shared_state_never_dispatched(first.candidate_id())
816 .await
817 .unwrap();
818 database
819 .create_backup_archive(&temp.path().join("blobs"), &backup)
820 .await
821 .unwrap();
822 let decoder = zstd::Decoder::new(File::open(backup).unwrap()).unwrap();
823 let mut archive = tar::Archive::new(decoder);
824 for entry in archive.entries().unwrap() {
825 let mut entry = entry.unwrap();
826 let mut bytes = Vec::new();
827 entry.read_to_end(&mut bytes).unwrap();
828 assert!(!bytes.windows(raw_key.len()).any(|window| window == raw_key));
829 }
830 }
831
832 #[tokio::test]
833 async fn existing_package_never_regenerates_destroyed_authority() {
834 let temp = tempfile::tempdir().unwrap();
835 let database = captured_database(temp.path()).await;
836 let key_root = temp.path().join("authority");
837 let store = isolated_store(&database, &key_root).await;
838 store
839 .package_local_capture(&database, temp.path(), [0x64; 32])
840 .await
841 .unwrap();
842 let key_path = store.file_path(KEYRING_ITEM);
843 fs::remove_file(&key_path).unwrap();
844 fs::remove_file(store.marker_path()).unwrap();
845
846 let error = store
847 .package_local_capture(&database, temp.path(), [0x64; 32])
848 .await
849 .unwrap_err();
850 assert_eq!(
851 error
852 .downcast_ref::<ProtectedLocalKeyStoreError>()
853 .unwrap()
854 .kind(),
855 ProtectedLocalKeyStoreErrorKind::MissingAuthority
856 );
857 assert!(!key_path.exists());
858 assert!(!store.marker_path().exists());
859 }
860
861 #[tokio::test]
862 async fn package_store_rejects_a_different_database_before_storage_effects() {
863 let first_root = tempfile::tempdir().unwrap();
864 let first = Database::open(&first_root.path().join("first.sqlite"))
865 .await
866 .unwrap();
867 let second_root = tempfile::tempdir().unwrap();
868 let second = captured_database(second_root.path()).await;
869 let key_root = first_root.path().join("authority");
870 let store = isolated_store(&first, &key_root).await;
871
872 let error = store
873 .package_local_capture(&second, second_root.path(), [0x64; 32])
874 .await
875 .unwrap_err();
876 assert_eq!(
877 error
878 .downcast_ref::<ProtectedLocalKeyStoreError>()
879 .unwrap()
880 .kind(),
881 ProtectedLocalKeyStoreErrorKind::WrongDatabase
882 );
883 assert!(!key_root.exists());
884 }
885
886 fn remove_database(path: &Path) {
888 for suffix in ["", "-wal", "-shm"] {
889 let _ = fs::remove_file(format!("{}{suffix}", path.display()));
890 }
891 }
892
893 #[tokio::test]
894 async fn new_database_at_reused_path_ignores_stale_markers() {
895 let temp = tempfile::tempdir().unwrap();
896 let path = temp.path().join("db.sqlite");
897 let key_root = temp.path().join("keys");
898 let database = Database::open(&path).await.unwrap();
899 let old = isolated_store(&database, &key_root).await;
900 old.prepare_seed_claim(&database, [9; 32]).await.unwrap();
901 drop(database);
902 remove_database(&path);
903 fs::remove_file(key_root.join(format!("{}.keyring", old.account))).unwrap();
905 old.delete_secret("seed").unwrap();
906 assert_eq!(
907 old.load_or_create().unwrap_err().kind(),
908 ProtectedLocalKeyStoreErrorKind::MissingAuthority
909 );
910
911 let database = Database::open(&path).await.unwrap();
912 let store = isolated_store(&database, &key_root).await;
913 assert_ne!(store.account, old.account);
914 store.prepare_seed_claim(&database, [7; 32]).await.unwrap();
915 assert!(old.marker_path().exists());
916 }
917
918 #[tokio::test]
919 async fn new_database_at_reused_path_never_inherits_old_keys() {
920 let temp = tempfile::tempdir().unwrap();
921 let path = temp.path().join("db.sqlite");
922 let key_root = temp.path().join("keys");
923 let database = Database::open(&path).await.unwrap();
924 let old = isolated_store(&database, &key_root).await;
925 let old_seed = old.prepare_seed_claim(&database, [9; 32]).await.unwrap();
926 drop(database);
927 remove_database(&path);
928
929 let database = Database::open(&path).await.unwrap();
930 let store = isolated_store(&database, &key_root).await;
931 let seed = store.prepare_seed_claim(&database, [7; 32]).await.unwrap();
932 assert_ne!(seed.genesis().context(), old_seed.genesis().context());
933 assert_ne!(seed.genesis().commitment(), old_seed.genesis().commitment());
934 assert_eq!(
936 old.load_required().unwrap().context(),
937 old_seed.genesis().context()
938 );
939 }
940
941 #[tokio::test]
942 async fn rolled_back_copy_of_the_same_database_keeps_missing_authority_detection() {
943 let temp = tempfile::tempdir().unwrap();
944 let path = temp.path().join("db.sqlite");
945 let before_setup = temp.path().join("before-setup.sqlite");
946 let key_root = temp.path().join("keys");
947 let database = Database::open(&path).await.unwrap();
948 let mut conn = crate::test_support::acquire(&database).await.unwrap();
949 sqlx::query("VACUUM INTO ?")
950 .bind(before_setup.to_str().unwrap())
951 .execute(&mut *conn)
952 .await
953 .unwrap();
954 drop(conn);
955 let store = isolated_store(&database, &key_root).await;
956 store.prepare_seed_claim(&database, [9; 32]).await.unwrap();
957 drop(database);
958 remove_database(&path);
959 fs::copy(&before_setup, &path).unwrap();
960 fs::remove_file(key_root.join(format!("{}.keyring", store.account))).unwrap();
961
962 let database = Database::open(&path).await.unwrap();
963 let rolled_back = isolated_store(&database, &key_root).await;
964 assert_eq!(rolled_back.account, store.account);
965 let error = rolled_back
966 .prepare_seed_claim(&database, [9; 32])
967 .await
968 .unwrap_err();
969 assert_eq!(
970 error
971 .downcast_ref::<ProtectedLocalKeyStoreError>()
972 .unwrap()
973 .kind(),
974 ProtectedLocalKeyStoreErrorKind::MissingAuthority
975 );
976 }
977
978 #[test]
979 fn missing_and_corrupt_authority_never_regenerate() {
980 let temp = tempfile::tempdir().unwrap();
981 let database_path = temp.path().join("database.sqlite");
982 File::create(&database_path).unwrap();
983 let root = temp.path().join("authority");
984 let store = account_store(raw_account(&database_path), &root);
985 let original = store.load_or_create().unwrap().context();
986 let key_path = store.file_path(KEYRING_ITEM);
987 fs::remove_file(&key_path).unwrap();
988
989 let error = store.load_or_create().unwrap_err();
990 assert_eq!(
991 error.kind(),
992 ProtectedLocalKeyStoreErrorKind::MissingAuthority
993 );
994 assert!(!key_path.exists());
995
996 fs::remove_file(store.marker_path()).unwrap();
997 fs::write(&key_path, vec![0_u8; KEYRING_BYTES]).unwrap();
998 let mut permissions = fs::metadata(&key_path).unwrap().permissions();
999 use std::os::unix::fs::PermissionsExt;
1000 permissions.set_mode(0o600);
1001 fs::set_permissions(&key_path, permissions).unwrap();
1002 let error = store.load_or_create().unwrap_err();
1003 assert_eq!(error.kind(), ProtectedLocalKeyStoreErrorKind::Corrupt);
1004 assert_ne!(
1005 store.load_or_create().unwrap_err().kind(),
1006 ProtectedLocalKeyStoreErrorKind::WriteFailed
1007 );
1008 let _ = original;
1009 }
1010
1011 #[test]
1012 fn unavailable_and_failed_writes_do_not_create_authority() {
1013 let temp = tempfile::tempdir().unwrap();
1014 let database_path = temp.path().join("database.sqlite");
1015 File::create(&database_path).unwrap();
1016 for (fault, expected) in [
1017 (
1018 Fault::Unavailable,
1019 ProtectedLocalKeyStoreErrorKind::Unavailable,
1020 ),
1021 (
1022 Fault::FailWrite,
1023 ProtectedLocalKeyStoreErrorKind::WriteFailed,
1024 ),
1025 ] {
1026 let root = temp.path().join(format!("authority-{expected:?}"));
1027 let store = faulty_store(raw_account(&database_path), &root, fault);
1028 let error = store.load_or_create().unwrap_err();
1029 assert_eq!(error.kind(), expected);
1030 assert!(!store.marker_path().exists());
1031 }
1032 }
1033
1034 #[test]
1035 fn incomplete_marker_write_recovers_existing_key_without_replacement() {
1036 let temp = tempfile::tempdir().unwrap();
1037 let database_path = temp.path().join("database.sqlite");
1038 File::create(&database_path).unwrap();
1039 let root = temp.path().join("authority");
1040 let store = account_store(raw_account(&database_path), &root);
1041 store.prepare().unwrap();
1042 let key = generate_keyring().unwrap();
1043 let encoded = encode_keyring(&key);
1044 store.create_secret(KEYRING_ITEM, &encoded).unwrap();
1045
1046 let recovered = store.load_or_create().unwrap();
1047 assert_eq!(recovered.context(), key.context());
1048 assert!(store.marker_path().exists());
1049 }
1050
1051 #[test]
1052 fn unsafe_file_permissions_fail_closed_without_secret_diagnostics() {
1053 let temp = tempfile::tempdir().unwrap();
1054 let database_path = temp.path().join("database.sqlite");
1055 File::create(&database_path).unwrap();
1056 let root = temp.path().join("authority");
1057 let store = account_store(raw_account(&database_path), &root);
1058 let protected = store.load_or_create().unwrap();
1059 let key_path = store.file_path(KEYRING_ITEM);
1060 use std::os::unix::fs::PermissionsExt;
1061 fs::set_permissions(&key_path, fs::Permissions::from_mode(0o644)).unwrap();
1062
1063 let error = store.load_required().unwrap_err();
1064 assert_eq!(error.kind(), ProtectedLocalKeyStoreErrorKind::Corrupt);
1065 let diagnostic = format!("{error:?} {error}");
1066 assert!(!diagnostic.contains(&hex::encode(protected.context().vault_id)));
1067 assert!(!diagnostic.contains(database_path.to_string_lossy().as_ref()));
1068 }
1069}