Skip to main content

provider_core/
local_credential_store.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3//! Device-local credential files addressed only by opaque references.
4
5#[cfg(not(target_os = "linux"))]
6use rustix::fs::openat;
7use rustix::fs::{
8    AtFlags, Dir, FileType, FlockOperation, Mode, OFlags, fchmod, flock, fstat, ftruncate, mkdirat,
9    open, renameat, statat, unlinkat,
10};
11#[cfg(any(target_os = "linux", target_os = "macos"))]
12use rustix::fs::{RenameFlags, renameat_with};
13#[cfg(target_os = "linux")]
14use rustix::fs::{ResolveFlags, StatxFlags, openat2};
15use rustix::process::geteuid;
16use serde::{Deserialize, Serialize};
17use sha2::{Digest, Sha256};
18use std::fmt;
19use std::fs::File;
20use std::io::{Read, Write};
21use std::os::fd::AsFd;
22use std::path::{Component, Path, PathBuf};
23use std::sync::Arc;
24#[cfg(test)]
25use std::sync::atomic::{AtomicUsize, Ordering};
26#[cfg(any(target_os = "linux", target_os = "macos"))]
27use std::time::{Duration, Instant};
28use std::time::{SystemTime, UNIX_EPOCH};
29use zeroize::Zeroizing;
30
31#[cfg(test)]
32use crate::local_credential_types::MAX_LABEL_BYTES;
33pub use crate::local_credential_types::{
34    CredentialDependencyImpact, CredentialHealthState, CredentialLifecycleApproval,
35    CredentialLifecycleOperation, CredentialLifecycleOutcome, CredentialLifecyclePreview,
36    CredentialLifecycleRequestIdentity, CredentialLifecycleState, CredentialMetadata,
37    CredentialRemovalReconciliation, CredentialRotationReconciliation, CredentialStatus,
38    CredentialStoreError, REMOVE_CONFIRMATION, ROTATE_CONFIRMATION,
39};
40use crate::local_credential_types::{
41    MAX_CREDENTIALS, MAX_CREDENTIALS_PER_PROVIDER, MAX_METADATA_BYTES, MAX_PROVIDER_BYTES,
42    MAX_SECRET_BYTES, REFERENCE_PREFIX, SCHEMA_VERSION, normalize_account_label, valid_descriptor,
43    valid_id, valid_sha256_id,
44};
45
46const ENTRIES_DIRECTORY: &str = "entries";
47const LIFECYCLE_DIRECTORY: &str = "lifecycle";
48const LOCK_FILE: &str = ".store.lock";
49const SECRET_FILE: &str = "secret";
50const METADATA_FILE: &str = "metadata.json";
51const NEXT_SECRET_FILE: &str = ".next-secret";
52const NEXT_METADATA_FILE: &str = ".next-metadata.json";
53#[cfg(any(target_os = "linux", target_os = "macos"))]
54const MAX_ACCOUNT_RECORD_BYTES: usize = 2048;
55#[cfg(target_os = "linux")]
56const MAX_LINUX_DATA_HOME_BYTES: usize = 4096;
57const MAX_PREVIEW_LIFETIME_SECONDS: u64 = 10 * 60;
58#[cfg(any(target_os = "linux", target_os = "macos"))]
59const ACCOUNT_LOOKUP_TIMEOUT: Duration = Duration::from_secs(2);
60#[cfg(any(target_os = "linux", target_os = "macos"))]
61const ACCOUNT_LOOKUP_POLL_INTERVAL: Duration = Duration::from_millis(5);
62#[cfg(any(target_os = "linux", target_os = "macos"))]
63const ACCOUNT_LOOKUP_CLEANUP_DEADLINE: Duration = Duration::from_millis(250);
64const FLOCK_EINTR_RETRY_BOUND: usize = 256;
65const FLOCK_EINTR_RETRY_SLEEP: Duration = Duration::from_millis(1);
66
67#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
68#[serde(deny_unknown_fields)]
69struct CredentialLifecycleJournal {
70    schema_version: String,
71    operation: CredentialLifecycleOperation,
72    credential_id: String,
73    credential_ref: String,
74    expected_generation: u64,
75    target_generation: Option<u64>,
76    preview_digest: String,
77    expires_at_epoch_seconds: u64,
78    required_confirmation: String,
79    old_secret_identity: FileIdentity,
80    staged_secret_identity: Option<FileIdentity>,
81    state: CredentialLifecycleJournalState,
82}
83
84#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
85#[serde(rename_all = "snake_case")]
86enum CredentialLifecycleJournalState {
87    Prepared,
88    Committed,
89}
90
91impl CredentialLifecycleJournal {
92    fn validate(&self) -> Result<(), CredentialStoreError> {
93        if self.schema_version != SCHEMA_VERSION
94            || !valid_id(&self.credential_id)
95            || self.credential_ref != format!("{REFERENCE_PREFIX}{}", self.credential_id)
96            || self.expected_generation == 0
97            || !valid_sha256_id(&self.preview_digest)
98            || !valid_journal_secret_identity(self.old_secret_identity)
99            || self
100                .staged_secret_identity
101                .is_some_and(|identity| !valid_journal_secret_identity(identity))
102            || match self.operation {
103                CredentialLifecycleOperation::Rotate => {
104                    self.required_confirmation != ROTATE_CONFIRMATION
105                        || self.target_generation != self.expected_generation.checked_add(1)
106                        || self.staged_secret_identity.is_none()
107                        || self.staged_secret_identity == Some(self.old_secret_identity)
108                        || self.staged_secret_identity.is_some_and(|identity| {
109                            identity.device != self.old_secret_identity.device
110                        })
111                }
112                CredentialLifecycleOperation::Remove => {
113                    self.required_confirmation != REMOVE_CONFIRMATION
114                        || self.target_generation.is_some()
115                        || self.staged_secret_identity.is_some()
116                }
117            }
118        {
119            return Err(CredentialStoreError::CorruptState);
120        }
121        Ok(())
122    }
123}
124
125fn valid_journal_secret_identity(identity: FileIdentity) -> bool {
126    identity.device != 0
127        && identity.inode != 0
128        && identity.owner == geteuid().as_raw()
129        && identity.mode == 0o600
130        && identity.link_count == 1
131        && identity.birth_time.is_some()
132}
133
134#[derive(Clone)]
135pub struct LocalCredentialStore {
136    root_path: PathBuf,
137    root: Arc<File>,
138    root_identity: FileIdentity,
139    #[cfg(test)]
140    flock_eintr_injections: Arc<AtomicUsize>,
141}
142
143/// A stable non-secret metadata inventory held under the store's exclusive
144/// lifecycle lock. Keeping this value alive prevents credential add, rotate,
145/// or remove from racing a generation-bound configuration commit.
146pub struct CredentialInventoryGuard {
147    store: LocalCredentialStore,
148    _locked: LockedRoot,
149    entries: File,
150    credentials: Vec<CredentialMetadata>,
151}
152
153impl CredentialInventoryGuard {
154    pub fn credentials(&self) -> &[CredentialMetadata] {
155        &self.credentials
156    }
157
158    pub fn rotate_preview_with_bindings(
159        &self,
160        credential_ref: &str,
161        bound_phases: &[String],
162        bound_routes: &[String],
163        expires_at_epoch_seconds: u64,
164    ) -> Result<CredentialLifecyclePreview, CredentialStoreError> {
165        self.lifecycle_preview_with_bindings(
166            credential_ref,
167            CredentialLifecycleOperation::Rotate,
168            bound_phases,
169            bound_routes,
170            current_epoch_seconds()?,
171            expires_at_epoch_seconds,
172        )
173    }
174
175    pub fn remove_preview_with_bindings(
176        &self,
177        credential_ref: &str,
178        bound_phases: &[String],
179        bound_routes: &[String],
180        expires_at_epoch_seconds: u64,
181    ) -> Result<CredentialLifecyclePreview, CredentialStoreError> {
182        self.lifecycle_preview_with_bindings(
183            credential_ref,
184            CredentialLifecycleOperation::Remove,
185            bound_phases,
186            bound_routes,
187            current_epoch_seconds()?,
188            expires_at_epoch_seconds,
189        )
190    }
191
192    fn lifecycle_preview_with_bindings(
193        &self,
194        credential_ref: &str,
195        operation: CredentialLifecycleOperation,
196        bound_phases: &[String],
197        bound_routes: &[String],
198        now_epoch_seconds: u64,
199        expires_at_epoch_seconds: u64,
200    ) -> Result<CredentialLifecyclePreview, CredentialStoreError> {
201        let id = parse_reference(credential_ref)?;
202        let mut metadata = self
203            .credentials
204            .iter()
205            .find(|metadata| metadata.id == id && metadata.credential_ref == credential_ref)
206            .cloned()
207            .ok_or(CredentialStoreError::NotFound)?;
208        if metadata.lifecycle_state == CredentialLifecycleState::Removed {
209            return Err(CredentialStoreError::NotFound);
210        }
211        metadata.bound_phases = bound_phases.to_vec();
212        metadata.bound_routes = bound_routes.to_vec();
213        metadata.validate()?;
214        lifecycle_preview_for_metadata(
215            &metadata,
216            operation,
217            now_epoch_seconds,
218            expires_at_epoch_seconds,
219        )
220    }
221
222    fn metadata(&self, credential_ref: &str) -> Result<CredentialMetadata, CredentialStoreError> {
223        let id = parse_reference(credential_ref)?;
224        self.credentials
225            .iter()
226            .find(|metadata| metadata.id == id && metadata.credential_ref == credential_ref)
227            .cloned()
228            .ok_or(CredentialStoreError::NotFound)
229    }
230
231    pub fn reconcile_rotate(
232        &self,
233        credential_ref: &str,
234        request: CredentialLifecycleRequestIdentity<'_>,
235    ) -> Result<CredentialRotationReconciliation, CredentialStoreError> {
236        let id = parse_reference(credential_ref)?;
237        let lifecycle = open_or_create_private_directory(&self._locked.root, LIFECYCLE_DIRECTORY)?;
238        reconcile_rotate_request_locked(&self.entries, &lifecycle, id, credential_ref, request)
239    }
240
241    pub fn reconcile_remove(
242        &self,
243        credential_ref: &str,
244        request: CredentialLifecycleRequestIdentity<'_>,
245    ) -> Result<CredentialRemovalReconciliation, CredentialStoreError> {
246        let id = parse_reference(credential_ref)?;
247        let lifecycle = open_or_create_private_directory(&self._locked.root, LIFECYCLE_DIRECTORY)?;
248        reconcile_remove_request_locked(&self.entries, &lifecycle, id, credential_ref, request)
249    }
250
251    pub fn rotate_apply_with_bindings(
252        &self,
253        credential_ref: &str,
254        secret: &[u8],
255        approval: CredentialLifecycleApproval<'_>,
256        bound_phases: &[String],
257        bound_routes: &[String],
258    ) -> Result<CredentialLifecycleOutcome, CredentialStoreError> {
259        self.rotate_apply_with_bindings_at(
260            credential_ref,
261            secret,
262            approval,
263            bound_phases,
264            bound_routes,
265            current_epoch_seconds()?,
266        )
267    }
268
269    fn rotate_apply_with_bindings_at(
270        &self,
271        credential_ref: &str,
272        secret: &[u8],
273        approval: CredentialLifecycleApproval<'_>,
274        bound_phases: &[String],
275        bound_routes: &[String],
276        now_epoch_seconds: u64,
277    ) -> Result<CredentialLifecycleOutcome, CredentialStoreError> {
278        self.store.rotate_apply_locked(
279            &self._locked.root,
280            &self.entries,
281            credential_ref,
282            secret,
283            approval,
284            bound_phases,
285            bound_routes,
286            now_epoch_seconds,
287        )
288    }
289
290    pub fn remove_apply_with_bindings(
291        &self,
292        credential_ref: &str,
293        approval: CredentialLifecycleApproval<'_>,
294        bound_phases: &[String],
295        bound_routes: &[String],
296    ) -> Result<CredentialLifecycleOutcome, CredentialStoreError> {
297        self.remove_apply_with_bindings_at(
298            credential_ref,
299            approval,
300            bound_phases,
301            bound_routes,
302            current_epoch_seconds()?,
303        )
304    }
305
306    fn remove_apply_with_bindings_at(
307        &self,
308        credential_ref: &str,
309        approval: CredentialLifecycleApproval<'_>,
310        bound_phases: &[String],
311        bound_routes: &[String],
312        now_epoch_seconds: u64,
313    ) -> Result<CredentialLifecycleOutcome, CredentialStoreError> {
314        self.store.remove_apply_locked(
315            &self._locked.root,
316            &self.entries,
317            credential_ref,
318            approval,
319            bound_phases,
320            bound_routes,
321            now_epoch_seconds,
322        )
323    }
324
325    /// Verifies that an active reference still resolves to the same private,
326    /// non-empty local file without reading any credential bytes.
327    pub fn verify_available(&self, credential_ref: &str) -> Result<(), CredentialStoreError> {
328        let id = parse_reference(credential_ref)?;
329        let metadata = self
330            .credentials
331            .iter()
332            .find(|metadata| metadata.id == id && metadata.credential_ref == credential_ref)
333            .ok_or(CredentialStoreError::NotFound)?;
334        if metadata.lifecycle_state != CredentialLifecycleState::Active
335            || metadata.health != CredentialHealthState::LocallyVerified
336            || metadata.last_verified_at_epoch_seconds.is_none()
337        {
338            return Err(CredentialStoreError::NotFound);
339        }
340        let entry = open_private_directory(&self.entries, id)?;
341        let (_, size) = read_secret_or_tombstone_identity(&entry)?;
342        if size == 0 {
343            return Err(CredentialStoreError::NotFound);
344        }
345        Ok(())
346    }
347
348    /// Builds status from the same metadata snapshot and held lifecycle lock.
349    pub fn status(&self, credential_ref: &str) -> Result<CredentialStatus, CredentialStoreError> {
350        let id = parse_reference(credential_ref)?;
351        let metadata = self
352            .credentials
353            .iter()
354            .find(|metadata| metadata.id == id && metadata.credential_ref == credential_ref)
355            .cloned()
356            .ok_or(CredentialStoreError::NotFound)?;
357        let entry = open_private_directory(&self.entries, id)?;
358        let status = match metadata.lifecycle_state {
359            CredentialLifecycleState::Active => {
360                self.verify_available(credential_ref)?;
361                "available"
362            }
363            CredentialLifecycleState::Removed => {
364                let _ = read_tombstone_identity(&entry)?;
365                "removed"
366            }
367        };
368        Ok(CredentialStatus { metadata, status })
369    }
370}
371
372impl fmt::Debug for LocalCredentialStore {
373    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
374        formatter
375            .debug_struct("LocalCredentialStore")
376            .field("root", &"[DEVICE_LOCAL]")
377            .finish()
378    }
379}
380
381impl LocalCredentialStore {
382    /// Returns the fixed device-setup root without touching the filesystem.
383    /// The path is suitable for internal use only and must never be emitted.
384    pub fn production_setup_root_path() -> Result<PathBuf, CredentialStoreError> {
385        #[cfg(target_os = "macos")]
386        {
387            let home = current_user_home_from_account_record()?;
388            validate_absolute_path(&home)?;
389            Ok(home
390                .join("Library")
391                .join("Application Support")
392                .join("SteadyMux")
393                .join("device-setup-v1"))
394        }
395        #[cfg(not(target_os = "macos"))]
396        {
397            Err(CredentialStoreError::UnsupportedPlatform)
398        }
399    }
400
401    /// Creates the fixed owner-only device-setup root and returns its path.
402    pub fn ensure_production_setup_root() -> Result<PathBuf, CredentialStoreError> {
403        #[cfg(target_os = "macos")]
404        {
405            let home = current_user_home_from_account_record()?;
406            validate_absolute_path(&home)?;
407            let home_directory = open_path_directory(&home)?;
408            validate_owned_directory(
409                &fstat(&home_directory).map_err(|_| CredentialStoreError::UnsafeStorage)?,
410            )?;
411            let library = open_owned_directory(&home_directory, "Library")?;
412            let application_support = open_owned_directory(&library, "Application Support")?;
413            let product = open_or_create_private_directory(&application_support, "SteadyMux")?;
414            let _setup = open_or_create_private_directory(&product, "device-setup-v1")?;
415            Ok(home
416                .join("Library")
417                .join("Application Support")
418                .join("SteadyMux")
419                .join("device-setup-v1"))
420        }
421        #[cfg(not(target_os = "macos"))]
422        {
423            Err(CredentialStoreError::UnsupportedPlatform)
424        }
425    }
426
427    /// Opens the fixed, non-synchronizing credential root for this OS account.
428    pub fn production() -> Result<Self, CredentialStoreError> {
429        #[cfg(target_os = "macos")]
430        {
431            let home = current_user_home_from_account_record()?;
432            let (root_path, root) = ensure_production_store(&home)?;
433            Self::open_verified(root_path, root)
434        }
435        #[cfg(target_os = "linux")]
436        {
437            let home = current_user_home_from_account_record()?;
438            let xdg_data_home = std::env::var_os("XDG_DATA_HOME");
439            let (root_path, root) = ensure_linux_production_store(&home, xdg_data_home.as_deref())?;
440            Self::open_verified(root_path, root)
441        }
442        #[cfg(not(any(target_os = "linux", target_os = "macos")))]
443        {
444            Err(CredentialStoreError::UnsupportedPlatform)
445        }
446    }
447
448    /// Opens an already initialized fixed store without creating any filesystem object.
449    pub fn production_existing() -> Result<Self, CredentialStoreError> {
450        #[cfg(target_os = "macos")]
451        {
452            let home = current_user_home_from_account_record()?;
453            let (root_path, root) = open_production_store(&home)?;
454            Self::open_verified_existing(root_path, root)
455        }
456        #[cfg(target_os = "linux")]
457        {
458            let home = current_user_home_from_account_record()?;
459            let xdg_data_home = std::env::var_os("XDG_DATA_HOME");
460            let (root_path, root) = open_linux_production_store(&home, xdg_data_home.as_deref())?;
461            Self::open_verified_existing(root_path, root)
462        }
463        #[cfg(not(any(target_os = "linux", target_os = "macos")))]
464        {
465            Err(CredentialStoreError::UnsupportedPlatform)
466        }
467    }
468
469    #[cfg(test)]
470    fn at_test_root(root: &Path) -> Result<Self, CredentialStoreError> {
471        let temporary_root = std::env::temp_dir();
472        let relative = root
473            .strip_prefix(&temporary_root)
474            .map_err(|_| CredentialStoreError::UnsafeStorage)?;
475        let normalized_root = temporary_root
476            .canonicalize()
477            .map_err(|_| CredentialStoreError::UnsafeStorage)?
478            .join(relative);
479        let boundary_path = temporary_root
480            .canonicalize()
481            .map_err(|_| CredentialStoreError::UnsafeStorage)?;
482        let boundary = open_path_directory(&boundary_path)?;
483        let relative = normalized_root
484            .strip_prefix(&boundary_path)
485            .map_err(|_| CredentialStoreError::UnsafeStorage)?;
486        let root = ensure_private_components(&boundary, relative)?;
487        Self::open_verified(normalized_root, root)
488    }
489
490    #[cfg(test)]
491    fn at_existing_test_root(root: &Path) -> Result<Self, CredentialStoreError> {
492        let temporary_root = std::env::temp_dir();
493        let relative = root
494            .strip_prefix(&temporary_root)
495            .map_err(|_| CredentialStoreError::UnsafeStorage)?;
496        let normalized_root = temporary_root
497            .canonicalize()
498            .map_err(|_| CredentialStoreError::UnsafeStorage)?
499            .join(relative);
500        let root = open_path_directory(&normalized_root)?;
501        Self::open_verified_existing(normalized_root, root)
502    }
503
504    fn open_verified(root_path: PathBuf, root: File) -> Result<Self, CredentialStoreError> {
505        validate_absolute_path(&root_path)?;
506        let root_stat = fstat(&root).map_err(|_| CredentialStoreError::UnsafeStorage)?;
507        validate_directory(&root_stat)?;
508        let store = Self {
509            root_path,
510            root: Arc::new(root),
511            root_identity: identity(&root_stat)?,
512            #[cfg(test)]
513            flock_eintr_injections: Arc::new(AtomicUsize::new(0)),
514        };
515        let locked = store.lock()?;
516        let _ = open_or_create_private_directory(&locked.root, ENTRIES_DIRECTORY)?;
517        Ok(store)
518    }
519
520    fn open_verified_existing(
521        root_path: PathBuf,
522        root: File,
523    ) -> Result<Self, CredentialStoreError> {
524        validate_absolute_path(&root_path)?;
525        let root_stat = fstat(&root).map_err(|_| CredentialStoreError::UnsafeStorage)?;
526        validate_directory(&root_stat)?;
527        let store = Self {
528            root_path,
529            root: Arc::new(root),
530            root_identity: identity(&root_stat)?,
531            #[cfg(test)]
532            flock_eintr_injections: Arc::new(AtomicUsize::new(0)),
533        };
534        let entries = open_private_directory(&store.root, ENTRIES_DIRECTORY)?;
535        drop(entries);
536        Ok(store)
537    }
538
539    pub fn add(
540        &self,
541        provider: &str,
542        label: &str,
543        secret: &[u8],
544    ) -> Result<CredentialMetadata, CredentialStoreError> {
545        let now_epoch_seconds = current_epoch_seconds()?;
546        self.add_at(provider, label, secret, now_epoch_seconds)
547    }
548
549    fn add_at(
550        &self,
551        provider: &str,
552        label: &str,
553        secret: &[u8],
554        now_epoch_seconds: u64,
555    ) -> Result<CredentialMetadata, CredentialStoreError> {
556        let label = normalize_account_label(label)?;
557        if !valid_descriptor(provider, MAX_PROVIDER_BYTES) || !valid_secret(secret) {
558            return Err(CredentialStoreError::InvalidInput);
559        }
560        let locked = self.lock()?;
561        let entries = open_or_create_private_directory(&locked.root, ENTRIES_DIRECTORY)?;
562        recover_add_staging(&entries)?;
563        ensure_store_needs_no_lifecycle_recovery(&locked.root, &entries)?;
564        let existing = self.list_locked(&entries)?;
565        if existing.len() >= MAX_CREDENTIALS
566            || existing
567                .iter()
568                .filter(|metadata| metadata.provider == provider)
569                .count()
570                >= MAX_CREDENTIALS_PER_PROVIDER
571        {
572            return Err(CredentialStoreError::CapacityExhausted);
573        }
574        if existing
575            .iter()
576            .any(|metadata| metadata.provider == provider && metadata.label == label)
577        {
578            return Err(CredentialStoreError::InvalidInput);
579        }
580        let id = generate_unused_id(&entries)?;
581        let metadata = CredentialMetadata {
582            schema_version: SCHEMA_VERSION.to_owned(),
583            credential_ref: format!("{REFERENCE_PREFIX}{id}"),
584            id: id.clone(),
585            provider: provider.to_owned(),
586            label,
587            generation: 1,
588            created_at_epoch_seconds: now_epoch_seconds,
589            rotated_at_epoch_seconds: None,
590            previous_generation: None,
591            last_lifecycle_preview_digest: None,
592            lifecycle_state: CredentialLifecycleState::Active,
593            health: CredentialHealthState::LocallyVerified,
594            last_verified_at_epoch_seconds: Some(now_epoch_seconds),
595            bound_phases: Vec::new(),
596            bound_routes: Vec::new(),
597        };
598        let metadata_bytes =
599            serde_json::to_vec(&metadata).map_err(|_| CredentialStoreError::CommitAmbiguous)?;
600        let staging = format!(".staging-{id}");
601        mkdirat(&entries, &staging, Mode::from_raw_mode(0o700))
602            .map_err(|_| CredentialStoreError::UnsafeStorage)?;
603        let staged = open_private_directory(&entries, &staging)?;
604        let mut committed = false;
605        let result = (|| {
606            write_new_file(&staged, SECRET_FILE, secret)?;
607            write_new_file(&staged, METADATA_FILE, &metadata_bytes)?;
608            durable_sync(&staged)?;
609            rename_new_entry(&entries, &staging, &id)?;
610            committed = true;
611            durable_sync(&entries)?;
612            let committed = self.read_metadata(&entries, &id)?;
613            if committed != metadata {
614                return Err(CredentialStoreError::CommitAmbiguous);
615            }
616            Ok(metadata.clone())
617        })();
618        if result.is_err() {
619            cleanup_failed_add(&entries, &staged, &staging, committed);
620        }
621        result
622    }
623
624    pub fn rotate_preview(
625        &self,
626        credential_ref: &str,
627        expires_at_epoch_seconds: u64,
628    ) -> Result<CredentialLifecyclePreview, CredentialStoreError> {
629        self.rotate_preview_at(
630            credential_ref,
631            current_epoch_seconds()?,
632            expires_at_epoch_seconds,
633        )
634    }
635
636    fn rotate_preview_at(
637        &self,
638        credential_ref: &str,
639        now_epoch_seconds: u64,
640        expires_at_epoch_seconds: u64,
641    ) -> Result<CredentialLifecyclePreview, CredentialStoreError> {
642        self.lifecycle_preview(
643            credential_ref,
644            CredentialLifecycleOperation::Rotate,
645            now_epoch_seconds,
646            expires_at_epoch_seconds,
647        )
648    }
649
650    pub fn remove_preview(
651        &self,
652        credential_ref: &str,
653        expires_at_epoch_seconds: u64,
654    ) -> Result<CredentialLifecyclePreview, CredentialStoreError> {
655        self.remove_preview_at(
656            credential_ref,
657            current_epoch_seconds()?,
658            expires_at_epoch_seconds,
659        )
660    }
661
662    fn remove_preview_at(
663        &self,
664        credential_ref: &str,
665        now_epoch_seconds: u64,
666        expires_at_epoch_seconds: u64,
667    ) -> Result<CredentialLifecyclePreview, CredentialStoreError> {
668        self.lifecycle_preview(
669            credential_ref,
670            CredentialLifecycleOperation::Remove,
671            now_epoch_seconds,
672            expires_at_epoch_seconds,
673        )
674    }
675
676    /// Reconciles an already-journaled rotation without reading a new secret
677    /// or requiring a second confirmation. The request identity is matched to
678    /// the durable journal, but its user-facing preview expiry is intentionally
679    /// not re-evaluated: authorization was validated before the journal became
680    /// durable.
681    pub fn reconcile_rotate(
682        &self,
683        credential_ref: &str,
684        request: CredentialLifecycleRequestIdentity<'_>,
685    ) -> Result<CredentialRotationReconciliation, CredentialStoreError> {
686        let id = parse_reference(credential_ref)?;
687        let locked = self.lock()?;
688        let entries = open_or_create_private_directory(&locked.root, ENTRIES_DIRECTORY)?;
689        self.ensure_normalized_label_inventory(&entries)?;
690        recover_add_staging(&entries)?;
691        let lifecycle = open_or_create_private_directory(&locked.root, LIFECYCLE_DIRECTORY)?;
692        reconcile_rotate_request_locked(&entries, &lifecycle, id, credential_ref, request)
693    }
694
695    /// Reconciles an already-authorized removal from its durable journal.
696    /// Matching recovery is independent of the expired preview clock because
697    /// the original confirmation and complete request identity are journaled
698    /// before the descriptor-bound erasure boundary.
699    pub fn reconcile_remove(
700        &self,
701        credential_ref: &str,
702        request: CredentialLifecycleRequestIdentity<'_>,
703    ) -> Result<CredentialRemovalReconciliation, CredentialStoreError> {
704        let id = parse_reference(credential_ref)?;
705        let locked = self.lock()?;
706        let entries = open_or_create_private_directory(&locked.root, ENTRIES_DIRECTORY)?;
707        self.ensure_normalized_label_inventory(&entries)?;
708        recover_add_staging(&entries)?;
709        let lifecycle = open_or_create_private_directory(&locked.root, LIFECYCLE_DIRECTORY)?;
710        reconcile_remove_request_locked(&entries, &lifecycle, id, credential_ref, request)
711    }
712
713    pub fn rotate_apply(
714        &self,
715        credential_ref: &str,
716        secret: &[u8],
717        approval: CredentialLifecycleApproval<'_>,
718    ) -> Result<CredentialLifecycleOutcome, CredentialStoreError> {
719        self.rotate_apply_at(credential_ref, secret, approval, current_epoch_seconds()?)
720    }
721
722    fn rotate_apply_at(
723        &self,
724        credential_ref: &str,
725        secret: &[u8],
726        approval: CredentialLifecycleApproval<'_>,
727        now_epoch_seconds: u64,
728    ) -> Result<CredentialLifecycleOutcome, CredentialStoreError> {
729        let guard = self.lock_lifecycle_inventory()?;
730        let metadata = guard.metadata(credential_ref)?;
731        guard.rotate_apply_with_bindings_at(
732            credential_ref,
733            secret,
734            approval,
735            &metadata.bound_phases,
736            &metadata.bound_routes,
737            now_epoch_seconds,
738        )
739    }
740
741    #[allow(clippy::too_many_arguments)]
742    fn rotate_apply_locked(
743        &self,
744        root: &File,
745        entries: &File,
746        credential_ref: &str,
747        secret: &[u8],
748        approval: CredentialLifecycleApproval<'_>,
749        bound_phases: &[String],
750        bound_routes: &[String],
751        now_epoch_seconds: u64,
752    ) -> Result<CredentialLifecycleOutcome, CredentialStoreError> {
753        let id = parse_reference(credential_ref)?;
754        let lifecycle = open_or_create_private_directory(root, LIFECYCLE_DIRECTORY)?;
755        match reconcile_rotate_request_locked(
756            entries,
757            &lifecycle,
758            id,
759            credential_ref,
760            CredentialLifecycleRequestIdentity {
761                expected_preview: approval.expected_preview,
762                expected_generation: approval.expected_generation,
763                expires_at_epoch_seconds: approval.expires_at_epoch_seconds,
764            },
765        )? {
766            CredentialRotationReconciliation::Committed(outcome) => return Ok(outcome),
767            CredentialRotationReconciliation::RolledBackFreshPreviewRequired => {
768                return Err(CredentialStoreError::FreshPreviewRequired);
769            }
770            CredentialRotationReconciliation::NotRequired => {}
771        }
772        if !valid_secret(secret) {
773            return Err(CredentialStoreError::InvalidInput);
774        }
775        if approval.confirmation != ROTATE_CONFIRMATION {
776            return Err(CredentialStoreError::ConfirmationRequired);
777        }
778        let metadata = self.read_unique_metadata(entries, id)?;
779        if metadata.generation != approval.expected_generation {
780            return Err(CredentialStoreError::GenerationConflict);
781        }
782        let (old_secret, old_secret_identity) = read_secret_with_identity(entries, id)?;
783        drop(old_secret);
784        let mut preview_metadata = metadata.clone();
785        preview_metadata.bound_phases = bound_phases.to_vec();
786        preview_metadata.bound_routes = bound_routes.to_vec();
787        preview_metadata.validate()?;
788        let preview = lifecycle_preview_for_metadata(
789            &preview_metadata,
790            CredentialLifecycleOperation::Rotate,
791            now_epoch_seconds,
792            approval.expires_at_epoch_seconds,
793        )?;
794        validate_expected_preview(&preview, approval.expected_preview, now_epoch_seconds)?;
795
796        let next_generation = metadata
797            .generation
798            .checked_add(1)
799            .ok_or(CredentialStoreError::GenerationConflict)?;
800        let mut replacement = metadata.clone();
801        replacement.previous_generation = Some(metadata.generation);
802        replacement.last_lifecycle_preview_digest = Some(preview.preview_digest.clone());
803        replacement.generation = next_generation;
804        replacement.rotated_at_epoch_seconds = Some(now_epoch_seconds);
805        replacement.last_verified_at_epoch_seconds = Some(now_epoch_seconds);
806        replacement.lifecycle_state = CredentialLifecycleState::Active;
807        replacement.health = CredentialHealthState::LocallyVerified;
808        replacement.validate()?;
809        let metadata_bytes =
810            serde_json::to_vec(&replacement).map_err(|_| CredentialStoreError::CommitAmbiguous)?;
811        if metadata_bytes.len() > MAX_METADATA_BYTES {
812            return Err(CredentialStoreError::InvalidInput);
813        }
814        let entry = open_private_directory(entries, id)?;
815        let precommit = (|| -> Result<FileIdentity, CredentialStoreError> {
816            write_new_file(&entry, NEXT_SECRET_FILE, secret)?;
817            write_new_file(&entry, NEXT_METADATA_FILE, &metadata_bytes)?;
818            durable_sync(&entry)?;
819            verify_staged_files(&entry, &replacement, secret)
820        })();
821        let staged_secret_identity = match precommit {
822            Ok(identity) => identity,
823            Err(error) => {
824                cleanup_precommit_files(&entry);
825                return Err(error);
826            }
827        };
828        let journal = CredentialLifecycleJournal {
829            schema_version: SCHEMA_VERSION.to_owned(),
830            operation: CredentialLifecycleOperation::Rotate,
831            credential_id: id.to_owned(),
832            credential_ref: credential_ref.to_owned(),
833            expected_generation: metadata.generation,
834            target_generation: Some(next_generation),
835            preview_digest: preview.preview_digest.clone(),
836            expires_at_epoch_seconds: preview.expires_at_epoch_seconds,
837            required_confirmation: ROTATE_CONFIRMATION.to_owned(),
838            old_secret_identity,
839            staged_secret_identity: Some(staged_secret_identity),
840            state: CredentialLifecycleJournalState::Prepared,
841        };
842        if let Err(error) = write_journal_new(&lifecycle, &journal) {
843            cleanup_precommit_files(&entry);
844            return Err(error);
845        }
846        let (live_secret, verified_old_identity) = read_secret_with_identity(entries, id)
847            .map_err(|_| CredentialStoreError::RecoveryRequired)?;
848        drop(live_secret);
849        let (staged_secret, verified_staged_identity) =
850            read_regular_file_with_identity(&entry, NEXT_SECRET_FILE, MAX_SECRET_BYTES, true)
851                .map_err(|_| CredentialStoreError::RecoveryRequired)?;
852        drop(staged_secret);
853        if verified_old_identity != journal.old_secret_identity
854            || Some(verified_staged_identity) != journal.staged_secret_identity
855        {
856            return Err(CredentialStoreError::RecoveryRequired);
857        }
858        if lifecycle_failpoint("rotate_before_secret_replace") {
859            return Err(CredentialStoreError::CommitAmbiguous);
860        }
861
862        // Irreversible commit: replacing `secret` atomically unlinks the old
863        // inode from the store. Exact live identity is revalidated before a
864        // committed outcome can be reported.
865        rename_replace(&entry, NEXT_SECRET_FILE, SECRET_FILE)?;
866        let (committed_secret, committed_identity) = read_secret_with_identity(entries, id)
867            .map_err(|_| CredentialStoreError::RecoveryRequired)?;
868        drop(committed_secret);
869        if Some(committed_identity) != journal.staged_secret_identity {
870            return Err(CredentialStoreError::RecoveryRequired);
871        }
872        if lifecycle_failpoint("rotate_after_secret_replace") {
873            return Ok(rotation_outcome(
874                credential_ref,
875                metadata.generation,
876                next_generation,
877                true,
878            ));
879        }
880        let mut reconciliation_required = false;
881        post_commit_step(
882            "rotate_secret_sync",
883            || durable_sync(&entry),
884            &mut reconciliation_required,
885        );
886        post_commit_step(
887            "rotate_metadata_replace",
888            || rename_replace(&entry, NEXT_METADATA_FILE, METADATA_FILE),
889            &mut reconciliation_required,
890        );
891        post_commit_step(
892            "rotate_metadata_sync",
893            || durable_sync(&entry),
894            &mut reconciliation_required,
895        );
896        let committed_journal = CredentialLifecycleJournal {
897            state: CredentialLifecycleJournalState::Committed,
898            ..journal
899        };
900        post_commit_step(
901            "rotate_journal_commit",
902            || replace_journal(&lifecycle, &committed_journal),
903            &mut reconciliation_required,
904        );
905        Ok(rotation_outcome(
906            credential_ref,
907            metadata.generation,
908            next_generation,
909            reconciliation_required,
910        ))
911    }
912
913    pub fn remove_apply(
914        &self,
915        credential_ref: &str,
916        approval: CredentialLifecycleApproval<'_>,
917    ) -> Result<CredentialLifecycleOutcome, CredentialStoreError> {
918        self.remove_apply_at(credential_ref, approval, current_epoch_seconds()?)
919    }
920
921    fn remove_apply_at(
922        &self,
923        credential_ref: &str,
924        approval: CredentialLifecycleApproval<'_>,
925        now_epoch_seconds: u64,
926    ) -> Result<CredentialLifecycleOutcome, CredentialStoreError> {
927        let guard = self.lock_lifecycle_inventory()?;
928        let metadata = guard.metadata(credential_ref)?;
929        guard.remove_apply_with_bindings_at(
930            credential_ref,
931            approval,
932            &metadata.bound_phases,
933            &metadata.bound_routes,
934            now_epoch_seconds,
935        )
936    }
937
938    #[allow(clippy::too_many_arguments)]
939    fn remove_apply_locked(
940        &self,
941        root: &File,
942        entries: &File,
943        credential_ref: &str,
944        approval: CredentialLifecycleApproval<'_>,
945        bound_phases: &[String],
946        bound_routes: &[String],
947        now_epoch_seconds: u64,
948    ) -> Result<CredentialLifecycleOutcome, CredentialStoreError> {
949        let id = parse_reference(credential_ref)?;
950        let lifecycle = open_or_create_private_directory(root, LIFECYCLE_DIRECTORY)?;
951        match reconcile_remove_request_locked(
952            entries,
953            &lifecycle,
954            id,
955            credential_ref,
956            CredentialLifecycleRequestIdentity {
957                expected_preview: approval.expected_preview,
958                expected_generation: approval.expected_generation,
959                expires_at_epoch_seconds: approval.expires_at_epoch_seconds,
960            },
961        )? {
962            CredentialRemovalReconciliation::Committed(outcome) => return Ok(outcome),
963            CredentialRemovalReconciliation::RolledBackFreshPreviewRequired => {
964                return Err(CredentialStoreError::FreshPreviewRequired);
965            }
966            CredentialRemovalReconciliation::NotRequired => {}
967        }
968        if approval.confirmation != REMOVE_CONFIRMATION {
969            return Err(CredentialStoreError::ConfirmationRequired);
970        }
971        let metadata = self.read_unique_metadata(entries, id)?;
972        if metadata.generation != approval.expected_generation {
973            return Err(CredentialStoreError::GenerationConflict);
974        }
975        if metadata.lifecycle_state == CredentialLifecycleState::Removed {
976            return Err(CredentialStoreError::NotFound);
977        }
978        let mut preview_metadata = metadata.clone();
979        preview_metadata.bound_phases = bound_phases.to_vec();
980        preview_metadata.bound_routes = bound_routes.to_vec();
981        preview_metadata.validate()?;
982        let preview = lifecycle_preview_for_metadata(
983            &preview_metadata,
984            CredentialLifecycleOperation::Remove,
985            now_epoch_seconds,
986            approval.expires_at_epoch_seconds,
987        )?;
988        validate_expected_preview(&preview, approval.expected_preview, now_epoch_seconds)?;
989        if !preview.dependency_impact.apply_allowed {
990            return Err(CredentialStoreError::DependencyConflict);
991        }
992        let entry = open_private_directory(entries, id)?;
993        let (secret_file, old_secret_identity) = open_secret_for_erasure(&entry)?;
994        let journal = CredentialLifecycleJournal {
995            schema_version: SCHEMA_VERSION.to_owned(),
996            operation: CredentialLifecycleOperation::Remove,
997            credential_id: id.to_owned(),
998            credential_ref: credential_ref.to_owned(),
999            expected_generation: metadata.generation,
1000            target_generation: None,
1001            preview_digest: preview.preview_digest,
1002            expires_at_epoch_seconds: preview.expires_at_epoch_seconds,
1003            required_confirmation: REMOVE_CONFIRMATION.to_owned(),
1004            old_secret_identity,
1005            staged_secret_identity: None,
1006            state: CredentialLifecycleJournalState::Prepared,
1007        };
1008        write_journal_new(&lifecycle, &journal)?;
1009        if lifecycle_failpoint("remove_before_secret_truncate") {
1010            return Err(CredentialStoreError::CommitAmbiguous);
1011        }
1012
1013        #[cfg(test)]
1014        if lifecycle_failpoint("remove_substitute_path_after_fd_validation") {
1015            inject_remove_secret_substitution_for_test(&entry)?;
1016        }
1017
1018        let parent_stat = fstat(&entry).map_err(|_| CredentialStoreError::RecoveryRequired)?;
1019        let before_truncate =
1020            fstat(&secret_file).map_err(|_| CredentialStoreError::RecoveryRequired)?;
1021        let before_size = usize::try_from(before_truncate.st_size)
1022            .map_err(|_| CredentialStoreError::RecoveryRequired)?;
1023        validate_regular(&before_truncate, device(&parent_stat)?, Some(before_size))
1024            .map_err(|_| CredentialStoreError::RecoveryRequired)?;
1025        let verified_old_identity = identity_with_birth_fd(&before_truncate, &secret_file)
1026            .map_err(|_| CredentialStoreError::RecoveryRequired)?;
1027        if verified_old_identity != journal.old_secret_identity {
1028            return Err(CredentialStoreError::RecoveryRequired);
1029        }
1030
1031        // Irreversible commit is descriptor-bound. The pathname is never
1032        // unlinked by the removal transaction, so a same-user path swap cannot
1033        // cause SteadyMux to erase the substituted file.
1034        ftruncate(&secret_file, 0).map_err(|_| CredentialStoreError::CommitAmbiguous)?;
1035        if lifecycle_failpoint("remove_after_secret_truncate")
1036            || lifecycle_failpoint("remove_before_secret_sync")
1037        {
1038            return Err(CredentialStoreError::RecoveryRequired);
1039        }
1040        if lifecycle_failpoint("remove_secret_sync_failure") || durable_sync(&secret_file).is_err()
1041        {
1042            return Err(CredentialStoreError::RecoveryRequired);
1043        }
1044        if lifecycle_failpoint("remove_after_secret_sync") {
1045            return Err(CredentialStoreError::RecoveryRequired);
1046        }
1047        let after_truncate =
1048            fstat(&secret_file).map_err(|_| CredentialStoreError::RecoveryRequired)?;
1049        validate_regular(&after_truncate, device(&parent_stat)?, Some(0))
1050            .map_err(|_| CredentialStoreError::RecoveryRequired)?;
1051        if identity_with_birth_fd(&after_truncate, &secret_file)
1052            .map_err(|_| CredentialStoreError::RecoveryRequired)?
1053            != journal.old_secret_identity
1054        {
1055            return Err(CredentialStoreError::RecoveryRequired);
1056        }
1057
1058        let mut reconciliation_required = match reconcile_removal(entries, &lifecycle, &journal)? {
1059            RemovalTransactionState::RolledBack => {
1060                return Err(CredentialStoreError::RecoveryRequired);
1061            }
1062            RemovalTransactionState::Committed {
1063                reconciliation_required,
1064            } => reconciliation_required,
1065        };
1066        let committed_journal = CredentialLifecycleJournal {
1067            state: CredentialLifecycleJournalState::Committed,
1068            ..journal
1069        };
1070        post_commit_step(
1071            "remove_journal_commit",
1072            || replace_journal(&lifecycle, &committed_journal),
1073            &mut reconciliation_required,
1074        );
1075        Ok(removal_outcome(
1076            credential_ref,
1077            metadata.generation,
1078            reconciliation_required,
1079        ))
1080    }
1081
1082    fn lifecycle_preview(
1083        &self,
1084        credential_ref: &str,
1085        operation: CredentialLifecycleOperation,
1086        now_epoch_seconds: u64,
1087        expires_at_epoch_seconds: u64,
1088    ) -> Result<CredentialLifecyclePreview, CredentialStoreError> {
1089        let id = parse_reference(credential_ref)?;
1090        let locked = self.lock()?;
1091        let entries = open_private_directory(&locked.root, ENTRIES_DIRECTORY)?;
1092        ensure_preview_needs_no_recovery(&locked.root, &entries, id)?;
1093        let metadata = self.read_unique_metadata(&entries, id)?;
1094        if metadata.lifecycle_state == CredentialLifecycleState::Removed {
1095            return Err(CredentialStoreError::NotFound);
1096        }
1097        lifecycle_preview_for_metadata(
1098            &metadata,
1099            operation,
1100            now_epoch_seconds,
1101            expires_at_epoch_seconds,
1102        )
1103    }
1104
1105    pub fn list(&self) -> Result<Vec<CredentialMetadata>, CredentialStoreError> {
1106        let locked = self.lock()?;
1107        let entries = open_private_directory(&locked.root, ENTRIES_DIRECTORY)?;
1108        ensure_store_needs_no_lifecycle_recovery(&locked.root, &entries)?;
1109        self.list_locked(&entries)
1110    }
1111
1112    pub fn lock_inventory(&self) -> Result<CredentialInventoryGuard, CredentialStoreError> {
1113        let locked = self.lock()?;
1114        let entries = open_private_directory(&locked.root, ENTRIES_DIRECTORY)?;
1115        ensure_store_needs_no_lifecycle_recovery(&locked.root, &entries)?;
1116        let credentials = self.list_locked(&entries)?;
1117        Ok(CredentialInventoryGuard {
1118            store: self.clone(),
1119            _locked: locked,
1120            entries,
1121            credentials,
1122        })
1123    }
1124
1125    pub fn lock_lifecycle_inventory(
1126        &self,
1127    ) -> Result<CredentialInventoryGuard, CredentialStoreError> {
1128        let locked = self.lock()?;
1129        let entries = open_private_directory(&locked.root, ENTRIES_DIRECTORY)?;
1130        self.ensure_normalized_label_inventory(&entries)?;
1131        recover_add_staging(&entries)?;
1132        let credentials = self.list_locked(&entries)?;
1133        Ok(CredentialInventoryGuard {
1134            store: self.clone(),
1135            _locked: locked,
1136            entries,
1137            credentials,
1138        })
1139    }
1140
1141    pub fn status(&self, credential_ref: &str) -> Result<CredentialStatus, CredentialStoreError> {
1142        let id = parse_reference(credential_ref)?;
1143        let locked = self.lock()?;
1144        let entries = open_private_directory(&locked.root, ENTRIES_DIRECTORY)?;
1145        ensure_store_needs_no_lifecycle_recovery(&locked.root, &entries)?;
1146        let metadata = self.read_unique_metadata(&entries, id)?;
1147        let status = match metadata.lifecycle_state {
1148            CredentialLifecycleState::Active => {
1149                let secret = read_secret(&entries, id)?;
1150                drop(secret);
1151                "available"
1152            }
1153            CredentialLifecycleState::Removed => {
1154                let entry = open_private_directory(&entries, id)?;
1155                let _ = read_tombstone_identity(&entry)?;
1156                "removed"
1157            }
1158        };
1159        Ok(CredentialStatus { metadata, status })
1160    }
1161
1162    pub fn resolve(
1163        &self,
1164        credential_ref: &str,
1165    ) -> Result<Zeroizing<Vec<u8>>, CredentialStoreError> {
1166        let id = parse_reference(credential_ref)?;
1167        let locked = self.lock()?;
1168        let entries = open_private_directory(&locked.root, ENTRIES_DIRECTORY)?;
1169        ensure_store_needs_no_lifecycle_recovery(&locked.root, &entries)?;
1170        let metadata = self.read_unique_metadata(&entries, id)?;
1171        if metadata.credential_ref != credential_ref {
1172            return Err(CredentialStoreError::CorruptState);
1173        }
1174        if metadata.lifecycle_state == CredentialLifecycleState::Removed {
1175            return Err(CredentialStoreError::NotFound);
1176        }
1177        read_secret(&entries, id)
1178    }
1179
1180    fn list_locked(&self, entries: &File) -> Result<Vec<CredentialMetadata>, CredentialStoreError> {
1181        self.recheck_root()?;
1182        let mut records = Vec::new();
1183        let directory = Dir::read_from(entries).map_err(|_| CredentialStoreError::UnsafeStorage)?;
1184        for item in directory {
1185            let item = item.map_err(|_| CredentialStoreError::UnsafeStorage)?;
1186            let name = item
1187                .file_name()
1188                .to_str()
1189                .map_err(|_| CredentialStoreError::UnsafeStorage)?;
1190            if matches!(name, "." | "..") {
1191                continue;
1192            }
1193            if !valid_id(name) {
1194                return Err(CredentialStoreError::CorruptState);
1195            }
1196            records.push(self.read_metadata(entries, name)?);
1197            if records.len() > MAX_CREDENTIALS {
1198                return Err(CredentialStoreError::CapacityExhausted);
1199            }
1200        }
1201        records.sort_by(|left, right| left.id.cmp(&right.id));
1202        for (index, record) in records.iter().enumerate() {
1203            if records[..index].iter().any(|existing| {
1204                existing.provider == record.provider && existing.label == record.label
1205            }) {
1206                return Err(CredentialStoreError::CorruptState);
1207            }
1208        }
1209        Ok(records)
1210    }
1211
1212    fn read_unique_metadata(
1213        &self,
1214        entries: &File,
1215        id: &str,
1216    ) -> Result<CredentialMetadata, CredentialStoreError> {
1217        self.list_locked(entries)?
1218            .into_iter()
1219            .find(|metadata| metadata.id == id)
1220            .ok_or(CredentialStoreError::NotFound)
1221    }
1222
1223    fn ensure_normalized_label_inventory(
1224        &self,
1225        entries: &File,
1226    ) -> Result<(), CredentialStoreError> {
1227        self.list_locked(entries).map(drop)
1228    }
1229
1230    fn read_metadata(
1231        &self,
1232        entries: &File,
1233        id: &str,
1234    ) -> Result<CredentialMetadata, CredentialStoreError> {
1235        let entry = open_private_directory(entries, id)?;
1236        let bytes = read_regular_file(&entry, METADATA_FILE, MAX_METADATA_BYTES, false)?;
1237        let mut metadata: CredentialMetadata =
1238            serde_json::from_slice(&bytes).map_err(|_| CredentialStoreError::CorruptState)?;
1239        metadata.validate()?;
1240        if metadata.id != id {
1241            return Err(CredentialStoreError::CorruptState);
1242        }
1243        metadata.label = normalize_account_label(&metadata.label)
1244            .map_err(|_| CredentialStoreError::CorruptState)?;
1245        Ok(metadata)
1246    }
1247
1248    fn lock(&self) -> Result<LockedRoot, CredentialStoreError> {
1249        self.recheck_root()?;
1250        let root = self
1251            .root
1252            .try_clone()
1253            .map_err(|_| CredentialStoreError::UnsafeStorage)?;
1254        let flags = OFlags::RDWR | OFlags::NOFOLLOW | OFlags::CLOEXEC;
1255        let lock_fd = match strict_openat(&root, LOCK_FILE, flags, Mode::empty()) {
1256            Ok(fd) => fd,
1257            Err(CredentialStoreError::NotFound) => strict_openat(
1258                &root,
1259                LOCK_FILE,
1260                flags | OFlags::CREATE | OFlags::EXCL,
1261                Mode::from_raw_mode(0o600),
1262            )?,
1263            Err(error) => return Err(error),
1264        };
1265        fchmod(&lock_fd, Mode::from_raw_mode(0o600))
1266            .map_err(|_| CredentialStoreError::UnsafeStorage)?;
1267        let lock = File::from(lock_fd);
1268        self.lock_exclusive(&lock)?;
1269        validate_regular(
1270            &fstat(&lock).map_err(|_| CredentialStoreError::UnsafeStorage)?,
1271            self.root_identity.device,
1272            Some(0),
1273        )?;
1274        Ok(LockedRoot { root, _lock: lock })
1275    }
1276
1277    /// Acquires the store's exclusive flock, retrying transient
1278    /// interruptions (EINTR) a bounded number of times before failing
1279    /// closed with `LockUnavailable` when the lock is genuinely held.
1280    /// A tiny settle sleep between retries lets a burst of delivered
1281    /// signals drain so a transient interruption under heavy parallel
1282    /// load never fails a credential-store operation.
1283    fn lock_exclusive(&self, lock: &File) -> Result<(), CredentialStoreError> {
1284        let mut interrupted_attempts = 0usize;
1285        loop {
1286            let interrupted = self.flock_attempt(lock)?;
1287            if !interrupted {
1288                return Ok(());
1289            }
1290            interrupted_attempts += 1;
1291            if interrupted_attempts > FLOCK_EINTR_RETRY_BOUND {
1292                return Err(CredentialStoreError::LockUnavailable);
1293            }
1294            std::thread::sleep(FLOCK_EINTR_RETRY_SLEEP);
1295        }
1296    }
1297
1298    /// Returns Ok(true) when the flock attempt was transiently interrupted
1299    /// and may be retried, Ok(false) when the exclusive lock was acquired,
1300    /// and Err when the lock is genuinely unavailable. Only EINTR (and the
1301    /// test-injected interruption) is retried; EWOULDBLOCK/EAGAIN and every
1302    /// other error still fail closed with `LockUnavailable`.
1303    fn flock_attempt(&self, lock: &File) -> Result<bool, CredentialStoreError> {
1304        #[cfg(test)]
1305        {
1306            if self.flock_eintr_injections.load(Ordering::SeqCst) > 0 {
1307                self.flock_eintr_injections.fetch_sub(1, Ordering::SeqCst);
1308                return Ok(true);
1309            }
1310        }
1311        match flock(lock, FlockOperation::NonBlockingLockExclusive) {
1312            Ok(()) => Ok(false),
1313            Err(error) if error == rustix::io::Errno::INTR => Ok(true),
1314            Err(_) => Err(CredentialStoreError::LockUnavailable),
1315        }
1316    }
1317
1318    fn recheck_root(&self) -> Result<(), CredentialStoreError> {
1319        let fresh = open_path_directory(&self.root_path)?;
1320        let fresh_stat = fstat(&fresh).map_err(|_| CredentialStoreError::UnsafeStorage)?;
1321        let held_stat = fstat(&*self.root).map_err(|_| CredentialStoreError::UnsafeStorage)?;
1322        validate_directory(&fresh_stat)?;
1323        validate_directory(&held_stat)?;
1324        if !same_filesystem_object(identity(&fresh_stat)?, self.root_identity)
1325            || !same_filesystem_object(identity(&held_stat)?, self.root_identity)
1326        {
1327            return Err(CredentialStoreError::UnsafeStorage);
1328        }
1329        Ok(())
1330    }
1331}
1332
1333struct LockedRoot {
1334    root: File,
1335    _lock: File,
1336}
1337
1338fn rotation_outcome(
1339    credential_ref: &str,
1340    previous_generation: u64,
1341    generation: u64,
1342    reconciliation_required: bool,
1343) -> CredentialLifecycleOutcome {
1344    CredentialLifecycleOutcome {
1345        operation: CredentialLifecycleOperation::Rotate,
1346        credential_ref: credential_ref.to_owned(),
1347        previous_generation,
1348        generation: Some(generation),
1349        lifecycle_state: "active",
1350        health: "locally_verified",
1351        commit_status: if reconciliation_required {
1352            "committed_reconciliation_required"
1353        } else {
1354            "committed"
1355        },
1356        reconciliation_required,
1357    }
1358}
1359
1360fn removal_outcome(
1361    credential_ref: &str,
1362    previous_generation: u64,
1363    reconciliation_required: bool,
1364) -> CredentialLifecycleOutcome {
1365    CredentialLifecycleOutcome {
1366        operation: CredentialLifecycleOperation::Remove,
1367        credential_ref: credential_ref.to_owned(),
1368        previous_generation,
1369        generation: None,
1370        lifecycle_state: "removed",
1371        health: "unavailable",
1372        commit_status: if reconciliation_required {
1373            "committed_reconciliation_required"
1374        } else {
1375            "committed"
1376        },
1377        reconciliation_required,
1378    }
1379}
1380
1381fn reconcile_rotate_request_locked(
1382    entries: &File,
1383    lifecycle: &File,
1384    id: &str,
1385    credential_ref: &str,
1386    request: CredentialLifecycleRequestIdentity<'_>,
1387) -> Result<CredentialRotationReconciliation, CredentialStoreError> {
1388    let Some(journal) = read_journal(lifecycle, id).map_err(|error| match error {
1389        CredentialStoreError::CorruptState => CredentialStoreError::RecoveryRequired,
1390        other => other,
1391    })?
1392    else {
1393        match open_private_directory(entries, id) {
1394            Ok(entry) => {
1395                if path_exists(&entry, NEXT_SECRET_FILE)?
1396                    || path_exists(&entry, NEXT_METADATA_FILE)?
1397                {
1398                    return Err(CredentialStoreError::RecoveryRequired);
1399                }
1400            }
1401            Err(CredentialStoreError::NotFound) => {}
1402            Err(error) => return Err(error),
1403        }
1404        return Ok(CredentialRotationReconciliation::NotRequired);
1405    };
1406    journal
1407        .validate()
1408        .map_err(|_| CredentialStoreError::RecoveryRequired)?;
1409    if journal.credential_id != id
1410        || journal.credential_ref != credential_ref
1411        || journal.operation != CredentialLifecycleOperation::Rotate
1412        || journal.required_confirmation != ROTATE_CONFIRMATION
1413        || journal.expected_generation != request.expected_generation
1414        || journal.expires_at_epoch_seconds != request.expires_at_epoch_seconds
1415        || !constant_time_eq(
1416            journal.preview_digest.as_bytes(),
1417            request.expected_preview.as_bytes(),
1418        )
1419    {
1420        return Err(CredentialStoreError::RecoveryRequired);
1421    }
1422    let mut reconciliation_required = match reconcile_rotation(entries, lifecycle, &journal)? {
1423        RotationTransactionState::RolledBack => {
1424            return Ok(CredentialRotationReconciliation::RolledBackFreshPreviewRequired);
1425        }
1426        RotationTransactionState::Committed {
1427            reconciliation_required,
1428        } => reconciliation_required,
1429    };
1430    if journal.state != CredentialLifecycleJournalState::Committed {
1431        let committed_journal = CredentialLifecycleJournal {
1432            state: CredentialLifecycleJournalState::Committed,
1433            ..journal.clone()
1434        };
1435        if replace_journal(lifecycle, &committed_journal).is_err() {
1436            reconciliation_required = true;
1437        }
1438    }
1439    Ok(CredentialRotationReconciliation::Committed(
1440        rotation_outcome(
1441            &journal.credential_ref,
1442            journal.expected_generation,
1443            journal
1444                .target_generation
1445                .ok_or(CredentialStoreError::CorruptState)?,
1446            reconciliation_required,
1447        ),
1448    ))
1449}
1450
1451fn reconcile_remove_request_locked(
1452    entries: &File,
1453    lifecycle: &File,
1454    id: &str,
1455    credential_ref: &str,
1456    request: CredentialLifecycleRequestIdentity<'_>,
1457) -> Result<CredentialRemovalReconciliation, CredentialStoreError> {
1458    let Some(journal) = read_journal(lifecycle, id).map_err(|error| match error {
1459        CredentialStoreError::CorruptState => CredentialStoreError::RecoveryRequired,
1460        other => other,
1461    })?
1462    else {
1463        return Ok(CredentialRemovalReconciliation::NotRequired);
1464    };
1465    journal
1466        .validate()
1467        .map_err(|_| CredentialStoreError::RecoveryRequired)?;
1468    if journal.credential_id != id
1469        || journal.credential_ref != credential_ref
1470        || journal.operation != CredentialLifecycleOperation::Remove
1471        || journal.required_confirmation != REMOVE_CONFIRMATION
1472        || journal.expected_generation != request.expected_generation
1473        || journal.expires_at_epoch_seconds != request.expires_at_epoch_seconds
1474        || !constant_time_eq(
1475            journal.preview_digest.as_bytes(),
1476            request.expected_preview.as_bytes(),
1477        )
1478    {
1479        return Err(CredentialStoreError::RecoveryRequired);
1480    }
1481    let mut reconciliation_required = match reconcile_removal(entries, lifecycle, &journal)? {
1482        RemovalTransactionState::RolledBack => {
1483            return Ok(CredentialRemovalReconciliation::RolledBackFreshPreviewRequired);
1484        }
1485        RemovalTransactionState::Committed {
1486            reconciliation_required,
1487        } => reconciliation_required,
1488    };
1489    if journal.state != CredentialLifecycleJournalState::Committed {
1490        let committed_journal = CredentialLifecycleJournal {
1491            state: CredentialLifecycleJournalState::Committed,
1492            ..journal.clone()
1493        };
1494        if replace_journal(lifecycle, &committed_journal).is_err() {
1495            reconciliation_required = true;
1496        }
1497    }
1498    Ok(CredentialRemovalReconciliation::Committed(removal_outcome(
1499        &journal.credential_ref,
1500        journal.expected_generation,
1501        reconciliation_required,
1502    )))
1503}
1504
1505enum RotationTransactionState {
1506    RolledBack,
1507    Committed { reconciliation_required: bool },
1508}
1509
1510fn reconcile_rotation(
1511    entries: &File,
1512    lifecycle: &File,
1513    journal: &CredentialLifecycleJournal,
1514) -> Result<RotationTransactionState, CredentialStoreError> {
1515    let entry = open_private_directory(entries, &journal.credential_id)?;
1516    let (live_secret, live_identity) = read_secret_with_identity(entries, &journal.credential_id)
1517        .map_err(|_| CredentialStoreError::RecoveryRequired)?;
1518    drop(live_secret);
1519    if live_identity == journal.old_secret_identity {
1520        if journal.state == CredentialLifecycleJournalState::Committed {
1521            return Err(CredentialStoreError::RecoveryRequired);
1522        }
1523        cleanup_precommit_files_checked(&entry)?;
1524        remove_journal(lifecycle, &journal.credential_id)?;
1525        return Ok(RotationTransactionState::RolledBack);
1526    }
1527    if Some(live_identity) != journal.staged_secret_identity {
1528        return Err(CredentialStoreError::RecoveryRequired);
1529    }
1530
1531    let metadata_bytes = read_regular_file(&entry, METADATA_FILE, MAX_METADATA_BYTES, false)
1532        .map_err(|_| CredentialStoreError::RecoveryRequired)?;
1533    let metadata: CredentialMetadata = serde_json::from_slice(&metadata_bytes)
1534        .map_err(|_| CredentialStoreError::RecoveryRequired)?;
1535    metadata
1536        .validate()
1537        .map_err(|_| CredentialStoreError::RecoveryRequired)?;
1538    let metadata_is_target = rotation_metadata_matches_journal(&metadata, journal);
1539    if !metadata_is_target {
1540        if metadata.generation != journal.expected_generation
1541            || metadata.id != journal.credential_id
1542            || !path_exists(&entry, NEXT_METADATA_FILE)?
1543        {
1544            return Err(CredentialStoreError::RecoveryRequired);
1545        }
1546        let next_bytes = read_regular_file(&entry, NEXT_METADATA_FILE, MAX_METADATA_BYTES, false)
1547            .map_err(|_| CredentialStoreError::RecoveryRequired)?;
1548        let next_metadata: CredentialMetadata = serde_json::from_slice(&next_bytes)
1549            .map_err(|_| CredentialStoreError::RecoveryRequired)?;
1550        next_metadata
1551            .validate()
1552            .map_err(|_| CredentialStoreError::RecoveryRequired)?;
1553        if !rotation_metadata_matches_journal(&next_metadata, journal) {
1554            return Err(CredentialStoreError::RecoveryRequired);
1555        }
1556        if lifecycle_failpoint("reconcile_rotate_metadata_replace")
1557            || rename_replace(&entry, NEXT_METADATA_FILE, METADATA_FILE).is_err()
1558        {
1559            return Ok(RotationTransactionState::Committed {
1560                reconciliation_required: true,
1561            });
1562        }
1563    }
1564
1565    let mut reconciliation_required = false;
1566    post_commit_step(
1567        "reconcile_rotate_staging_cleanup",
1568        || cleanup_postcommit_rotation_files(&entry),
1569        &mut reconciliation_required,
1570    );
1571    post_commit_step(
1572        "reconcile_rotate_entry_sync",
1573        || durable_sync(&entry),
1574        &mut reconciliation_required,
1575    );
1576    Ok(RotationTransactionState::Committed {
1577        reconciliation_required,
1578    })
1579}
1580
1581fn rotation_metadata_matches_journal(
1582    metadata: &CredentialMetadata,
1583    journal: &CredentialLifecycleJournal,
1584) -> bool {
1585    metadata.id == journal.credential_id
1586        && metadata.credential_ref == journal.credential_ref
1587        && Some(metadata.generation) == journal.target_generation
1588        && metadata.previous_generation == Some(journal.expected_generation)
1589        && metadata.last_lifecycle_preview_digest.as_deref()
1590            == Some(journal.preview_digest.as_str())
1591}
1592
1593enum RemovalTransactionState {
1594    RolledBack,
1595    Committed { reconciliation_required: bool },
1596}
1597
1598fn reconcile_removal(
1599    entries: &File,
1600    lifecycle: &File,
1601    journal: &CredentialLifecycleJournal,
1602) -> Result<RemovalTransactionState, CredentialStoreError> {
1603    let entry = open_private_directory(entries, &journal.credential_id)
1604        .map_err(|_| CredentialStoreError::RecoveryRequired)?;
1605    let (secret_file, live_identity, secret_size) =
1606        open_secret_or_tombstone_for_recovery(&entry)
1607            .map_err(|_| CredentialStoreError::RecoveryRequired)?;
1608    if live_identity != journal.old_secret_identity {
1609        return Err(CredentialStoreError::RecoveryRequired);
1610    }
1611    let metadata_bytes = read_regular_file(&entry, METADATA_FILE, MAX_METADATA_BYTES, false)
1612        .map_err(|_| CredentialStoreError::RecoveryRequired)?;
1613    let metadata: CredentialMetadata = serde_json::from_slice(&metadata_bytes)
1614        .map_err(|_| CredentialStoreError::RecoveryRequired)?;
1615    metadata
1616        .validate()
1617        .map_err(|_| CredentialStoreError::RecoveryRequired)?;
1618
1619    if secret_size != 0 {
1620        if journal.state == CredentialLifecycleJournalState::Committed
1621            || !active_removal_metadata_matches_journal(&metadata, journal)
1622            || path_exists(&entry, NEXT_METADATA_FILE)?
1623        {
1624            return Err(CredentialStoreError::RecoveryRequired);
1625        }
1626        remove_journal(lifecycle, &journal.credential_id)?;
1627        return Ok(RemovalTransactionState::RolledBack);
1628    }
1629
1630    if lifecycle_failpoint("remove_secret_sync_failure") || durable_sync(&secret_file).is_err() {
1631        return Err(CredentialStoreError::RecoveryRequired);
1632    }
1633    if lifecycle_failpoint("remove_after_secret_sync") {
1634        return Err(CredentialStoreError::RecoveryRequired);
1635    }
1636    let parent_stat = fstat(&entry).map_err(|_| CredentialStoreError::RecoveryRequired)?;
1637    let descriptor_stat =
1638        fstat(&secret_file).map_err(|_| CredentialStoreError::RecoveryRequired)?;
1639    let path_stat = statat(&entry, SECRET_FILE, AtFlags::SYMLINK_NOFOLLOW)
1640        .map_err(|_| CredentialStoreError::RecoveryRequired)?;
1641    let parent_device = device(&parent_stat).map_err(|_| CredentialStoreError::RecoveryRequired)?;
1642    validate_regular(&descriptor_stat, parent_device, Some(0))
1643        .map_err(|_| CredentialStoreError::RecoveryRequired)?;
1644    validate_regular(&path_stat, parent_device, Some(0))
1645        .map_err(|_| CredentialStoreError::RecoveryRequired)?;
1646    let descriptor_identity = identity_with_birth(&descriptor_stat, &entry, SECRET_FILE)
1647        .map_err(|_| CredentialStoreError::RecoveryRequired)?;
1648    if descriptor_identity != journal.old_secret_identity
1649        || identity_with_birth(&path_stat, &entry, SECRET_FILE)
1650            .map_err(|_| CredentialStoreError::RecoveryRequired)?
1651            != journal.old_secret_identity
1652    {
1653        return Err(CredentialStoreError::RecoveryRequired);
1654    }
1655
1656    let reconciliation_required = finalize_removal_metadata(&entry, &metadata, journal)?;
1657    Ok(RemovalTransactionState::Committed {
1658        reconciliation_required,
1659    })
1660}
1661
1662fn active_removal_metadata_matches_journal(
1663    metadata: &CredentialMetadata,
1664    journal: &CredentialLifecycleJournal,
1665) -> bool {
1666    metadata.id == journal.credential_id
1667        && metadata.credential_ref == journal.credential_ref
1668        && metadata.generation == journal.expected_generation
1669        && metadata.lifecycle_state == CredentialLifecycleState::Active
1670        && metadata.health == CredentialHealthState::LocallyVerified
1671        && metadata.bound_phases.is_empty()
1672        && metadata.bound_routes.is_empty()
1673}
1674
1675fn removed_metadata_matches_journal(
1676    metadata: &CredentialMetadata,
1677    journal: &CredentialLifecycleJournal,
1678) -> bool {
1679    metadata.id == journal.credential_id
1680        && metadata.credential_ref == journal.credential_ref
1681        && metadata.generation == journal.expected_generation
1682        && metadata.lifecycle_state == CredentialLifecycleState::Removed
1683        && metadata.health == CredentialHealthState::Unavailable
1684        && metadata.last_lifecycle_preview_digest.as_deref()
1685            == Some(journal.preview_digest.as_str())
1686        && metadata.last_verified_at_epoch_seconds.is_none()
1687        && metadata.bound_phases.is_empty()
1688        && metadata.bound_routes.is_empty()
1689}
1690
1691fn finalize_removal_metadata(
1692    entry: &File,
1693    metadata: &CredentialMetadata,
1694    journal: &CredentialLifecycleJournal,
1695) -> Result<bool, CredentialStoreError> {
1696    if removed_metadata_matches_journal(metadata, journal) {
1697        return Ok(false);
1698    }
1699    if !active_removal_metadata_matches_journal(metadata, journal) {
1700        return Err(CredentialStoreError::RecoveryRequired);
1701    }
1702
1703    let mut tombstone = metadata.clone();
1704    tombstone.lifecycle_state = CredentialLifecycleState::Removed;
1705    tombstone.health = CredentialHealthState::Unavailable;
1706    tombstone.last_lifecycle_preview_digest = Some(journal.preview_digest.clone());
1707    tombstone.last_verified_at_epoch_seconds = None;
1708    tombstone.validate()?;
1709    let tombstone_bytes =
1710        serde_json::to_vec(&tombstone).map_err(|_| CredentialStoreError::RecoveryRequired)?;
1711    if tombstone_bytes.len() > MAX_METADATA_BYTES {
1712        return Err(CredentialStoreError::RecoveryRequired);
1713    }
1714
1715    if path_exists(entry, NEXT_METADATA_FILE)? {
1716        let staged = read_regular_file(entry, NEXT_METADATA_FILE, MAX_METADATA_BYTES, false)
1717            .map_err(|_| CredentialStoreError::RecoveryRequired)?;
1718        let staged: CredentialMetadata =
1719            serde_json::from_slice(&staged).map_err(|_| CredentialStoreError::RecoveryRequired)?;
1720        staged
1721            .validate()
1722            .map_err(|_| CredentialStoreError::RecoveryRequired)?;
1723        if staged != tombstone {
1724            return Err(CredentialStoreError::RecoveryRequired);
1725        }
1726    } else {
1727        if lifecycle_failpoint("remove_metadata_stage") {
1728            return Ok(true);
1729        }
1730        if write_new_file(entry, NEXT_METADATA_FILE, &tombstone_bytes).is_err() {
1731            return Ok(true);
1732        }
1733    }
1734    if lifecycle_failpoint("remove_metadata_replace")
1735        || rename_replace(entry, NEXT_METADATA_FILE, METADATA_FILE).is_err()
1736    {
1737        return Ok(true);
1738    }
1739    if lifecycle_failpoint("remove_metadata_sync") || durable_sync(entry).is_err() {
1740        return Ok(true);
1741    }
1742    Ok(false)
1743}
1744
1745fn ensure_preview_needs_no_recovery(
1746    root: &File,
1747    entries: &File,
1748    id: &str,
1749) -> Result<(), CredentialStoreError> {
1750    ensure_store_needs_no_lifecycle_recovery(root, entries)?;
1751    let _ = open_private_directory(entries, id)?;
1752    Ok(())
1753}
1754
1755fn ensure_store_needs_no_lifecycle_recovery(
1756    root: &File,
1757    entries: &File,
1758) -> Result<(), CredentialStoreError> {
1759    let directory = Dir::read_from(entries).map_err(|_| CredentialStoreError::UnsafeStorage)?;
1760    for item in directory {
1761        let item = item.map_err(|_| CredentialStoreError::UnsafeStorage)?;
1762        let name = item
1763            .file_name()
1764            .to_str()
1765            .map_err(|_| CredentialStoreError::UnsafeStorage)?;
1766        if matches!(name, "." | "..") {
1767            continue;
1768        }
1769        if valid_staging_name(name) {
1770            return Err(CredentialStoreError::RecoveryRequired);
1771        }
1772        if !valid_id(name) {
1773            return Err(CredentialStoreError::CorruptState);
1774        }
1775        let entry = open_private_directory(entries, name)?;
1776        if path_exists(&entry, NEXT_SECRET_FILE)? || path_exists(&entry, NEXT_METADATA_FILE)? {
1777            return Err(CredentialStoreError::RecoveryRequired);
1778        }
1779    }
1780    let lifecycle = match open_private_directory(root, LIFECYCLE_DIRECTORY) {
1781        Ok(directory) => directory,
1782        Err(CredentialStoreError::NotFound) => return Ok(()),
1783        Err(error) => return Err(error),
1784    };
1785    let directory = Dir::read_from(&lifecycle).map_err(|_| CredentialStoreError::UnsafeStorage)?;
1786    for item in directory {
1787        let item = item.map_err(|_| CredentialStoreError::UnsafeStorage)?;
1788        let name = item
1789            .file_name()
1790            .to_str()
1791            .map_err(|_| CredentialStoreError::UnsafeStorage)?;
1792        if matches!(name, "." | "..") {
1793            continue;
1794        }
1795        if name.starts_with(".journal-") {
1796            return Err(CredentialStoreError::RecoveryRequired);
1797        }
1798        if valid_id(name) {
1799            let journal =
1800                read_journal(&lifecycle, name)?.ok_or(CredentialStoreError::CorruptState)?;
1801            if journal.credential_id != name {
1802                return Err(CredentialStoreError::CorruptState);
1803            }
1804            if journal.state == CredentialLifecycleJournalState::Prepared {
1805                return Err(CredentialStoreError::RecoveryRequired);
1806            }
1807            match journal.operation {
1808                CredentialLifecycleOperation::Rotate => {
1809                    let entry = open_private_directory(entries, name)
1810                        .map_err(|_| CredentialStoreError::RecoveryRequired)?;
1811                    let bytes = read_regular_file(&entry, METADATA_FILE, MAX_METADATA_BYTES, false)
1812                        .map_err(|_| CredentialStoreError::RecoveryRequired)?;
1813                    let metadata: CredentialMetadata = serde_json::from_slice(&bytes)
1814                        .map_err(|_| CredentialStoreError::CorruptState)?;
1815                    metadata.validate()?;
1816                    let (secret, live_identity) = read_secret_with_identity(entries, name)
1817                        .map_err(|_| CredentialStoreError::RecoveryRequired)?;
1818                    drop(secret);
1819                    if metadata.id != name
1820                        || metadata.generation != journal.target_generation.unwrap_or_default()
1821                        || metadata.previous_generation != Some(journal.expected_generation)
1822                        || metadata.last_lifecycle_preview_digest.as_deref()
1823                            != Some(journal.preview_digest.as_str())
1824                        || Some(live_identity) != journal.staged_secret_identity
1825                    {
1826                        return Err(CredentialStoreError::RecoveryRequired);
1827                    }
1828                }
1829                CredentialLifecycleOperation::Remove => {
1830                    let entry = open_private_directory(entries, name)
1831                        .map_err(|_| CredentialStoreError::RecoveryRequired)?;
1832                    let tombstone_identity = read_tombstone_identity(&entry)
1833                        .map_err(|_| CredentialStoreError::RecoveryRequired)?;
1834                    let bytes = read_regular_file(&entry, METADATA_FILE, MAX_METADATA_BYTES, false)
1835                        .map_err(|_| CredentialStoreError::RecoveryRequired)?;
1836                    let metadata: CredentialMetadata = serde_json::from_slice(&bytes)
1837                        .map_err(|_| CredentialStoreError::RecoveryRequired)?;
1838                    metadata
1839                        .validate()
1840                        .map_err(|_| CredentialStoreError::RecoveryRequired)?;
1841                    if tombstone_identity != journal.old_secret_identity
1842                        || !removed_metadata_matches_journal(&metadata, &journal)
1843                        || path_exists(&entry, NEXT_METADATA_FILE)?
1844                    {
1845                        return Err(CredentialStoreError::RecoveryRequired);
1846                    }
1847                }
1848            }
1849        } else {
1850            return Err(CredentialStoreError::CorruptState);
1851        }
1852    }
1853    Ok(())
1854}
1855
1856fn post_commit_step(
1857    edge: &'static str,
1858    operation: impl FnOnce() -> Result<(), CredentialStoreError>,
1859    reconciliation_required: &mut bool,
1860) {
1861    if lifecycle_failpoint(edge) || operation().is_err() {
1862        *reconciliation_required = true;
1863    }
1864}
1865
1866#[cfg(test)]
1867std::thread_local! {
1868    static LIFECYCLE_FAILPOINT: std::cell::Cell<Option<&'static str>> = const { std::cell::Cell::new(None) };
1869}
1870
1871#[cfg(test)]
1872fn lifecycle_failpoint(edge: &'static str) -> bool {
1873    LIFECYCLE_FAILPOINT.with(|configured| configured.get() == Some(edge))
1874}
1875
1876#[cfg(not(test))]
1877fn lifecycle_failpoint(_edge: &'static str) -> bool {
1878    false
1879}
1880
1881#[cfg(test)]
1882fn inject_remove_secret_substitution_for_test(entry: &File) -> Result<(), CredentialStoreError> {
1883    renameat(entry, SECRET_FILE, entry, ".test-original-secret")
1884        .map_err(|_| CredentialStoreError::RecoveryRequired)?;
1885    write_new_file(entry, SECRET_FILE, b"same-user-substitute")
1886        .map_err(|_| CredentialStoreError::RecoveryRequired)
1887}
1888
1889fn read_journal(
1890    lifecycle: &File,
1891    id: &str,
1892) -> Result<Option<CredentialLifecycleJournal>, CredentialStoreError> {
1893    match read_regular_file(lifecycle, id, MAX_METADATA_BYTES, false) {
1894        Ok(bytes) => {
1895            let journal: CredentialLifecycleJournal =
1896                serde_json::from_slice(&bytes).map_err(|_| CredentialStoreError::CorruptState)?;
1897            journal.validate()?;
1898            Ok(Some(journal))
1899        }
1900        Err(CredentialStoreError::NotFound) => Ok(None),
1901        Err(error) => Err(error),
1902    }
1903}
1904
1905fn write_journal_new(
1906    lifecycle: &File,
1907    journal: &CredentialLifecycleJournal,
1908) -> Result<(), CredentialStoreError> {
1909    journal.validate()?;
1910    let bytes = serde_json::to_vec(journal).map_err(|_| CredentialStoreError::CommitAmbiguous)?;
1911    if bytes.len() > MAX_METADATA_BYTES {
1912        return Err(CredentialStoreError::InvalidInput);
1913    }
1914    write_new_file(lifecycle, &journal.credential_id, &bytes)?;
1915    durable_sync(lifecycle)
1916}
1917
1918fn replace_journal(
1919    lifecycle: &File,
1920    journal: &CredentialLifecycleJournal,
1921) -> Result<(), CredentialStoreError> {
1922    journal.validate()?;
1923    let bytes = serde_json::to_vec(journal).map_err(|_| CredentialStoreError::CommitAmbiguous)?;
1924    if bytes.len() > MAX_METADATA_BYTES {
1925        return Err(CredentialStoreError::InvalidInput);
1926    }
1927    let staging = format!(".journal-{}", journal.credential_id);
1928    unlink_regular_if_exists(lifecycle, &staging)?;
1929    write_new_file(lifecycle, &staging, &bytes)?;
1930    durable_sync(lifecycle)?;
1931    rename_replace(lifecycle, &staging, &journal.credential_id)?;
1932    durable_sync(lifecycle)
1933}
1934
1935fn remove_journal(lifecycle: &File, id: &str) -> Result<(), CredentialStoreError> {
1936    unlink_regular_if_exists(lifecycle, &format!(".journal-{id}"))?;
1937    unlink_regular_if_exists(lifecycle, id)?;
1938    durable_sync(lifecycle)
1939}
1940
1941fn path_exists(parent: &File, name: &str) -> Result<bool, CredentialStoreError> {
1942    match statat(parent, name, AtFlags::SYMLINK_NOFOLLOW) {
1943        Ok(_) => Ok(true),
1944        Err(error) if error == rustix::io::Errno::NOENT => Ok(false),
1945        Err(_) => Err(CredentialStoreError::UnsafeStorage),
1946    }
1947}
1948
1949fn unlink_regular_if_exists(parent: &File, name: &str) -> Result<(), CredentialStoreError> {
1950    let parent_device = device(&fstat(parent).map_err(|_| CredentialStoreError::UnsafeStorage)?)?;
1951    match statat(parent, name, AtFlags::SYMLINK_NOFOLLOW) {
1952        Ok(stat) => validate_regular(&stat, parent_device, None)?,
1953        Err(error) if error == rustix::io::Errno::NOENT => return Ok(()),
1954        Err(_) => return Err(CredentialStoreError::UnsafeStorage),
1955    }
1956    unlinkat(parent, name, AtFlags::empty()).map_err(|_| CredentialStoreError::UnsafeStorage)
1957}
1958
1959fn cleanup_precommit_files(entry: &File) {
1960    let _ = cleanup_precommit_files_checked(entry);
1961}
1962
1963fn cleanup_precommit_files_checked(entry: &File) -> Result<(), CredentialStoreError> {
1964    unlink_regular_if_exists(entry, NEXT_SECRET_FILE)?;
1965    unlink_regular_if_exists(entry, NEXT_METADATA_FILE)?;
1966    durable_sync(entry)
1967}
1968
1969fn cleanup_postcommit_rotation_files(entry: &File) -> Result<(), CredentialStoreError> {
1970    unlink_regular_if_exists(entry, NEXT_SECRET_FILE)?;
1971    unlink_regular_if_exists(entry, NEXT_METADATA_FILE)
1972}
1973
1974fn lifecycle_preview_for_metadata(
1975    metadata: &CredentialMetadata,
1976    operation: CredentialLifecycleOperation,
1977    now_epoch_seconds: u64,
1978    expires_at_epoch_seconds: u64,
1979) -> Result<CredentialLifecyclePreview, CredentialStoreError> {
1980    metadata.validate()?;
1981    validate_preview_window(now_epoch_seconds, expires_at_epoch_seconds)?;
1982    let binding_count = metadata
1983        .bound_phases
1984        .len()
1985        .checked_add(metadata.bound_routes.len())
1986        .ok_or(CredentialStoreError::CorruptState)?;
1987    let removal_allowed = operation != CredentialLifecycleOperation::Remove || binding_count == 0;
1988    let required_confirmation = match operation {
1989        CredentialLifecycleOperation::Rotate => ROTATE_CONFIRMATION,
1990        CredentialLifecycleOperation::Remove => REMOVE_CONFIRMATION,
1991    };
1992    let mut preview = CredentialLifecyclePreview {
1993        schema_version: SCHEMA_VERSION.to_owned(),
1994        operation,
1995        provider: metadata.provider.clone(),
1996        label: metadata.label.clone(),
1997        credential_ref: metadata.credential_ref.clone(),
1998        dependency_impact: CredentialDependencyImpact {
1999            bound_phases: metadata.bound_phases.clone(),
2000            bound_routes: metadata.bound_routes.clone(),
2001            binding_count,
2002            apply_allowed: removal_allowed,
2003            policy: if operation == CredentialLifecycleOperation::Remove && !removal_allowed {
2004                "blocked_until_zero_bindings"
2005            } else if operation == CredentialLifecycleOperation::Remove {
2006                "zero_binding_removal"
2007            } else {
2008                "bindings_preserved"
2009            },
2010        },
2011        expected_generation: metadata.generation,
2012        expires_at_epoch_seconds,
2013        required_confirmation,
2014        preview_digest: String::new(),
2015    };
2016    let encoded = serde_json::to_vec(&preview).map_err(|_| CredentialStoreError::CorruptState)?;
2017    preview.preview_digest = sha256_id(&encoded);
2018    Ok(preview)
2019}
2020
2021fn validate_preview_window(
2022    now_epoch_seconds: u64,
2023    expires_at_epoch_seconds: u64,
2024) -> Result<(), CredentialStoreError> {
2025    let lifetime = expires_at_epoch_seconds
2026        .checked_sub(now_epoch_seconds)
2027        .ok_or(CredentialStoreError::PreviewExpired)?;
2028    if lifetime == 0 || lifetime > MAX_PREVIEW_LIFETIME_SECONDS {
2029        return Err(CredentialStoreError::PreviewExpired);
2030    }
2031    Ok(())
2032}
2033
2034fn validate_expected_preview(
2035    preview: &CredentialLifecyclePreview,
2036    expected_preview: &str,
2037    now_epoch_seconds: u64,
2038) -> Result<(), CredentialStoreError> {
2039    if now_epoch_seconds >= preview.expires_at_epoch_seconds {
2040        return Err(CredentialStoreError::PreviewExpired);
2041    }
2042    if !constant_time_eq(
2043        preview.preview_digest.as_bytes(),
2044        expected_preview.as_bytes(),
2045    ) {
2046        return Err(CredentialStoreError::PreviewMismatch);
2047    }
2048    Ok(())
2049}
2050
2051fn sha256_id(bytes: &[u8]) -> String {
2052    let digest = Sha256::digest(bytes);
2053    let mut encoded = String::with_capacity(7 + digest.len() * 2);
2054    encoded.push_str("sha256:");
2055    for byte in digest {
2056        use std::fmt::Write as _;
2057        let _ = write!(encoded, "{byte:02x}");
2058    }
2059    encoded
2060}
2061
2062fn current_epoch_seconds() -> Result<u64, CredentialStoreError> {
2063    SystemTime::now()
2064        .duration_since(UNIX_EPOCH)
2065        .map(|duration| duration.as_secs())
2066        .map_err(|_| CredentialStoreError::CommitAmbiguous)
2067}
2068
2069fn constant_time_eq(left: &[u8], right: &[u8]) -> bool {
2070    if left.len() != right.len() {
2071        return false;
2072    }
2073    left.iter()
2074        .zip(right)
2075        .fold(0_u8, |difference, (left, right)| {
2076            difference | (left ^ right)
2077        })
2078        == 0
2079}
2080
2081fn verify_staged_files(
2082    entry: &File,
2083    expected_metadata: &CredentialMetadata,
2084    expected_secret: &[u8],
2085) -> Result<FileIdentity, CredentialStoreError> {
2086    let metadata_bytes = read_regular_file(entry, NEXT_METADATA_FILE, MAX_METADATA_BYTES, false)?;
2087    let metadata: CredentialMetadata =
2088        serde_json::from_slice(&metadata_bytes).map_err(|_| CredentialStoreError::CorruptState)?;
2089    metadata.validate()?;
2090    if &metadata != expected_metadata {
2091        return Err(CredentialStoreError::CommitAmbiguous);
2092    }
2093    let (secret, identity) =
2094        read_regular_file_with_identity(entry, NEXT_SECRET_FILE, MAX_SECRET_BYTES, true)?;
2095    let secret = Zeroizing::new(secret);
2096    if !valid_secret(&secret) || !constant_time_eq(&secret, expected_secret) {
2097        return Err(CredentialStoreError::CommitAmbiguous);
2098    }
2099    Ok(identity)
2100}
2101
2102fn read_secret(entries: &File, id: &str) -> Result<Zeroizing<Vec<u8>>, CredentialStoreError> {
2103    read_secret_with_identity(entries, id).map(|(secret, _)| secret)
2104}
2105
2106fn read_secret_with_identity(
2107    entries: &File,
2108    id: &str,
2109) -> Result<(Zeroizing<Vec<u8>>, FileIdentity), CredentialStoreError> {
2110    let entry = open_private_directory(entries, id)?;
2111    let (value, identity) =
2112        read_regular_file_with_identity(&entry, SECRET_FILE, MAX_SECRET_BYTES, true)?;
2113    let value = Zeroizing::new(value);
2114    if !valid_secret(&value) {
2115        return Err(CredentialStoreError::CorruptState);
2116    }
2117    Ok((value, identity))
2118}
2119
2120fn open_secret_for_erasure(entry: &File) -> Result<(File, FileIdentity), CredentialStoreError> {
2121    let (file, descriptor_identity, size) = open_secret_or_tombstone_for_recovery(entry)?;
2122    if size == 0 {
2123        return Err(CredentialStoreError::CorruptState);
2124    }
2125    Ok((file, descriptor_identity))
2126}
2127
2128fn open_secret_or_tombstone_for_recovery(
2129    entry: &File,
2130) -> Result<(File, FileIdentity, usize), CredentialStoreError> {
2131    let file = File::from(strict_openat(
2132        entry,
2133        SECRET_FILE,
2134        OFlags::RDWR | OFlags::NOFOLLOW | OFlags::CLOEXEC,
2135        Mode::empty(),
2136    )?);
2137    let parent_stat = fstat(entry).map_err(|_| CredentialStoreError::UnsafeStorage)?;
2138    let descriptor_stat = fstat(&file).map_err(|_| CredentialStoreError::UnsafeStorage)?;
2139    let path_stat = statat(entry, SECRET_FILE, AtFlags::SYMLINK_NOFOLLOW)
2140        .map_err(|_| CredentialStoreError::UnsafeStorage)?;
2141    let size =
2142        usize::try_from(descriptor_stat.st_size).map_err(|_| CredentialStoreError::CorruptState)?;
2143    validate_regular(&descriptor_stat, device(&parent_stat)?, Some(size))?;
2144    validate_regular(&path_stat, device(&parent_stat)?, Some(size))?;
2145    let descriptor_identity = identity_with_birth(&descriptor_stat, entry, SECRET_FILE)?;
2146    if size > MAX_SECRET_BYTES
2147        || descriptor_identity != identity_with_birth(&path_stat, entry, SECRET_FILE)?
2148    {
2149        return Err(CredentialStoreError::CorruptState);
2150    }
2151    Ok((file, descriptor_identity, size))
2152}
2153
2154fn read_secret_or_tombstone_identity(
2155    entry: &File,
2156) -> Result<(FileIdentity, usize), CredentialStoreError> {
2157    let file = File::from(strict_openat(
2158        entry,
2159        SECRET_FILE,
2160        OFlags::RDONLY | OFlags::NOFOLLOW | OFlags::CLOEXEC,
2161        Mode::empty(),
2162    )?);
2163    let parent_stat = fstat(entry).map_err(|_| CredentialStoreError::UnsafeStorage)?;
2164    let descriptor_stat = fstat(&file).map_err(|_| CredentialStoreError::UnsafeStorage)?;
2165    let path_stat = statat(entry, SECRET_FILE, AtFlags::SYMLINK_NOFOLLOW)
2166        .map_err(|_| CredentialStoreError::UnsafeStorage)?;
2167    let size =
2168        usize::try_from(descriptor_stat.st_size).map_err(|_| CredentialStoreError::CorruptState)?;
2169    if size > MAX_SECRET_BYTES {
2170        return Err(CredentialStoreError::CorruptState);
2171    }
2172    validate_regular(&descriptor_stat, device(&parent_stat)?, Some(size))?;
2173    validate_regular(&path_stat, device(&parent_stat)?, Some(size))?;
2174    let descriptor_identity = identity_with_birth(&descriptor_stat, entry, SECRET_FILE)?;
2175    if descriptor_identity != identity_with_birth(&path_stat, entry, SECRET_FILE)? {
2176        return Err(CredentialStoreError::UnsafeStorage);
2177    }
2178    Ok((descriptor_identity, size))
2179}
2180
2181fn read_tombstone_identity(entry: &File) -> Result<FileIdentity, CredentialStoreError> {
2182    let (identity, size) = read_secret_or_tombstone_identity(entry)?;
2183    if size != 0 {
2184        return Err(CredentialStoreError::CorruptState);
2185    }
2186    Ok(identity)
2187}
2188
2189fn parse_reference(value: &str) -> Result<&str, CredentialStoreError> {
2190    let id = value
2191        .strip_prefix(REFERENCE_PREFIX)
2192        .ok_or(CredentialStoreError::InvalidInput)?;
2193    valid_id(id)
2194        .then_some(id)
2195        .ok_or(CredentialStoreError::InvalidInput)
2196}
2197
2198#[cfg(target_os = "macos")]
2199fn current_user_home_from_account_record() -> Result<PathBuf, CredentialStoreError> {
2200    use std::os::unix::process::CommandExt as _;
2201    use std::process::{Command, Stdio};
2202
2203    let uid = geteuid().as_raw();
2204    let uid_text = uid.to_string();
2205    let mut command = Command::new("/usr/bin/dscacheutil");
2206    command
2207        .args(["-q", "user", "-a", "uid", &uid_text])
2208        .env_clear()
2209        .stdin(Stdio::null())
2210        .stderr(Stdio::null())
2211        .stdout(Stdio::piped())
2212        .process_group(0);
2213    let output = run_bounded_account_lookup(
2214        &mut command,
2215        ACCOUNT_LOOKUP_TIMEOUT,
2216        MAX_ACCOUNT_RECORD_BYTES,
2217    )?;
2218    parse_account_home(&output, uid)
2219}
2220
2221#[cfg(target_os = "linux")]
2222fn current_user_home_from_account_record() -> Result<PathBuf, CredentialStoreError> {
2223    use std::os::unix::process::CommandExt as _;
2224    use std::process::{Command, Stdio};
2225
2226    let uid = geteuid().as_raw();
2227    let uid_text = uid.to_string();
2228    let mut command = Command::new("/usr/bin/getent");
2229    command
2230        .args(["passwd", &uid_text])
2231        .env_clear()
2232        .stdin(Stdio::null())
2233        .stderr(Stdio::null())
2234        .stdout(Stdio::piped())
2235        .process_group(0);
2236    let output = run_bounded_account_lookup(
2237        &mut command,
2238        ACCOUNT_LOOKUP_TIMEOUT,
2239        MAX_ACCOUNT_RECORD_BYTES,
2240    )?;
2241    parse_linux_account_home(&output, uid)
2242}
2243
2244#[cfg(any(target_os = "linux", target_os = "macos"))]
2245fn run_bounded_account_lookup(
2246    command: &mut std::process::Command,
2247    timeout: Duration,
2248    max_bytes: usize,
2249) -> Result<Vec<u8>, CredentialStoreError> {
2250    let child = command
2251        .spawn()
2252        .map_err(|_| CredentialStoreError::UnsafeStorage)?;
2253    let mut child = AccountLookupChild {
2254        child: Some(child),
2255        completed: false,
2256    };
2257    let mut stdout = child
2258        .child_mut()?
2259        .stdout
2260        .take()
2261        .ok_or(CredentialStoreError::UnsafeStorage)?;
2262    rustix::fs::fcntl_setfl(&stdout, OFlags::NONBLOCK)
2263        .map_err(|_| CredentialStoreError::UnsafeStorage)?;
2264    let started = Instant::now();
2265    let mut output = Vec::with_capacity(256);
2266    let status = loop {
2267        let _ = read_bounded_nonblocking(&mut stdout, &mut output, max_bytes)?;
2268        if let Some(status) = child
2269            .child_mut()?
2270            .try_wait()
2271            .map_err(|_| CredentialStoreError::UnsafeStorage)?
2272        {
2273            break status;
2274        }
2275        if started.elapsed() >= timeout {
2276            return Err(CredentialStoreError::UnsafeStorage);
2277        }
2278        std::thread::sleep(ACCOUNT_LOOKUP_POLL_INTERVAL.min(timeout));
2279    };
2280    let reached_eof = read_bounded_nonblocking(&mut stdout, &mut output, max_bytes)?;
2281    if !status.success() || !reached_eof {
2282        return Err(CredentialStoreError::UnsafeStorage);
2283    }
2284    child.completed = true;
2285    Ok(output)
2286}
2287
2288#[cfg(any(target_os = "linux", target_os = "macos"))]
2289fn read_bounded_nonblocking(
2290    stdout: &mut std::process::ChildStdout,
2291    output: &mut Vec<u8>,
2292    max_bytes: usize,
2293) -> Result<bool, CredentialStoreError> {
2294    let mut chunk = [0_u8; 512];
2295    loop {
2296        match stdout.read(&mut chunk) {
2297            Ok(0) => return Ok(true),
2298            Ok(length) => {
2299                if output.len().saturating_add(length) > max_bytes {
2300                    return Err(CredentialStoreError::UnsafeStorage);
2301                }
2302                output.extend_from_slice(&chunk[..length]);
2303            }
2304            Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => return Ok(false),
2305            Err(_) => return Err(CredentialStoreError::UnsafeStorage),
2306        }
2307    }
2308}
2309
2310#[cfg(any(target_os = "linux", target_os = "macos"))]
2311struct AccountLookupChild {
2312    child: Option<std::process::Child>,
2313    completed: bool,
2314}
2315
2316#[cfg(any(target_os = "linux", target_os = "macos"))]
2317impl AccountLookupChild {
2318    fn child_mut(&mut self) -> Result<&mut std::process::Child, CredentialStoreError> {
2319        self.child
2320            .as_mut()
2321            .ok_or(CredentialStoreError::UnsafeStorage)
2322    }
2323}
2324
2325#[cfg(any(target_os = "linux", target_os = "macos"))]
2326impl Drop for AccountLookupChild {
2327    fn drop(&mut self) {
2328        if self.completed {
2329            return;
2330        }
2331        let Some(mut child) = self.child.take() else {
2332            return;
2333        };
2334        let process_group = rustix::process::Pid::from_child(&child);
2335        let _ = rustix::process::kill_process_group(process_group, rustix::process::Signal::KILL);
2336        let _ = child.kill();
2337        let deadline = Instant::now() + ACCOUNT_LOOKUP_CLEANUP_DEADLINE;
2338        loop {
2339            match child.try_wait() {
2340                Ok(Some(_)) => return,
2341                Ok(None) if Instant::now() < deadline => {
2342                    std::thread::sleep(ACCOUNT_LOOKUP_POLL_INTERVAL)
2343                }
2344                _ => break,
2345            }
2346        }
2347        let _ = std::thread::Builder::new()
2348            .name("steadymux-account-lookup-reaper".to_owned())
2349            .spawn(move || {
2350                let _ = child.wait();
2351            });
2352    }
2353}
2354
2355#[cfg(target_os = "macos")]
2356fn parse_account_home(bytes: &[u8], expected_uid: u32) -> Result<PathBuf, CredentialStoreError> {
2357    let text = std::str::from_utf8(bytes).map_err(|_| CredentialStoreError::UnsafeStorage)?;
2358    let mut observed_uid = None;
2359    let mut home = None;
2360    for line in text.lines() {
2361        let Some((key, value)) = line.split_once(':') else {
2362            continue;
2363        };
2364        match key.trim() {
2365            "uid" if observed_uid.is_none() => {
2366                observed_uid = Some(
2367                    value
2368                        .trim()
2369                        .parse::<u32>()
2370                        .map_err(|_| CredentialStoreError::UnsafeStorage)?,
2371                );
2372            }
2373            "uid" => return Err(CredentialStoreError::UnsafeStorage),
2374            "dir" if home.is_none() => home = Some(PathBuf::from(value.trim())),
2375            "dir" => return Err(CredentialStoreError::UnsafeStorage),
2376            _ => {}
2377        }
2378    }
2379    let home = home.ok_or(CredentialStoreError::UnsafeStorage)?;
2380    if observed_uid != Some(expected_uid) {
2381        return Err(CredentialStoreError::UnsafeStorage);
2382    }
2383    validate_absolute_path(&home)?;
2384    Ok(home)
2385}
2386
2387#[cfg(target_os = "linux")]
2388fn parse_linux_account_home(
2389    bytes: &[u8],
2390    expected_uid: u32,
2391) -> Result<PathBuf, CredentialStoreError> {
2392    use std::ffi::OsStr;
2393    use std::os::unix::ffi::OsStrExt as _;
2394
2395    let mut records = bytes
2396        .split(|byte| *byte == b'\n')
2397        .filter(|record| !record.is_empty());
2398    let record = records.next().ok_or(CredentialStoreError::UnsafeStorage)?;
2399    if records.next().is_some() {
2400        return Err(CredentialStoreError::UnsafeStorage);
2401    }
2402    let fields = record.split(|byte| *byte == b':').collect::<Vec<_>>();
2403    if fields.len() != 7 {
2404        return Err(CredentialStoreError::UnsafeStorage);
2405    }
2406    let uid = std::str::from_utf8(fields[2])
2407        .map_err(|_| CredentialStoreError::UnsafeStorage)?
2408        .parse::<u32>()
2409        .map_err(|_| CredentialStoreError::UnsafeStorage)?;
2410    if uid != expected_uid || fields[5].is_empty() || fields[5].contains(&0) {
2411        return Err(CredentialStoreError::UnsafeStorage);
2412    }
2413    let home = PathBuf::from(OsStr::from_bytes(fields[5]));
2414    validate_absolute_path(&home)?;
2415    Ok(home)
2416}
2417
2418fn generate_unused_id(entries: &File) -> Result<String, CredentialStoreError> {
2419    for _ in 0..8 {
2420        let mut random = [0_u8; 16];
2421        getrandom::fill(&mut random).map_err(|_| CredentialStoreError::CommitAmbiguous)?;
2422        let id = random
2423            .iter()
2424            .fold(String::with_capacity(32), |mut output, byte| {
2425                use std::fmt::Write as _;
2426                let _ = write!(output, "{byte:02x}");
2427                output
2428            });
2429        match statat(entries, &id, AtFlags::SYMLINK_NOFOLLOW) {
2430            Ok(_) => continue,
2431            Err(error) if error == rustix::io::Errno::NOENT => return Ok(id),
2432            Err(_) => return Err(CredentialStoreError::UnsafeStorage),
2433        }
2434    }
2435    Err(CredentialStoreError::CapacityExhausted)
2436}
2437
2438fn recover_add_staging(entries: &File) -> Result<(), CredentialStoreError> {
2439    let directory = Dir::read_from(entries).map_err(|_| CredentialStoreError::UnsafeStorage)?;
2440    let mut staging = Vec::new();
2441    for item in directory {
2442        let item = item.map_err(|_| CredentialStoreError::UnsafeStorage)?;
2443        let name = item
2444            .file_name()
2445            .to_str()
2446            .map_err(|_| CredentialStoreError::UnsafeStorage)?;
2447        if matches!(name, "." | "..") || !name.starts_with('.') {
2448            continue;
2449        }
2450        if !name.starts_with(".staging-") || !valid_staging_name(name) {
2451            if valid_staging_name(name) {
2452                return Err(CredentialStoreError::RecoveryRequired);
2453            }
2454            return Err(CredentialStoreError::CorruptState);
2455        }
2456        staging.push(name.to_owned());
2457    }
2458    for name in staging {
2459        remove_entry_directory(entries, &name)?;
2460    }
2461    if !staging_is_empty(entries)? {
2462        return Err(CredentialStoreError::CorruptState);
2463    }
2464    durable_sync(entries)
2465}
2466
2467fn cleanup_failed_add(entries: &File, staged: &File, staging: &str, committed: bool) {
2468    if committed {
2469        return;
2470    }
2471    let _ = unlinkat(staged, SECRET_FILE, AtFlags::empty());
2472    let _ = unlinkat(staged, METADATA_FILE, AtFlags::empty());
2473    let _ = unlinkat(entries, staging, AtFlags::REMOVEDIR);
2474}
2475
2476fn remove_entry_directory(entries: &File, name: &str) -> Result<(), CredentialStoreError> {
2477    let directory = open_private_directory(entries, name)?;
2478    let directory_device =
2479        device(&fstat(&directory).map_err(|_| CredentialStoreError::UnsafeStorage)?)?;
2480    let contents = Dir::read_from(&directory).map_err(|_| CredentialStoreError::UnsafeStorage)?;
2481    for item in contents {
2482        let item = item.map_err(|_| CredentialStoreError::UnsafeStorage)?;
2483        let child = item
2484            .file_name()
2485            .to_str()
2486            .map_err(|_| CredentialStoreError::UnsafeStorage)?;
2487        if matches!(child, "." | "..") {
2488            continue;
2489        }
2490        if !matches!(child, SECRET_FILE | METADATA_FILE) {
2491            return Err(CredentialStoreError::CorruptState);
2492        }
2493        let child_stat = statat(&directory, child, AtFlags::SYMLINK_NOFOLLOW)
2494            .map_err(|_| CredentialStoreError::UnsafeStorage)?;
2495        validate_regular(&child_stat, directory_device, None)?;
2496    }
2497    for file in [SECRET_FILE, METADATA_FILE] {
2498        match unlinkat(&directory, file, AtFlags::empty()) {
2499            Ok(()) => {}
2500            Err(error) if error == rustix::io::Errno::NOENT => {}
2501            Err(_) => return Err(CredentialStoreError::UnsafeStorage),
2502        }
2503    }
2504    let mut remaining =
2505        Dir::read_from(&directory).map_err(|_| CredentialStoreError::UnsafeStorage)?;
2506    if remaining.any(|entry| {
2507        entry
2508            .ok()
2509            .and_then(|entry| entry.file_name().to_str().ok().map(str::to_owned))
2510            .is_none_or(|entry| !matches!(entry.as_str(), "." | ".."))
2511    }) {
2512        return Err(CredentialStoreError::CorruptState);
2513    }
2514    unlinkat(entries, name, AtFlags::REMOVEDIR).map_err(|_| CredentialStoreError::UnsafeStorage)
2515}
2516
2517fn valid_staging_name(name: &str) -> bool {
2518    if let Some(id) = name.strip_prefix(".staging-") {
2519        return valid_id(id);
2520    }
2521    for prefix in [".rotate-", ".remove-"] {
2522        if let Some(remainder) = name.strip_prefix(prefix)
2523            && let Some((id, generation)) = remainder.rsplit_once('-')
2524        {
2525            return valid_id(id)
2526                && generation
2527                    .parse::<u64>()
2528                    .is_ok_and(|generation| generation > 0);
2529        }
2530    }
2531    false
2532}
2533
2534fn staging_is_empty(entries: &File) -> Result<bool, CredentialStoreError> {
2535    let directory = Dir::read_from(entries).map_err(|_| CredentialStoreError::UnsafeStorage)?;
2536    for item in directory {
2537        let item = item.map_err(|_| CredentialStoreError::UnsafeStorage)?;
2538        if valid_staging_name(
2539            item.file_name()
2540                .to_str()
2541                .map_err(|_| CredentialStoreError::UnsafeStorage)?,
2542        ) {
2543            return Ok(false);
2544        }
2545    }
2546    Ok(true)
2547}
2548
2549fn valid_secret(value: &[u8]) -> bool {
2550    !value.is_empty()
2551        && value.len() <= MAX_SECRET_BYTES
2552        && !value.contains(&b'\r')
2553        && !value.contains(&b'\n')
2554}
2555
2556#[cfg(target_os = "macos")]
2557fn ensure_production_store(home: &Path) -> Result<(PathBuf, File), CredentialStoreError> {
2558    validate_absolute_path(home)?;
2559    let home_directory = open_path_directory(home)?;
2560    validate_owned_directory(
2561        &fstat(&home_directory).map_err(|_| CredentialStoreError::UnsafeStorage)?,
2562    )?;
2563    let library = open_owned_directory(&home_directory, "Library")?;
2564    let application_support = open_owned_directory(&library, "Application Support")?;
2565    let product = open_or_create_private_directory(&application_support, "SteadyMux")?;
2566    let root = open_or_create_private_directory(&product, "credentials-v1")?;
2567    Ok((
2568        home.join("Library")
2569            .join("Application Support")
2570            .join("SteadyMux")
2571            .join("credentials-v1"),
2572        root,
2573    ))
2574}
2575
2576#[cfg(target_os = "macos")]
2577fn open_production_store(home: &Path) -> Result<(PathBuf, File), CredentialStoreError> {
2578    validate_absolute_path(home)?;
2579    let home_directory = open_path_directory(home)?;
2580    validate_owned_directory(
2581        &fstat(&home_directory).map_err(|_| CredentialStoreError::UnsafeStorage)?,
2582    )?;
2583    let library = open_owned_directory(&home_directory, "Library")?;
2584    let application_support = open_owned_directory(&library, "Application Support")?;
2585    let product = open_private_directory(&application_support, "SteadyMux")?;
2586    let root = open_private_directory(&product, "credentials-v1")?;
2587    Ok((
2588        home.join("Library")
2589            .join("Application Support")
2590            .join("SteadyMux")
2591            .join("credentials-v1"),
2592        root,
2593    ))
2594}
2595
2596#[cfg(target_os = "linux")]
2597fn linux_data_home_path(
2598    home: &Path,
2599    xdg_data_home: Option<&std::ffi::OsStr>,
2600) -> Result<PathBuf, CredentialStoreError> {
2601    validate_absolute_path(home)?;
2602    let data_home = match xdg_data_home.filter(|value| !value.is_empty()) {
2603        Some(value) => PathBuf::from(value),
2604        None => home.join(".local").join("share"),
2605    };
2606    validate_absolute_path(&data_home)?;
2607    if data_home.as_os_str().as_encoded_bytes().len() > MAX_LINUX_DATA_HOME_BYTES {
2608        return Err(CredentialStoreError::UnsafeStorage);
2609    }
2610    Ok(data_home)
2611}
2612
2613#[cfg(target_os = "linux")]
2614fn linux_production_root_path(
2615    home: &Path,
2616    xdg_data_home: Option<&std::ffi::OsStr>,
2617) -> Result<PathBuf, CredentialStoreError> {
2618    Ok(linux_data_home_path(home, xdg_data_home)?
2619        .join("SteadyMux")
2620        .join("credentials-v1"))
2621}
2622
2623#[cfg(target_os = "linux")]
2624fn ensure_linux_production_store(
2625    home: &Path,
2626    xdg_data_home: Option<&std::ffi::OsStr>,
2627) -> Result<(PathBuf, File), CredentialStoreError> {
2628    let data_home_path = linux_data_home_path(home, xdg_data_home)?;
2629    let data_home = if xdg_data_home.is_some_and(|value| !value.is_empty()) {
2630        let directory = open_path_directory(&data_home_path)?;
2631        validate_owned_directory(
2632            &fstat(&directory).map_err(|_| CredentialStoreError::UnsafeStorage)?,
2633        )?;
2634        directory
2635    } else {
2636        let home_directory = open_path_directory(home)?;
2637        validate_owned_directory(
2638            &fstat(&home_directory).map_err(|_| CredentialStoreError::UnsafeStorage)?,
2639        )?;
2640        let local = open_or_create_owned_directory(&home_directory, ".local")?;
2641        open_or_create_owned_directory(&local, "share")?
2642    };
2643    let product = open_or_create_private_directory(&data_home, "SteadyMux")?;
2644    let root = open_or_create_private_directory(&product, "credentials-v1")?;
2645    Ok((linux_production_root_path(home, xdg_data_home)?, root))
2646}
2647
2648#[cfg(target_os = "linux")]
2649fn open_linux_production_store(
2650    home: &Path,
2651    xdg_data_home: Option<&std::ffi::OsStr>,
2652) -> Result<(PathBuf, File), CredentialStoreError> {
2653    let data_home_path = linux_data_home_path(home, xdg_data_home)?;
2654    let data_home = if xdg_data_home.is_some_and(|value| !value.is_empty()) {
2655        let directory = open_path_directory(&data_home_path)?;
2656        validate_owned_directory(
2657            &fstat(&directory).map_err(|_| CredentialStoreError::UnsafeStorage)?,
2658        )?;
2659        directory
2660    } else {
2661        let home_directory = open_path_directory(home)?;
2662        validate_owned_directory(
2663            &fstat(&home_directory).map_err(|_| CredentialStoreError::UnsafeStorage)?,
2664        )?;
2665        let local = open_owned_directory(&home_directory, ".local")?;
2666        open_owned_directory(&local, "share")?
2667    };
2668    let product = open_private_directory(&data_home, "SteadyMux")?;
2669    let root = open_private_directory(&product, "credentials-v1")?;
2670    Ok((linux_production_root_path(home, xdg_data_home)?, root))
2671}
2672
2673#[cfg(test)]
2674fn ensure_private_components(
2675    boundary: &File,
2676    relative: &Path,
2677) -> Result<File, CredentialStoreError> {
2678    if relative.as_os_str().is_empty()
2679        || relative
2680            .components()
2681            .any(|component| !matches!(component, Component::Normal(_)))
2682    {
2683        return Err(CredentialStoreError::UnsafeStorage);
2684    }
2685    let mut current = boundary
2686        .try_clone()
2687        .map_err(|_| CredentialStoreError::UnsafeStorage)?;
2688    for component in relative.components() {
2689        let Component::Normal(name) = component else {
2690            return Err(CredentialStoreError::UnsafeStorage);
2691        };
2692        current = open_or_create_owned_directory(&current, name)?;
2693    }
2694    Ok(current)
2695}
2696
2697#[cfg(any(test, target_os = "linux"))]
2698fn open_or_create_owned_directory(
2699    parent: &File,
2700    name: impl AsRef<Path>,
2701) -> Result<File, CredentialStoreError> {
2702    let name = name.as_ref();
2703    match open_owned_directory(parent, name) {
2704        Ok(directory) => Ok(directory),
2705        Err(CredentialStoreError::NotFound) => {
2706            mkdirat(parent, name, Mode::from_raw_mode(0o700))
2707                .map_err(|_| CredentialStoreError::UnsafeStorage)?;
2708            let directory = open_private_directory(parent, name)?;
2709            durable_sync(parent)?;
2710            Ok(directory)
2711        }
2712        Err(error) => Err(error),
2713    }
2714}
2715
2716fn open_owned_directory(
2717    parent: &File,
2718    name: impl AsRef<Path>,
2719) -> Result<File, CredentialStoreError> {
2720    let directory = File::from(strict_openat(
2721        parent,
2722        name,
2723        OFlags::RDONLY | OFlags::DIRECTORY | OFlags::NOFOLLOW | OFlags::CLOEXEC,
2724        Mode::empty(),
2725    )?);
2726    let parent_stat = fstat(parent).map_err(|_| CredentialStoreError::UnsafeStorage)?;
2727    let directory_stat = fstat(&directory).map_err(|_| CredentialStoreError::UnsafeStorage)?;
2728    validate_owned_directory(&directory_stat)?;
2729    if device(&directory_stat)? != device(&parent_stat)? {
2730        return Err(CredentialStoreError::UnsafeStorage);
2731    }
2732    Ok(directory)
2733}
2734
2735fn validate_owned_directory(stat: &rustix::fs::Stat) -> Result<(), CredentialStoreError> {
2736    if FileType::from_raw_mode(stat.st_mode) != FileType::Directory
2737        || stat.st_uid != geteuid().as_raw()
2738        || stat.st_mode & 0o022 != 0
2739    {
2740        return Err(CredentialStoreError::UnsafeStorage);
2741    }
2742    Ok(())
2743}
2744
2745fn validate_absolute_path(path: &Path) -> Result<(), CredentialStoreError> {
2746    if !path.is_absolute()
2747        || path.as_os_str().as_encoded_bytes().contains(&0)
2748        || path
2749            .components()
2750            .any(|component| matches!(component, Component::ParentDir | Component::CurDir))
2751    {
2752        return Err(CredentialStoreError::UnsafeStorage);
2753    }
2754    Ok(())
2755}
2756
2757fn open_path_directory(path: &Path) -> Result<File, CredentialStoreError> {
2758    validate_absolute_path(path)?;
2759    let mut current = File::from(
2760        open(
2761            "/",
2762            OFlags::RDONLY | OFlags::DIRECTORY | OFlags::NOFOLLOW | OFlags::CLOEXEC,
2763            Mode::empty(),
2764        )
2765        .map_err(|_| CredentialStoreError::UnsafeStorage)?,
2766    );
2767    for component in path.components() {
2768        let Component::Normal(name) = component else {
2769            continue;
2770        };
2771        current = File::from(strict_openat(
2772            &current,
2773            name,
2774            OFlags::RDONLY | OFlags::DIRECTORY | OFlags::NOFOLLOW | OFlags::CLOEXEC,
2775            Mode::empty(),
2776        )?);
2777    }
2778    Ok(current)
2779}
2780
2781fn open_or_create_private_directory(
2782    parent: &File,
2783    name: impl AsRef<Path>,
2784) -> Result<File, CredentialStoreError> {
2785    let name = name.as_ref();
2786    match open_private_directory(parent, name) {
2787        Ok(directory) => Ok(directory),
2788        Err(CredentialStoreError::NotFound) => {
2789            mkdirat(parent, name, Mode::from_raw_mode(0o700))
2790                .map_err(|_| CredentialStoreError::UnsafeStorage)?;
2791            let directory = open_private_directory(parent, name)?;
2792            durable_sync(parent)?;
2793            Ok(directory)
2794        }
2795        Err(error) => Err(error),
2796    }
2797}
2798
2799fn open_private_directory(
2800    parent: &File,
2801    name: impl AsRef<Path>,
2802) -> Result<File, CredentialStoreError> {
2803    let directory = File::from(strict_openat(
2804        parent,
2805        name,
2806        OFlags::RDONLY | OFlags::DIRECTORY | OFlags::NOFOLLOW | OFlags::CLOEXEC,
2807        Mode::empty(),
2808    )?);
2809    let parent_stat = fstat(parent).map_err(|_| CredentialStoreError::UnsafeStorage)?;
2810    let directory_stat = fstat(&directory).map_err(|_| CredentialStoreError::UnsafeStorage)?;
2811    validate_directory(&directory_stat)?;
2812    if device(&directory_stat)? != device(&parent_stat)? {
2813        return Err(CredentialStoreError::UnsafeStorage);
2814    }
2815    Ok(directory)
2816}
2817
2818fn write_new_file(parent: &File, name: &str, bytes: &[u8]) -> Result<(), CredentialStoreError> {
2819    let fd = strict_openat(
2820        parent,
2821        name,
2822        OFlags::WRONLY | OFlags::CREATE | OFlags::EXCL | OFlags::NOFOLLOW | OFlags::CLOEXEC,
2823        Mode::from_raw_mode(0o600),
2824    )?;
2825    fchmod(&fd, Mode::from_raw_mode(0o600)).map_err(|_| CredentialStoreError::UnsafeStorage)?;
2826    let mut file = File::from(fd);
2827    file.write_all(bytes)
2828        .map_err(|_| CredentialStoreError::CommitAmbiguous)?;
2829    file.flush()
2830        .map_err(|_| CredentialStoreError::CommitAmbiguous)?;
2831    durable_sync(&file)?;
2832    let parent_stat = fstat(parent).map_err(|_| CredentialStoreError::UnsafeStorage)?;
2833    let file_stat = fstat(&file).map_err(|_| CredentialStoreError::UnsafeStorage)?;
2834    let path_stat = statat(parent, name, AtFlags::SYMLINK_NOFOLLOW)
2835        .map_err(|_| CredentialStoreError::UnsafeStorage)?;
2836    validate_regular(&file_stat, device(&parent_stat)?, Some(bytes.len()))?;
2837    validate_regular(&path_stat, device(&parent_stat)?, Some(bytes.len()))?;
2838    if identity(&file_stat)? != identity(&path_stat)? {
2839        return Err(CredentialStoreError::UnsafeStorage);
2840    }
2841    Ok(())
2842}
2843
2844fn read_regular_file(
2845    parent: &File,
2846    name: &str,
2847    limit: usize,
2848    secret: bool,
2849) -> Result<Vec<u8>, CredentialStoreError> {
2850    read_regular_file_with_identity(parent, name, limit, secret).map(|(bytes, _)| bytes)
2851}
2852
2853fn read_regular_file_with_identity(
2854    parent: &File,
2855    name: &str,
2856    limit: usize,
2857    secret: bool,
2858) -> Result<(Vec<u8>, FileIdentity), CredentialStoreError> {
2859    let file = File::from(strict_openat(
2860        parent,
2861        name,
2862        OFlags::RDONLY | OFlags::NOFOLLOW | OFlags::CLOEXEC,
2863        Mode::empty(),
2864    )?);
2865    let parent_stat = fstat(parent).map_err(|_| CredentialStoreError::UnsafeStorage)?;
2866    let before = fstat(&file).map_err(|_| CredentialStoreError::UnsafeStorage)?;
2867    validate_regular(&before, device(&parent_stat)?, None)?;
2868    let path_stat = statat(parent, name, AtFlags::SYMLINK_NOFOLLOW)
2869        .map_err(|_| CredentialStoreError::UnsafeStorage)?;
2870    if identity(&before)? != identity(&path_stat)? {
2871        return Err(CredentialStoreError::UnsafeStorage);
2872    }
2873    let mut bytes = Vec::new();
2874    (&file)
2875        .take((limit + 1) as u64)
2876        .read_to_end(&mut bytes)
2877        .map_err(|_| CredentialStoreError::CorruptState)?;
2878    let after = fstat(&file).map_err(|_| CredentialStoreError::UnsafeStorage)?;
2879    let final_path_stat = statat(parent, name, AtFlags::SYMLINK_NOFOLLOW)
2880        .map_err(|_| CredentialStoreError::UnsafeStorage)?;
2881    validate_regular(&after, device(&parent_stat)?, Some(bytes.len()))?;
2882    if bytes.len() > limit
2883        || bytes.is_empty()
2884        || identity(&before)? != identity(&after)?
2885        || identity(&before)? != identity(&final_path_stat)?
2886        || (secret && after.st_mode & 0o777 != 0o600)
2887    {
2888        return Err(CredentialStoreError::CorruptState);
2889    }
2890    Ok((bytes, identity_with_birth(&after, parent, name)?))
2891}
2892
2893fn rename_replace(
2894    parent: &File,
2895    source: &str,
2896    destination: &str,
2897) -> Result<(), CredentialStoreError> {
2898    renameat(parent, source, parent, destination).map_err(|_| CredentialStoreError::CommitAmbiguous)
2899}
2900
2901#[cfg(any(target_os = "linux", target_os = "macos"))]
2902fn rename_new_entry(entries: &File, staging: &str, id: &str) -> Result<(), CredentialStoreError> {
2903    renameat_with(entries, staging, entries, id, RenameFlags::NOREPLACE)
2904        .map_err(|_| CredentialStoreError::CommitAmbiguous)
2905}
2906
2907#[cfg(not(any(target_os = "linux", target_os = "macos")))]
2908fn rename_new_entry(entries: &File, staging: &str, id: &str) -> Result<(), CredentialStoreError> {
2909    match statat(entries, id, AtFlags::SYMLINK_NOFOLLOW) {
2910        Err(error) if error == rustix::io::Errno::NOENT => {}
2911        _ => return Err(CredentialStoreError::CommitAmbiguous),
2912    }
2913    renameat(entries, staging, entries, id).map_err(|_| CredentialStoreError::CommitAmbiguous)
2914}
2915
2916#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
2917#[serde(deny_unknown_fields)]
2918struct BirthTime {
2919    seconds: i64,
2920    nanoseconds: u32,
2921}
2922
2923#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
2924#[serde(deny_unknown_fields)]
2925struct FileIdentity {
2926    device: u64,
2927    inode: u64,
2928    owner: u32,
2929    mode: u32,
2930    link_count: u64,
2931    #[serde(default)]
2932    birth_time: Option<BirthTime>,
2933}
2934
2935fn identity(stat: &rustix::fs::Stat) -> Result<FileIdentity, CredentialStoreError> {
2936    Ok(FileIdentity {
2937        device: device(stat)?,
2938        inode: stat.st_ino,
2939        owner: stat.st_uid,
2940        mode: mode_bits(stat),
2941        link_count: link_count(stat),
2942        birth_time: birth_time_from_stat(stat),
2943    })
2944}
2945
2946#[cfg(target_os = "linux")]
2947fn birth_time_from_stat(_stat: &rustix::fs::Stat) -> Option<BirthTime> {
2948    None
2949}
2950
2951#[cfg(target_os = "macos")]
2952fn birth_time_from_stat(stat: &rustix::fs::Stat) -> Option<BirthTime> {
2953    Some(BirthTime {
2954        seconds: stat.st_birthtime,
2955        nanoseconds: u32::try_from(stat.st_birthtime_nsec).ok()?,
2956    })
2957}
2958
2959/// Computes the full journal identity for a file opened from `parent`/`name`.
2960/// Linux `Stat` carries no birth time, so the immutable `statx` birth time is
2961/// attached here with an exact-inode revalidation; macOS `Stat` already
2962/// exposes `st_birthtime`.
2963#[cfg(target_os = "linux")]
2964fn identity_with_birth(
2965    stat: &rustix::fs::Stat,
2966    parent: &File,
2967    name: &str,
2968) -> Result<FileIdentity, CredentialStoreError> {
2969    let mut file_identity = identity(stat)?;
2970    let statx = rustix::fs::statx(parent, name, AtFlags::SYMLINK_NOFOLLOW, StatxFlags::BTIME)
2971        .map_err(|_| CredentialStoreError::UnsafeStorage)?;
2972    if statx.stx_ino != file_identity.inode {
2973        return Err(CredentialStoreError::UnsafeStorage);
2974    }
2975    if statx.stx_mask & StatxFlags::BTIME.bits() != 0 {
2976        file_identity.birth_time = Some(BirthTime {
2977            seconds: statx.stx_btime.tv_sec,
2978            nanoseconds: statx.stx_btime.tv_nsec,
2979        });
2980    }
2981    Ok(file_identity)
2982}
2983
2984#[cfg(target_os = "macos")]
2985fn identity_with_birth(
2986    stat: &rustix::fs::Stat,
2987    _parent: &File,
2988    _name: &str,
2989) -> Result<FileIdentity, CredentialStoreError> {
2990    identity(stat)
2991}
2992
2993/// Computes the full journal identity for an already-open descriptor without
2994/// consulting its pathname. Removal holds the descriptor across a possible
2995/// same-user path substitution, so the immutable birth time must come from the
2996/// descriptor itself (`statx` with `AT_EMPTY_PATH` on Linux, `st_birthtime`
2997/// on macOS) rather than from a path that may no longer name the same file.
2998#[cfg(target_os = "linux")]
2999fn identity_with_birth_fd(
3000    stat: &rustix::fs::Stat,
3001    fd: &File,
3002) -> Result<FileIdentity, CredentialStoreError> {
3003    let mut file_identity = identity(stat)?;
3004    let statx = rustix::fs::statx(fd, "", AtFlags::EMPTY_PATH, StatxFlags::BTIME)
3005        .map_err(|_| CredentialStoreError::UnsafeStorage)?;
3006    if statx.stx_ino != file_identity.inode {
3007        return Err(CredentialStoreError::UnsafeStorage);
3008    }
3009    if statx.stx_mask & StatxFlags::BTIME.bits() != 0 {
3010        file_identity.birth_time = Some(BirthTime {
3011            seconds: statx.stx_btime.tv_sec,
3012            nanoseconds: statx.stx_btime.tv_nsec,
3013        });
3014    }
3015    Ok(file_identity)
3016}
3017
3018#[cfg(target_os = "macos")]
3019fn identity_with_birth_fd(
3020    stat: &rustix::fs::Stat,
3021    _fd: &File,
3022) -> Result<FileIdentity, CredentialStoreError> {
3023    identity(stat)
3024}
3025
3026#[cfg(target_os = "linux")]
3027fn mode_bits(stat: &rustix::fs::Stat) -> u32 {
3028    stat.st_mode & 0o777
3029}
3030
3031#[cfg(not(target_os = "linux"))]
3032fn mode_bits(stat: &rustix::fs::Stat) -> u32 {
3033    u32::from(stat.st_mode & 0o777)
3034}
3035
3036#[cfg(target_os = "linux")]
3037fn link_count(stat: &rustix::fs::Stat) -> u64 {
3038    stat.st_nlink
3039}
3040
3041#[cfg(not(target_os = "linux"))]
3042fn link_count(stat: &rustix::fs::Stat) -> u64 {
3043    u64::from(stat.st_nlink)
3044}
3045
3046fn same_filesystem_object(left: FileIdentity, right: FileIdentity) -> bool {
3047    left.device == right.device && left.inode == right.inode
3048}
3049
3050fn validate_directory(stat: &rustix::fs::Stat) -> Result<(), CredentialStoreError> {
3051    if FileType::from_raw_mode(stat.st_mode) != FileType::Directory
3052        || stat.st_uid != geteuid().as_raw()
3053        || stat.st_mode & 0o777 != 0o700
3054    {
3055        return Err(CredentialStoreError::UnsafeStorage);
3056    }
3057    Ok(())
3058}
3059
3060fn validate_regular(
3061    stat: &rustix::fs::Stat,
3062    device: u64,
3063    expected_len: Option<usize>,
3064) -> Result<(), CredentialStoreError> {
3065    if FileType::from_raw_mode(stat.st_mode) != FileType::RegularFile
3066        || stat.st_uid != geteuid().as_raw()
3067        || self::device(stat)? != device
3068        || stat.st_nlink != 1
3069        || stat.st_mode & 0o777 != 0o600
3070        || expected_len.is_some_and(|length| usize::try_from(stat.st_size).ok() != Some(length))
3071    {
3072        return Err(CredentialStoreError::UnsafeStorage);
3073    }
3074    Ok(())
3075}
3076
3077fn device(stat: &rustix::fs::Stat) -> Result<u64, CredentialStoreError> {
3078    #[cfg(target_os = "linux")]
3079    {
3080        Ok(stat.st_dev)
3081    }
3082    #[cfg(not(target_os = "linux"))]
3083    {
3084        u64::try_from(stat.st_dev).map_err(|_| CredentialStoreError::UnsafeStorage)
3085    }
3086}
3087
3088#[cfg(target_os = "macos")]
3089fn durable_sync(file: &impl AsFd) -> Result<(), CredentialStoreError> {
3090    rustix::fs::fcntl_fullfsync(file).map_err(|_| CredentialStoreError::CommitAmbiguous)
3091}
3092
3093#[cfg(not(target_os = "macos"))]
3094fn durable_sync(file: &impl AsFd) -> Result<(), CredentialStoreError> {
3095    rustix::fs::fsync(file).map_err(|_| CredentialStoreError::CommitAmbiguous)
3096}
3097
3098#[cfg(target_os = "linux")]
3099fn strict_openat(
3100    parent: &File,
3101    path: impl AsRef<Path>,
3102    flags: OFlags,
3103    mode: Mode,
3104) -> Result<rustix::fd::OwnedFd, CredentialStoreError> {
3105    openat2(
3106        parent,
3107        path.as_ref(),
3108        flags,
3109        mode,
3110        ResolveFlags::NO_SYMLINKS | ResolveFlags::BENEATH,
3111    )
3112    .map_err(map_open_error)
3113}
3114
3115#[cfg(not(target_os = "linux"))]
3116fn strict_openat(
3117    parent: &File,
3118    path: impl AsRef<Path>,
3119    flags: OFlags,
3120    mode: Mode,
3121) -> Result<rustix::fd::OwnedFd, CredentialStoreError> {
3122    openat(parent, path.as_ref(), flags, mode).map_err(map_open_error)
3123}
3124
3125fn map_open_error(error: rustix::io::Errno) -> CredentialStoreError {
3126    if error == rustix::io::Errno::NOENT {
3127        CredentialStoreError::NotFound
3128    } else {
3129        CredentialStoreError::UnsafeStorage
3130    }
3131}
3132
3133#[cfg(test)]
3134mod tests {
3135    use super::*;
3136    use std::collections::BTreeMap;
3137    use std::os::unix::fs::{MetadataExt, PermissionsExt, symlink};
3138    use std::sync::{Arc, Barrier};
3139
3140    #[test]
3141    fn explicit_test_root_reopen_enforces_owner_only_store_invariants() {
3142        let temporary = tempfile::tempdir().expect("temporary root");
3143        let root = temporary.path().join("credentials-v1");
3144        let store = LocalCredentialStore::at_test_root(&root).expect("create explicit store");
3145        let metadata = store
3146            .add("openrouter", "primary", b"secret-canary")
3147            .expect("add credential");
3148        drop(store);
3149
3150        let entry = root.join(ENTRIES_DIRECTORY).join(&metadata.id);
3151        for directory in [&root, &root.join(ENTRIES_DIRECTORY), &entry] {
3152            let mode = std::fs::metadata(directory)
3153                .expect("directory metadata")
3154                .mode()
3155                & 0o777;
3156            assert_eq!(mode, 0o700);
3157        }
3158        for file in [entry.join(SECRET_FILE), entry.join(METADATA_FILE)] {
3159            let file_metadata = std::fs::metadata(file).expect("credential file metadata");
3160            assert_eq!(file_metadata.mode() & 0o777, 0o600);
3161            assert_eq!(file_metadata.nlink(), 1);
3162        }
3163
3164        LocalCredentialStore::at_existing_test_root(&root).expect("reopen private store");
3165        std::fs::set_permissions(
3166            root.join(ENTRIES_DIRECTORY),
3167            std::fs::Permissions::from_mode(0o755),
3168        )
3169        .expect("relax entries mode");
3170        assert!(matches!(
3171            LocalCredentialStore::at_existing_test_root(&root),
3172            Err(CredentialStoreError::UnsafeStorage)
3173        ));
3174        std::fs::set_permissions(
3175            root.join(ENTRIES_DIRECTORY),
3176            std::fs::Permissions::from_mode(0o700),
3177        )
3178        .expect("restore entries mode");
3179        let linked_root = temporary.path().join("linked-credentials-v1");
3180        symlink(&root, &linked_root).expect("symlink store root");
3181        assert!(matches!(
3182            LocalCredentialStore::at_existing_test_root(&linked_root),
3183            Err(CredentialStoreError::UnsafeStorage)
3184        ));
3185    }
3186
3187    #[test]
3188    fn lock_retries_transient_flock_interruption_and_succeeds() {
3189        let temporary = tempfile::tempdir().expect("temporary root");
3190        let root = temporary.path().join("provider-credentials-v1");
3191        let store = LocalCredentialStore::at_test_root(&root).expect("open store");
3192        store.flock_eintr_injections.store(8, Ordering::SeqCst);
3193        let metadata = store
3194            .add("openrouter", "primary", b"secret-canary")
3195            .expect("add credential after transient flock interruption");
3196        assert_eq!(
3197            store.flock_eintr_injections.load(Ordering::SeqCst),
3198            0,
3199            "injected EINTR interruption was consumed by the retry loop"
3200        );
3201        assert_eq!(
3202            store
3203                .resolve(&metadata.credential_ref)
3204                .expect("resolve after interrupted flock retry")
3205                .as_slice(),
3206            b"secret-canary"
3207        );
3208    }
3209
3210    #[test]
3211    fn lock_exhausts_interrupted_retry_bound_and_fails_closed() {
3212        let temporary = tempfile::tempdir().expect("temporary root");
3213        let root = temporary.path().join("provider-credentials-v1");
3214        let store = LocalCredentialStore::at_test_root(&root).expect("open store");
3215        store
3216            .flock_eintr_injections
3217            .store(FLOCK_EINTR_RETRY_BOUND + 1, Ordering::SeqCst);
3218        assert!(matches!(
3219            store.list(),
3220            Err(CredentialStoreError::LockUnavailable)
3221        ));
3222        assert_eq!(
3223            store.flock_eintr_injections.load(Ordering::SeqCst),
3224            0,
3225            "persistent interruptions consumed the retry bound and failed closed"
3226        );
3227    }
3228
3229    #[test]
3230    fn add_list_status_and_resolve_keep_secret_out_of_metadata() {
3231        let temporary = tempfile::tempdir().expect("temporary root");
3232        let root = temporary.path().join("provider-credentials-v1");
3233        let store = LocalCredentialStore::at_test_root(&root).expect("open store");
3234        let metadata = store
3235            .add("openrouter", "primary", b"secret-canary")
3236            .expect("add credential");
3237
3238        assert!(metadata.credential_ref.starts_with(REFERENCE_PREFIX));
3239        assert_eq!(store.list().expect("list"), vec![metadata.clone()]);
3240        assert_eq!(
3241            store
3242                .status(&metadata.credential_ref)
3243                .expect("status")
3244                .status,
3245            "available"
3246        );
3247        assert_eq!(
3248            store
3249                .resolve(&metadata.credential_ref)
3250                .expect("resolve")
3251                .as_slice(),
3252            b"secret-canary"
3253        );
3254
3255        let entry = root.join(ENTRIES_DIRECTORY).join(&metadata.id);
3256        let encoded = std::fs::read(entry.join(METADATA_FILE)).expect("metadata");
3257        assert!(
3258            !encoded
3259                .windows(b"secret-canary".len())
3260                .any(|value| value == b"secret-canary")
3261        );
3262        assert_eq!(
3263            std::fs::metadata(entry.join(SECRET_FILE))
3264                .expect("secret metadata")
3265                .mode()
3266                & 0o777,
3267            0o600
3268        );
3269        assert_eq!(
3270            std::fs::metadata(root).expect("root metadata").mode() & 0o777,
3271            0o700
3272        );
3273    }
3274
3275    #[test]
3276    fn inventory_guard_keeps_status_and_availability_on_one_locked_snapshot() {
3277        let temporary = tempfile::tempdir().expect("temporary root");
3278        let root = temporary.path().join("provider-credentials-v1");
3279        let store = LocalCredentialStore::at_test_root(&root).expect("open store");
3280        let metadata = store
3281            .add("openrouter", "primary", b"secret-canary")
3282            .expect("add credential");
3283        let guard = store.lock_inventory().expect("inventory guard");
3284        guard
3285            .verify_available(&metadata.credential_ref)
3286            .expect("descriptor availability");
3287        assert_eq!(
3288            guard
3289                .status(&metadata.credential_ref)
3290                .expect("guarded status")
3291                .status,
3292            "available"
3293        );
3294        assert!(matches!(
3295            store.remove_preview(
3296                &metadata.credential_ref,
3297                current_epoch_seconds().unwrap() + 60
3298            ),
3299            Err(CredentialStoreError::LockUnavailable)
3300        ));
3301        assert!(matches!(
3302            store.rotate_preview(
3303                &metadata.credential_ref,
3304                current_epoch_seconds().unwrap() + 60
3305            ),
3306            Err(CredentialStoreError::LockUnavailable)
3307        ));
3308        drop(guard);
3309        assert!(
3310            store
3311                .remove_preview(
3312                    &metadata.credential_ref,
3313                    current_epoch_seconds().unwrap() + 60
3314                )
3315                .is_ok()
3316        );
3317    }
3318
3319    #[test]
3320    fn authoritative_projected_bindings_block_remove_and_are_reported_for_rotation() {
3321        let temporary = tempfile::tempdir().expect("temporary root");
3322        let root = temporary.path().join("provider-credentials-v1");
3323        let store = LocalCredentialStore::at_test_root(&root).expect("open store");
3324        let metadata = store
3325            .add("openrouter", "primary", b"secret-canary")
3326            .expect("add credential");
3327        assert!(metadata.bound_phases.is_empty());
3328        assert!(metadata.bound_routes.is_empty());
3329
3330        let guard = store
3331            .lock_lifecycle_inventory()
3332            .expect("lifecycle inventory guard");
3333        let phases = vec!["openrouter_api".to_owned()];
3334        let routes = vec!["openrouter-primary".to_owned()];
3335        let expires = current_epoch_seconds().expect("clock") + 60;
3336        let removal = guard
3337            .remove_preview_with_bindings(&metadata.credential_ref, &phases, &routes, expires)
3338            .expect("projected removal preview");
3339        assert_eq!(removal.dependency_impact.bound_phases, phases);
3340        assert_eq!(removal.dependency_impact.bound_routes, routes);
3341        assert_eq!(removal.dependency_impact.binding_count, 2);
3342        assert!(!removal.dependency_impact.apply_allowed);
3343        assert_eq!(
3344            guard.remove_apply_with_bindings(
3345                &metadata.credential_ref,
3346                lifecycle_approval(&removal, REMOVE_CONFIRMATION),
3347                &phases,
3348                &routes,
3349            ),
3350            Err(CredentialStoreError::DependencyConflict)
3351        );
3352
3353        let rotation = guard
3354            .rotate_preview_with_bindings(&metadata.credential_ref, &phases, &routes, expires)
3355            .expect("projected rotation preview");
3356        assert_eq!(rotation.dependency_impact.bound_phases, phases);
3357        assert_eq!(rotation.dependency_impact.bound_routes, routes);
3358        assert_eq!(rotation.dependency_impact.binding_count, 2);
3359        assert!(rotation.dependency_impact.apply_allowed);
3360        assert!(matches!(
3361            store.rotate_preview(&metadata.credential_ref, expires),
3362            Err(CredentialStoreError::LockUnavailable)
3363        ));
3364        guard
3365            .verify_available(&metadata.credential_ref)
3366            .expect("blocked removal preserves availability");
3367        drop(guard);
3368        assert_eq!(
3369            store
3370                .resolve(&metadata.credential_ref)
3371                .expect("blocked removal preserves secret")
3372                .as_slice(),
3373            b"secret-canary"
3374        );
3375    }
3376
3377    #[test]
3378    fn rejects_newlines_oversize_values_and_secret_symlinks() {
3379        let temporary = tempfile::tempdir().expect("temporary root");
3380        let root = temporary.path().join("provider-credentials-v1");
3381        let store = LocalCredentialStore::at_test_root(&root).expect("open store");
3382        assert_eq!(
3383            store.add("openrouter", "primary", b"secret\n"),
3384            Err(CredentialStoreError::InvalidInput)
3385        );
3386        assert_eq!(
3387            store.add("openrouter", "primary", &vec![b'x'; MAX_SECRET_BYTES + 1]),
3388            Err(CredentialStoreError::InvalidInput)
3389        );
3390
3391        let metadata = store.add("openrouter", "primary", b"safe").expect("add");
3392        let secret = root
3393            .join(ENTRIES_DIRECTORY)
3394            .join(&metadata.id)
3395            .join(SECRET_FILE);
3396        std::fs::remove_file(&secret).expect("remove secret");
3397        symlink("/etc/passwd", &secret).expect("replace with symlink");
3398        assert!(matches!(
3399            store.resolve(&metadata.credential_ref),
3400            Err(CredentialStoreError::UnsafeStorage)
3401        ));
3402    }
3403
3404    #[test]
3405    fn rejects_an_intermediate_symlink_in_the_store_root() {
3406        let temporary = tempfile::tempdir().expect("temporary root");
3407        let actual_parent = temporary.path().join("actual");
3408        std::fs::create_dir(&actual_parent).expect("actual parent");
3409        std::fs::set_permissions(&actual_parent, std::fs::Permissions::from_mode(0o700))
3410            .expect("private actual parent");
3411        let linked_parent = temporary.path().join("linked");
3412        symlink(&actual_parent, &linked_parent).expect("intermediate symlink");
3413
3414        assert!(matches!(
3415            LocalCredentialStore::at_test_root(&linked_parent.join("credentials-v1")),
3416            Err(CredentialStoreError::UnsafeStorage)
3417        ));
3418        assert!(
3419            !actual_parent.join("credentials-v1").exists(),
3420            "rejection must occur before any escaped directory mutation"
3421        );
3422    }
3423
3424    #[test]
3425    fn rejects_relaxed_permissions_and_hardlinked_secret() {
3426        let temporary = tempfile::tempdir().expect("temporary root");
3427        let root = temporary.path().join("provider-credentials-v1");
3428        let store = LocalCredentialStore::at_test_root(&root).expect("open store");
3429        let metadata = store.add("ollama", "cloud", b"safe").expect("add");
3430        let secret = root
3431            .join(ENTRIES_DIRECTORY)
3432            .join(&metadata.id)
3433            .join(SECRET_FILE);
3434        let hardlink = temporary.path().join("copied-link");
3435        std::fs::hard_link(&secret, &hardlink).expect("hardlink");
3436        assert!(matches!(
3437            store.resolve(&metadata.credential_ref),
3438            Err(CredentialStoreError::UnsafeStorage)
3439        ));
3440        std::fs::remove_file(hardlink).expect("remove hardlink");
3441        std::fs::set_permissions(&secret, std::fs::Permissions::from_mode(0o644))
3442            .expect("relax mode");
3443        assert!(store.resolve(&metadata.credential_ref).is_err());
3444    }
3445
3446    #[test]
3447    fn rejects_hardlinked_metadata() {
3448        let temporary = tempfile::tempdir().expect("temporary root");
3449        let root = temporary.path().join("credentials-v1");
3450        let store = LocalCredentialStore::at_test_root(&root).expect("open store");
3451        let metadata = store.add("ollama", "cloud", b"safe").expect("add");
3452        let metadata_path = std::env::temp_dir()
3453            .canonicalize()
3454            .expect("canonical temporary root")
3455            .join(
3456                root.strip_prefix(std::env::temp_dir())
3457                    .expect("temporary relative root"),
3458            )
3459            .join(ENTRIES_DIRECTORY)
3460            .join(&metadata.id)
3461            .join(METADATA_FILE);
3462        let hardlink = temporary.path().join("metadata-link");
3463        std::fs::hard_link(&metadata_path, &hardlink).expect("hardlink metadata");
3464
3465        assert!(matches!(
3466            store.list(),
3467            Err(CredentialStoreError::UnsafeStorage)
3468        ));
3469    }
3470
3471    #[test]
3472    fn preview_reports_recovery_required_without_mutating_staging_or_store_bytes() {
3473        let temporary = tempfile::tempdir().expect("temporary root");
3474        let root = temporary.path().join("provider-credentials-v1");
3475        let store = LocalCredentialStore::at_test_root(&root).expect("open store");
3476        let metadata = store
3477            .add_at("openrouter", "primary", b"original", 90)
3478            .expect("add");
3479        let id = "0123456789abcdef0123456789abcdef";
3480        let staging = root.join(ENTRIES_DIRECTORY).join(format!(".staging-{id}"));
3481        std::fs::create_dir(&staging).expect("staging directory");
3482        std::fs::set_permissions(&staging, std::fs::Permissions::from_mode(0o700))
3483            .expect("private staging mode");
3484        std::fs::write(staging.join(SECRET_FILE), b"partial").expect("partial secret");
3485        std::fs::set_permissions(
3486            staging.join(SECRET_FILE),
3487            std::fs::Permissions::from_mode(0o600),
3488        )
3489        .expect("private secret mode");
3490
3491        let before = store_tree_snapshot(&root);
3492        assert!(matches!(
3493            store.rotate_preview_at(&metadata.credential_ref, 100, 200),
3494            Err(CredentialStoreError::RecoveryRequired)
3495        ));
3496        assert_eq!(store_tree_snapshot(&root), before);
3497        assert!(staging.exists());
3498    }
3499
3500    #[test]
3501    fn post_rename_failure_cleanup_does_not_delete_committed_files() {
3502        let temporary = tempfile::tempdir().expect("temporary root");
3503        let root = temporary.path().join("credentials-v1");
3504        let store = LocalCredentialStore::at_test_root(&root).expect("open store");
3505        let locked = store.lock().expect("lock store");
3506        let entries = open_or_create_private_directory(&locked.root, ENTRIES_DIRECTORY)
3507            .expect("entries directory");
3508        let id = "0123456789abcdef0123456789abcdef";
3509        let staging = format!(".staging-{id}");
3510        mkdirat(&entries, &staging, Mode::from_raw_mode(0o700)).expect("staging directory");
3511        let staged = open_private_directory(&entries, &staging).expect("open staging");
3512        write_new_file(&staged, SECRET_FILE, b"secret-canary").expect("secret file");
3513        write_new_file(&staged, METADATA_FILE, b"metadata-canary").expect("metadata file");
3514        rename_new_entry(&entries, &staging, id).expect("commit entry");
3515
3516        cleanup_failed_add(&entries, &staged, &staging, true);
3517
3518        let committed = root.join(ENTRIES_DIRECTORY).join(id);
3519        assert_eq!(
3520            std::fs::read(committed.join(SECRET_FILE)).expect("committed secret"),
3521            b"secret-canary"
3522        );
3523        assert_eq!(
3524            std::fs::read(committed.join(METADATA_FILE)).expect("committed metadata"),
3525            b"metadata-canary"
3526        );
3527    }
3528
3529    #[test]
3530    fn duplicate_provider_label_is_rejected_without_replacing_the_secret() {
3531        let temporary = tempfile::tempdir().expect("temporary root");
3532        let root = temporary.path().join("provider-credentials-v1");
3533        let store = LocalCredentialStore::at_test_root(&root).expect("open store");
3534        let metadata = store.add("openrouter", "primary", b"first").expect("add");
3535        assert_eq!(
3536            store.add("openrouter", "primary", b"second"),
3537            Err(CredentialStoreError::InvalidInput)
3538        );
3539        assert_eq!(
3540            store
3541                .resolve(&metadata.credential_ref)
3542                .expect("resolve")
3543                .as_slice(),
3544            b"first"
3545        );
3546    }
3547
3548    #[test]
3549    fn account_label_aliases_store_one_canonical_identity() {
3550        let temporary = tempfile::tempdir().expect("temporary root");
3551        let root = temporary.path().join("provider-credentials-v1");
3552        let store = LocalCredentialStore::at_test_root(&root).expect("open store");
3553        let metadata = store
3554            .add("openrouter", "  PRIMARY  ", b"first")
3555            .expect("canonicalized add");
3556        assert_eq!(metadata.label, "primary");
3557        assert_eq!(store.list().expect("list")[0].label, "primary");
3558        assert_eq!(
3559            store.add("openrouter", "Primary", b"second"),
3560            Err(CredentialStoreError::InvalidInput)
3561        );
3562        assert_eq!(
3563            store
3564                .resolve(&metadata.credential_ref)
3565                .expect("original secret remains")
3566                .as_slice(),
3567            b"first"
3568        );
3569    }
3570
3571    #[test]
3572    fn malformed_account_labels_fail_before_store_mutation() {
3573        let temporary = tempfile::tempdir().expect("temporary root");
3574        let root = temporary.path().join("provider-credentials-v1");
3575        let store = LocalCredentialStore::at_test_root(&root).expect("open store");
3576        let before = store_tree_snapshot(&root);
3577        let oversized = "a".repeat(MAX_LABEL_BYTES + 1);
3578        for label in [
3579            "",
3580            "   ",
3581            ".primary",
3582            "_primary",
3583            "-primary",
3584            "primary account",
3585            "primary/backup",
3586            "primary\\backup",
3587            "prímary",
3588            "primary\n",
3589            "primary\t",
3590            oversized.as_str(),
3591        ] {
3592            assert_eq!(
3593                store.add("openrouter", label, b"secret"),
3594                Err(CredentialStoreError::InvalidInput),
3595                "malformed label must fail closed: {label:?}"
3596            );
3597            assert_eq!(store_tree_snapshot(&root), before);
3598        }
3599        let canonical = store
3600            .add("openrouter", "  A.B_C-1  ", b"secret")
3601            .expect("conservative alphabet is accepted");
3602        assert_eq!(canonical.label, "a.b_c-1");
3603    }
3604
3605    #[test]
3606    fn legacy_noncanonical_label_reads_canonically_and_reserves_aliases() {
3607        let temporary = tempfile::tempdir().expect("temporary root");
3608        let root = temporary.path().join("provider-credentials-v1");
3609        let store = LocalCredentialStore::at_test_root(&root).expect("open store");
3610        let metadata = store
3611            .add("openrouter", "legacy", b"legacy-secret")
3612            .expect("add legacy fixture");
3613        rewrite_metadata_for_test(&root, &metadata.id, |record| {
3614            record.label = "  PRIMARY  ".to_owned();
3615        });
3616
3617        assert_eq!(store.list().expect("legacy list")[0].label, "primary");
3618        assert_eq!(
3619            store
3620                .status(&metadata.credential_ref)
3621                .expect("legacy status")
3622                .metadata
3623                .label,
3624            "primary"
3625        );
3626        assert_eq!(
3627            store
3628                .rotate_preview_at(&metadata.credential_ref, 100, 200)
3629                .expect("legacy preview")
3630                .label,
3631            "primary"
3632        );
3633        let before_duplicate = store_tree_snapshot(&root);
3634        assert_eq!(
3635            store.add("openrouter", "primary", b"replacement"),
3636            Err(CredentialStoreError::InvalidInput)
3637        );
3638        assert_eq!(store_tree_snapshot(&root), before_duplicate);
3639        assert_eq!(
3640            store
3641                .resolve(&metadata.credential_ref)
3642                .expect("legacy secret remains")
3643                .as_slice(),
3644            b"legacy-secret"
3645        );
3646    }
3647
3648    #[test]
3649    fn ambiguous_legacy_normalized_labels_fail_closed_without_mutation() {
3650        let temporary = tempfile::tempdir().expect("temporary root");
3651        let root = temporary.path().join("provider-credentials-v1");
3652        let store = LocalCredentialStore::at_test_root(&root).expect("open store");
3653        let first = store
3654            .add("openrouter", "first", b"first-secret")
3655            .expect("first fixture");
3656        let second = store
3657            .add("openrouter", "second", b"second-secret")
3658            .expect("second fixture");
3659        rewrite_metadata_for_test(&root, &first.id, |record| {
3660            record.label = " PRIMARY ".to_owned();
3661        });
3662        rewrite_metadata_for_test(&root, &second.id, |record| {
3663            record.label = "primary".to_owned();
3664        });
3665        let before = store_tree_snapshot(&root);
3666
3667        assert_eq!(store.list(), Err(CredentialStoreError::CorruptState));
3668        assert_eq!(
3669            store.status(&first.credential_ref),
3670            Err(CredentialStoreError::CorruptState)
3671        );
3672        assert_eq!(
3673            store.rotate_preview_at(&first.credential_ref, 100, 200),
3674            Err(CredentialStoreError::CorruptState)
3675        );
3676        assert_eq!(
3677            store.remove_preview_at(&first.credential_ref, 100, 200),
3678            Err(CredentialStoreError::CorruptState)
3679        );
3680        assert_eq!(
3681            store.resolve(&first.credential_ref),
3682            Err(CredentialStoreError::CorruptState)
3683        );
3684        assert_eq!(
3685            store.add("openrouter", "third", b"third-secret"),
3686            Err(CredentialStoreError::CorruptState)
3687        );
3688        assert_eq!(store_tree_snapshot(&root), before);
3689    }
3690
3691    #[test]
3692    fn prepared_rotation_collision_scan_is_concurrent_and_byte_preserving() {
3693        let temporary = tempfile::tempdir().expect("temporary root");
3694        let root = temporary.path().join("provider-credentials-v1");
3695        let store = LocalCredentialStore::at_test_root(&root).expect("open store");
3696        let primary = store
3697            .add_at("openrouter", "primary", b"rotation-old", 90)
3698            .expect("primary fixture");
3699        let alias = store
3700            .add_at("openrouter", "secondary", b"alias-secret", 90)
3701            .expect("alias fixture");
3702        let preview = store
3703            .rotate_preview_at(&primary.credential_ref, 100, 200)
3704            .expect("rotation preview");
3705        set_lifecycle_failpoint(Some("rotate_before_secret_replace"));
3706        assert_eq!(
3707            store.rotate_apply_at(
3708                &primary.credential_ref,
3709                b"rotation-new",
3710                lifecycle_approval(&preview, ROTATE_CONFIRMATION),
3711                110,
3712            ),
3713            Err(CredentialStoreError::CommitAmbiguous)
3714        );
3715        set_lifecycle_failpoint(None);
3716        rewrite_metadata_for_test(&root, &alias.id, |record| {
3717            record.label = " PRIMARY ".to_owned();
3718        });
3719        let before = store_tree_snapshot(&root);
3720
3721        let barrier = Arc::new(Barrier::new(3));
3722        let mut workers = Vec::new();
3723        for _ in 0..2 {
3724            let store = store.clone();
3725            let barrier = Arc::clone(&barrier);
3726            let credential_ref = primary.credential_ref.clone();
3727            let preview = preview.clone();
3728            workers.push(std::thread::spawn(move || {
3729                barrier.wait();
3730                store.reconcile_rotate(&credential_ref, lifecycle_request_identity(&preview))
3731            }));
3732        }
3733        barrier.wait();
3734        let results = workers
3735            .into_iter()
3736            .map(|worker| worker.join().expect("collision worker"))
3737            .collect::<Vec<_>>();
3738        assert!(results.contains(&Err(CredentialStoreError::CorruptState)));
3739        assert!(results.iter().all(|result| matches!(
3740            result,
3741            Err(CredentialStoreError::CorruptState | CredentialStoreError::LockUnavailable)
3742        )));
3743        assert_eq!(store_tree_snapshot(&root), before);
3744        assert_eq!(
3745            store.reconcile_rotate(
3746                &primary.credential_ref,
3747                lifecycle_request_identity(&preview),
3748            ),
3749            Err(CredentialStoreError::CorruptState)
3750        );
3751        assert_eq!(
3752            store.rotate_apply_at(
3753                &primary.credential_ref,
3754                b"must-not-stage",
3755                CredentialLifecycleApproval {
3756                    expected_preview: &preview.preview_digest,
3757                    expected_generation: preview.expected_generation,
3758                    confirmation: "",
3759                    expires_at_epoch_seconds: preview.expires_at_epoch_seconds,
3760                },
3761                500,
3762            ),
3763            Err(CredentialStoreError::CorruptState)
3764        );
3765        assert_eq!(store_tree_snapshot(&root), before);
3766    }
3767
3768    #[test]
3769    fn prepared_removal_collisions_never_reconcile_pre_or_post_truncate() {
3770        for (phase, failpoint, expected_initial) in [
3771            (
3772                "pretruncate",
3773                "remove_before_secret_truncate",
3774                CredentialStoreError::CommitAmbiguous,
3775            ),
3776            (
3777                "posttruncate",
3778                "remove_after_secret_truncate",
3779                CredentialStoreError::RecoveryRequired,
3780            ),
3781        ] {
3782            let temporary = tempfile::tempdir().expect("temporary root");
3783            let root = temporary.path().join(format!("credentials-{phase}"));
3784            let store = LocalCredentialStore::at_test_root(&root).expect("open store");
3785            let primary = store
3786                .add_at("openrouter", "primary", b"removal-primary", 90)
3787                .expect("primary fixture");
3788            let alias = store
3789                .add_at("openrouter", "secondary", b"removal-alias", 90)
3790                .expect("alias fixture");
3791            let preview = store
3792                .remove_preview_at(&primary.credential_ref, 100, 200)
3793                .expect("removal preview");
3794            set_lifecycle_failpoint(Some(failpoint));
3795            assert_eq!(
3796                store.remove_apply_at(
3797                    &primary.credential_ref,
3798                    lifecycle_approval(&preview, REMOVE_CONFIRMATION),
3799                    110,
3800                ),
3801                Err(expected_initial)
3802            );
3803            set_lifecycle_failpoint(None);
3804            rewrite_metadata_for_test(&root, &alias.id, |record| {
3805                record.label = " PRIMARY ".to_owned();
3806            });
3807            let before = store_tree_snapshot(&root);
3808
3809            assert_eq!(
3810                store.reconcile_remove(
3811                    &primary.credential_ref,
3812                    lifecycle_request_identity(&preview),
3813                ),
3814                Err(CredentialStoreError::CorruptState)
3815            );
3816            assert_eq!(store_tree_snapshot(&root), before);
3817            assert_eq!(
3818                store.remove_apply_at(
3819                    &primary.credential_ref,
3820                    CredentialLifecycleApproval {
3821                        expected_preview: &preview.preview_digest,
3822                        expected_generation: preview.expected_generation,
3823                        confirmation: "",
3824                        expires_at_epoch_seconds: preview.expires_at_epoch_seconds,
3825                    },
3826                    500,
3827                ),
3828                Err(CredentialStoreError::CorruptState)
3829            );
3830            assert_eq!(store_tree_snapshot(&root), before);
3831        }
3832    }
3833
3834    #[test]
3835    fn concurrent_duplicate_add_has_exactly_one_committed_winner() {
3836        let temporary = tempfile::tempdir().expect("temporary root");
3837        let root = temporary.path().join("credentials-v1");
3838        let store = LocalCredentialStore::at_test_root(&root).expect("open store");
3839        let barrier = Arc::new(Barrier::new(3));
3840        let mut workers = Vec::new();
3841        for (label, secret) in [
3842            ("PRIMARY", b"first".as_slice()),
3843            (" primary ", b"second".as_slice()),
3844        ] {
3845            let store = store.clone();
3846            let barrier = Arc::clone(&barrier);
3847            let secret = secret.to_vec();
3848            workers.push(std::thread::spawn(move || {
3849                barrier.wait();
3850                store.add("openrouter", label, &secret)
3851            }));
3852        }
3853        barrier.wait();
3854        let results = workers
3855            .into_iter()
3856            .map(|worker| worker.join().expect("worker result"))
3857            .collect::<Vec<_>>();
3858
3859        assert_eq!(results.iter().filter(|result| result.is_ok()).count(), 1);
3860        let records = store.list().expect("list committed records");
3861        assert_eq!(records.len(), 1);
3862        assert_eq!(records[0].label, "primary");
3863        let secret = store
3864            .resolve(&records[0].credential_ref)
3865            .expect("resolve winner");
3866        assert!(matches!(secret.as_slice(), b"first" | b"second"));
3867    }
3868
3869    #[test]
3870    fn provider_capacity_is_bounded_to_sixteen_credentials() {
3871        let temporary = tempfile::tempdir().expect("temporary root");
3872        let root = temporary.path().join("provider-credentials-v1");
3873        let store = LocalCredentialStore::at_test_root(&root).expect("open store");
3874        for index in 0..MAX_CREDENTIALS_PER_PROVIDER {
3875            store
3876                .add("openrouter", &format!("account-{index}"), b"secret")
3877                .expect("credential within provider capacity");
3878        }
3879        assert_eq!(
3880            store.add("openrouter", "account-overflow", b"secret"),
3881            Err(CredentialStoreError::CapacityExhausted)
3882        );
3883    }
3884
3885    #[test]
3886    fn device_capacity_is_bounded_to_sixty_four_credentials() {
3887        let temporary = tempfile::tempdir().expect("temporary root");
3888        let root = temporary.path().join("credentials-v1");
3889        let store = LocalCredentialStore::at_test_root(&root).expect("open store");
3890        let mut first = None;
3891        for provider in ["provider-a", "provider-b", "provider-c", "provider-d"] {
3892            for index in 0..MAX_CREDENTIALS_PER_PROVIDER {
3893                let metadata = store
3894                    .add_at(provider, &format!("account-{index}"), b"secret", 90)
3895                    .expect("credential within device capacity");
3896                first.get_or_insert(metadata);
3897            }
3898        }
3899        assert_eq!(store.list().expect("full store").len(), MAX_CREDENTIALS);
3900        assert_eq!(
3901            store.add_at("provider-e", "overflow", b"secret", 90),
3902            Err(CredentialStoreError::CapacityExhausted)
3903        );
3904        let first = first.expect("first credential");
3905        let preview = store
3906            .remove_preview_at(&first.credential_ref, 100, 200)
3907            .expect("removal preview at device capacity");
3908        store
3909            .remove_apply_at(
3910                &first.credential_ref,
3911                lifecycle_approval(&preview, REMOVE_CONFIRMATION),
3912                110,
3913            )
3914            .expect("remove to bounded tombstone");
3915        assert_eq!(
3916            store.list().expect("tombstone-inclusive store").len(),
3917            MAX_CREDENTIALS
3918        );
3919        assert_eq!(
3920            store.add_at("provider-e", "still-overflow", b"secret", 120),
3921            Err(CredentialStoreError::CapacityExhausted)
3922        );
3923    }
3924
3925    #[test]
3926    fn lifecycle_preview_is_deterministic_complete_and_no_write() {
3927        let temporary = tempfile::tempdir().expect("temporary root");
3928        let root = temporary.path().join("credentials-v1");
3929        let store = LocalCredentialStore::at_test_root(&root).expect("open store");
3930        let metadata = store
3931            .add_at("openrouter", "primary", b"secret-canary", 90)
3932            .expect("add");
3933        let before_metadata = std::fs::read(
3934            root.join(ENTRIES_DIRECTORY)
3935                .join(&metadata.id)
3936                .join(METADATA_FILE),
3937        )
3938        .expect("metadata before preview");
3939        let before_secret = store
3940            .resolve(&metadata.credential_ref)
3941            .expect("secret before");
3942
3943        let first = store
3944            .rotate_preview_at(&metadata.credential_ref, 100, 200)
3945            .expect("first preview");
3946        let second = store
3947            .rotate_preview_at(&metadata.credential_ref, 100, 200)
3948            .expect("second preview");
3949
3950        assert_eq!(first, second);
3951        assert_eq!(first.provider, "openrouter");
3952        assert_eq!(first.label, "primary");
3953        assert_eq!(first.credential_ref, metadata.credential_ref);
3954        assert_eq!(first.expected_generation, 1);
3955        assert_eq!(first.expires_at_epoch_seconds, 200);
3956        assert_eq!(first.required_confirmation, ROTATE_CONFIRMATION);
3957        assert_eq!(first.dependency_impact.policy, "bindings_preserved");
3958        assert_eq!(first.dependency_impact.binding_count, 0);
3959        assert!(first.preview_digest.starts_with("sha256:"));
3960        assert_eq!(
3961            std::fs::read(
3962                root.join(ENTRIES_DIRECTORY)
3963                    .join(&metadata.id)
3964                    .join(METADATA_FILE)
3965            )
3966            .expect("metadata after preview"),
3967            before_metadata
3968        );
3969        assert_eq!(
3970            store
3971                .resolve(&metadata.credential_ref)
3972                .expect("secret after preview")
3973                .as_slice(),
3974            before_secret.as_slice()
3975        );
3976        let encoded = serde_json::to_string(&first).expect("preview JSON");
3977        assert!(encoded.contains("\"provider\":\"openrouter\""));
3978        assert!(encoded.contains("\"label\":\"primary\""));
3979        assert!(encoded.contains("\"bound_phases\":[]"));
3980        assert!(encoded.contains("\"bound_routes\":[]"));
3981        assert!(encoded.contains("\"required_confirmation\""));
3982        assert!(!encoded.contains("secret-canary"));
3983        assert!(!encoded.contains(root.to_string_lossy().as_ref()));
3984    }
3985
3986    #[test]
3987    fn slice_a_metadata_remains_readable_with_redacted_lifecycle_defaults() {
3988        let id = "0123456789abcdef0123456789abcdef";
3989        let encoded = format!(
3990            "{{\"schema_version\":\"1.0.0\",\"id\":\"{id}\",\"credential_ref\":\"local-file-v1:{id}\",\"provider\":\"openrouter\",\"label\":\"primary\"}}"
3991        );
3992        let metadata: CredentialMetadata =
3993            serde_json::from_str(&encoded).expect("Slice A metadata remains compatible");
3994        metadata.validate().expect("defaulted metadata valid");
3995        assert_eq!(metadata.generation, 1);
3996        assert_eq!(metadata.lifecycle_state, CredentialLifecycleState::Active);
3997        assert_eq!(metadata.health, CredentialHealthState::LocallyVerified);
3998        assert_eq!(metadata.last_verified_at_epoch_seconds, None);
3999        assert!(metadata.bound_phases.is_empty());
4000        assert!(metadata.bound_routes.is_empty());
4001    }
4002
4003    #[test]
4004    fn rotate_apply_is_atomic_generation_bound_and_removes_old_secret() {
4005        let temporary = tempfile::tempdir().expect("temporary root");
4006        let root = temporary.path().join("credentials-v1");
4007        let store = LocalCredentialStore::at_test_root(&root).expect("open store");
4008        let metadata = store
4009            .add_at("ollama", "cloud", b"old-secret-canary", 90)
4010            .expect("add");
4011        let preview = store
4012            .rotate_preview_at(&metadata.credential_ref, 100, 200)
4013            .expect("preview");
4014
4015        let outcome = store
4016            .rotate_apply_at(
4017                &metadata.credential_ref,
4018                b"new-secret-canary",
4019                lifecycle_approval(&preview, ROTATE_CONFIRMATION),
4020                110,
4021            )
4022            .expect("rotate");
4023
4024        assert_eq!(outcome.credential_ref, metadata.credential_ref);
4025        assert_eq!(outcome.previous_generation, 1);
4026        assert_eq!(outcome.generation, Some(2));
4027        let current = store.status(&metadata.credential_ref).expect("status");
4028        assert_eq!(current.metadata.generation, 2);
4029        assert_eq!(current.metadata.previous_generation, Some(1));
4030        assert_eq!(current.metadata.rotated_at_epoch_seconds, Some(110));
4031        assert_eq!(
4032            store
4033                .resolve(&metadata.credential_ref)
4034                .expect("new secret")
4035                .as_slice(),
4036            b"new-secret-canary"
4037        );
4038        assert_no_file_contains(&root, b"old-secret-canary");
4039        let replay = store
4040            .rotate_apply_at(
4041                &metadata.credential_ref,
4042                b"replay-secret",
4043                lifecycle_approval(&preview, ROTATE_CONFIRMATION),
4044                120,
4045            )
4046            .expect("committed replay is idempotent");
4047        assert_eq!(replay.commit_status, "committed");
4048        assert!(!replay.reconciliation_required);
4049        assert_eq!(
4050            store
4051                .resolve(&metadata.credential_ref)
4052                .expect("replay did not replace secret")
4053                .as_slice(),
4054            b"new-secret-canary"
4055        );
4056    }
4057
4058    #[test]
4059    fn lifecycle_apply_rejects_digest_confirmation_expiry_and_storage_attacks_without_mutation() {
4060        let temporary = tempfile::tempdir().expect("temporary root");
4061        let root = temporary.path().join("credentials-v1");
4062        let store = LocalCredentialStore::at_test_root(&root).expect("open store");
4063        let metadata = store
4064            .add_at("openrouter", "primary", b"original-secret", 90)
4065            .expect("add");
4066        let preview = store
4067            .rotate_preview_at(&metadata.credential_ref, 100, 200)
4068            .expect("preview");
4069
4070        for result in [
4071            store.rotate_apply_at(
4072                &metadata.credential_ref,
4073                b"replacement",
4074                CredentialLifecycleApproval {
4075                    expected_preview: "sha256:0000000000000000000000000000000000000000000000000000000000000000",
4076                    expected_generation: 1,
4077                    confirmation: ROTATE_CONFIRMATION,
4078                    expires_at_epoch_seconds: 200,
4079                },
4080                110,
4081            ),
4082            store.rotate_apply_at(
4083                &metadata.credential_ref,
4084                b"replacement",
4085                lifecycle_approval(&preview, "NO"),
4086                110,
4087            ),
4088            store.rotate_apply_at(
4089                &metadata.credential_ref,
4090                b"replacement",
4091                lifecycle_approval(&preview, ROTATE_CONFIRMATION),
4092                200,
4093            ),
4094        ] {
4095            let error = result.expect_err("apply rejected");
4096            assert_eq!(
4097                error.to_string(),
4098                "device-local credential operation failed"
4099            );
4100        }
4101        assert_eq!(
4102            store
4103                .resolve(&metadata.credential_ref)
4104                .expect("original preserved")
4105                .as_slice(),
4106            b"original-secret"
4107        );
4108
4109        let secret_path = root
4110            .join(ENTRIES_DIRECTORY)
4111            .join(&metadata.id)
4112            .join(SECRET_FILE);
4113        let hardlink = temporary.path().join("hardlink");
4114        std::fs::hard_link(&secret_path, &hardlink).expect("hardlink attack");
4115        assert!(matches!(
4116            store.rotate_apply_at(
4117                &metadata.credential_ref,
4118                b"replacement",
4119                lifecycle_approval(&preview, ROTATE_CONFIRMATION),
4120                110,
4121            ),
4122            Err(CredentialStoreError::UnsafeStorage)
4123        ));
4124        assert_no_file_contains(&root, b"replacement");
4125    }
4126
4127    #[test]
4128    fn remove_is_atomic_and_fails_closed_for_any_binding() {
4129        let temporary = tempfile::tempdir().expect("temporary root");
4130        let root = temporary.path().join("credentials-v1");
4131        let store = LocalCredentialStore::at_test_root(&root).expect("open store");
4132        let metadata = store
4133            .add_at("openrouter", "primary", b"remove-canary", 90)
4134            .expect("add");
4135        let before_preview = store_tree_snapshot(&root);
4136        let preview = store
4137            .remove_preview_at(&metadata.credential_ref, 100, 200)
4138            .expect("remove preview");
4139        assert_eq!(store_tree_snapshot(&root), before_preview);
4140        assert!(preview.dependency_impact.apply_allowed);
4141        assert_eq!(preview.dependency_impact.policy, "zero_binding_removal");
4142        let outcome = store
4143            .remove_apply_at(
4144                &metadata.credential_ref,
4145                lifecycle_approval(&preview, REMOVE_CONFIRMATION),
4146                110,
4147            )
4148            .expect("remove");
4149        assert_eq!(outcome.lifecycle_state, "removed");
4150        let removed = store
4151            .status(&metadata.credential_ref)
4152            .expect("removed status");
4153        assert_eq!(removed.status, "removed");
4154        assert_eq!(
4155            removed.metadata.lifecycle_state,
4156            CredentialLifecycleState::Removed
4157        );
4158        assert_eq!(removed.metadata.health, CredentialHealthState::Unavailable);
4159        assert!(matches!(
4160            store.resolve(&metadata.credential_ref),
4161            Err(CredentialStoreError::NotFound)
4162        ));
4163        assert_no_file_contains(&root, b"remove-canary");
4164
4165        let bound = store
4166            .add_at("ollama", "cloud", b"bound-canary", 120)
4167            .expect("bound add");
4168        rewrite_metadata_for_test(&root, &bound.id, |metadata| {
4169            metadata.bound_phases = vec!["phase-2".to_owned()];
4170            metadata.bound_routes = vec!["route-ollama-cloud".to_owned()];
4171        });
4172        let blocked = store
4173            .remove_preview_at(&bound.credential_ref, 130, 200)
4174            .expect("blocked preview");
4175        assert!(!blocked.dependency_impact.apply_allowed);
4176        assert_eq!(blocked.dependency_impact.binding_count, 2);
4177        assert_eq!(blocked.dependency_impact.bound_phases, ["phase-2"]);
4178        assert_eq!(
4179            blocked.dependency_impact.bound_routes,
4180            ["route-ollama-cloud"]
4181        );
4182        assert_eq!(
4183            blocked.dependency_impact.policy,
4184            "blocked_until_zero_bindings"
4185        );
4186        assert!(matches!(
4187            store.remove_apply_at(
4188                &bound.credential_ref,
4189                lifecycle_approval(&blocked, REMOVE_CONFIRMATION),
4190                140,
4191            ),
4192            Err(CredentialStoreError::DependencyConflict)
4193        ));
4194        assert_eq!(
4195            store
4196                .resolve(&bound.credential_ref)
4197                .expect("bound secret preserved")
4198                .as_slice(),
4199            b"bound-canary"
4200        );
4201    }
4202
4203    #[test]
4204    fn every_rotation_post_commit_failure_is_successful_reconcileable_and_has_no_old_secret() {
4205        for edge in [
4206            "rotate_secret_sync",
4207            "rotate_metadata_replace",
4208            "rotate_metadata_sync",
4209            "rotate_journal_commit",
4210        ] {
4211            let temporary = tempfile::tempdir().expect("temporary root");
4212            let root = temporary.path().join(format!("credentials-{edge}"));
4213            let store = LocalCredentialStore::at_test_root(&root).expect("open store");
4214            let metadata = store
4215                .add_at("openrouter", "primary", b"old-crash-canary", 90)
4216                .expect("add");
4217            let preview = store
4218                .rotate_preview_at(&metadata.credential_ref, 100, 200)
4219                .expect("preview");
4220            set_lifecycle_failpoint(Some(edge));
4221            let outcome = store
4222                .rotate_apply_at(
4223                    &metadata.credential_ref,
4224                    b"new-crash-canary",
4225                    lifecycle_approval(&preview, ROTATE_CONFIRMATION),
4226                    110,
4227                )
4228                .expect("post-commit failure is a committed outcome");
4229            set_lifecycle_failpoint(None);
4230            assert_eq!(outcome.commit_status, "committed_reconciliation_required");
4231            assert!(outcome.reconciliation_required);
4232            assert_no_file_contains(&root, b"old-crash-canary");
4233
4234            let reconciled = store
4235                .rotate_apply_at(
4236                    &metadata.credential_ref,
4237                    b"ignored-retry-secret",
4238                    lifecycle_approval(&preview, ROTATE_CONFIRMATION),
4239                    120,
4240                )
4241                .expect("restart/retry reconciles");
4242            assert_eq!(reconciled.commit_status, "committed");
4243            assert!(!reconciled.reconciliation_required);
4244            assert_eq!(
4245                store
4246                    .resolve(&metadata.credential_ref)
4247                    .expect("new secret survives")
4248                    .as_slice(),
4249                b"new-crash-canary"
4250            );
4251            assert_no_file_contains(&root, b"old-crash-canary");
4252            assert_no_file_contains(&root, b"ignored-retry-secret");
4253        }
4254    }
4255
4256    #[test]
4257    fn rotation_recovery_classifies_exact_inode_before_and_after_replace() {
4258        let temporary = tempfile::tempdir().expect("temporary root");
4259        let root = temporary.path().join("credentials-before-replace");
4260        let store = LocalCredentialStore::at_test_root(&root).expect("open store");
4261        let metadata = store
4262            .add_at("openrouter", "primary", b"old-before-canary", 90)
4263            .expect("add");
4264        let preview = store
4265            .rotate_preview_at(&metadata.credential_ref, 100, 200)
4266            .expect("preview");
4267        set_lifecycle_failpoint(Some("rotate_before_secret_replace"));
4268        assert!(matches!(
4269            store.rotate_apply_at(
4270                &metadata.credential_ref,
4271                b"staged-before-canary",
4272                lifecycle_approval(&preview, ROTATE_CONFIRMATION),
4273                110,
4274            ),
4275            Err(CredentialStoreError::CommitAmbiguous)
4276        ));
4277        set_lifecycle_failpoint(None);
4278
4279        let journal_path = root.join(LIFECYCLE_DIRECTORY).join(&metadata.id);
4280        let journal_bytes = std::fs::read(&journal_path).expect("prepared journal");
4281        let journal: CredentialLifecycleJournal =
4282            serde_json::from_slice(&journal_bytes).expect("decode journal");
4283        assert_ne!(
4284            journal.old_secret_identity,
4285            journal.staged_secret_identity.expect("staged identity")
4286        );
4287        assert!(
4288            !journal_bytes
4289                .windows(b"old-before-canary".len())
4290                .any(|window| window == b"old-before-canary")
4291        );
4292        assert!(
4293            !journal_bytes
4294                .windows(b"staged-before-canary".len())
4295                .any(|window| window == b"staged-before-canary")
4296        );
4297
4298        let reconciled = store
4299            .reconcile_rotate(
4300                &metadata.credential_ref,
4301                lifecycle_request_identity(&preview),
4302            )
4303            .expect("precommit rollback");
4304        assert_eq!(
4305            reconciled,
4306            CredentialRotationReconciliation::RolledBackFreshPreviewRequired
4307        );
4308        assert_eq!(
4309            store
4310                .resolve(&metadata.credential_ref)
4311                .expect("old secret remains")
4312                .as_slice(),
4313            b"old-before-canary"
4314        );
4315        assert_no_file_contains(&root, b"staged-before-canary");
4316
4317        let after_root = temporary.path().join("credentials-after-replace");
4318        let after_store =
4319            LocalCredentialStore::at_test_root(&after_root).expect("open after store");
4320        let after_metadata = after_store
4321            .add_at("openrouter", "secondary", b"old-after-canary", 90)
4322            .expect("after add");
4323        let after_preview = after_store
4324            .rotate_preview_at(&after_metadata.credential_ref, 100, 101)
4325            .expect("short preview");
4326        set_lifecycle_failpoint(Some("rotate_after_secret_replace"));
4327        let pending = after_store
4328            .rotate_apply_at(
4329                &after_metadata.credential_ref,
4330                b"new-after-canary",
4331                lifecycle_approval(&after_preview, ROTATE_CONFIRMATION),
4332                100,
4333            )
4334            .expect("post-commit outcome");
4335        set_lifecycle_failpoint(None);
4336        assert!(pending.reconciliation_required);
4337
4338        let after_reconciled = after_store
4339            .reconcile_rotate(
4340                &after_metadata.credential_ref,
4341                lifecycle_request_identity(&after_preview),
4342            )
4343            .expect("expired journal authorization remains durable");
4344        let CredentialRotationReconciliation::Committed(outcome) = after_reconciled else {
4345            panic!("committed rotation reconciliation expected")
4346        };
4347        assert_eq!(outcome.generation, Some(2));
4348        assert_eq!(outcome.commit_status, "committed");
4349        assert_eq!(
4350            after_store
4351                .resolve(&after_metadata.credential_ref)
4352                .expect("new secret remains")
4353                .as_slice(),
4354            b"new-after-canary"
4355        );
4356        assert_no_file_contains(&after_root, b"old-after-canary");
4357    }
4358
4359    #[test]
4360    fn missing_staged_secret_with_old_inode_live_rolls_back_without_inference() {
4361        let temporary = tempfile::tempdir().expect("temporary root");
4362        let root = temporary.path().join("credentials-v1");
4363        let store = LocalCredentialStore::at_test_root(&root).expect("open store");
4364        let metadata = store
4365            .add_at("openrouter", "primary", b"old-live-canary", 90)
4366            .expect("add");
4367        let preview = store
4368            .rotate_preview_at(&metadata.credential_ref, 100, 200)
4369            .expect("preview");
4370        set_lifecycle_failpoint(Some("rotate_before_secret_replace"));
4371        let _ = store.rotate_apply_at(
4372            &metadata.credential_ref,
4373            b"missing-stage-canary",
4374            lifecycle_approval(&preview, ROTATE_CONFIRMATION),
4375            110,
4376        );
4377        set_lifecycle_failpoint(None);
4378        std::fs::remove_file(
4379            root.join(ENTRIES_DIRECTORY)
4380                .join(&metadata.id)
4381                .join(NEXT_SECRET_FILE),
4382        )
4383        .expect("remove staged secret");
4384
4385        assert_eq!(
4386            store
4387                .reconcile_rotate(
4388                    &metadata.credential_ref,
4389                    lifecycle_request_identity(&preview),
4390                )
4391                .expect("old inode proves precommit"),
4392            CredentialRotationReconciliation::RolledBackFreshPreviewRequired
4393        );
4394        assert_eq!(
4395            store
4396                .resolve(&metadata.credential_ref)
4397                .expect("old secret preserved")
4398                .as_slice(),
4399            b"old-live-canary"
4400        );
4401    }
4402
4403    #[test]
4404    fn foreign_or_missing_live_secret_identity_is_ambiguous_and_fails_closed() {
4405        for missing in [false, true] {
4406            let temporary = tempfile::tempdir().expect("temporary root");
4407            let root = temporary
4408                .path()
4409                .join(format!("credentials-missing-{missing}"));
4410            let store = LocalCredentialStore::at_test_root(&root).expect("open store");
4411            let metadata = store
4412                .add_at("openrouter", "primary", b"old-ambiguous-canary", 90)
4413                .expect("add");
4414            let preview = store
4415                .rotate_preview_at(&metadata.credential_ref, 100, 200)
4416                .expect("preview");
4417            set_lifecycle_failpoint(Some("rotate_before_secret_replace"));
4418            let _ = store.rotate_apply_at(
4419                &metadata.credential_ref,
4420                b"staged-ambiguous-canary",
4421                lifecycle_approval(&preview, ROTATE_CONFIRMATION),
4422                110,
4423            );
4424            set_lifecycle_failpoint(None);
4425            let live_secret = root
4426                .join(ENTRIES_DIRECTORY)
4427                .join(&metadata.id)
4428                .join(SECRET_FILE);
4429            std::fs::remove_file(&live_secret).expect("remove recorded old inode");
4430            if !missing {
4431                std::fs::write(&live_secret, b"foreign-identity").expect("foreign live secret");
4432                std::fs::set_permissions(&live_secret, std::fs::Permissions::from_mode(0o600))
4433                    .expect("foreign secret mode");
4434            }
4435
4436            assert!(matches!(
4437                store.reconcile_rotate(
4438                    &metadata.credential_ref,
4439                    lifecycle_request_identity(&preview),
4440                ),
4441                Err(CredentialStoreError::RecoveryRequired)
4442            ));
4443            assert!(root.join(LIFECYCLE_DIRECTORY).join(&metadata.id).exists());
4444        }
4445    }
4446
4447    #[test]
4448    fn every_removal_post_durability_failure_is_successful_and_replay_safe() {
4449        for edge in [
4450            "remove_metadata_stage",
4451            "remove_metadata_replace",
4452            "remove_metadata_sync",
4453            "remove_journal_commit",
4454        ] {
4455            let temporary = tempfile::tempdir().expect("temporary root");
4456            let root = temporary.path().join(format!("credentials-{edge}"));
4457            let store = LocalCredentialStore::at_test_root(&root).expect("open store");
4458            let metadata = store
4459                .add_at("openrouter", "primary", b"remove-crash-canary", 90)
4460                .expect("add");
4461            let preview = store
4462                .remove_preview_at(&metadata.credential_ref, 100, 200)
4463                .expect("preview");
4464            set_lifecycle_failpoint(Some(edge));
4465            let outcome = store
4466                .remove_apply_at(
4467                    &metadata.credential_ref,
4468                    lifecycle_approval(&preview, REMOVE_CONFIRMATION),
4469                    110,
4470                )
4471                .expect("post-commit failure is a committed outcome");
4472            set_lifecycle_failpoint(None);
4473            assert_eq!(outcome.commit_status, "committed_reconciliation_required");
4474            assert!(outcome.reconciliation_required);
4475            assert_no_file_contains(&root, b"remove-crash-canary");
4476
4477            let reconciled = store
4478                .remove_apply_at(
4479                    &metadata.credential_ref,
4480                    lifecycle_approval(&preview, REMOVE_CONFIRMATION),
4481                    120,
4482                )
4483                .expect("restart/retry reconciles");
4484            assert_eq!(reconciled.commit_status, "committed");
4485            assert!(!reconciled.reconciliation_required);
4486            assert_no_file_contains(&root, b"remove-crash-canary");
4487        }
4488    }
4489
4490    #[test]
4491    fn removal_never_reports_committed_before_sync_and_post_sync_verification() {
4492        for edge in [
4493            "remove_after_secret_truncate",
4494            "remove_before_secret_sync",
4495            "remove_after_secret_sync",
4496        ] {
4497            let temporary = tempfile::tempdir().expect("temporary root");
4498            let root = temporary.path().join(format!("credentials-{edge}"));
4499            let store = LocalCredentialStore::at_test_root(&root).expect("open store");
4500            let metadata = store
4501                .add_at("openrouter", "primary", b"remove-pending-canary", 90)
4502                .expect("add");
4503            let preview = store
4504                .remove_preview_at(&metadata.credential_ref, 100, 101)
4505                .expect("short preview");
4506
4507            set_lifecycle_failpoint(Some(edge));
4508            assert_eq!(
4509                store.remove_apply_at(
4510                    &metadata.credential_ref,
4511                    lifecycle_approval(&preview, REMOVE_CONFIRMATION),
4512                    100,
4513                ),
4514                Err(CredentialStoreError::RecoveryRequired),
4515                "{edge} must not report committed"
4516            );
4517            set_lifecycle_failpoint(None);
4518
4519            let journal_path = root.join(LIFECYCLE_DIRECTORY).join(&metadata.id);
4520            let journal_bytes = std::fs::read(&journal_path).expect("prepared removal journal");
4521            let journal: CredentialLifecycleJournal =
4522                serde_json::from_slice(&journal_bytes).expect("decode journal");
4523            assert_eq!(journal.state, CredentialLifecycleJournalState::Prepared);
4524            assert_eq!(
4525                std::fs::metadata(
4526                    root.join(ENTRIES_DIRECTORY)
4527                        .join(&metadata.id)
4528                        .join(SECRET_FILE),
4529                )
4530                .expect("pending tombstone")
4531                .len(),
4532                0
4533            );
4534
4535            let reconciled = store
4536                .remove_apply_at(
4537                    &metadata.credential_ref,
4538                    CredentialLifecycleApproval {
4539                        expected_preview: &preview.preview_digest,
4540                        expected_generation: preview.expected_generation,
4541                        confirmation: "",
4542                        expires_at_epoch_seconds: preview.expires_at_epoch_seconds,
4543                    },
4544                    500,
4545                )
4546                .expect("exact expired retry durably reconciles without confirmation");
4547            assert_eq!(reconciled.commit_status, "committed");
4548            assert!(!reconciled.reconciliation_required);
4549            assert_no_file_contains(&root, b"remove-pending-canary");
4550        }
4551    }
4552
4553    #[test]
4554    fn descriptor_sync_failure_preserves_prepared_removal_journal() {
4555        let temporary = tempfile::tempdir().expect("temporary root");
4556        let root = temporary.path().join("credentials-v1");
4557        let store = LocalCredentialStore::at_test_root(&root).expect("open store");
4558        let metadata = store
4559            .add_at("openrouter", "primary", b"remove-sync-failure-canary", 90)
4560            .expect("add");
4561        let preview = store
4562            .remove_preview_at(&metadata.credential_ref, 100, 200)
4563            .expect("preview");
4564
4565        set_lifecycle_failpoint(Some("remove_secret_sync_failure"));
4566        assert_eq!(
4567            store.remove_apply_at(
4568                &metadata.credential_ref,
4569                lifecycle_approval(&preview, REMOVE_CONFIRMATION),
4570                110,
4571            ),
4572            Err(CredentialStoreError::RecoveryRequired)
4573        );
4574        set_lifecycle_failpoint(None);
4575
4576        let journal_path = root.join(LIFECYCLE_DIRECTORY).join(&metadata.id);
4577        let journal_bytes = std::fs::read(&journal_path).expect("prepared removal journal");
4578        let journal: CredentialLifecycleJournal =
4579            serde_json::from_slice(&journal_bytes).expect("decode journal");
4580        assert_eq!(journal.state, CredentialLifecycleJournalState::Prepared);
4581        set_lifecycle_failpoint(Some("remove_secret_sync_failure"));
4582        assert_eq!(
4583            store.reconcile_remove(
4584                &metadata.credential_ref,
4585                lifecycle_request_identity(&preview),
4586            ),
4587            Err(CredentialStoreError::RecoveryRequired)
4588        );
4589        set_lifecycle_failpoint(None);
4590        assert_eq!(
4591            std::fs::read(&journal_path).expect("failed recovery preserves prepared journal"),
4592            journal_bytes
4593        );
4594        assert!(matches!(
4595            store
4596                .reconcile_remove(
4597                    &metadata.credential_ref,
4598                    lifecycle_request_identity(&preview),
4599                )
4600                .expect("exact retry completes descriptor sync"),
4601            CredentialRemovalReconciliation::Committed(_)
4602        ));
4603        assert_no_file_contains(&root, b"remove-sync-failure-canary");
4604    }
4605
4606    #[test]
4607    fn pending_zero_size_removal_reverted_to_nonzero_rolls_back_for_fresh_preview() {
4608        let temporary = tempfile::tempdir().expect("temporary root");
4609        let root = temporary.path().join("credentials-v1");
4610        let store = LocalCredentialStore::at_test_root(&root).expect("open store");
4611        let original_secret = b"remove-reverted-canary";
4612        let metadata = store
4613            .add_at("openrouter", "primary", original_secret, 90)
4614            .expect("add");
4615        let preview = store
4616            .remove_preview_at(&metadata.credential_ref, 100, 200)
4617            .expect("preview");
4618
4619        set_lifecycle_failpoint(Some("remove_after_secret_truncate"));
4620        assert_eq!(
4621            store.remove_apply_at(
4622                &metadata.credential_ref,
4623                lifecycle_approval(&preview, REMOVE_CONFIRMATION),
4624                110,
4625            ),
4626            Err(CredentialStoreError::RecoveryRequired)
4627        );
4628        set_lifecycle_failpoint(None);
4629
4630        let secret_path = root
4631            .join(ENTRIES_DIRECTORY)
4632            .join(&metadata.id)
4633            .join(SECRET_FILE);
4634        std::fs::write(&secret_path, original_secret).expect("simulate crash rollback bytes");
4635        assert_eq!(
4636            store.remove_apply_at(
4637                &metadata.credential_ref,
4638                CredentialLifecycleApproval {
4639                    expected_preview: &preview.preview_digest,
4640                    expected_generation: preview.expected_generation,
4641                    confirmation: "",
4642                    expires_at_epoch_seconds: preview.expires_at_epoch_seconds,
4643                },
4644                500,
4645            ),
4646            Err(CredentialStoreError::FreshPreviewRequired)
4647        );
4648        assert_eq!(
4649            store
4650                .resolve(&metadata.credential_ref)
4651                .expect("precommit secret remains active")
4652                .as_slice(),
4653            original_secret
4654        );
4655        assert!(!root.join(LIFECYCLE_DIRECTORY).join(&metadata.id).exists());
4656    }
4657
4658    #[test]
4659    fn expired_post_truncate_removal_retry_reconciles_from_durable_authorization() {
4660        let temporary = tempfile::tempdir().expect("temporary root");
4661        let root = temporary.path().join("credentials-v1");
4662        let store = LocalCredentialStore::at_test_root(&root).expect("open store");
4663        let metadata = store
4664            .add_at("openrouter", "primary", b"remove-expired-canary", 90)
4665            .expect("add");
4666        let preview = store
4667            .remove_preview_at(&metadata.credential_ref, 100, 101)
4668            .expect("short preview");
4669        set_lifecycle_failpoint(Some("remove_after_secret_truncate"));
4670        assert_eq!(
4671            store.remove_apply_at(
4672                &metadata.credential_ref,
4673                lifecycle_approval(&preview, REMOVE_CONFIRMATION),
4674                100,
4675            ),
4676            Err(CredentialStoreError::RecoveryRequired)
4677        );
4678        set_lifecycle_failpoint(None);
4679
4680        let journal_bytes = std::fs::read(root.join(LIFECYCLE_DIRECTORY).join(&metadata.id))
4681            .expect("prepared removal journal");
4682        let journal: CredentialLifecycleJournal =
4683            serde_json::from_slice(&journal_bytes).expect("decode journal");
4684        assert_eq!(journal.required_confirmation, REMOVE_CONFIRMATION);
4685        assert!(
4686            !journal_bytes
4687                .windows(b"remove-expired-canary".len())
4688                .any(|window| window == b"remove-expired-canary")
4689        );
4690
4691        let reconciled = store
4692            .reconcile_remove(
4693                &metadata.credential_ref,
4694                lifecycle_request_identity(&preview),
4695            )
4696            .expect("expired authorized removal reconciles");
4697        let CredentialRemovalReconciliation::Committed(outcome) = reconciled else {
4698            panic!("committed removal expected")
4699        };
4700        assert_eq!(outcome.commit_status, "committed");
4701        assert_eq!(outcome.lifecycle_state, "removed");
4702        assert_no_file_contains(&root, b"remove-expired-canary");
4703    }
4704
4705    #[test]
4706    fn expired_precommit_removal_with_exact_old_inode_rolls_back_for_fresh_preview() {
4707        let temporary = tempfile::tempdir().expect("temporary root");
4708        let root = temporary.path().join("credentials-v1");
4709        let store = LocalCredentialStore::at_test_root(&root).expect("open store");
4710        let metadata = store
4711            .add_at("openrouter", "primary", b"remove-old-live-canary", 90)
4712            .expect("add");
4713        let preview = store
4714            .remove_preview_at(&metadata.credential_ref, 100, 101)
4715            .expect("short preview");
4716        set_lifecycle_failpoint(Some("remove_before_secret_truncate"));
4717        assert!(matches!(
4718            store.remove_apply_at(
4719                &metadata.credential_ref,
4720                lifecycle_approval(&preview, REMOVE_CONFIRMATION),
4721                100,
4722            ),
4723            Err(CredentialStoreError::CommitAmbiguous)
4724        ));
4725        set_lifecycle_failpoint(None);
4726
4727        let result = store.remove_apply_at(
4728            &metadata.credential_ref,
4729            CredentialLifecycleApproval {
4730                expected_preview: &preview.preview_digest,
4731                expected_generation: preview.expected_generation,
4732                confirmation: "",
4733                expires_at_epoch_seconds: preview.expires_at_epoch_seconds,
4734            },
4735            500,
4736        );
4737        assert_eq!(result, Err(CredentialStoreError::FreshPreviewRequired));
4738        assert_eq!(
4739            store
4740                .resolve(&metadata.credential_ref)
4741                .expect("old secret remains live")
4742                .as_slice(),
4743            b"remove-old-live-canary"
4744        );
4745        let fresh = store
4746            .remove_preview_at(&metadata.credential_ref, 500, 600)
4747            .expect("fresh preview is available after rollback");
4748        assert_eq!(
4749            store.remove_apply_at(
4750                &metadata.credential_ref,
4751                lifecycle_approval(&fresh, REMOVE_CONFIRMATION),
4752                600,
4753            ),
4754            Err(CredentialStoreError::PreviewExpired)
4755        );
4756    }
4757
4758    #[test]
4759    fn mismatched_removal_reconciliation_fails_closed_then_exact_retry_succeeds() {
4760        let temporary = tempfile::tempdir().expect("temporary root");
4761        let root = temporary.path().join("credentials-v1");
4762        let store = LocalCredentialStore::at_test_root(&root).expect("open store");
4763        let metadata = store
4764            .add_at("openrouter", "primary", b"remove-mismatch-canary", 90)
4765            .expect("add");
4766        let preview = store
4767            .remove_preview_at(&metadata.credential_ref, 100, 200)
4768            .expect("preview");
4769        set_lifecycle_failpoint(Some("remove_after_secret_truncate"));
4770        let _ = store.remove_apply_at(
4771            &metadata.credential_ref,
4772            lifecycle_approval(&preview, REMOVE_CONFIRMATION),
4773            110,
4774        );
4775        set_lifecycle_failpoint(None);
4776
4777        let before_mismatch = store_tree_snapshot(&root);
4778        assert!(matches!(
4779            store.reconcile_remove(
4780                &metadata.credential_ref,
4781                CredentialLifecycleRequestIdentity {
4782                    expected_preview: "sha256:0000000000000000000000000000000000000000000000000000000000000000",
4783                    expected_generation: preview.expected_generation,
4784                    expires_at_epoch_seconds: preview.expires_at_epoch_seconds,
4785                },
4786            ),
4787            Err(CredentialStoreError::RecoveryRequired)
4788        ));
4789        assert_eq!(store_tree_snapshot(&root), before_mismatch);
4790        assert!(root.join(LIFECYCLE_DIRECTORY).join(&metadata.id).exists());
4791        assert!(matches!(
4792            store
4793                .reconcile_remove(
4794                    &metadata.credential_ref,
4795                    lifecycle_request_identity(&preview),
4796                )
4797                .expect("exact retry reconciles"),
4798            CredentialRemovalReconciliation::Committed(_)
4799        ));
4800        assert_no_file_contains(&root, b"remove-mismatch-canary");
4801    }
4802
4803    #[test]
4804    fn pre_truncate_removal_mismatch_preserves_store_and_journal_byte_for_byte() {
4805        let temporary = tempfile::tempdir().expect("temporary root");
4806        let root = temporary.path().join("credentials-v1");
4807        let store = LocalCredentialStore::at_test_root(&root).expect("open store");
4808        let metadata = store
4809            .add_at("openrouter", "primary", b"remove-preunlink-canary", 90)
4810            .expect("add");
4811        let preview = store
4812            .remove_preview_at(&metadata.credential_ref, 100, 200)
4813            .expect("preview");
4814        set_lifecycle_failpoint(Some("remove_before_secret_truncate"));
4815        assert_eq!(
4816            store.remove_apply_at(
4817                &metadata.credential_ref,
4818                lifecycle_approval(&preview, REMOVE_CONFIRMATION),
4819                110,
4820            ),
4821            Err(CredentialStoreError::CommitAmbiguous)
4822        );
4823        set_lifecycle_failpoint(None);
4824
4825        for mismatch in [
4826            CredentialLifecycleRequestIdentity {
4827                expected_preview: "sha256:0000000000000000000000000000000000000000000000000000000000000000",
4828                expected_generation: preview.expected_generation,
4829                expires_at_epoch_seconds: preview.expires_at_epoch_seconds,
4830            },
4831            CredentialLifecycleRequestIdentity {
4832                expected_preview: &preview.preview_digest,
4833                expected_generation: preview.expected_generation + 1,
4834                expires_at_epoch_seconds: preview.expires_at_epoch_seconds,
4835            },
4836            CredentialLifecycleRequestIdentity {
4837                expected_preview: &preview.preview_digest,
4838                expected_generation: preview.expected_generation,
4839                expires_at_epoch_seconds: preview.expires_at_epoch_seconds + 1,
4840            },
4841        ] {
4842            let before = store_tree_snapshot(&root);
4843            assert_eq!(
4844                store.reconcile_remove(&metadata.credential_ref, mismatch),
4845                Err(CredentialStoreError::RecoveryRequired)
4846            );
4847            assert_eq!(store_tree_snapshot(&root), before);
4848        }
4849    }
4850
4851    #[test]
4852    fn durable_removal_ref_and_literal_mismatch_are_recovery_only_and_non_mutating() {
4853        for field in ["credential_ref", "required_confirmation"] {
4854            let temporary = tempfile::tempdir().expect("temporary root");
4855            let root = temporary.path().join(format!("credentials-{field}"));
4856            let store = LocalCredentialStore::at_test_root(&root).expect("open store");
4857            let metadata = store
4858                .add_at("openrouter", "primary", b"remove-binding-canary", 90)
4859                .expect("add");
4860            let preview = store
4861                .remove_preview_at(&metadata.credential_ref, 100, 200)
4862                .expect("preview");
4863            set_lifecycle_failpoint(Some("remove_before_secret_truncate"));
4864            assert_eq!(
4865                store.remove_apply_at(
4866                    &metadata.credential_ref,
4867                    lifecycle_approval(&preview, REMOVE_CONFIRMATION),
4868                    110,
4869                ),
4870                Err(CredentialStoreError::CommitAmbiguous)
4871            );
4872            set_lifecycle_failpoint(None);
4873
4874            let journal_path = root.join(LIFECYCLE_DIRECTORY).join(&metadata.id);
4875            let mut journal: CredentialLifecycleJournal =
4876                serde_json::from_slice(&std::fs::read(&journal_path).expect("prepared journal"))
4877                    .expect("decode journal");
4878            match field {
4879                "credential_ref" => {
4880                    journal.credential_ref =
4881                        "local-file-v1:00000000000000000000000000000000".to_owned();
4882                }
4883                "required_confirmation" => {
4884                    journal.required_confirmation = ROTATE_CONFIRMATION.to_owned();
4885                }
4886                _ => unreachable!("bounded test field"),
4887            }
4888            std::fs::write(
4889                &journal_path,
4890                serde_json::to_vec(&journal).expect("encode tampered journal"),
4891            )
4892            .expect("write tampered journal");
4893            let before = store_tree_snapshot(&root);
4894
4895            assert_eq!(
4896                store.reconcile_remove(
4897                    &metadata.credential_ref,
4898                    lifecycle_request_identity(&preview),
4899                ),
4900                Err(CredentialStoreError::RecoveryRequired)
4901            );
4902            assert_eq!(store_tree_snapshot(&root), before);
4903        }
4904    }
4905
4906    #[test]
4907    fn same_user_substitute_path_is_untouched_by_descriptor_erasure() {
4908        let temporary = tempfile::tempdir().expect("temporary root");
4909        let root = temporary.path().join("credentials-v1");
4910        let store = LocalCredentialStore::at_test_root(&root).expect("open store");
4911        let metadata = store
4912            .add_at("openrouter", "primary", b"remove-race-original", 90)
4913            .expect("add");
4914        let preview = store
4915            .remove_preview_at(&metadata.credential_ref, 100, 200)
4916            .expect("preview");
4917
4918        set_lifecycle_failpoint(Some("remove_substitute_path_after_fd_validation"));
4919        assert_eq!(
4920            store.remove_apply_at(
4921                &metadata.credential_ref,
4922                lifecycle_approval(&preview, REMOVE_CONFIRMATION),
4923                110,
4924            ),
4925            Err(CredentialStoreError::RecoveryRequired)
4926        );
4927        set_lifecycle_failpoint(None);
4928
4929        assert_eq!(
4930            std::fs::read(
4931                root.join(ENTRIES_DIRECTORY)
4932                    .join(&metadata.id)
4933                    .join(SECRET_FILE),
4934            )
4935            .expect("substitute remains live"),
4936            b"same-user-substitute"
4937        );
4938        assert_eq!(
4939            std::fs::metadata(
4940                root.join(ENTRIES_DIRECTORY)
4941                    .join(&metadata.id)
4942                    .join(".test-original-secret"),
4943            )
4944            .expect("descriptor-bound original remains inspectable")
4945            .len(),
4946            0
4947        );
4948        assert!(root.join(LIFECYCLE_DIRECTORY).join(&metadata.id).exists());
4949        assert!(
4950            root.join(ENTRIES_DIRECTORY)
4951                .join(&metadata.id)
4952                .join(METADATA_FILE)
4953                .exists()
4954        );
4955    }
4956
4957    #[test]
4958    fn removal_recovery_rejects_missing_or_foreign_secret_identity() {
4959        for missing in [true, false] {
4960            let temporary = tempfile::tempdir().expect("temporary root");
4961            let root = temporary.path().join(format!("removal-foreign-{missing}"));
4962            let store = LocalCredentialStore::at_test_root(&root).expect("open store");
4963            let metadata = store
4964                .add_at("openrouter", "primary", b"removal-identity-old", 90)
4965                .expect("add");
4966            let preview = store
4967                .remove_preview_at(&metadata.credential_ref, 100, 200)
4968                .expect("preview");
4969            set_lifecycle_failpoint(Some("remove_before_secret_truncate"));
4970            let _ = store.remove_apply_at(
4971                &metadata.credential_ref,
4972                lifecycle_approval(&preview, REMOVE_CONFIRMATION),
4973                110,
4974            );
4975            set_lifecycle_failpoint(None);
4976
4977            let secret_path = root
4978                .join(ENTRIES_DIRECTORY)
4979                .join(&metadata.id)
4980                .join(SECRET_FILE);
4981            std::fs::remove_file(&secret_path).expect("remove journaled inode");
4982            if !missing {
4983                std::fs::write(&secret_path, b"same-user-foreign").expect("foreign secret");
4984                std::fs::set_permissions(&secret_path, std::fs::Permissions::from_mode(0o600))
4985                    .expect("foreign secret mode");
4986            }
4987            let before = store_tree_snapshot(&root);
4988            assert_eq!(
4989                store.reconcile_remove(
4990                    &metadata.credential_ref,
4991                    lifecycle_request_identity(&preview),
4992                ),
4993                Err(CredentialStoreError::RecoveryRequired)
4994            );
4995            assert_eq!(store_tree_snapshot(&root), before);
4996        }
4997    }
4998
4999    #[test]
5000    fn rotation_request_mismatch_is_pre_reconciliation_and_byte_preserving() {
5001        for phase in ["precommit", "postcommit"] {
5002            for mismatch in ["digest", "generation", "expiry"] {
5003                let temporary = tempfile::tempdir().expect("temporary root");
5004                let root = temporary
5005                    .path()
5006                    .join(format!("rotation-{phase}-{mismatch}"));
5007                let store = LocalCredentialStore::at_test_root(&root).expect("open store");
5008                let metadata = store
5009                    .add_at("openrouter", "primary", b"rotation-old-canary", 90)
5010                    .expect("add");
5011                let preview = store
5012                    .rotate_preview_at(&metadata.credential_ref, 100, 200)
5013                    .expect("preview");
5014                set_lifecycle_failpoint(Some(if phase == "precommit" {
5015                    "rotate_before_secret_replace"
5016                } else {
5017                    "rotate_after_secret_replace"
5018                }));
5019                let _ = store.rotate_apply_at(
5020                    &metadata.credential_ref,
5021                    b"rotation-new-canary",
5022                    lifecycle_approval(&preview, ROTATE_CONFIRMATION),
5023                    110,
5024                );
5025                set_lifecycle_failpoint(None);
5026
5027                let request = match mismatch {
5028                    "digest" => CredentialLifecycleRequestIdentity {
5029                        expected_preview: "sha256:0000000000000000000000000000000000000000000000000000000000000000",
5030                        expected_generation: preview.expected_generation,
5031                        expires_at_epoch_seconds: preview.expires_at_epoch_seconds,
5032                    },
5033                    "generation" => CredentialLifecycleRequestIdentity {
5034                        expected_preview: &preview.preview_digest,
5035                        expected_generation: preview.expected_generation + 1,
5036                        expires_at_epoch_seconds: preview.expires_at_epoch_seconds,
5037                    },
5038                    "expiry" => CredentialLifecycleRequestIdentity {
5039                        expected_preview: &preview.preview_digest,
5040                        expected_generation: preview.expected_generation,
5041                        expires_at_epoch_seconds: preview.expires_at_epoch_seconds + 1,
5042                    },
5043                    _ => unreachable!("bounded mismatch"),
5044                };
5045                let before = store_tree_snapshot(&root);
5046                assert_eq!(
5047                    store.reconcile_rotate(&metadata.credential_ref, request),
5048                    Err(CredentialStoreError::RecoveryRequired)
5049                );
5050                assert_eq!(store_tree_snapshot(&root), before);
5051            }
5052        }
5053    }
5054
5055    #[test]
5056    fn rotation_durable_ref_and_literal_mismatch_are_non_mutating() {
5057        for phase in ["precommit", "postcommit"] {
5058            for field in ["credential_ref", "required_confirmation"] {
5059                let temporary = tempfile::tempdir().expect("temporary root");
5060                let root = temporary.path().join(format!("rotation-{phase}-{field}"));
5061                let store = LocalCredentialStore::at_test_root(&root).expect("open store");
5062                let metadata = store
5063                    .add_at("openrouter", "primary", b"rotation-binding-old", 90)
5064                    .expect("add");
5065                let preview = store
5066                    .rotate_preview_at(&metadata.credential_ref, 100, 200)
5067                    .expect("preview");
5068                set_lifecycle_failpoint(Some(if phase == "precommit" {
5069                    "rotate_before_secret_replace"
5070                } else {
5071                    "rotate_after_secret_replace"
5072                }));
5073                let _ = store.rotate_apply_at(
5074                    &metadata.credential_ref,
5075                    b"rotation-binding-new",
5076                    lifecycle_approval(&preview, ROTATE_CONFIRMATION),
5077                    110,
5078                );
5079                set_lifecycle_failpoint(None);
5080
5081                let journal_path = root.join(LIFECYCLE_DIRECTORY).join(&metadata.id);
5082                let mut journal: CredentialLifecycleJournal = serde_json::from_slice(
5083                    &std::fs::read(&journal_path).expect("prepared journal"),
5084                )
5085                .expect("decode journal");
5086                if field == "credential_ref" {
5087                    journal.credential_ref =
5088                        "local-file-v1:00000000000000000000000000000000".to_owned();
5089                } else {
5090                    journal.required_confirmation = REMOVE_CONFIRMATION.to_owned();
5091                }
5092                std::fs::write(
5093                    &journal_path,
5094                    serde_json::to_vec(&journal).expect("encode tampered journal"),
5095                )
5096                .expect("write tampered journal");
5097                let before = store_tree_snapshot(&root);
5098                assert_eq!(
5099                    store.reconcile_rotate(
5100                        &metadata.credential_ref,
5101                        lifecycle_request_identity(&preview),
5102                    ),
5103                    Err(CredentialStoreError::RecoveryRequired)
5104                );
5105                assert_eq!(store_tree_snapshot(&root), before);
5106            }
5107        }
5108    }
5109
5110    #[test]
5111    fn removed_tombstone_reserves_label_and_provider_capacity() {
5112        let temporary = tempfile::tempdir().expect("temporary root");
5113        let root = temporary.path().join("credentials-v1");
5114        let store = LocalCredentialStore::at_test_root(&root).expect("open store");
5115        let removed = store
5116            .add_at("openrouter", "primary", b"tombstone-canary", 90)
5117            .expect("add");
5118        let preview = store
5119            .remove_preview_at(&removed.credential_ref, 100, 200)
5120            .expect("preview");
5121        store
5122            .remove_apply_at(
5123                &removed.credential_ref,
5124                lifecycle_approval(&preview, REMOVE_CONFIRMATION),
5125                110,
5126            )
5127            .expect("remove to tombstone");
5128
5129        assert_eq!(
5130            store.add_at("openrouter", " PRIMARY ", b"replacement", 120),
5131            Err(CredentialStoreError::InvalidInput)
5132        );
5133        for index in 1..MAX_CREDENTIALS_PER_PROVIDER {
5134            store
5135                .add_at(
5136                    "openrouter",
5137                    &format!("account-{index}"),
5138                    b"active-secret",
5139                    120,
5140                )
5141                .expect("active record within tombstone-inclusive capacity");
5142        }
5143        assert_eq!(
5144            store.add_at("openrouter", "overflow", b"secret", 120),
5145            Err(CredentialStoreError::CapacityExhausted)
5146        );
5147        let listed = store.list().expect("tombstone-visible list");
5148        assert_eq!(listed.len(), MAX_CREDENTIALS_PER_PROVIDER);
5149        assert_eq!(
5150            listed
5151                .iter()
5152                .filter(|metadata| metadata.lifecycle_state == CredentialLifecycleState::Removed)
5153                .count(),
5154            1
5155        );
5156        assert_eq!(
5157            std::fs::metadata(
5158                root.join(ENTRIES_DIRECTORY)
5159                    .join(&removed.id)
5160                    .join(SECRET_FILE),
5161            )
5162            .expect("zero-byte tombstone")
5163            .len(),
5164            0
5165        );
5166        assert_eq!(
5167            store.remove_preview_at(&removed.credential_ref, 120, 200),
5168            Err(CredentialStoreError::NotFound)
5169        );
5170    }
5171
5172    #[test]
5173    fn concurrent_rotation_has_one_generation_winner() {
5174        let temporary = tempfile::tempdir().expect("temporary root");
5175        let root = temporary.path().join("credentials-v1");
5176        let store = LocalCredentialStore::at_test_root(&root).expect("open store");
5177        let metadata = store
5178            .add_at("openrouter", "primary", b"original", 90)
5179            .expect("add");
5180        let preview = store
5181            .rotate_preview_at(&metadata.credential_ref, 100, 200)
5182            .expect("preview");
5183        let barrier = Arc::new(Barrier::new(3));
5184        let mut workers = Vec::new();
5185        for secret in [b"first".to_vec(), b"second".to_vec()] {
5186            let store = store.clone();
5187            let preview = preview.clone();
5188            let credential_ref = metadata.credential_ref.clone();
5189            let barrier = Arc::clone(&barrier);
5190            workers.push(std::thread::spawn(move || {
5191                barrier.wait();
5192                store.rotate_apply_at(
5193                    &credential_ref,
5194                    &secret,
5195                    lifecycle_approval(&preview, ROTATE_CONFIRMATION),
5196                    110,
5197                )
5198            }));
5199        }
5200        barrier.wait();
5201        let results = workers
5202            .into_iter()
5203            .map(|worker| worker.join().expect("worker result"))
5204            .collect::<Vec<_>>();
5205        let success_count = results.iter().filter(|result| result.is_ok()).count();
5206        assert!((1..=2).contains(&success_count));
5207        assert!(
5208            results
5209                .iter()
5210                .filter(|result| result.is_err())
5211                .all(|result| matches!(result, Err(CredentialStoreError::LockUnavailable)))
5212        );
5213        assert_eq!(
5214            store
5215                .status(&metadata.credential_ref)
5216                .expect("status")
5217                .metadata
5218                .generation,
5219            2
5220        );
5221        let secret = store
5222            .resolve(&metadata.credential_ref)
5223            .expect("winning secret");
5224        assert!(matches!(secret.as_slice(), b"first" | b"second"));
5225    }
5226
5227    fn rewrite_metadata_for_test(
5228        root: &Path,
5229        id: &str,
5230        mutate: impl FnOnce(&mut CredentialMetadata),
5231    ) {
5232        let path = root.join(ENTRIES_DIRECTORY).join(id).join(METADATA_FILE);
5233        let bytes = std::fs::read(&path).expect("read metadata");
5234        let mut metadata: CredentialMetadata =
5235            serde_json::from_slice(&bytes).expect("decode metadata");
5236        mutate(&mut metadata);
5237        metadata.validate().expect("valid mutated metadata");
5238        std::fs::write(
5239            &path,
5240            serde_json::to_vec(&metadata).expect("encode metadata"),
5241        )
5242        .expect("write metadata");
5243        std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600))
5244            .expect("metadata mode");
5245    }
5246
5247    fn lifecycle_approval<'a>(
5248        preview: &'a CredentialLifecyclePreview,
5249        confirmation: &'a str,
5250    ) -> CredentialLifecycleApproval<'a> {
5251        CredentialLifecycleApproval {
5252            expected_preview: &preview.preview_digest,
5253            expected_generation: preview.expected_generation,
5254            confirmation,
5255            expires_at_epoch_seconds: preview.expires_at_epoch_seconds,
5256        }
5257    }
5258
5259    fn lifecycle_request_identity(
5260        preview: &CredentialLifecyclePreview,
5261    ) -> CredentialLifecycleRequestIdentity<'_> {
5262        CredentialLifecycleRequestIdentity {
5263            expected_preview: &preview.preview_digest,
5264            expected_generation: preview.expected_generation,
5265            expires_at_epoch_seconds: preview.expires_at_epoch_seconds,
5266        }
5267    }
5268
5269    fn set_lifecycle_failpoint(edge: Option<&'static str>) {
5270        LIFECYCLE_FAILPOINT.with(|configured| configured.set(edge));
5271    }
5272
5273    fn store_tree_snapshot(root: &Path) -> BTreeMap<PathBuf, (u32, Option<Vec<u8>>)> {
5274        fn visit(
5275            root: &Path,
5276            path: &Path,
5277            snapshot: &mut BTreeMap<PathBuf, (u32, Option<Vec<u8>>)>,
5278        ) {
5279            let metadata = std::fs::symlink_metadata(path).expect("snapshot metadata");
5280            let relative = path.strip_prefix(root).expect("snapshot relative path");
5281            let bytes = if metadata.is_file() {
5282                Some(std::fs::read(path).expect("snapshot file"))
5283            } else {
5284                None
5285            };
5286            snapshot.insert(relative.to_path_buf(), (metadata.mode() & 0o777, bytes));
5287            if metadata.is_dir() {
5288                for entry in std::fs::read_dir(path).expect("snapshot directory") {
5289                    visit(root, &entry.expect("snapshot entry").path(), snapshot);
5290                }
5291            }
5292        }
5293
5294        let mut snapshot = BTreeMap::new();
5295        visit(root, root, &mut snapshot);
5296        snapshot
5297    }
5298
5299    fn assert_no_file_contains(root: &Path, canary: &[u8]) {
5300        fn visit(path: &Path, canary: &[u8]) {
5301            for entry in std::fs::read_dir(path).expect("read directory") {
5302                let entry = entry.expect("directory entry");
5303                let file_type = entry.file_type().expect("file type");
5304                if file_type.is_dir() {
5305                    visit(&entry.path(), canary);
5306                } else if file_type.is_file() {
5307                    let bytes = std::fs::read(entry.path()).expect("read file");
5308                    assert!(!bytes.windows(canary.len()).any(|window| window == canary));
5309                }
5310            }
5311        }
5312        visit(root, canary);
5313    }
5314
5315    #[cfg(target_os = "linux")]
5316    #[test]
5317    fn linux_default_production_root_uses_account_home_xdg_layout() {
5318        let home = Path::new("/srv/steadymux-owner");
5319        assert_eq!(
5320            linux_production_root_path(home, None).expect("default Linux root"),
5321            home.join(".local")
5322                .join("share")
5323                .join("SteadyMux")
5324                .join("credentials-v1")
5325        );
5326        assert!(linux_production_root_path(home, Some(std::ffi::OsStr::new("relative"))).is_err());
5327    }
5328
5329    #[cfg(target_os = "linux")]
5330    #[test]
5331    fn linux_default_store_creates_private_xdg_tree_and_reopens() {
5332        let temporary = tempfile::tempdir().expect("temporary root");
5333        let home = temporary.path().join("home");
5334        std::fs::create_dir(&home).expect("account home");
5335        std::fs::set_permissions(&home, std::fs::Permissions::from_mode(0o700))
5336            .expect("private account home");
5337
5338        let (root_path, root) =
5339            ensure_linux_production_store(&home, None).expect("ensure default Linux store");
5340        drop(root);
5341        assert_eq!(
5342            root_path,
5343            home.join(".local")
5344                .join("share")
5345                .join("SteadyMux")
5346                .join("credentials-v1")
5347        );
5348        for directory in [
5349            home.join(".local"),
5350            home.join(".local").join("share"),
5351            home.join(".local").join("share").join("SteadyMux"),
5352            root_path.clone(),
5353        ] {
5354            assert_eq!(
5355                std::fs::metadata(directory)
5356                    .expect("Linux store directory metadata")
5357                    .mode()
5358                    & 0o777,
5359                0o700
5360            );
5361        }
5362        let (reopened_path, reopened) =
5363            open_linux_production_store(&home, None).expect("reopen default Linux store");
5364        drop(reopened);
5365        assert_eq!(reopened_path, root_path);
5366    }
5367
5368    #[cfg(target_os = "linux")]
5369    #[test]
5370    fn linux_xdg_override_store_is_private_and_reopens_without_creation() {
5371        let temporary = tempfile::tempdir().expect("temporary root");
5372        let home = temporary.path().join("home");
5373        let data_home = temporary.path().join("xdg-data");
5374        std::fs::create_dir(&home).expect("account home");
5375        std::fs::create_dir(&data_home).expect("XDG data home");
5376        std::fs::set_permissions(&home, std::fs::Permissions::from_mode(0o700))
5377            .expect("private account home");
5378        std::fs::set_permissions(&data_home, std::fs::Permissions::from_mode(0o700))
5379            .expect("private XDG data home");
5380
5381        let (root_path, root) = ensure_linux_production_store(&home, Some(data_home.as_os_str()))
5382            .expect("ensure Linux store");
5383        drop(root);
5384        assert_eq!(
5385            root_path,
5386            data_home.join("SteadyMux").join("credentials-v1")
5387        );
5388        assert_eq!(
5389            std::fs::metadata(data_home.join("SteadyMux"))
5390                .expect("product metadata")
5391                .mode()
5392                & 0o777,
5393            0o700
5394        );
5395        assert_eq!(
5396            std::fs::metadata(&root_path).expect("root metadata").mode() & 0o777,
5397            0o700
5398        );
5399
5400        let (reopened_path, reopened) =
5401            open_linux_production_store(&home, Some(data_home.as_os_str()))
5402                .expect("reopen Linux store");
5403        drop(reopened);
5404        assert_eq!(reopened_path, root_path);
5405
5406        std::fs::set_permissions(&data_home, std::fs::Permissions::from_mode(0o777))
5407            .expect("relax XDG data home");
5408        assert!(matches!(
5409            open_linux_production_store(&home, Some(data_home.as_os_str())),
5410            Err(CredentialStoreError::UnsafeStorage)
5411        ));
5412    }
5413
5414    #[cfg(target_os = "linux")]
5415    #[test]
5416    fn linux_account_record_home_parser_requires_matching_uid_and_absolute_home() {
5417        let record = b"owner:x:1001:1001:Owner:/srv/owner:/bin/bash\n";
5418        assert_eq!(
5419            parse_linux_account_home(record, 1001).expect("valid account record"),
5420            PathBuf::from("/srv/owner")
5421        );
5422        assert!(parse_linux_account_home(record, 1002).is_err());
5423        assert!(
5424            parse_linux_account_home(b"owner:x:1001:1001:Owner:relative:/bin/sh\n", 1001).is_err()
5425        );
5426        assert!(
5427            parse_linux_account_home(
5428                b"owner:x:1001:1001:Owner:/a:/bin/sh\nsecond:x:1001:1001:Owner:/b:/bin/sh\n",
5429                1001
5430            )
5431            .is_err()
5432        );
5433    }
5434
5435    #[cfg(target_os = "linux")]
5436    #[test]
5437    fn linux_current_user_home_resolves_from_effective_uid_account_record() {
5438        let home = current_user_home_from_account_record().expect("effective-UID account home");
5439        validate_absolute_path(&home).expect("absolute account home");
5440        let home_directory = open_path_directory(&home).expect("open account home");
5441        validate_owned_directory(
5442            &fstat(&home_directory).expect("effective-UID account home metadata"),
5443        )
5444        .expect("owned account home");
5445    }
5446
5447    #[cfg(target_os = "macos")]
5448    #[test]
5449    fn account_record_home_parser_requires_one_matching_uid_and_absolute_home() {
5450        let record = b"name: owner\nuid: 501\ndir: /Users/owner\n";
5451        assert_eq!(
5452            parse_account_home(record, 501).expect("valid account record"),
5453            PathBuf::from("/Users/owner")
5454        );
5455        assert!(parse_account_home(record, 502).is_err());
5456        assert!(parse_account_home(b"uid: 501\ndir: relative\n", 501).is_err());
5457        assert!(parse_account_home(b"uid: 501\ndir: /Users/a\ndir: /Users/b\n", 501).is_err());
5458    }
5459
5460    #[cfg(target_os = "macos")]
5461    fn account_lookup_test_command(script: &str) -> std::process::Command {
5462        use std::os::unix::process::CommandExt as _;
5463        use std::process::Stdio;
5464
5465        let mut command = std::process::Command::new("/bin/sh");
5466        command
5467            .args(["-c", script])
5468            .env_clear()
5469            .stdin(Stdio::null())
5470            .stderr(Stdio::null())
5471            .stdout(Stdio::piped())
5472            .process_group(0);
5473        command
5474    }
5475
5476    #[cfg(target_os = "macos")]
5477    #[test]
5478    fn account_lookup_runner_is_bounded_by_output_and_wall_clock() {
5479        let mut valid = account_lookup_test_command("printf 'uid: 501\\ndir: /Users/owner\\n'");
5480        assert_eq!(
5481            run_bounded_account_lookup(&mut valid, Duration::from_secs(1), 128)
5482                .expect("bounded local output"),
5483            b"uid: 501\ndir: /Users/owner\n"
5484        );
5485
5486        let mut oversized = account_lookup_test_command("printf '0123456789abcdef'");
5487        let error = run_bounded_account_lookup(&mut oversized, Duration::from_secs(1), 8)
5488            .expect_err("oversized output rejected");
5489        assert_eq!(
5490            format!("{error:?} {error}"),
5491            "UnsafeStorage device-local credential operation failed"
5492        );
5493
5494        let mut stalled = account_lookup_test_command("sleep 2; printf 'late-canary'");
5495        let started = Instant::now();
5496        let error = run_bounded_account_lookup(&mut stalled, Duration::from_millis(25), 128)
5497            .expect_err("stalled lookup rejected");
5498        assert!(started.elapsed() < Duration::from_secs(1));
5499        let diagnostic = format!("{error:?} {error}");
5500        assert_eq!(
5501            diagnostic,
5502            "UnsafeStorage device-local credential operation failed"
5503        );
5504        assert!(!diagnostic.contains("late-canary"));
5505    }
5506}