#[cfg(not(target_os = "linux"))]
use rustix::fs::openat;
use rustix::fs::{
AtFlags, Dir, FileType, FlockOperation, Mode, OFlags, fchmod, flock, fstat, ftruncate, mkdirat,
open, renameat, statat, unlinkat,
};
#[cfg(any(target_os = "linux", target_os = "macos"))]
use rustix::fs::{RenameFlags, renameat_with};
#[cfg(target_os = "linux")]
use rustix::fs::{ResolveFlags, StatxFlags, openat2};
use rustix::process::geteuid;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::fmt;
use std::fs::File;
use std::io::{Read, Write};
use std::os::fd::AsFd;
use std::path::{Component, Path, PathBuf};
use std::sync::Arc;
#[cfg(test)]
use std::sync::atomic::{AtomicUsize, Ordering};
#[cfg(any(target_os = "linux", target_os = "macos"))]
use std::time::{Duration, Instant};
use std::time::{SystemTime, UNIX_EPOCH};
use zeroize::Zeroizing;
#[cfg(test)]
use crate::local_credential_types::MAX_LABEL_BYTES;
pub use crate::local_credential_types::{
CredentialDependencyImpact, CredentialHealthState, CredentialLifecycleApproval,
CredentialLifecycleOperation, CredentialLifecycleOutcome, CredentialLifecyclePreview,
CredentialLifecycleRequestIdentity, CredentialLifecycleState, CredentialMetadata,
CredentialRemovalReconciliation, CredentialRotationReconciliation, CredentialStatus,
CredentialStoreError, REMOVE_CONFIRMATION, ROTATE_CONFIRMATION,
};
use crate::local_credential_types::{
MAX_CREDENTIALS, MAX_CREDENTIALS_PER_PROVIDER, MAX_METADATA_BYTES, MAX_PROVIDER_BYTES,
MAX_SECRET_BYTES, REFERENCE_PREFIX, SCHEMA_VERSION, normalize_account_label, valid_descriptor,
valid_id, valid_sha256_id,
};
const ENTRIES_DIRECTORY: &str = "entries";
const LIFECYCLE_DIRECTORY: &str = "lifecycle";
const LOCK_FILE: &str = ".store.lock";
const SECRET_FILE: &str = "secret";
const METADATA_FILE: &str = "metadata.json";
const NEXT_SECRET_FILE: &str = ".next-secret";
const NEXT_METADATA_FILE: &str = ".next-metadata.json";
#[cfg(any(target_os = "linux", target_os = "macos"))]
const MAX_ACCOUNT_RECORD_BYTES: usize = 2048;
#[cfg(target_os = "linux")]
const MAX_LINUX_DATA_HOME_BYTES: usize = 4096;
const MAX_PREVIEW_LIFETIME_SECONDS: u64 = 10 * 60;
#[cfg(any(target_os = "linux", target_os = "macos"))]
const ACCOUNT_LOOKUP_TIMEOUT: Duration = Duration::from_secs(2);
#[cfg(any(target_os = "linux", target_os = "macos"))]
const ACCOUNT_LOOKUP_POLL_INTERVAL: Duration = Duration::from_millis(5);
#[cfg(any(target_os = "linux", target_os = "macos"))]
const ACCOUNT_LOOKUP_CLEANUP_DEADLINE: Duration = Duration::from_millis(250);
const FLOCK_EINTR_RETRY_BOUND: usize = 256;
const FLOCK_EINTR_RETRY_SLEEP: Duration = Duration::from_millis(1);
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct CredentialLifecycleJournal {
schema_version: String,
operation: CredentialLifecycleOperation,
credential_id: String,
credential_ref: String,
expected_generation: u64,
target_generation: Option<u64>,
preview_digest: String,
expires_at_epoch_seconds: u64,
required_confirmation: String,
old_secret_identity: FileIdentity,
staged_secret_identity: Option<FileIdentity>,
state: CredentialLifecycleJournalState,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
enum CredentialLifecycleJournalState {
Prepared,
Committed,
}
impl CredentialLifecycleJournal {
fn validate(&self) -> Result<(), CredentialStoreError> {
if self.schema_version != SCHEMA_VERSION
|| !valid_id(&self.credential_id)
|| self.credential_ref != format!("{REFERENCE_PREFIX}{}", self.credential_id)
|| self.expected_generation == 0
|| !valid_sha256_id(&self.preview_digest)
|| !valid_journal_secret_identity(self.old_secret_identity)
|| self
.staged_secret_identity
.is_some_and(|identity| !valid_journal_secret_identity(identity))
|| match self.operation {
CredentialLifecycleOperation::Rotate => {
self.required_confirmation != ROTATE_CONFIRMATION
|| self.target_generation != self.expected_generation.checked_add(1)
|| self.staged_secret_identity.is_none()
|| self.staged_secret_identity == Some(self.old_secret_identity)
|| self.staged_secret_identity.is_some_and(|identity| {
identity.device != self.old_secret_identity.device
})
}
CredentialLifecycleOperation::Remove => {
self.required_confirmation != REMOVE_CONFIRMATION
|| self.target_generation.is_some()
|| self.staged_secret_identity.is_some()
}
}
{
return Err(CredentialStoreError::CorruptState);
}
Ok(())
}
}
fn valid_journal_secret_identity(identity: FileIdentity) -> bool {
identity.device != 0
&& identity.inode != 0
&& identity.owner == geteuid().as_raw()
&& identity.mode == 0o600
&& identity.link_count == 1
&& identity.birth_time.is_some()
}
#[derive(Clone)]
pub struct LocalCredentialStore {
root_path: PathBuf,
root: Arc<File>,
root_identity: FileIdentity,
#[cfg(test)]
flock_eintr_injections: Arc<AtomicUsize>,
}
pub struct CredentialInventoryGuard {
store: LocalCredentialStore,
_locked: LockedRoot,
entries: File,
credentials: Vec<CredentialMetadata>,
}
impl CredentialInventoryGuard {
pub fn credentials(&self) -> &[CredentialMetadata] {
&self.credentials
}
pub fn rotate_preview_with_bindings(
&self,
credential_ref: &str,
bound_phases: &[String],
bound_routes: &[String],
expires_at_epoch_seconds: u64,
) -> Result<CredentialLifecyclePreview, CredentialStoreError> {
self.lifecycle_preview_with_bindings(
credential_ref,
CredentialLifecycleOperation::Rotate,
bound_phases,
bound_routes,
current_epoch_seconds()?,
expires_at_epoch_seconds,
)
}
pub fn remove_preview_with_bindings(
&self,
credential_ref: &str,
bound_phases: &[String],
bound_routes: &[String],
expires_at_epoch_seconds: u64,
) -> Result<CredentialLifecyclePreview, CredentialStoreError> {
self.lifecycle_preview_with_bindings(
credential_ref,
CredentialLifecycleOperation::Remove,
bound_phases,
bound_routes,
current_epoch_seconds()?,
expires_at_epoch_seconds,
)
}
fn lifecycle_preview_with_bindings(
&self,
credential_ref: &str,
operation: CredentialLifecycleOperation,
bound_phases: &[String],
bound_routes: &[String],
now_epoch_seconds: u64,
expires_at_epoch_seconds: u64,
) -> Result<CredentialLifecyclePreview, CredentialStoreError> {
let id = parse_reference(credential_ref)?;
let mut metadata = self
.credentials
.iter()
.find(|metadata| metadata.id == id && metadata.credential_ref == credential_ref)
.cloned()
.ok_or(CredentialStoreError::NotFound)?;
if metadata.lifecycle_state == CredentialLifecycleState::Removed {
return Err(CredentialStoreError::NotFound);
}
metadata.bound_phases = bound_phases.to_vec();
metadata.bound_routes = bound_routes.to_vec();
metadata.validate()?;
lifecycle_preview_for_metadata(
&metadata,
operation,
now_epoch_seconds,
expires_at_epoch_seconds,
)
}
fn metadata(&self, credential_ref: &str) -> Result<CredentialMetadata, CredentialStoreError> {
let id = parse_reference(credential_ref)?;
self.credentials
.iter()
.find(|metadata| metadata.id == id && metadata.credential_ref == credential_ref)
.cloned()
.ok_or(CredentialStoreError::NotFound)
}
pub fn reconcile_rotate(
&self,
credential_ref: &str,
request: CredentialLifecycleRequestIdentity<'_>,
) -> Result<CredentialRotationReconciliation, CredentialStoreError> {
let id = parse_reference(credential_ref)?;
let lifecycle = open_or_create_private_directory(&self._locked.root, LIFECYCLE_DIRECTORY)?;
reconcile_rotate_request_locked(&self.entries, &lifecycle, id, credential_ref, request)
}
pub fn reconcile_remove(
&self,
credential_ref: &str,
request: CredentialLifecycleRequestIdentity<'_>,
) -> Result<CredentialRemovalReconciliation, CredentialStoreError> {
let id = parse_reference(credential_ref)?;
let lifecycle = open_or_create_private_directory(&self._locked.root, LIFECYCLE_DIRECTORY)?;
reconcile_remove_request_locked(&self.entries, &lifecycle, id, credential_ref, request)
}
pub fn rotate_apply_with_bindings(
&self,
credential_ref: &str,
secret: &[u8],
approval: CredentialLifecycleApproval<'_>,
bound_phases: &[String],
bound_routes: &[String],
) -> Result<CredentialLifecycleOutcome, CredentialStoreError> {
self.rotate_apply_with_bindings_at(
credential_ref,
secret,
approval,
bound_phases,
bound_routes,
current_epoch_seconds()?,
)
}
fn rotate_apply_with_bindings_at(
&self,
credential_ref: &str,
secret: &[u8],
approval: CredentialLifecycleApproval<'_>,
bound_phases: &[String],
bound_routes: &[String],
now_epoch_seconds: u64,
) -> Result<CredentialLifecycleOutcome, CredentialStoreError> {
self.store.rotate_apply_locked(
&self._locked.root,
&self.entries,
credential_ref,
secret,
approval,
bound_phases,
bound_routes,
now_epoch_seconds,
)
}
pub fn remove_apply_with_bindings(
&self,
credential_ref: &str,
approval: CredentialLifecycleApproval<'_>,
bound_phases: &[String],
bound_routes: &[String],
) -> Result<CredentialLifecycleOutcome, CredentialStoreError> {
self.remove_apply_with_bindings_at(
credential_ref,
approval,
bound_phases,
bound_routes,
current_epoch_seconds()?,
)
}
fn remove_apply_with_bindings_at(
&self,
credential_ref: &str,
approval: CredentialLifecycleApproval<'_>,
bound_phases: &[String],
bound_routes: &[String],
now_epoch_seconds: u64,
) -> Result<CredentialLifecycleOutcome, CredentialStoreError> {
self.store.remove_apply_locked(
&self._locked.root,
&self.entries,
credential_ref,
approval,
bound_phases,
bound_routes,
now_epoch_seconds,
)
}
pub fn verify_available(&self, credential_ref: &str) -> Result<(), CredentialStoreError> {
let id = parse_reference(credential_ref)?;
let metadata = self
.credentials
.iter()
.find(|metadata| metadata.id == id && metadata.credential_ref == credential_ref)
.ok_or(CredentialStoreError::NotFound)?;
if metadata.lifecycle_state != CredentialLifecycleState::Active
|| metadata.health != CredentialHealthState::LocallyVerified
|| metadata.last_verified_at_epoch_seconds.is_none()
{
return Err(CredentialStoreError::NotFound);
}
let entry = open_private_directory(&self.entries, id)?;
let (_, size) = read_secret_or_tombstone_identity(&entry)?;
if size == 0 {
return Err(CredentialStoreError::NotFound);
}
Ok(())
}
pub fn status(&self, credential_ref: &str) -> Result<CredentialStatus, CredentialStoreError> {
let id = parse_reference(credential_ref)?;
let metadata = self
.credentials
.iter()
.find(|metadata| metadata.id == id && metadata.credential_ref == credential_ref)
.cloned()
.ok_or(CredentialStoreError::NotFound)?;
let entry = open_private_directory(&self.entries, id)?;
let status = match metadata.lifecycle_state {
CredentialLifecycleState::Active => {
self.verify_available(credential_ref)?;
"available"
}
CredentialLifecycleState::Removed => {
let _ = read_tombstone_identity(&entry)?;
"removed"
}
};
Ok(CredentialStatus { metadata, status })
}
}
impl fmt::Debug for LocalCredentialStore {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("LocalCredentialStore")
.field("root", &"[DEVICE_LOCAL]")
.finish()
}
}
impl LocalCredentialStore {
pub fn production_setup_root_path() -> Result<PathBuf, CredentialStoreError> {
#[cfg(target_os = "macos")]
{
let home = current_user_home_from_account_record()?;
validate_absolute_path(&home)?;
Ok(home
.join("Library")
.join("Application Support")
.join("SteadyMux")
.join("device-setup-v1"))
}
#[cfg(not(target_os = "macos"))]
{
Err(CredentialStoreError::UnsupportedPlatform)
}
}
pub fn ensure_production_setup_root() -> Result<PathBuf, CredentialStoreError> {
#[cfg(target_os = "macos")]
{
let home = current_user_home_from_account_record()?;
validate_absolute_path(&home)?;
let home_directory = open_path_directory(&home)?;
validate_owned_directory(
&fstat(&home_directory).map_err(|_| CredentialStoreError::UnsafeStorage)?,
)?;
let library = open_owned_directory(&home_directory, "Library")?;
let application_support = open_owned_directory(&library, "Application Support")?;
let product = open_or_create_private_directory(&application_support, "SteadyMux")?;
let _setup = open_or_create_private_directory(&product, "device-setup-v1")?;
Ok(home
.join("Library")
.join("Application Support")
.join("SteadyMux")
.join("device-setup-v1"))
}
#[cfg(not(target_os = "macos"))]
{
Err(CredentialStoreError::UnsupportedPlatform)
}
}
pub fn production() -> Result<Self, CredentialStoreError> {
#[cfg(target_os = "macos")]
{
let home = current_user_home_from_account_record()?;
let (root_path, root) = ensure_production_store(&home)?;
Self::open_verified(root_path, root)
}
#[cfg(target_os = "linux")]
{
let home = current_user_home_from_account_record()?;
let xdg_data_home = std::env::var_os("XDG_DATA_HOME");
let (root_path, root) = ensure_linux_production_store(&home, xdg_data_home.as_deref())?;
Self::open_verified(root_path, root)
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
{
Err(CredentialStoreError::UnsupportedPlatform)
}
}
pub fn production_existing() -> Result<Self, CredentialStoreError> {
#[cfg(target_os = "macos")]
{
let home = current_user_home_from_account_record()?;
let (root_path, root) = open_production_store(&home)?;
Self::open_verified_existing(root_path, root)
}
#[cfg(target_os = "linux")]
{
let home = current_user_home_from_account_record()?;
let xdg_data_home = std::env::var_os("XDG_DATA_HOME");
let (root_path, root) = open_linux_production_store(&home, xdg_data_home.as_deref())?;
Self::open_verified_existing(root_path, root)
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
{
Err(CredentialStoreError::UnsupportedPlatform)
}
}
#[cfg(test)]
fn at_test_root(root: &Path) -> Result<Self, CredentialStoreError> {
let temporary_root = std::env::temp_dir();
let relative = root
.strip_prefix(&temporary_root)
.map_err(|_| CredentialStoreError::UnsafeStorage)?;
let normalized_root = temporary_root
.canonicalize()
.map_err(|_| CredentialStoreError::UnsafeStorage)?
.join(relative);
let boundary_path = temporary_root
.canonicalize()
.map_err(|_| CredentialStoreError::UnsafeStorage)?;
let boundary = open_path_directory(&boundary_path)?;
let relative = normalized_root
.strip_prefix(&boundary_path)
.map_err(|_| CredentialStoreError::UnsafeStorage)?;
let root = ensure_private_components(&boundary, relative)?;
Self::open_verified(normalized_root, root)
}
#[cfg(test)]
fn at_existing_test_root(root: &Path) -> Result<Self, CredentialStoreError> {
let temporary_root = std::env::temp_dir();
let relative = root
.strip_prefix(&temporary_root)
.map_err(|_| CredentialStoreError::UnsafeStorage)?;
let normalized_root = temporary_root
.canonicalize()
.map_err(|_| CredentialStoreError::UnsafeStorage)?
.join(relative);
let root = open_path_directory(&normalized_root)?;
Self::open_verified_existing(normalized_root, root)
}
fn open_verified(root_path: PathBuf, root: File) -> Result<Self, CredentialStoreError> {
validate_absolute_path(&root_path)?;
let root_stat = fstat(&root).map_err(|_| CredentialStoreError::UnsafeStorage)?;
validate_directory(&root_stat)?;
let store = Self {
root_path,
root: Arc::new(root),
root_identity: identity(&root_stat)?,
#[cfg(test)]
flock_eintr_injections: Arc::new(AtomicUsize::new(0)),
};
let locked = store.lock()?;
let _ = open_or_create_private_directory(&locked.root, ENTRIES_DIRECTORY)?;
Ok(store)
}
fn open_verified_existing(
root_path: PathBuf,
root: File,
) -> Result<Self, CredentialStoreError> {
validate_absolute_path(&root_path)?;
let root_stat = fstat(&root).map_err(|_| CredentialStoreError::UnsafeStorage)?;
validate_directory(&root_stat)?;
let store = Self {
root_path,
root: Arc::new(root),
root_identity: identity(&root_stat)?,
#[cfg(test)]
flock_eintr_injections: Arc::new(AtomicUsize::new(0)),
};
let entries = open_private_directory(&store.root, ENTRIES_DIRECTORY)?;
drop(entries);
Ok(store)
}
pub fn add(
&self,
provider: &str,
label: &str,
secret: &[u8],
) -> Result<CredentialMetadata, CredentialStoreError> {
let now_epoch_seconds = current_epoch_seconds()?;
self.add_at(provider, label, secret, now_epoch_seconds)
}
fn add_at(
&self,
provider: &str,
label: &str,
secret: &[u8],
now_epoch_seconds: u64,
) -> Result<CredentialMetadata, CredentialStoreError> {
let label = normalize_account_label(label)?;
if !valid_descriptor(provider, MAX_PROVIDER_BYTES) || !valid_secret(secret) {
return Err(CredentialStoreError::InvalidInput);
}
let locked = self.lock()?;
let entries = open_or_create_private_directory(&locked.root, ENTRIES_DIRECTORY)?;
recover_add_staging(&entries)?;
ensure_store_needs_no_lifecycle_recovery(&locked.root, &entries)?;
let existing = self.list_locked(&entries)?;
if existing.len() >= MAX_CREDENTIALS
|| existing
.iter()
.filter(|metadata| metadata.provider == provider)
.count()
>= MAX_CREDENTIALS_PER_PROVIDER
{
return Err(CredentialStoreError::CapacityExhausted);
}
if existing
.iter()
.any(|metadata| metadata.provider == provider && metadata.label == label)
{
return Err(CredentialStoreError::InvalidInput);
}
let id = generate_unused_id(&entries)?;
let metadata = CredentialMetadata {
schema_version: SCHEMA_VERSION.to_owned(),
credential_ref: format!("{REFERENCE_PREFIX}{id}"),
id: id.clone(),
provider: provider.to_owned(),
label,
generation: 1,
created_at_epoch_seconds: now_epoch_seconds,
rotated_at_epoch_seconds: None,
previous_generation: None,
last_lifecycle_preview_digest: None,
lifecycle_state: CredentialLifecycleState::Active,
health: CredentialHealthState::LocallyVerified,
last_verified_at_epoch_seconds: Some(now_epoch_seconds),
bound_phases: Vec::new(),
bound_routes: Vec::new(),
};
let metadata_bytes =
serde_json::to_vec(&metadata).map_err(|_| CredentialStoreError::CommitAmbiguous)?;
let staging = format!(".staging-{id}");
mkdirat(&entries, &staging, Mode::from_raw_mode(0o700))
.map_err(|_| CredentialStoreError::UnsafeStorage)?;
let staged = open_private_directory(&entries, &staging)?;
let mut committed = false;
let result = (|| {
write_new_file(&staged, SECRET_FILE, secret)?;
write_new_file(&staged, METADATA_FILE, &metadata_bytes)?;
durable_sync(&staged)?;
rename_new_entry(&entries, &staging, &id)?;
committed = true;
durable_sync(&entries)?;
let committed = self.read_metadata(&entries, &id)?;
if committed != metadata {
return Err(CredentialStoreError::CommitAmbiguous);
}
Ok(metadata.clone())
})();
if result.is_err() {
cleanup_failed_add(&entries, &staged, &staging, committed);
}
result
}
pub fn rotate_preview(
&self,
credential_ref: &str,
expires_at_epoch_seconds: u64,
) -> Result<CredentialLifecyclePreview, CredentialStoreError> {
self.rotate_preview_at(
credential_ref,
current_epoch_seconds()?,
expires_at_epoch_seconds,
)
}
fn rotate_preview_at(
&self,
credential_ref: &str,
now_epoch_seconds: u64,
expires_at_epoch_seconds: u64,
) -> Result<CredentialLifecyclePreview, CredentialStoreError> {
self.lifecycle_preview(
credential_ref,
CredentialLifecycleOperation::Rotate,
now_epoch_seconds,
expires_at_epoch_seconds,
)
}
pub fn remove_preview(
&self,
credential_ref: &str,
expires_at_epoch_seconds: u64,
) -> Result<CredentialLifecyclePreview, CredentialStoreError> {
self.remove_preview_at(
credential_ref,
current_epoch_seconds()?,
expires_at_epoch_seconds,
)
}
fn remove_preview_at(
&self,
credential_ref: &str,
now_epoch_seconds: u64,
expires_at_epoch_seconds: u64,
) -> Result<CredentialLifecyclePreview, CredentialStoreError> {
self.lifecycle_preview(
credential_ref,
CredentialLifecycleOperation::Remove,
now_epoch_seconds,
expires_at_epoch_seconds,
)
}
pub fn reconcile_rotate(
&self,
credential_ref: &str,
request: CredentialLifecycleRequestIdentity<'_>,
) -> Result<CredentialRotationReconciliation, CredentialStoreError> {
let id = parse_reference(credential_ref)?;
let locked = self.lock()?;
let entries = open_or_create_private_directory(&locked.root, ENTRIES_DIRECTORY)?;
self.ensure_normalized_label_inventory(&entries)?;
recover_add_staging(&entries)?;
let lifecycle = open_or_create_private_directory(&locked.root, LIFECYCLE_DIRECTORY)?;
reconcile_rotate_request_locked(&entries, &lifecycle, id, credential_ref, request)
}
pub fn reconcile_remove(
&self,
credential_ref: &str,
request: CredentialLifecycleRequestIdentity<'_>,
) -> Result<CredentialRemovalReconciliation, CredentialStoreError> {
let id = parse_reference(credential_ref)?;
let locked = self.lock()?;
let entries = open_or_create_private_directory(&locked.root, ENTRIES_DIRECTORY)?;
self.ensure_normalized_label_inventory(&entries)?;
recover_add_staging(&entries)?;
let lifecycle = open_or_create_private_directory(&locked.root, LIFECYCLE_DIRECTORY)?;
reconcile_remove_request_locked(&entries, &lifecycle, id, credential_ref, request)
}
pub fn rotate_apply(
&self,
credential_ref: &str,
secret: &[u8],
approval: CredentialLifecycleApproval<'_>,
) -> Result<CredentialLifecycleOutcome, CredentialStoreError> {
self.rotate_apply_at(credential_ref, secret, approval, current_epoch_seconds()?)
}
fn rotate_apply_at(
&self,
credential_ref: &str,
secret: &[u8],
approval: CredentialLifecycleApproval<'_>,
now_epoch_seconds: u64,
) -> Result<CredentialLifecycleOutcome, CredentialStoreError> {
let guard = self.lock_lifecycle_inventory()?;
let metadata = guard.metadata(credential_ref)?;
guard.rotate_apply_with_bindings_at(
credential_ref,
secret,
approval,
&metadata.bound_phases,
&metadata.bound_routes,
now_epoch_seconds,
)
}
#[allow(clippy::too_many_arguments)]
fn rotate_apply_locked(
&self,
root: &File,
entries: &File,
credential_ref: &str,
secret: &[u8],
approval: CredentialLifecycleApproval<'_>,
bound_phases: &[String],
bound_routes: &[String],
now_epoch_seconds: u64,
) -> Result<CredentialLifecycleOutcome, CredentialStoreError> {
let id = parse_reference(credential_ref)?;
let lifecycle = open_or_create_private_directory(root, LIFECYCLE_DIRECTORY)?;
match reconcile_rotate_request_locked(
entries,
&lifecycle,
id,
credential_ref,
CredentialLifecycleRequestIdentity {
expected_preview: approval.expected_preview,
expected_generation: approval.expected_generation,
expires_at_epoch_seconds: approval.expires_at_epoch_seconds,
},
)? {
CredentialRotationReconciliation::Committed(outcome) => return Ok(outcome),
CredentialRotationReconciliation::RolledBackFreshPreviewRequired => {
return Err(CredentialStoreError::FreshPreviewRequired);
}
CredentialRotationReconciliation::NotRequired => {}
}
if !valid_secret(secret) {
return Err(CredentialStoreError::InvalidInput);
}
if approval.confirmation != ROTATE_CONFIRMATION {
return Err(CredentialStoreError::ConfirmationRequired);
}
let metadata = self.read_unique_metadata(entries, id)?;
if metadata.generation != approval.expected_generation {
return Err(CredentialStoreError::GenerationConflict);
}
let (old_secret, old_secret_identity) = read_secret_with_identity(entries, id)?;
drop(old_secret);
let mut preview_metadata = metadata.clone();
preview_metadata.bound_phases = bound_phases.to_vec();
preview_metadata.bound_routes = bound_routes.to_vec();
preview_metadata.validate()?;
let preview = lifecycle_preview_for_metadata(
&preview_metadata,
CredentialLifecycleOperation::Rotate,
now_epoch_seconds,
approval.expires_at_epoch_seconds,
)?;
validate_expected_preview(&preview, approval.expected_preview, now_epoch_seconds)?;
let next_generation = metadata
.generation
.checked_add(1)
.ok_or(CredentialStoreError::GenerationConflict)?;
let mut replacement = metadata.clone();
replacement.previous_generation = Some(metadata.generation);
replacement.last_lifecycle_preview_digest = Some(preview.preview_digest.clone());
replacement.generation = next_generation;
replacement.rotated_at_epoch_seconds = Some(now_epoch_seconds);
replacement.last_verified_at_epoch_seconds = Some(now_epoch_seconds);
replacement.lifecycle_state = CredentialLifecycleState::Active;
replacement.health = CredentialHealthState::LocallyVerified;
replacement.validate()?;
let metadata_bytes =
serde_json::to_vec(&replacement).map_err(|_| CredentialStoreError::CommitAmbiguous)?;
if metadata_bytes.len() > MAX_METADATA_BYTES {
return Err(CredentialStoreError::InvalidInput);
}
let entry = open_private_directory(entries, id)?;
let precommit = (|| -> Result<FileIdentity, CredentialStoreError> {
write_new_file(&entry, NEXT_SECRET_FILE, secret)?;
write_new_file(&entry, NEXT_METADATA_FILE, &metadata_bytes)?;
durable_sync(&entry)?;
verify_staged_files(&entry, &replacement, secret)
})();
let staged_secret_identity = match precommit {
Ok(identity) => identity,
Err(error) => {
cleanup_precommit_files(&entry);
return Err(error);
}
};
let journal = CredentialLifecycleJournal {
schema_version: SCHEMA_VERSION.to_owned(),
operation: CredentialLifecycleOperation::Rotate,
credential_id: id.to_owned(),
credential_ref: credential_ref.to_owned(),
expected_generation: metadata.generation,
target_generation: Some(next_generation),
preview_digest: preview.preview_digest.clone(),
expires_at_epoch_seconds: preview.expires_at_epoch_seconds,
required_confirmation: ROTATE_CONFIRMATION.to_owned(),
old_secret_identity,
staged_secret_identity: Some(staged_secret_identity),
state: CredentialLifecycleJournalState::Prepared,
};
if let Err(error) = write_journal_new(&lifecycle, &journal) {
cleanup_precommit_files(&entry);
return Err(error);
}
let (live_secret, verified_old_identity) = read_secret_with_identity(entries, id)
.map_err(|_| CredentialStoreError::RecoveryRequired)?;
drop(live_secret);
let (staged_secret, verified_staged_identity) =
read_regular_file_with_identity(&entry, NEXT_SECRET_FILE, MAX_SECRET_BYTES, true)
.map_err(|_| CredentialStoreError::RecoveryRequired)?;
drop(staged_secret);
if verified_old_identity != journal.old_secret_identity
|| Some(verified_staged_identity) != journal.staged_secret_identity
{
return Err(CredentialStoreError::RecoveryRequired);
}
if lifecycle_failpoint("rotate_before_secret_replace") {
return Err(CredentialStoreError::CommitAmbiguous);
}
rename_replace(&entry, NEXT_SECRET_FILE, SECRET_FILE)?;
let (committed_secret, committed_identity) = read_secret_with_identity(entries, id)
.map_err(|_| CredentialStoreError::RecoveryRequired)?;
drop(committed_secret);
if Some(committed_identity) != journal.staged_secret_identity {
return Err(CredentialStoreError::RecoveryRequired);
}
if lifecycle_failpoint("rotate_after_secret_replace") {
return Ok(rotation_outcome(
credential_ref,
metadata.generation,
next_generation,
true,
));
}
let mut reconciliation_required = false;
post_commit_step(
"rotate_secret_sync",
|| durable_sync(&entry),
&mut reconciliation_required,
);
post_commit_step(
"rotate_metadata_replace",
|| rename_replace(&entry, NEXT_METADATA_FILE, METADATA_FILE),
&mut reconciliation_required,
);
post_commit_step(
"rotate_metadata_sync",
|| durable_sync(&entry),
&mut reconciliation_required,
);
let committed_journal = CredentialLifecycleJournal {
state: CredentialLifecycleJournalState::Committed,
..journal
};
post_commit_step(
"rotate_journal_commit",
|| replace_journal(&lifecycle, &committed_journal),
&mut reconciliation_required,
);
Ok(rotation_outcome(
credential_ref,
metadata.generation,
next_generation,
reconciliation_required,
))
}
pub fn remove_apply(
&self,
credential_ref: &str,
approval: CredentialLifecycleApproval<'_>,
) -> Result<CredentialLifecycleOutcome, CredentialStoreError> {
self.remove_apply_at(credential_ref, approval, current_epoch_seconds()?)
}
fn remove_apply_at(
&self,
credential_ref: &str,
approval: CredentialLifecycleApproval<'_>,
now_epoch_seconds: u64,
) -> Result<CredentialLifecycleOutcome, CredentialStoreError> {
let guard = self.lock_lifecycle_inventory()?;
let metadata = guard.metadata(credential_ref)?;
guard.remove_apply_with_bindings_at(
credential_ref,
approval,
&metadata.bound_phases,
&metadata.bound_routes,
now_epoch_seconds,
)
}
#[allow(clippy::too_many_arguments)]
fn remove_apply_locked(
&self,
root: &File,
entries: &File,
credential_ref: &str,
approval: CredentialLifecycleApproval<'_>,
bound_phases: &[String],
bound_routes: &[String],
now_epoch_seconds: u64,
) -> Result<CredentialLifecycleOutcome, CredentialStoreError> {
let id = parse_reference(credential_ref)?;
let lifecycle = open_or_create_private_directory(root, LIFECYCLE_DIRECTORY)?;
match reconcile_remove_request_locked(
entries,
&lifecycle,
id,
credential_ref,
CredentialLifecycleRequestIdentity {
expected_preview: approval.expected_preview,
expected_generation: approval.expected_generation,
expires_at_epoch_seconds: approval.expires_at_epoch_seconds,
},
)? {
CredentialRemovalReconciliation::Committed(outcome) => return Ok(outcome),
CredentialRemovalReconciliation::RolledBackFreshPreviewRequired => {
return Err(CredentialStoreError::FreshPreviewRequired);
}
CredentialRemovalReconciliation::NotRequired => {}
}
if approval.confirmation != REMOVE_CONFIRMATION {
return Err(CredentialStoreError::ConfirmationRequired);
}
let metadata = self.read_unique_metadata(entries, id)?;
if metadata.generation != approval.expected_generation {
return Err(CredentialStoreError::GenerationConflict);
}
if metadata.lifecycle_state == CredentialLifecycleState::Removed {
return Err(CredentialStoreError::NotFound);
}
let mut preview_metadata = metadata.clone();
preview_metadata.bound_phases = bound_phases.to_vec();
preview_metadata.bound_routes = bound_routes.to_vec();
preview_metadata.validate()?;
let preview = lifecycle_preview_for_metadata(
&preview_metadata,
CredentialLifecycleOperation::Remove,
now_epoch_seconds,
approval.expires_at_epoch_seconds,
)?;
validate_expected_preview(&preview, approval.expected_preview, now_epoch_seconds)?;
if !preview.dependency_impact.apply_allowed {
return Err(CredentialStoreError::DependencyConflict);
}
let entry = open_private_directory(entries, id)?;
let (secret_file, old_secret_identity) = open_secret_for_erasure(&entry)?;
let journal = CredentialLifecycleJournal {
schema_version: SCHEMA_VERSION.to_owned(),
operation: CredentialLifecycleOperation::Remove,
credential_id: id.to_owned(),
credential_ref: credential_ref.to_owned(),
expected_generation: metadata.generation,
target_generation: None,
preview_digest: preview.preview_digest,
expires_at_epoch_seconds: preview.expires_at_epoch_seconds,
required_confirmation: REMOVE_CONFIRMATION.to_owned(),
old_secret_identity,
staged_secret_identity: None,
state: CredentialLifecycleJournalState::Prepared,
};
write_journal_new(&lifecycle, &journal)?;
if lifecycle_failpoint("remove_before_secret_truncate") {
return Err(CredentialStoreError::CommitAmbiguous);
}
#[cfg(test)]
if lifecycle_failpoint("remove_substitute_path_after_fd_validation") {
inject_remove_secret_substitution_for_test(&entry)?;
}
let parent_stat = fstat(&entry).map_err(|_| CredentialStoreError::RecoveryRequired)?;
let before_truncate =
fstat(&secret_file).map_err(|_| CredentialStoreError::RecoveryRequired)?;
let before_size = usize::try_from(before_truncate.st_size)
.map_err(|_| CredentialStoreError::RecoveryRequired)?;
validate_regular(&before_truncate, device(&parent_stat)?, Some(before_size))
.map_err(|_| CredentialStoreError::RecoveryRequired)?;
let verified_old_identity = identity_with_birth_fd(&before_truncate, &secret_file)
.map_err(|_| CredentialStoreError::RecoveryRequired)?;
if verified_old_identity != journal.old_secret_identity {
return Err(CredentialStoreError::RecoveryRequired);
}
ftruncate(&secret_file, 0).map_err(|_| CredentialStoreError::CommitAmbiguous)?;
if lifecycle_failpoint("remove_after_secret_truncate")
|| lifecycle_failpoint("remove_before_secret_sync")
{
return Err(CredentialStoreError::RecoveryRequired);
}
if lifecycle_failpoint("remove_secret_sync_failure") || durable_sync(&secret_file).is_err()
{
return Err(CredentialStoreError::RecoveryRequired);
}
if lifecycle_failpoint("remove_after_secret_sync") {
return Err(CredentialStoreError::RecoveryRequired);
}
let after_truncate =
fstat(&secret_file).map_err(|_| CredentialStoreError::RecoveryRequired)?;
validate_regular(&after_truncate, device(&parent_stat)?, Some(0))
.map_err(|_| CredentialStoreError::RecoveryRequired)?;
if identity_with_birth_fd(&after_truncate, &secret_file)
.map_err(|_| CredentialStoreError::RecoveryRequired)?
!= journal.old_secret_identity
{
return Err(CredentialStoreError::RecoveryRequired);
}
let mut reconciliation_required = match reconcile_removal(entries, &lifecycle, &journal)? {
RemovalTransactionState::RolledBack => {
return Err(CredentialStoreError::RecoveryRequired);
}
RemovalTransactionState::Committed {
reconciliation_required,
} => reconciliation_required,
};
let committed_journal = CredentialLifecycleJournal {
state: CredentialLifecycleJournalState::Committed,
..journal
};
post_commit_step(
"remove_journal_commit",
|| replace_journal(&lifecycle, &committed_journal),
&mut reconciliation_required,
);
Ok(removal_outcome(
credential_ref,
metadata.generation,
reconciliation_required,
))
}
fn lifecycle_preview(
&self,
credential_ref: &str,
operation: CredentialLifecycleOperation,
now_epoch_seconds: u64,
expires_at_epoch_seconds: u64,
) -> Result<CredentialLifecyclePreview, CredentialStoreError> {
let id = parse_reference(credential_ref)?;
let locked = self.lock()?;
let entries = open_private_directory(&locked.root, ENTRIES_DIRECTORY)?;
ensure_preview_needs_no_recovery(&locked.root, &entries, id)?;
let metadata = self.read_unique_metadata(&entries, id)?;
if metadata.lifecycle_state == CredentialLifecycleState::Removed {
return Err(CredentialStoreError::NotFound);
}
lifecycle_preview_for_metadata(
&metadata,
operation,
now_epoch_seconds,
expires_at_epoch_seconds,
)
}
pub fn list(&self) -> Result<Vec<CredentialMetadata>, CredentialStoreError> {
let locked = self.lock()?;
let entries = open_private_directory(&locked.root, ENTRIES_DIRECTORY)?;
ensure_store_needs_no_lifecycle_recovery(&locked.root, &entries)?;
self.list_locked(&entries)
}
pub fn lock_inventory(&self) -> Result<CredentialInventoryGuard, CredentialStoreError> {
let locked = self.lock()?;
let entries = open_private_directory(&locked.root, ENTRIES_DIRECTORY)?;
ensure_store_needs_no_lifecycle_recovery(&locked.root, &entries)?;
let credentials = self.list_locked(&entries)?;
Ok(CredentialInventoryGuard {
store: self.clone(),
_locked: locked,
entries,
credentials,
})
}
pub fn lock_lifecycle_inventory(
&self,
) -> Result<CredentialInventoryGuard, CredentialStoreError> {
let locked = self.lock()?;
let entries = open_private_directory(&locked.root, ENTRIES_DIRECTORY)?;
self.ensure_normalized_label_inventory(&entries)?;
recover_add_staging(&entries)?;
let credentials = self.list_locked(&entries)?;
Ok(CredentialInventoryGuard {
store: self.clone(),
_locked: locked,
entries,
credentials,
})
}
pub fn status(&self, credential_ref: &str) -> Result<CredentialStatus, CredentialStoreError> {
let id = parse_reference(credential_ref)?;
let locked = self.lock()?;
let entries = open_private_directory(&locked.root, ENTRIES_DIRECTORY)?;
ensure_store_needs_no_lifecycle_recovery(&locked.root, &entries)?;
let metadata = self.read_unique_metadata(&entries, id)?;
let status = match metadata.lifecycle_state {
CredentialLifecycleState::Active => {
let secret = read_secret(&entries, id)?;
drop(secret);
"available"
}
CredentialLifecycleState::Removed => {
let entry = open_private_directory(&entries, id)?;
let _ = read_tombstone_identity(&entry)?;
"removed"
}
};
Ok(CredentialStatus { metadata, status })
}
pub fn resolve(
&self,
credential_ref: &str,
) -> Result<Zeroizing<Vec<u8>>, CredentialStoreError> {
let id = parse_reference(credential_ref)?;
let locked = self.lock()?;
let entries = open_private_directory(&locked.root, ENTRIES_DIRECTORY)?;
ensure_store_needs_no_lifecycle_recovery(&locked.root, &entries)?;
let metadata = self.read_unique_metadata(&entries, id)?;
if metadata.credential_ref != credential_ref {
return Err(CredentialStoreError::CorruptState);
}
if metadata.lifecycle_state == CredentialLifecycleState::Removed {
return Err(CredentialStoreError::NotFound);
}
read_secret(&entries, id)
}
fn list_locked(&self, entries: &File) -> Result<Vec<CredentialMetadata>, CredentialStoreError> {
self.recheck_root()?;
let mut records = Vec::new();
let directory = Dir::read_from(entries).map_err(|_| CredentialStoreError::UnsafeStorage)?;
for item in directory {
let item = item.map_err(|_| CredentialStoreError::UnsafeStorage)?;
let name = item
.file_name()
.to_str()
.map_err(|_| CredentialStoreError::UnsafeStorage)?;
if matches!(name, "." | "..") {
continue;
}
if !valid_id(name) {
return Err(CredentialStoreError::CorruptState);
}
records.push(self.read_metadata(entries, name)?);
if records.len() > MAX_CREDENTIALS {
return Err(CredentialStoreError::CapacityExhausted);
}
}
records.sort_by(|left, right| left.id.cmp(&right.id));
for (index, record) in records.iter().enumerate() {
if records[..index].iter().any(|existing| {
existing.provider == record.provider && existing.label == record.label
}) {
return Err(CredentialStoreError::CorruptState);
}
}
Ok(records)
}
fn read_unique_metadata(
&self,
entries: &File,
id: &str,
) -> Result<CredentialMetadata, CredentialStoreError> {
self.list_locked(entries)?
.into_iter()
.find(|metadata| metadata.id == id)
.ok_or(CredentialStoreError::NotFound)
}
fn ensure_normalized_label_inventory(
&self,
entries: &File,
) -> Result<(), CredentialStoreError> {
self.list_locked(entries).map(drop)
}
fn read_metadata(
&self,
entries: &File,
id: &str,
) -> Result<CredentialMetadata, CredentialStoreError> {
let entry = open_private_directory(entries, id)?;
let bytes = read_regular_file(&entry, METADATA_FILE, MAX_METADATA_BYTES, false)?;
let mut metadata: CredentialMetadata =
serde_json::from_slice(&bytes).map_err(|_| CredentialStoreError::CorruptState)?;
metadata.validate()?;
if metadata.id != id {
return Err(CredentialStoreError::CorruptState);
}
metadata.label = normalize_account_label(&metadata.label)
.map_err(|_| CredentialStoreError::CorruptState)?;
Ok(metadata)
}
fn lock(&self) -> Result<LockedRoot, CredentialStoreError> {
self.recheck_root()?;
let root = self
.root
.try_clone()
.map_err(|_| CredentialStoreError::UnsafeStorage)?;
let flags = OFlags::RDWR | OFlags::NOFOLLOW | OFlags::CLOEXEC;
let lock_fd = match strict_openat(&root, LOCK_FILE, flags, Mode::empty()) {
Ok(fd) => fd,
Err(CredentialStoreError::NotFound) => strict_openat(
&root,
LOCK_FILE,
flags | OFlags::CREATE | OFlags::EXCL,
Mode::from_raw_mode(0o600),
)?,
Err(error) => return Err(error),
};
fchmod(&lock_fd, Mode::from_raw_mode(0o600))
.map_err(|_| CredentialStoreError::UnsafeStorage)?;
let lock = File::from(lock_fd);
self.lock_exclusive(&lock)?;
validate_regular(
&fstat(&lock).map_err(|_| CredentialStoreError::UnsafeStorage)?,
self.root_identity.device,
Some(0),
)?;
Ok(LockedRoot { root, _lock: lock })
}
fn lock_exclusive(&self, lock: &File) -> Result<(), CredentialStoreError> {
let mut interrupted_attempts = 0usize;
loop {
let interrupted = self.flock_attempt(lock)?;
if !interrupted {
return Ok(());
}
interrupted_attempts += 1;
if interrupted_attempts > FLOCK_EINTR_RETRY_BOUND {
return Err(CredentialStoreError::LockUnavailable);
}
std::thread::sleep(FLOCK_EINTR_RETRY_SLEEP);
}
}
fn flock_attempt(&self, lock: &File) -> Result<bool, CredentialStoreError> {
#[cfg(test)]
{
if self.flock_eintr_injections.load(Ordering::SeqCst) > 0 {
self.flock_eintr_injections.fetch_sub(1, Ordering::SeqCst);
return Ok(true);
}
}
match flock(lock, FlockOperation::NonBlockingLockExclusive) {
Ok(()) => Ok(false),
Err(error) if error == rustix::io::Errno::INTR => Ok(true),
Err(_) => Err(CredentialStoreError::LockUnavailable),
}
}
fn recheck_root(&self) -> Result<(), CredentialStoreError> {
let fresh = open_path_directory(&self.root_path)?;
let fresh_stat = fstat(&fresh).map_err(|_| CredentialStoreError::UnsafeStorage)?;
let held_stat = fstat(&*self.root).map_err(|_| CredentialStoreError::UnsafeStorage)?;
validate_directory(&fresh_stat)?;
validate_directory(&held_stat)?;
if !same_filesystem_object(identity(&fresh_stat)?, self.root_identity)
|| !same_filesystem_object(identity(&held_stat)?, self.root_identity)
{
return Err(CredentialStoreError::UnsafeStorage);
}
Ok(())
}
}
struct LockedRoot {
root: File,
_lock: File,
}
fn rotation_outcome(
credential_ref: &str,
previous_generation: u64,
generation: u64,
reconciliation_required: bool,
) -> CredentialLifecycleOutcome {
CredentialLifecycleOutcome {
operation: CredentialLifecycleOperation::Rotate,
credential_ref: credential_ref.to_owned(),
previous_generation,
generation: Some(generation),
lifecycle_state: "active",
health: "locally_verified",
commit_status: if reconciliation_required {
"committed_reconciliation_required"
} else {
"committed"
},
reconciliation_required,
}
}
fn removal_outcome(
credential_ref: &str,
previous_generation: u64,
reconciliation_required: bool,
) -> CredentialLifecycleOutcome {
CredentialLifecycleOutcome {
operation: CredentialLifecycleOperation::Remove,
credential_ref: credential_ref.to_owned(),
previous_generation,
generation: None,
lifecycle_state: "removed",
health: "unavailable",
commit_status: if reconciliation_required {
"committed_reconciliation_required"
} else {
"committed"
},
reconciliation_required,
}
}
fn reconcile_rotate_request_locked(
entries: &File,
lifecycle: &File,
id: &str,
credential_ref: &str,
request: CredentialLifecycleRequestIdentity<'_>,
) -> Result<CredentialRotationReconciliation, CredentialStoreError> {
let Some(journal) = read_journal(lifecycle, id).map_err(|error| match error {
CredentialStoreError::CorruptState => CredentialStoreError::RecoveryRequired,
other => other,
})?
else {
match open_private_directory(entries, id) {
Ok(entry) => {
if path_exists(&entry, NEXT_SECRET_FILE)?
|| path_exists(&entry, NEXT_METADATA_FILE)?
{
return Err(CredentialStoreError::RecoveryRequired);
}
}
Err(CredentialStoreError::NotFound) => {}
Err(error) => return Err(error),
}
return Ok(CredentialRotationReconciliation::NotRequired);
};
journal
.validate()
.map_err(|_| CredentialStoreError::RecoveryRequired)?;
if journal.credential_id != id
|| journal.credential_ref != credential_ref
|| journal.operation != CredentialLifecycleOperation::Rotate
|| journal.required_confirmation != ROTATE_CONFIRMATION
|| journal.expected_generation != request.expected_generation
|| journal.expires_at_epoch_seconds != request.expires_at_epoch_seconds
|| !constant_time_eq(
journal.preview_digest.as_bytes(),
request.expected_preview.as_bytes(),
)
{
return Err(CredentialStoreError::RecoveryRequired);
}
let mut reconciliation_required = match reconcile_rotation(entries, lifecycle, &journal)? {
RotationTransactionState::RolledBack => {
return Ok(CredentialRotationReconciliation::RolledBackFreshPreviewRequired);
}
RotationTransactionState::Committed {
reconciliation_required,
} => reconciliation_required,
};
if journal.state != CredentialLifecycleJournalState::Committed {
let committed_journal = CredentialLifecycleJournal {
state: CredentialLifecycleJournalState::Committed,
..journal.clone()
};
if replace_journal(lifecycle, &committed_journal).is_err() {
reconciliation_required = true;
}
}
Ok(CredentialRotationReconciliation::Committed(
rotation_outcome(
&journal.credential_ref,
journal.expected_generation,
journal
.target_generation
.ok_or(CredentialStoreError::CorruptState)?,
reconciliation_required,
),
))
}
fn reconcile_remove_request_locked(
entries: &File,
lifecycle: &File,
id: &str,
credential_ref: &str,
request: CredentialLifecycleRequestIdentity<'_>,
) -> Result<CredentialRemovalReconciliation, CredentialStoreError> {
let Some(journal) = read_journal(lifecycle, id).map_err(|error| match error {
CredentialStoreError::CorruptState => CredentialStoreError::RecoveryRequired,
other => other,
})?
else {
return Ok(CredentialRemovalReconciliation::NotRequired);
};
journal
.validate()
.map_err(|_| CredentialStoreError::RecoveryRequired)?;
if journal.credential_id != id
|| journal.credential_ref != credential_ref
|| journal.operation != CredentialLifecycleOperation::Remove
|| journal.required_confirmation != REMOVE_CONFIRMATION
|| journal.expected_generation != request.expected_generation
|| journal.expires_at_epoch_seconds != request.expires_at_epoch_seconds
|| !constant_time_eq(
journal.preview_digest.as_bytes(),
request.expected_preview.as_bytes(),
)
{
return Err(CredentialStoreError::RecoveryRequired);
}
let mut reconciliation_required = match reconcile_removal(entries, lifecycle, &journal)? {
RemovalTransactionState::RolledBack => {
return Ok(CredentialRemovalReconciliation::RolledBackFreshPreviewRequired);
}
RemovalTransactionState::Committed {
reconciliation_required,
} => reconciliation_required,
};
if journal.state != CredentialLifecycleJournalState::Committed {
let committed_journal = CredentialLifecycleJournal {
state: CredentialLifecycleJournalState::Committed,
..journal.clone()
};
if replace_journal(lifecycle, &committed_journal).is_err() {
reconciliation_required = true;
}
}
Ok(CredentialRemovalReconciliation::Committed(removal_outcome(
&journal.credential_ref,
journal.expected_generation,
reconciliation_required,
)))
}
enum RotationTransactionState {
RolledBack,
Committed { reconciliation_required: bool },
}
fn reconcile_rotation(
entries: &File,
lifecycle: &File,
journal: &CredentialLifecycleJournal,
) -> Result<RotationTransactionState, CredentialStoreError> {
let entry = open_private_directory(entries, &journal.credential_id)?;
let (live_secret, live_identity) = read_secret_with_identity(entries, &journal.credential_id)
.map_err(|_| CredentialStoreError::RecoveryRequired)?;
drop(live_secret);
if live_identity == journal.old_secret_identity {
if journal.state == CredentialLifecycleJournalState::Committed {
return Err(CredentialStoreError::RecoveryRequired);
}
cleanup_precommit_files_checked(&entry)?;
remove_journal(lifecycle, &journal.credential_id)?;
return Ok(RotationTransactionState::RolledBack);
}
if Some(live_identity) != journal.staged_secret_identity {
return Err(CredentialStoreError::RecoveryRequired);
}
let metadata_bytes = read_regular_file(&entry, METADATA_FILE, MAX_METADATA_BYTES, false)
.map_err(|_| CredentialStoreError::RecoveryRequired)?;
let metadata: CredentialMetadata = serde_json::from_slice(&metadata_bytes)
.map_err(|_| CredentialStoreError::RecoveryRequired)?;
metadata
.validate()
.map_err(|_| CredentialStoreError::RecoveryRequired)?;
let metadata_is_target = rotation_metadata_matches_journal(&metadata, journal);
if !metadata_is_target {
if metadata.generation != journal.expected_generation
|| metadata.id != journal.credential_id
|| !path_exists(&entry, NEXT_METADATA_FILE)?
{
return Err(CredentialStoreError::RecoveryRequired);
}
let next_bytes = read_regular_file(&entry, NEXT_METADATA_FILE, MAX_METADATA_BYTES, false)
.map_err(|_| CredentialStoreError::RecoveryRequired)?;
let next_metadata: CredentialMetadata = serde_json::from_slice(&next_bytes)
.map_err(|_| CredentialStoreError::RecoveryRequired)?;
next_metadata
.validate()
.map_err(|_| CredentialStoreError::RecoveryRequired)?;
if !rotation_metadata_matches_journal(&next_metadata, journal) {
return Err(CredentialStoreError::RecoveryRequired);
}
if lifecycle_failpoint("reconcile_rotate_metadata_replace")
|| rename_replace(&entry, NEXT_METADATA_FILE, METADATA_FILE).is_err()
{
return Ok(RotationTransactionState::Committed {
reconciliation_required: true,
});
}
}
let mut reconciliation_required = false;
post_commit_step(
"reconcile_rotate_staging_cleanup",
|| cleanup_postcommit_rotation_files(&entry),
&mut reconciliation_required,
);
post_commit_step(
"reconcile_rotate_entry_sync",
|| durable_sync(&entry),
&mut reconciliation_required,
);
Ok(RotationTransactionState::Committed {
reconciliation_required,
})
}
fn rotation_metadata_matches_journal(
metadata: &CredentialMetadata,
journal: &CredentialLifecycleJournal,
) -> bool {
metadata.id == journal.credential_id
&& metadata.credential_ref == journal.credential_ref
&& Some(metadata.generation) == journal.target_generation
&& metadata.previous_generation == Some(journal.expected_generation)
&& metadata.last_lifecycle_preview_digest.as_deref()
== Some(journal.preview_digest.as_str())
}
enum RemovalTransactionState {
RolledBack,
Committed { reconciliation_required: bool },
}
fn reconcile_removal(
entries: &File,
lifecycle: &File,
journal: &CredentialLifecycleJournal,
) -> Result<RemovalTransactionState, CredentialStoreError> {
let entry = open_private_directory(entries, &journal.credential_id)
.map_err(|_| CredentialStoreError::RecoveryRequired)?;
let (secret_file, live_identity, secret_size) =
open_secret_or_tombstone_for_recovery(&entry)
.map_err(|_| CredentialStoreError::RecoveryRequired)?;
if live_identity != journal.old_secret_identity {
return Err(CredentialStoreError::RecoveryRequired);
}
let metadata_bytes = read_regular_file(&entry, METADATA_FILE, MAX_METADATA_BYTES, false)
.map_err(|_| CredentialStoreError::RecoveryRequired)?;
let metadata: CredentialMetadata = serde_json::from_slice(&metadata_bytes)
.map_err(|_| CredentialStoreError::RecoveryRequired)?;
metadata
.validate()
.map_err(|_| CredentialStoreError::RecoveryRequired)?;
if secret_size != 0 {
if journal.state == CredentialLifecycleJournalState::Committed
|| !active_removal_metadata_matches_journal(&metadata, journal)
|| path_exists(&entry, NEXT_METADATA_FILE)?
{
return Err(CredentialStoreError::RecoveryRequired);
}
remove_journal(lifecycle, &journal.credential_id)?;
return Ok(RemovalTransactionState::RolledBack);
}
if lifecycle_failpoint("remove_secret_sync_failure") || durable_sync(&secret_file).is_err() {
return Err(CredentialStoreError::RecoveryRequired);
}
if lifecycle_failpoint("remove_after_secret_sync") {
return Err(CredentialStoreError::RecoveryRequired);
}
let parent_stat = fstat(&entry).map_err(|_| CredentialStoreError::RecoveryRequired)?;
let descriptor_stat =
fstat(&secret_file).map_err(|_| CredentialStoreError::RecoveryRequired)?;
let path_stat = statat(&entry, SECRET_FILE, AtFlags::SYMLINK_NOFOLLOW)
.map_err(|_| CredentialStoreError::RecoveryRequired)?;
let parent_device = device(&parent_stat).map_err(|_| CredentialStoreError::RecoveryRequired)?;
validate_regular(&descriptor_stat, parent_device, Some(0))
.map_err(|_| CredentialStoreError::RecoveryRequired)?;
validate_regular(&path_stat, parent_device, Some(0))
.map_err(|_| CredentialStoreError::RecoveryRequired)?;
let descriptor_identity = identity_with_birth(&descriptor_stat, &entry, SECRET_FILE)
.map_err(|_| CredentialStoreError::RecoveryRequired)?;
if descriptor_identity != journal.old_secret_identity
|| identity_with_birth(&path_stat, &entry, SECRET_FILE)
.map_err(|_| CredentialStoreError::RecoveryRequired)?
!= journal.old_secret_identity
{
return Err(CredentialStoreError::RecoveryRequired);
}
let reconciliation_required = finalize_removal_metadata(&entry, &metadata, journal)?;
Ok(RemovalTransactionState::Committed {
reconciliation_required,
})
}
fn active_removal_metadata_matches_journal(
metadata: &CredentialMetadata,
journal: &CredentialLifecycleJournal,
) -> bool {
metadata.id == journal.credential_id
&& metadata.credential_ref == journal.credential_ref
&& metadata.generation == journal.expected_generation
&& metadata.lifecycle_state == CredentialLifecycleState::Active
&& metadata.health == CredentialHealthState::LocallyVerified
&& metadata.bound_phases.is_empty()
&& metadata.bound_routes.is_empty()
}
fn removed_metadata_matches_journal(
metadata: &CredentialMetadata,
journal: &CredentialLifecycleJournal,
) -> bool {
metadata.id == journal.credential_id
&& metadata.credential_ref == journal.credential_ref
&& metadata.generation == journal.expected_generation
&& metadata.lifecycle_state == CredentialLifecycleState::Removed
&& metadata.health == CredentialHealthState::Unavailable
&& metadata.last_lifecycle_preview_digest.as_deref()
== Some(journal.preview_digest.as_str())
&& metadata.last_verified_at_epoch_seconds.is_none()
&& metadata.bound_phases.is_empty()
&& metadata.bound_routes.is_empty()
}
fn finalize_removal_metadata(
entry: &File,
metadata: &CredentialMetadata,
journal: &CredentialLifecycleJournal,
) -> Result<bool, CredentialStoreError> {
if removed_metadata_matches_journal(metadata, journal) {
return Ok(false);
}
if !active_removal_metadata_matches_journal(metadata, journal) {
return Err(CredentialStoreError::RecoveryRequired);
}
let mut tombstone = metadata.clone();
tombstone.lifecycle_state = CredentialLifecycleState::Removed;
tombstone.health = CredentialHealthState::Unavailable;
tombstone.last_lifecycle_preview_digest = Some(journal.preview_digest.clone());
tombstone.last_verified_at_epoch_seconds = None;
tombstone.validate()?;
let tombstone_bytes =
serde_json::to_vec(&tombstone).map_err(|_| CredentialStoreError::RecoveryRequired)?;
if tombstone_bytes.len() > MAX_METADATA_BYTES {
return Err(CredentialStoreError::RecoveryRequired);
}
if path_exists(entry, NEXT_METADATA_FILE)? {
let staged = read_regular_file(entry, NEXT_METADATA_FILE, MAX_METADATA_BYTES, false)
.map_err(|_| CredentialStoreError::RecoveryRequired)?;
let staged: CredentialMetadata =
serde_json::from_slice(&staged).map_err(|_| CredentialStoreError::RecoveryRequired)?;
staged
.validate()
.map_err(|_| CredentialStoreError::RecoveryRequired)?;
if staged != tombstone {
return Err(CredentialStoreError::RecoveryRequired);
}
} else {
if lifecycle_failpoint("remove_metadata_stage") {
return Ok(true);
}
if write_new_file(entry, NEXT_METADATA_FILE, &tombstone_bytes).is_err() {
return Ok(true);
}
}
if lifecycle_failpoint("remove_metadata_replace")
|| rename_replace(entry, NEXT_METADATA_FILE, METADATA_FILE).is_err()
{
return Ok(true);
}
if lifecycle_failpoint("remove_metadata_sync") || durable_sync(entry).is_err() {
return Ok(true);
}
Ok(false)
}
fn ensure_preview_needs_no_recovery(
root: &File,
entries: &File,
id: &str,
) -> Result<(), CredentialStoreError> {
ensure_store_needs_no_lifecycle_recovery(root, entries)?;
let _ = open_private_directory(entries, id)?;
Ok(())
}
fn ensure_store_needs_no_lifecycle_recovery(
root: &File,
entries: &File,
) -> Result<(), CredentialStoreError> {
let directory = Dir::read_from(entries).map_err(|_| CredentialStoreError::UnsafeStorage)?;
for item in directory {
let item = item.map_err(|_| CredentialStoreError::UnsafeStorage)?;
let name = item
.file_name()
.to_str()
.map_err(|_| CredentialStoreError::UnsafeStorage)?;
if matches!(name, "." | "..") {
continue;
}
if valid_staging_name(name) {
return Err(CredentialStoreError::RecoveryRequired);
}
if !valid_id(name) {
return Err(CredentialStoreError::CorruptState);
}
let entry = open_private_directory(entries, name)?;
if path_exists(&entry, NEXT_SECRET_FILE)? || path_exists(&entry, NEXT_METADATA_FILE)? {
return Err(CredentialStoreError::RecoveryRequired);
}
}
let lifecycle = match open_private_directory(root, LIFECYCLE_DIRECTORY) {
Ok(directory) => directory,
Err(CredentialStoreError::NotFound) => return Ok(()),
Err(error) => return Err(error),
};
let directory = Dir::read_from(&lifecycle).map_err(|_| CredentialStoreError::UnsafeStorage)?;
for item in directory {
let item = item.map_err(|_| CredentialStoreError::UnsafeStorage)?;
let name = item
.file_name()
.to_str()
.map_err(|_| CredentialStoreError::UnsafeStorage)?;
if matches!(name, "." | "..") {
continue;
}
if name.starts_with(".journal-") {
return Err(CredentialStoreError::RecoveryRequired);
}
if valid_id(name) {
let journal =
read_journal(&lifecycle, name)?.ok_or(CredentialStoreError::CorruptState)?;
if journal.credential_id != name {
return Err(CredentialStoreError::CorruptState);
}
if journal.state == CredentialLifecycleJournalState::Prepared {
return Err(CredentialStoreError::RecoveryRequired);
}
match journal.operation {
CredentialLifecycleOperation::Rotate => {
let entry = open_private_directory(entries, name)
.map_err(|_| CredentialStoreError::RecoveryRequired)?;
let bytes = read_regular_file(&entry, METADATA_FILE, MAX_METADATA_BYTES, false)
.map_err(|_| CredentialStoreError::RecoveryRequired)?;
let metadata: CredentialMetadata = serde_json::from_slice(&bytes)
.map_err(|_| CredentialStoreError::CorruptState)?;
metadata.validate()?;
let (secret, live_identity) = read_secret_with_identity(entries, name)
.map_err(|_| CredentialStoreError::RecoveryRequired)?;
drop(secret);
if metadata.id != name
|| metadata.generation != journal.target_generation.unwrap_or_default()
|| metadata.previous_generation != Some(journal.expected_generation)
|| metadata.last_lifecycle_preview_digest.as_deref()
!= Some(journal.preview_digest.as_str())
|| Some(live_identity) != journal.staged_secret_identity
{
return Err(CredentialStoreError::RecoveryRequired);
}
}
CredentialLifecycleOperation::Remove => {
let entry = open_private_directory(entries, name)
.map_err(|_| CredentialStoreError::RecoveryRequired)?;
let tombstone_identity = read_tombstone_identity(&entry)
.map_err(|_| CredentialStoreError::RecoveryRequired)?;
let bytes = read_regular_file(&entry, METADATA_FILE, MAX_METADATA_BYTES, false)
.map_err(|_| CredentialStoreError::RecoveryRequired)?;
let metadata: CredentialMetadata = serde_json::from_slice(&bytes)
.map_err(|_| CredentialStoreError::RecoveryRequired)?;
metadata
.validate()
.map_err(|_| CredentialStoreError::RecoveryRequired)?;
if tombstone_identity != journal.old_secret_identity
|| !removed_metadata_matches_journal(&metadata, &journal)
|| path_exists(&entry, NEXT_METADATA_FILE)?
{
return Err(CredentialStoreError::RecoveryRequired);
}
}
}
} else {
return Err(CredentialStoreError::CorruptState);
}
}
Ok(())
}
fn post_commit_step(
edge: &'static str,
operation: impl FnOnce() -> Result<(), CredentialStoreError>,
reconciliation_required: &mut bool,
) {
if lifecycle_failpoint(edge) || operation().is_err() {
*reconciliation_required = true;
}
}
#[cfg(test)]
std::thread_local! {
static LIFECYCLE_FAILPOINT: std::cell::Cell<Option<&'static str>> = const { std::cell::Cell::new(None) };
}
#[cfg(test)]
fn lifecycle_failpoint(edge: &'static str) -> bool {
LIFECYCLE_FAILPOINT.with(|configured| configured.get() == Some(edge))
}
#[cfg(not(test))]
fn lifecycle_failpoint(_edge: &'static str) -> bool {
false
}
#[cfg(test)]
fn inject_remove_secret_substitution_for_test(entry: &File) -> Result<(), CredentialStoreError> {
renameat(entry, SECRET_FILE, entry, ".test-original-secret")
.map_err(|_| CredentialStoreError::RecoveryRequired)?;
write_new_file(entry, SECRET_FILE, b"same-user-substitute")
.map_err(|_| CredentialStoreError::RecoveryRequired)
}
fn read_journal(
lifecycle: &File,
id: &str,
) -> Result<Option<CredentialLifecycleJournal>, CredentialStoreError> {
match read_regular_file(lifecycle, id, MAX_METADATA_BYTES, false) {
Ok(bytes) => {
let journal: CredentialLifecycleJournal =
serde_json::from_slice(&bytes).map_err(|_| CredentialStoreError::CorruptState)?;
journal.validate()?;
Ok(Some(journal))
}
Err(CredentialStoreError::NotFound) => Ok(None),
Err(error) => Err(error),
}
}
fn write_journal_new(
lifecycle: &File,
journal: &CredentialLifecycleJournal,
) -> Result<(), CredentialStoreError> {
journal.validate()?;
let bytes = serde_json::to_vec(journal).map_err(|_| CredentialStoreError::CommitAmbiguous)?;
if bytes.len() > MAX_METADATA_BYTES {
return Err(CredentialStoreError::InvalidInput);
}
write_new_file(lifecycle, &journal.credential_id, &bytes)?;
durable_sync(lifecycle)
}
fn replace_journal(
lifecycle: &File,
journal: &CredentialLifecycleJournal,
) -> Result<(), CredentialStoreError> {
journal.validate()?;
let bytes = serde_json::to_vec(journal).map_err(|_| CredentialStoreError::CommitAmbiguous)?;
if bytes.len() > MAX_METADATA_BYTES {
return Err(CredentialStoreError::InvalidInput);
}
let staging = format!(".journal-{}", journal.credential_id);
unlink_regular_if_exists(lifecycle, &staging)?;
write_new_file(lifecycle, &staging, &bytes)?;
durable_sync(lifecycle)?;
rename_replace(lifecycle, &staging, &journal.credential_id)?;
durable_sync(lifecycle)
}
fn remove_journal(lifecycle: &File, id: &str) -> Result<(), CredentialStoreError> {
unlink_regular_if_exists(lifecycle, &format!(".journal-{id}"))?;
unlink_regular_if_exists(lifecycle, id)?;
durable_sync(lifecycle)
}
fn path_exists(parent: &File, name: &str) -> Result<bool, CredentialStoreError> {
match statat(parent, name, AtFlags::SYMLINK_NOFOLLOW) {
Ok(_) => Ok(true),
Err(error) if error == rustix::io::Errno::NOENT => Ok(false),
Err(_) => Err(CredentialStoreError::UnsafeStorage),
}
}
fn unlink_regular_if_exists(parent: &File, name: &str) -> Result<(), CredentialStoreError> {
let parent_device = device(&fstat(parent).map_err(|_| CredentialStoreError::UnsafeStorage)?)?;
match statat(parent, name, AtFlags::SYMLINK_NOFOLLOW) {
Ok(stat) => validate_regular(&stat, parent_device, None)?,
Err(error) if error == rustix::io::Errno::NOENT => return Ok(()),
Err(_) => return Err(CredentialStoreError::UnsafeStorage),
}
unlinkat(parent, name, AtFlags::empty()).map_err(|_| CredentialStoreError::UnsafeStorage)
}
fn cleanup_precommit_files(entry: &File) {
let _ = cleanup_precommit_files_checked(entry);
}
fn cleanup_precommit_files_checked(entry: &File) -> Result<(), CredentialStoreError> {
unlink_regular_if_exists(entry, NEXT_SECRET_FILE)?;
unlink_regular_if_exists(entry, NEXT_METADATA_FILE)?;
durable_sync(entry)
}
fn cleanup_postcommit_rotation_files(entry: &File) -> Result<(), CredentialStoreError> {
unlink_regular_if_exists(entry, NEXT_SECRET_FILE)?;
unlink_regular_if_exists(entry, NEXT_METADATA_FILE)
}
fn lifecycle_preview_for_metadata(
metadata: &CredentialMetadata,
operation: CredentialLifecycleOperation,
now_epoch_seconds: u64,
expires_at_epoch_seconds: u64,
) -> Result<CredentialLifecyclePreview, CredentialStoreError> {
metadata.validate()?;
validate_preview_window(now_epoch_seconds, expires_at_epoch_seconds)?;
let binding_count = metadata
.bound_phases
.len()
.checked_add(metadata.bound_routes.len())
.ok_or(CredentialStoreError::CorruptState)?;
let removal_allowed = operation != CredentialLifecycleOperation::Remove || binding_count == 0;
let required_confirmation = match operation {
CredentialLifecycleOperation::Rotate => ROTATE_CONFIRMATION,
CredentialLifecycleOperation::Remove => REMOVE_CONFIRMATION,
};
let mut preview = CredentialLifecyclePreview {
schema_version: SCHEMA_VERSION.to_owned(),
operation,
provider: metadata.provider.clone(),
label: metadata.label.clone(),
credential_ref: metadata.credential_ref.clone(),
dependency_impact: CredentialDependencyImpact {
bound_phases: metadata.bound_phases.clone(),
bound_routes: metadata.bound_routes.clone(),
binding_count,
apply_allowed: removal_allowed,
policy: if operation == CredentialLifecycleOperation::Remove && !removal_allowed {
"blocked_until_zero_bindings"
} else if operation == CredentialLifecycleOperation::Remove {
"zero_binding_removal"
} else {
"bindings_preserved"
},
},
expected_generation: metadata.generation,
expires_at_epoch_seconds,
required_confirmation,
preview_digest: String::new(),
};
let encoded = serde_json::to_vec(&preview).map_err(|_| CredentialStoreError::CorruptState)?;
preview.preview_digest = sha256_id(&encoded);
Ok(preview)
}
fn validate_preview_window(
now_epoch_seconds: u64,
expires_at_epoch_seconds: u64,
) -> Result<(), CredentialStoreError> {
let lifetime = expires_at_epoch_seconds
.checked_sub(now_epoch_seconds)
.ok_or(CredentialStoreError::PreviewExpired)?;
if lifetime == 0 || lifetime > MAX_PREVIEW_LIFETIME_SECONDS {
return Err(CredentialStoreError::PreviewExpired);
}
Ok(())
}
fn validate_expected_preview(
preview: &CredentialLifecyclePreview,
expected_preview: &str,
now_epoch_seconds: u64,
) -> Result<(), CredentialStoreError> {
if now_epoch_seconds >= preview.expires_at_epoch_seconds {
return Err(CredentialStoreError::PreviewExpired);
}
if !constant_time_eq(
preview.preview_digest.as_bytes(),
expected_preview.as_bytes(),
) {
return Err(CredentialStoreError::PreviewMismatch);
}
Ok(())
}
fn sha256_id(bytes: &[u8]) -> String {
let digest = Sha256::digest(bytes);
let mut encoded = String::with_capacity(7 + digest.len() * 2);
encoded.push_str("sha256:");
for byte in digest {
use std::fmt::Write as _;
let _ = write!(encoded, "{byte:02x}");
}
encoded
}
fn current_epoch_seconds() -> Result<u64, CredentialStoreError> {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|duration| duration.as_secs())
.map_err(|_| CredentialStoreError::CommitAmbiguous)
}
fn constant_time_eq(left: &[u8], right: &[u8]) -> bool {
if left.len() != right.len() {
return false;
}
left.iter()
.zip(right)
.fold(0_u8, |difference, (left, right)| {
difference | (left ^ right)
})
== 0
}
fn verify_staged_files(
entry: &File,
expected_metadata: &CredentialMetadata,
expected_secret: &[u8],
) -> Result<FileIdentity, CredentialStoreError> {
let metadata_bytes = read_regular_file(entry, NEXT_METADATA_FILE, MAX_METADATA_BYTES, false)?;
let metadata: CredentialMetadata =
serde_json::from_slice(&metadata_bytes).map_err(|_| CredentialStoreError::CorruptState)?;
metadata.validate()?;
if &metadata != expected_metadata {
return Err(CredentialStoreError::CommitAmbiguous);
}
let (secret, identity) =
read_regular_file_with_identity(entry, NEXT_SECRET_FILE, MAX_SECRET_BYTES, true)?;
let secret = Zeroizing::new(secret);
if !valid_secret(&secret) || !constant_time_eq(&secret, expected_secret) {
return Err(CredentialStoreError::CommitAmbiguous);
}
Ok(identity)
}
fn read_secret(entries: &File, id: &str) -> Result<Zeroizing<Vec<u8>>, CredentialStoreError> {
read_secret_with_identity(entries, id).map(|(secret, _)| secret)
}
fn read_secret_with_identity(
entries: &File,
id: &str,
) -> Result<(Zeroizing<Vec<u8>>, FileIdentity), CredentialStoreError> {
let entry = open_private_directory(entries, id)?;
let (value, identity) =
read_regular_file_with_identity(&entry, SECRET_FILE, MAX_SECRET_BYTES, true)?;
let value = Zeroizing::new(value);
if !valid_secret(&value) {
return Err(CredentialStoreError::CorruptState);
}
Ok((value, identity))
}
fn open_secret_for_erasure(entry: &File) -> Result<(File, FileIdentity), CredentialStoreError> {
let (file, descriptor_identity, size) = open_secret_or_tombstone_for_recovery(entry)?;
if size == 0 {
return Err(CredentialStoreError::CorruptState);
}
Ok((file, descriptor_identity))
}
fn open_secret_or_tombstone_for_recovery(
entry: &File,
) -> Result<(File, FileIdentity, usize), CredentialStoreError> {
let file = File::from(strict_openat(
entry,
SECRET_FILE,
OFlags::RDWR | OFlags::NOFOLLOW | OFlags::CLOEXEC,
Mode::empty(),
)?);
let parent_stat = fstat(entry).map_err(|_| CredentialStoreError::UnsafeStorage)?;
let descriptor_stat = fstat(&file).map_err(|_| CredentialStoreError::UnsafeStorage)?;
let path_stat = statat(entry, SECRET_FILE, AtFlags::SYMLINK_NOFOLLOW)
.map_err(|_| CredentialStoreError::UnsafeStorage)?;
let size =
usize::try_from(descriptor_stat.st_size).map_err(|_| CredentialStoreError::CorruptState)?;
validate_regular(&descriptor_stat, device(&parent_stat)?, Some(size))?;
validate_regular(&path_stat, device(&parent_stat)?, Some(size))?;
let descriptor_identity = identity_with_birth(&descriptor_stat, entry, SECRET_FILE)?;
if size > MAX_SECRET_BYTES
|| descriptor_identity != identity_with_birth(&path_stat, entry, SECRET_FILE)?
{
return Err(CredentialStoreError::CorruptState);
}
Ok((file, descriptor_identity, size))
}
fn read_secret_or_tombstone_identity(
entry: &File,
) -> Result<(FileIdentity, usize), CredentialStoreError> {
let file = File::from(strict_openat(
entry,
SECRET_FILE,
OFlags::RDONLY | OFlags::NOFOLLOW | OFlags::CLOEXEC,
Mode::empty(),
)?);
let parent_stat = fstat(entry).map_err(|_| CredentialStoreError::UnsafeStorage)?;
let descriptor_stat = fstat(&file).map_err(|_| CredentialStoreError::UnsafeStorage)?;
let path_stat = statat(entry, SECRET_FILE, AtFlags::SYMLINK_NOFOLLOW)
.map_err(|_| CredentialStoreError::UnsafeStorage)?;
let size =
usize::try_from(descriptor_stat.st_size).map_err(|_| CredentialStoreError::CorruptState)?;
if size > MAX_SECRET_BYTES {
return Err(CredentialStoreError::CorruptState);
}
validate_regular(&descriptor_stat, device(&parent_stat)?, Some(size))?;
validate_regular(&path_stat, device(&parent_stat)?, Some(size))?;
let descriptor_identity = identity_with_birth(&descriptor_stat, entry, SECRET_FILE)?;
if descriptor_identity != identity_with_birth(&path_stat, entry, SECRET_FILE)? {
return Err(CredentialStoreError::UnsafeStorage);
}
Ok((descriptor_identity, size))
}
fn read_tombstone_identity(entry: &File) -> Result<FileIdentity, CredentialStoreError> {
let (identity, size) = read_secret_or_tombstone_identity(entry)?;
if size != 0 {
return Err(CredentialStoreError::CorruptState);
}
Ok(identity)
}
fn parse_reference(value: &str) -> Result<&str, CredentialStoreError> {
let id = value
.strip_prefix(REFERENCE_PREFIX)
.ok_or(CredentialStoreError::InvalidInput)?;
valid_id(id)
.then_some(id)
.ok_or(CredentialStoreError::InvalidInput)
}
#[cfg(target_os = "macos")]
fn current_user_home_from_account_record() -> Result<PathBuf, CredentialStoreError> {
use std::os::unix::process::CommandExt as _;
use std::process::{Command, Stdio};
let uid = geteuid().as_raw();
let uid_text = uid.to_string();
let mut command = Command::new("/usr/bin/dscacheutil");
command
.args(["-q", "user", "-a", "uid", &uid_text])
.env_clear()
.stdin(Stdio::null())
.stderr(Stdio::null())
.stdout(Stdio::piped())
.process_group(0);
let output = run_bounded_account_lookup(
&mut command,
ACCOUNT_LOOKUP_TIMEOUT,
MAX_ACCOUNT_RECORD_BYTES,
)?;
parse_account_home(&output, uid)
}
#[cfg(target_os = "linux")]
fn current_user_home_from_account_record() -> Result<PathBuf, CredentialStoreError> {
use std::os::unix::process::CommandExt as _;
use std::process::{Command, Stdio};
let uid = geteuid().as_raw();
let uid_text = uid.to_string();
let mut command = Command::new("/usr/bin/getent");
command
.args(["passwd", &uid_text])
.env_clear()
.stdin(Stdio::null())
.stderr(Stdio::null())
.stdout(Stdio::piped())
.process_group(0);
let output = run_bounded_account_lookup(
&mut command,
ACCOUNT_LOOKUP_TIMEOUT,
MAX_ACCOUNT_RECORD_BYTES,
)?;
parse_linux_account_home(&output, uid)
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
fn run_bounded_account_lookup(
command: &mut std::process::Command,
timeout: Duration,
max_bytes: usize,
) -> Result<Vec<u8>, CredentialStoreError> {
let child = command
.spawn()
.map_err(|_| CredentialStoreError::UnsafeStorage)?;
let mut child = AccountLookupChild {
child: Some(child),
completed: false,
};
let mut stdout = child
.child_mut()?
.stdout
.take()
.ok_or(CredentialStoreError::UnsafeStorage)?;
rustix::fs::fcntl_setfl(&stdout, OFlags::NONBLOCK)
.map_err(|_| CredentialStoreError::UnsafeStorage)?;
let started = Instant::now();
let mut output = Vec::with_capacity(256);
let status = loop {
let _ = read_bounded_nonblocking(&mut stdout, &mut output, max_bytes)?;
if let Some(status) = child
.child_mut()?
.try_wait()
.map_err(|_| CredentialStoreError::UnsafeStorage)?
{
break status;
}
if started.elapsed() >= timeout {
return Err(CredentialStoreError::UnsafeStorage);
}
std::thread::sleep(ACCOUNT_LOOKUP_POLL_INTERVAL.min(timeout));
};
let reached_eof = read_bounded_nonblocking(&mut stdout, &mut output, max_bytes)?;
if !status.success() || !reached_eof {
return Err(CredentialStoreError::UnsafeStorage);
}
child.completed = true;
Ok(output)
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
fn read_bounded_nonblocking(
stdout: &mut std::process::ChildStdout,
output: &mut Vec<u8>,
max_bytes: usize,
) -> Result<bool, CredentialStoreError> {
let mut chunk = [0_u8; 512];
loop {
match stdout.read(&mut chunk) {
Ok(0) => return Ok(true),
Ok(length) => {
if output.len().saturating_add(length) > max_bytes {
return Err(CredentialStoreError::UnsafeStorage);
}
output.extend_from_slice(&chunk[..length]);
}
Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => return Ok(false),
Err(_) => return Err(CredentialStoreError::UnsafeStorage),
}
}
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
struct AccountLookupChild {
child: Option<std::process::Child>,
completed: bool,
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
impl AccountLookupChild {
fn child_mut(&mut self) -> Result<&mut std::process::Child, CredentialStoreError> {
self.child
.as_mut()
.ok_or(CredentialStoreError::UnsafeStorage)
}
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
impl Drop for AccountLookupChild {
fn drop(&mut self) {
if self.completed {
return;
}
let Some(mut child) = self.child.take() else {
return;
};
let process_group = rustix::process::Pid::from_child(&child);
let _ = rustix::process::kill_process_group(process_group, rustix::process::Signal::KILL);
let _ = child.kill();
let deadline = Instant::now() + ACCOUNT_LOOKUP_CLEANUP_DEADLINE;
loop {
match child.try_wait() {
Ok(Some(_)) => return,
Ok(None) if Instant::now() < deadline => {
std::thread::sleep(ACCOUNT_LOOKUP_POLL_INTERVAL)
}
_ => break,
}
}
let _ = std::thread::Builder::new()
.name("steadymux-account-lookup-reaper".to_owned())
.spawn(move || {
let _ = child.wait();
});
}
}
#[cfg(target_os = "macos")]
fn parse_account_home(bytes: &[u8], expected_uid: u32) -> Result<PathBuf, CredentialStoreError> {
let text = std::str::from_utf8(bytes).map_err(|_| CredentialStoreError::UnsafeStorage)?;
let mut observed_uid = None;
let mut home = None;
for line in text.lines() {
let Some((key, value)) = line.split_once(':') else {
continue;
};
match key.trim() {
"uid" if observed_uid.is_none() => {
observed_uid = Some(
value
.trim()
.parse::<u32>()
.map_err(|_| CredentialStoreError::UnsafeStorage)?,
);
}
"uid" => return Err(CredentialStoreError::UnsafeStorage),
"dir" if home.is_none() => home = Some(PathBuf::from(value.trim())),
"dir" => return Err(CredentialStoreError::UnsafeStorage),
_ => {}
}
}
let home = home.ok_or(CredentialStoreError::UnsafeStorage)?;
if observed_uid != Some(expected_uid) {
return Err(CredentialStoreError::UnsafeStorage);
}
validate_absolute_path(&home)?;
Ok(home)
}
#[cfg(target_os = "linux")]
fn parse_linux_account_home(
bytes: &[u8],
expected_uid: u32,
) -> Result<PathBuf, CredentialStoreError> {
use std::ffi::OsStr;
use std::os::unix::ffi::OsStrExt as _;
let mut records = bytes
.split(|byte| *byte == b'\n')
.filter(|record| !record.is_empty());
let record = records.next().ok_or(CredentialStoreError::UnsafeStorage)?;
if records.next().is_some() {
return Err(CredentialStoreError::UnsafeStorage);
}
let fields = record.split(|byte| *byte == b':').collect::<Vec<_>>();
if fields.len() != 7 {
return Err(CredentialStoreError::UnsafeStorage);
}
let uid = std::str::from_utf8(fields[2])
.map_err(|_| CredentialStoreError::UnsafeStorage)?
.parse::<u32>()
.map_err(|_| CredentialStoreError::UnsafeStorage)?;
if uid != expected_uid || fields[5].is_empty() || fields[5].contains(&0) {
return Err(CredentialStoreError::UnsafeStorage);
}
let home = PathBuf::from(OsStr::from_bytes(fields[5]));
validate_absolute_path(&home)?;
Ok(home)
}
fn generate_unused_id(entries: &File) -> Result<String, CredentialStoreError> {
for _ in 0..8 {
let mut random = [0_u8; 16];
getrandom::fill(&mut random).map_err(|_| CredentialStoreError::CommitAmbiguous)?;
let id = random
.iter()
.fold(String::with_capacity(32), |mut output, byte| {
use std::fmt::Write as _;
let _ = write!(output, "{byte:02x}");
output
});
match statat(entries, &id, AtFlags::SYMLINK_NOFOLLOW) {
Ok(_) => continue,
Err(error) if error == rustix::io::Errno::NOENT => return Ok(id),
Err(_) => return Err(CredentialStoreError::UnsafeStorage),
}
}
Err(CredentialStoreError::CapacityExhausted)
}
fn recover_add_staging(entries: &File) -> Result<(), CredentialStoreError> {
let directory = Dir::read_from(entries).map_err(|_| CredentialStoreError::UnsafeStorage)?;
let mut staging = Vec::new();
for item in directory {
let item = item.map_err(|_| CredentialStoreError::UnsafeStorage)?;
let name = item
.file_name()
.to_str()
.map_err(|_| CredentialStoreError::UnsafeStorage)?;
if matches!(name, "." | "..") || !name.starts_with('.') {
continue;
}
if !name.starts_with(".staging-") || !valid_staging_name(name) {
if valid_staging_name(name) {
return Err(CredentialStoreError::RecoveryRequired);
}
return Err(CredentialStoreError::CorruptState);
}
staging.push(name.to_owned());
}
for name in staging {
remove_entry_directory(entries, &name)?;
}
if !staging_is_empty(entries)? {
return Err(CredentialStoreError::CorruptState);
}
durable_sync(entries)
}
fn cleanup_failed_add(entries: &File, staged: &File, staging: &str, committed: bool) {
if committed {
return;
}
let _ = unlinkat(staged, SECRET_FILE, AtFlags::empty());
let _ = unlinkat(staged, METADATA_FILE, AtFlags::empty());
let _ = unlinkat(entries, staging, AtFlags::REMOVEDIR);
}
fn remove_entry_directory(entries: &File, name: &str) -> Result<(), CredentialStoreError> {
let directory = open_private_directory(entries, name)?;
let directory_device =
device(&fstat(&directory).map_err(|_| CredentialStoreError::UnsafeStorage)?)?;
let contents = Dir::read_from(&directory).map_err(|_| CredentialStoreError::UnsafeStorage)?;
for item in contents {
let item = item.map_err(|_| CredentialStoreError::UnsafeStorage)?;
let child = item
.file_name()
.to_str()
.map_err(|_| CredentialStoreError::UnsafeStorage)?;
if matches!(child, "." | "..") {
continue;
}
if !matches!(child, SECRET_FILE | METADATA_FILE) {
return Err(CredentialStoreError::CorruptState);
}
let child_stat = statat(&directory, child, AtFlags::SYMLINK_NOFOLLOW)
.map_err(|_| CredentialStoreError::UnsafeStorage)?;
validate_regular(&child_stat, directory_device, None)?;
}
for file in [SECRET_FILE, METADATA_FILE] {
match unlinkat(&directory, file, AtFlags::empty()) {
Ok(()) => {}
Err(error) if error == rustix::io::Errno::NOENT => {}
Err(_) => return Err(CredentialStoreError::UnsafeStorage),
}
}
let mut remaining =
Dir::read_from(&directory).map_err(|_| CredentialStoreError::UnsafeStorage)?;
if remaining.any(|entry| {
entry
.ok()
.and_then(|entry| entry.file_name().to_str().ok().map(str::to_owned))
.is_none_or(|entry| !matches!(entry.as_str(), "." | ".."))
}) {
return Err(CredentialStoreError::CorruptState);
}
unlinkat(entries, name, AtFlags::REMOVEDIR).map_err(|_| CredentialStoreError::UnsafeStorage)
}
fn valid_staging_name(name: &str) -> bool {
if let Some(id) = name.strip_prefix(".staging-") {
return valid_id(id);
}
for prefix in [".rotate-", ".remove-"] {
if let Some(remainder) = name.strip_prefix(prefix)
&& let Some((id, generation)) = remainder.rsplit_once('-')
{
return valid_id(id)
&& generation
.parse::<u64>()
.is_ok_and(|generation| generation > 0);
}
}
false
}
fn staging_is_empty(entries: &File) -> Result<bool, CredentialStoreError> {
let directory = Dir::read_from(entries).map_err(|_| CredentialStoreError::UnsafeStorage)?;
for item in directory {
let item = item.map_err(|_| CredentialStoreError::UnsafeStorage)?;
if valid_staging_name(
item.file_name()
.to_str()
.map_err(|_| CredentialStoreError::UnsafeStorage)?,
) {
return Ok(false);
}
}
Ok(true)
}
fn valid_secret(value: &[u8]) -> bool {
!value.is_empty()
&& value.len() <= MAX_SECRET_BYTES
&& !value.contains(&b'\r')
&& !value.contains(&b'\n')
}
#[cfg(target_os = "macos")]
fn ensure_production_store(home: &Path) -> Result<(PathBuf, File), CredentialStoreError> {
validate_absolute_path(home)?;
let home_directory = open_path_directory(home)?;
validate_owned_directory(
&fstat(&home_directory).map_err(|_| CredentialStoreError::UnsafeStorage)?,
)?;
let library = open_owned_directory(&home_directory, "Library")?;
let application_support = open_owned_directory(&library, "Application Support")?;
let product = open_or_create_private_directory(&application_support, "SteadyMux")?;
let root = open_or_create_private_directory(&product, "credentials-v1")?;
Ok((
home.join("Library")
.join("Application Support")
.join("SteadyMux")
.join("credentials-v1"),
root,
))
}
#[cfg(target_os = "macos")]
fn open_production_store(home: &Path) -> Result<(PathBuf, File), CredentialStoreError> {
validate_absolute_path(home)?;
let home_directory = open_path_directory(home)?;
validate_owned_directory(
&fstat(&home_directory).map_err(|_| CredentialStoreError::UnsafeStorage)?,
)?;
let library = open_owned_directory(&home_directory, "Library")?;
let application_support = open_owned_directory(&library, "Application Support")?;
let product = open_private_directory(&application_support, "SteadyMux")?;
let root = open_private_directory(&product, "credentials-v1")?;
Ok((
home.join("Library")
.join("Application Support")
.join("SteadyMux")
.join("credentials-v1"),
root,
))
}
#[cfg(target_os = "linux")]
fn linux_data_home_path(
home: &Path,
xdg_data_home: Option<&std::ffi::OsStr>,
) -> Result<PathBuf, CredentialStoreError> {
validate_absolute_path(home)?;
let data_home = match xdg_data_home.filter(|value| !value.is_empty()) {
Some(value) => PathBuf::from(value),
None => home.join(".local").join("share"),
};
validate_absolute_path(&data_home)?;
if data_home.as_os_str().as_encoded_bytes().len() > MAX_LINUX_DATA_HOME_BYTES {
return Err(CredentialStoreError::UnsafeStorage);
}
Ok(data_home)
}
#[cfg(target_os = "linux")]
fn linux_production_root_path(
home: &Path,
xdg_data_home: Option<&std::ffi::OsStr>,
) -> Result<PathBuf, CredentialStoreError> {
Ok(linux_data_home_path(home, xdg_data_home)?
.join("SteadyMux")
.join("credentials-v1"))
}
#[cfg(target_os = "linux")]
fn ensure_linux_production_store(
home: &Path,
xdg_data_home: Option<&std::ffi::OsStr>,
) -> Result<(PathBuf, File), CredentialStoreError> {
let data_home_path = linux_data_home_path(home, xdg_data_home)?;
let data_home = if xdg_data_home.is_some_and(|value| !value.is_empty()) {
let directory = open_path_directory(&data_home_path)?;
validate_owned_directory(
&fstat(&directory).map_err(|_| CredentialStoreError::UnsafeStorage)?,
)?;
directory
} else {
let home_directory = open_path_directory(home)?;
validate_owned_directory(
&fstat(&home_directory).map_err(|_| CredentialStoreError::UnsafeStorage)?,
)?;
let local = open_or_create_owned_directory(&home_directory, ".local")?;
open_or_create_owned_directory(&local, "share")?
};
let product = open_or_create_private_directory(&data_home, "SteadyMux")?;
let root = open_or_create_private_directory(&product, "credentials-v1")?;
Ok((linux_production_root_path(home, xdg_data_home)?, root))
}
#[cfg(target_os = "linux")]
fn open_linux_production_store(
home: &Path,
xdg_data_home: Option<&std::ffi::OsStr>,
) -> Result<(PathBuf, File), CredentialStoreError> {
let data_home_path = linux_data_home_path(home, xdg_data_home)?;
let data_home = if xdg_data_home.is_some_and(|value| !value.is_empty()) {
let directory = open_path_directory(&data_home_path)?;
validate_owned_directory(
&fstat(&directory).map_err(|_| CredentialStoreError::UnsafeStorage)?,
)?;
directory
} else {
let home_directory = open_path_directory(home)?;
validate_owned_directory(
&fstat(&home_directory).map_err(|_| CredentialStoreError::UnsafeStorage)?,
)?;
let local = open_owned_directory(&home_directory, ".local")?;
open_owned_directory(&local, "share")?
};
let product = open_private_directory(&data_home, "SteadyMux")?;
let root = open_private_directory(&product, "credentials-v1")?;
Ok((linux_production_root_path(home, xdg_data_home)?, root))
}
#[cfg(test)]
fn ensure_private_components(
boundary: &File,
relative: &Path,
) -> Result<File, CredentialStoreError> {
if relative.as_os_str().is_empty()
|| relative
.components()
.any(|component| !matches!(component, Component::Normal(_)))
{
return Err(CredentialStoreError::UnsafeStorage);
}
let mut current = boundary
.try_clone()
.map_err(|_| CredentialStoreError::UnsafeStorage)?;
for component in relative.components() {
let Component::Normal(name) = component else {
return Err(CredentialStoreError::UnsafeStorage);
};
current = open_or_create_owned_directory(¤t, name)?;
}
Ok(current)
}
#[cfg(any(test, target_os = "linux"))]
fn open_or_create_owned_directory(
parent: &File,
name: impl AsRef<Path>,
) -> Result<File, CredentialStoreError> {
let name = name.as_ref();
match open_owned_directory(parent, name) {
Ok(directory) => Ok(directory),
Err(CredentialStoreError::NotFound) => {
mkdirat(parent, name, Mode::from_raw_mode(0o700))
.map_err(|_| CredentialStoreError::UnsafeStorage)?;
let directory = open_private_directory(parent, name)?;
durable_sync(parent)?;
Ok(directory)
}
Err(error) => Err(error),
}
}
fn open_owned_directory(
parent: &File,
name: impl AsRef<Path>,
) -> Result<File, CredentialStoreError> {
let directory = File::from(strict_openat(
parent,
name,
OFlags::RDONLY | OFlags::DIRECTORY | OFlags::NOFOLLOW | OFlags::CLOEXEC,
Mode::empty(),
)?);
let parent_stat = fstat(parent).map_err(|_| CredentialStoreError::UnsafeStorage)?;
let directory_stat = fstat(&directory).map_err(|_| CredentialStoreError::UnsafeStorage)?;
validate_owned_directory(&directory_stat)?;
if device(&directory_stat)? != device(&parent_stat)? {
return Err(CredentialStoreError::UnsafeStorage);
}
Ok(directory)
}
fn validate_owned_directory(stat: &rustix::fs::Stat) -> Result<(), CredentialStoreError> {
if FileType::from_raw_mode(stat.st_mode) != FileType::Directory
|| stat.st_uid != geteuid().as_raw()
|| stat.st_mode & 0o022 != 0
{
return Err(CredentialStoreError::UnsafeStorage);
}
Ok(())
}
fn validate_absolute_path(path: &Path) -> Result<(), CredentialStoreError> {
if !path.is_absolute()
|| path.as_os_str().as_encoded_bytes().contains(&0)
|| path
.components()
.any(|component| matches!(component, Component::ParentDir | Component::CurDir))
{
return Err(CredentialStoreError::UnsafeStorage);
}
Ok(())
}
fn open_path_directory(path: &Path) -> Result<File, CredentialStoreError> {
validate_absolute_path(path)?;
let mut current = File::from(
open(
"/",
OFlags::RDONLY | OFlags::DIRECTORY | OFlags::NOFOLLOW | OFlags::CLOEXEC,
Mode::empty(),
)
.map_err(|_| CredentialStoreError::UnsafeStorage)?,
);
for component in path.components() {
let Component::Normal(name) = component else {
continue;
};
current = File::from(strict_openat(
¤t,
name,
OFlags::RDONLY | OFlags::DIRECTORY | OFlags::NOFOLLOW | OFlags::CLOEXEC,
Mode::empty(),
)?);
}
Ok(current)
}
fn open_or_create_private_directory(
parent: &File,
name: impl AsRef<Path>,
) -> Result<File, CredentialStoreError> {
let name = name.as_ref();
match open_private_directory(parent, name) {
Ok(directory) => Ok(directory),
Err(CredentialStoreError::NotFound) => {
mkdirat(parent, name, Mode::from_raw_mode(0o700))
.map_err(|_| CredentialStoreError::UnsafeStorage)?;
let directory = open_private_directory(parent, name)?;
durable_sync(parent)?;
Ok(directory)
}
Err(error) => Err(error),
}
}
fn open_private_directory(
parent: &File,
name: impl AsRef<Path>,
) -> Result<File, CredentialStoreError> {
let directory = File::from(strict_openat(
parent,
name,
OFlags::RDONLY | OFlags::DIRECTORY | OFlags::NOFOLLOW | OFlags::CLOEXEC,
Mode::empty(),
)?);
let parent_stat = fstat(parent).map_err(|_| CredentialStoreError::UnsafeStorage)?;
let directory_stat = fstat(&directory).map_err(|_| CredentialStoreError::UnsafeStorage)?;
validate_directory(&directory_stat)?;
if device(&directory_stat)? != device(&parent_stat)? {
return Err(CredentialStoreError::UnsafeStorage);
}
Ok(directory)
}
fn write_new_file(parent: &File, name: &str, bytes: &[u8]) -> Result<(), CredentialStoreError> {
let fd = strict_openat(
parent,
name,
OFlags::WRONLY | OFlags::CREATE | OFlags::EXCL | OFlags::NOFOLLOW | OFlags::CLOEXEC,
Mode::from_raw_mode(0o600),
)?;
fchmod(&fd, Mode::from_raw_mode(0o600)).map_err(|_| CredentialStoreError::UnsafeStorage)?;
let mut file = File::from(fd);
file.write_all(bytes)
.map_err(|_| CredentialStoreError::CommitAmbiguous)?;
file.flush()
.map_err(|_| CredentialStoreError::CommitAmbiguous)?;
durable_sync(&file)?;
let parent_stat = fstat(parent).map_err(|_| CredentialStoreError::UnsafeStorage)?;
let file_stat = fstat(&file).map_err(|_| CredentialStoreError::UnsafeStorage)?;
let path_stat = statat(parent, name, AtFlags::SYMLINK_NOFOLLOW)
.map_err(|_| CredentialStoreError::UnsafeStorage)?;
validate_regular(&file_stat, device(&parent_stat)?, Some(bytes.len()))?;
validate_regular(&path_stat, device(&parent_stat)?, Some(bytes.len()))?;
if identity(&file_stat)? != identity(&path_stat)? {
return Err(CredentialStoreError::UnsafeStorage);
}
Ok(())
}
fn read_regular_file(
parent: &File,
name: &str,
limit: usize,
secret: bool,
) -> Result<Vec<u8>, CredentialStoreError> {
read_regular_file_with_identity(parent, name, limit, secret).map(|(bytes, _)| bytes)
}
fn read_regular_file_with_identity(
parent: &File,
name: &str,
limit: usize,
secret: bool,
) -> Result<(Vec<u8>, FileIdentity), CredentialStoreError> {
let file = File::from(strict_openat(
parent,
name,
OFlags::RDONLY | OFlags::NOFOLLOW | OFlags::CLOEXEC,
Mode::empty(),
)?);
let parent_stat = fstat(parent).map_err(|_| CredentialStoreError::UnsafeStorage)?;
let before = fstat(&file).map_err(|_| CredentialStoreError::UnsafeStorage)?;
validate_regular(&before, device(&parent_stat)?, None)?;
let path_stat = statat(parent, name, AtFlags::SYMLINK_NOFOLLOW)
.map_err(|_| CredentialStoreError::UnsafeStorage)?;
if identity(&before)? != identity(&path_stat)? {
return Err(CredentialStoreError::UnsafeStorage);
}
let mut bytes = Vec::new();
(&file)
.take((limit + 1) as u64)
.read_to_end(&mut bytes)
.map_err(|_| CredentialStoreError::CorruptState)?;
let after = fstat(&file).map_err(|_| CredentialStoreError::UnsafeStorage)?;
let final_path_stat = statat(parent, name, AtFlags::SYMLINK_NOFOLLOW)
.map_err(|_| CredentialStoreError::UnsafeStorage)?;
validate_regular(&after, device(&parent_stat)?, Some(bytes.len()))?;
if bytes.len() > limit
|| bytes.is_empty()
|| identity(&before)? != identity(&after)?
|| identity(&before)? != identity(&final_path_stat)?
|| (secret && after.st_mode & 0o777 != 0o600)
{
return Err(CredentialStoreError::CorruptState);
}
Ok((bytes, identity_with_birth(&after, parent, name)?))
}
fn rename_replace(
parent: &File,
source: &str,
destination: &str,
) -> Result<(), CredentialStoreError> {
renameat(parent, source, parent, destination).map_err(|_| CredentialStoreError::CommitAmbiguous)
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
fn rename_new_entry(entries: &File, staging: &str, id: &str) -> Result<(), CredentialStoreError> {
renameat_with(entries, staging, entries, id, RenameFlags::NOREPLACE)
.map_err(|_| CredentialStoreError::CommitAmbiguous)
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
fn rename_new_entry(entries: &File, staging: &str, id: &str) -> Result<(), CredentialStoreError> {
match statat(entries, id, AtFlags::SYMLINK_NOFOLLOW) {
Err(error) if error == rustix::io::Errno::NOENT => {}
_ => return Err(CredentialStoreError::CommitAmbiguous),
}
renameat(entries, staging, entries, id).map_err(|_| CredentialStoreError::CommitAmbiguous)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct BirthTime {
seconds: i64,
nanoseconds: u32,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct FileIdentity {
device: u64,
inode: u64,
owner: u32,
mode: u32,
link_count: u64,
#[serde(default)]
birth_time: Option<BirthTime>,
}
fn identity(stat: &rustix::fs::Stat) -> Result<FileIdentity, CredentialStoreError> {
Ok(FileIdentity {
device: device(stat)?,
inode: stat.st_ino,
owner: stat.st_uid,
mode: mode_bits(stat),
link_count: link_count(stat),
birth_time: birth_time_from_stat(stat),
})
}
#[cfg(target_os = "linux")]
fn birth_time_from_stat(_stat: &rustix::fs::Stat) -> Option<BirthTime> {
None
}
#[cfg(target_os = "macos")]
fn birth_time_from_stat(stat: &rustix::fs::Stat) -> Option<BirthTime> {
Some(BirthTime {
seconds: stat.st_birthtime,
nanoseconds: u32::try_from(stat.st_birthtime_nsec).ok()?,
})
}
#[cfg(target_os = "linux")]
fn identity_with_birth(
stat: &rustix::fs::Stat,
parent: &File,
name: &str,
) -> Result<FileIdentity, CredentialStoreError> {
let mut file_identity = identity(stat)?;
let statx = rustix::fs::statx(parent, name, AtFlags::SYMLINK_NOFOLLOW, StatxFlags::BTIME)
.map_err(|_| CredentialStoreError::UnsafeStorage)?;
if statx.stx_ino != file_identity.inode {
return Err(CredentialStoreError::UnsafeStorage);
}
if statx.stx_mask & StatxFlags::BTIME.bits() != 0 {
file_identity.birth_time = Some(BirthTime {
seconds: statx.stx_btime.tv_sec,
nanoseconds: statx.stx_btime.tv_nsec,
});
}
Ok(file_identity)
}
#[cfg(target_os = "macos")]
fn identity_with_birth(
stat: &rustix::fs::Stat,
_parent: &File,
_name: &str,
) -> Result<FileIdentity, CredentialStoreError> {
identity(stat)
}
#[cfg(target_os = "linux")]
fn identity_with_birth_fd(
stat: &rustix::fs::Stat,
fd: &File,
) -> Result<FileIdentity, CredentialStoreError> {
let mut file_identity = identity(stat)?;
let statx = rustix::fs::statx(fd, "", AtFlags::EMPTY_PATH, StatxFlags::BTIME)
.map_err(|_| CredentialStoreError::UnsafeStorage)?;
if statx.stx_ino != file_identity.inode {
return Err(CredentialStoreError::UnsafeStorage);
}
if statx.stx_mask & StatxFlags::BTIME.bits() != 0 {
file_identity.birth_time = Some(BirthTime {
seconds: statx.stx_btime.tv_sec,
nanoseconds: statx.stx_btime.tv_nsec,
});
}
Ok(file_identity)
}
#[cfg(target_os = "macos")]
fn identity_with_birth_fd(
stat: &rustix::fs::Stat,
_fd: &File,
) -> Result<FileIdentity, CredentialStoreError> {
identity(stat)
}
#[cfg(target_os = "linux")]
fn mode_bits(stat: &rustix::fs::Stat) -> u32 {
stat.st_mode & 0o777
}
#[cfg(not(target_os = "linux"))]
fn mode_bits(stat: &rustix::fs::Stat) -> u32 {
u32::from(stat.st_mode & 0o777)
}
#[cfg(target_os = "linux")]
fn link_count(stat: &rustix::fs::Stat) -> u64 {
stat.st_nlink
}
#[cfg(not(target_os = "linux"))]
fn link_count(stat: &rustix::fs::Stat) -> u64 {
u64::from(stat.st_nlink)
}
fn same_filesystem_object(left: FileIdentity, right: FileIdentity) -> bool {
left.device == right.device && left.inode == right.inode
}
fn validate_directory(stat: &rustix::fs::Stat) -> Result<(), CredentialStoreError> {
if FileType::from_raw_mode(stat.st_mode) != FileType::Directory
|| stat.st_uid != geteuid().as_raw()
|| stat.st_mode & 0o777 != 0o700
{
return Err(CredentialStoreError::UnsafeStorage);
}
Ok(())
}
fn validate_regular(
stat: &rustix::fs::Stat,
device: u64,
expected_len: Option<usize>,
) -> Result<(), CredentialStoreError> {
if FileType::from_raw_mode(stat.st_mode) != FileType::RegularFile
|| stat.st_uid != geteuid().as_raw()
|| self::device(stat)? != device
|| stat.st_nlink != 1
|| stat.st_mode & 0o777 != 0o600
|| expected_len.is_some_and(|length| usize::try_from(stat.st_size).ok() != Some(length))
{
return Err(CredentialStoreError::UnsafeStorage);
}
Ok(())
}
fn device(stat: &rustix::fs::Stat) -> Result<u64, CredentialStoreError> {
#[cfg(target_os = "linux")]
{
Ok(stat.st_dev)
}
#[cfg(not(target_os = "linux"))]
{
u64::try_from(stat.st_dev).map_err(|_| CredentialStoreError::UnsafeStorage)
}
}
#[cfg(target_os = "macos")]
fn durable_sync(file: &impl AsFd) -> Result<(), CredentialStoreError> {
rustix::fs::fcntl_fullfsync(file).map_err(|_| CredentialStoreError::CommitAmbiguous)
}
#[cfg(not(target_os = "macos"))]
fn durable_sync(file: &impl AsFd) -> Result<(), CredentialStoreError> {
rustix::fs::fsync(file).map_err(|_| CredentialStoreError::CommitAmbiguous)
}
#[cfg(target_os = "linux")]
fn strict_openat(
parent: &File,
path: impl AsRef<Path>,
flags: OFlags,
mode: Mode,
) -> Result<rustix::fd::OwnedFd, CredentialStoreError> {
openat2(
parent,
path.as_ref(),
flags,
mode,
ResolveFlags::NO_SYMLINKS | ResolveFlags::BENEATH,
)
.map_err(map_open_error)
}
#[cfg(not(target_os = "linux"))]
fn strict_openat(
parent: &File,
path: impl AsRef<Path>,
flags: OFlags,
mode: Mode,
) -> Result<rustix::fd::OwnedFd, CredentialStoreError> {
openat(parent, path.as_ref(), flags, mode).map_err(map_open_error)
}
fn map_open_error(error: rustix::io::Errno) -> CredentialStoreError {
if error == rustix::io::Errno::NOENT {
CredentialStoreError::NotFound
} else {
CredentialStoreError::UnsafeStorage
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::BTreeMap;
use std::os::unix::fs::{MetadataExt, PermissionsExt, symlink};
use std::sync::{Arc, Barrier};
#[test]
fn explicit_test_root_reopen_enforces_owner_only_store_invariants() {
let temporary = tempfile::tempdir().expect("temporary root");
let root = temporary.path().join("credentials-v1");
let store = LocalCredentialStore::at_test_root(&root).expect("create explicit store");
let metadata = store
.add("openrouter", "primary", b"secret-canary")
.expect("add credential");
drop(store);
let entry = root.join(ENTRIES_DIRECTORY).join(&metadata.id);
for directory in [&root, &root.join(ENTRIES_DIRECTORY), &entry] {
let mode = std::fs::metadata(directory)
.expect("directory metadata")
.mode()
& 0o777;
assert_eq!(mode, 0o700);
}
for file in [entry.join(SECRET_FILE), entry.join(METADATA_FILE)] {
let file_metadata = std::fs::metadata(file).expect("credential file metadata");
assert_eq!(file_metadata.mode() & 0o777, 0o600);
assert_eq!(file_metadata.nlink(), 1);
}
LocalCredentialStore::at_existing_test_root(&root).expect("reopen private store");
std::fs::set_permissions(
root.join(ENTRIES_DIRECTORY),
std::fs::Permissions::from_mode(0o755),
)
.expect("relax entries mode");
assert!(matches!(
LocalCredentialStore::at_existing_test_root(&root),
Err(CredentialStoreError::UnsafeStorage)
));
std::fs::set_permissions(
root.join(ENTRIES_DIRECTORY),
std::fs::Permissions::from_mode(0o700),
)
.expect("restore entries mode");
let linked_root = temporary.path().join("linked-credentials-v1");
symlink(&root, &linked_root).expect("symlink store root");
assert!(matches!(
LocalCredentialStore::at_existing_test_root(&linked_root),
Err(CredentialStoreError::UnsafeStorage)
));
}
#[test]
fn lock_retries_transient_flock_interruption_and_succeeds() {
let temporary = tempfile::tempdir().expect("temporary root");
let root = temporary.path().join("provider-credentials-v1");
let store = LocalCredentialStore::at_test_root(&root).expect("open store");
store.flock_eintr_injections.store(8, Ordering::SeqCst);
let metadata = store
.add("openrouter", "primary", b"secret-canary")
.expect("add credential after transient flock interruption");
assert_eq!(
store.flock_eintr_injections.load(Ordering::SeqCst),
0,
"injected EINTR interruption was consumed by the retry loop"
);
assert_eq!(
store
.resolve(&metadata.credential_ref)
.expect("resolve after interrupted flock retry")
.as_slice(),
b"secret-canary"
);
}
#[test]
fn lock_exhausts_interrupted_retry_bound_and_fails_closed() {
let temporary = tempfile::tempdir().expect("temporary root");
let root = temporary.path().join("provider-credentials-v1");
let store = LocalCredentialStore::at_test_root(&root).expect("open store");
store
.flock_eintr_injections
.store(FLOCK_EINTR_RETRY_BOUND + 1, Ordering::SeqCst);
assert!(matches!(
store.list(),
Err(CredentialStoreError::LockUnavailable)
));
assert_eq!(
store.flock_eintr_injections.load(Ordering::SeqCst),
0,
"persistent interruptions consumed the retry bound and failed closed"
);
}
#[test]
fn add_list_status_and_resolve_keep_secret_out_of_metadata() {
let temporary = tempfile::tempdir().expect("temporary root");
let root = temporary.path().join("provider-credentials-v1");
let store = LocalCredentialStore::at_test_root(&root).expect("open store");
let metadata = store
.add("openrouter", "primary", b"secret-canary")
.expect("add credential");
assert!(metadata.credential_ref.starts_with(REFERENCE_PREFIX));
assert_eq!(store.list().expect("list"), vec![metadata.clone()]);
assert_eq!(
store
.status(&metadata.credential_ref)
.expect("status")
.status,
"available"
);
assert_eq!(
store
.resolve(&metadata.credential_ref)
.expect("resolve")
.as_slice(),
b"secret-canary"
);
let entry = root.join(ENTRIES_DIRECTORY).join(&metadata.id);
let encoded = std::fs::read(entry.join(METADATA_FILE)).expect("metadata");
assert!(
!encoded
.windows(b"secret-canary".len())
.any(|value| value == b"secret-canary")
);
assert_eq!(
std::fs::metadata(entry.join(SECRET_FILE))
.expect("secret metadata")
.mode()
& 0o777,
0o600
);
assert_eq!(
std::fs::metadata(root).expect("root metadata").mode() & 0o777,
0o700
);
}
#[test]
fn inventory_guard_keeps_status_and_availability_on_one_locked_snapshot() {
let temporary = tempfile::tempdir().expect("temporary root");
let root = temporary.path().join("provider-credentials-v1");
let store = LocalCredentialStore::at_test_root(&root).expect("open store");
let metadata = store
.add("openrouter", "primary", b"secret-canary")
.expect("add credential");
let guard = store.lock_inventory().expect("inventory guard");
guard
.verify_available(&metadata.credential_ref)
.expect("descriptor availability");
assert_eq!(
guard
.status(&metadata.credential_ref)
.expect("guarded status")
.status,
"available"
);
assert!(matches!(
store.remove_preview(
&metadata.credential_ref,
current_epoch_seconds().unwrap() + 60
),
Err(CredentialStoreError::LockUnavailable)
));
assert!(matches!(
store.rotate_preview(
&metadata.credential_ref,
current_epoch_seconds().unwrap() + 60
),
Err(CredentialStoreError::LockUnavailable)
));
drop(guard);
assert!(
store
.remove_preview(
&metadata.credential_ref,
current_epoch_seconds().unwrap() + 60
)
.is_ok()
);
}
#[test]
fn authoritative_projected_bindings_block_remove_and_are_reported_for_rotation() {
let temporary = tempfile::tempdir().expect("temporary root");
let root = temporary.path().join("provider-credentials-v1");
let store = LocalCredentialStore::at_test_root(&root).expect("open store");
let metadata = store
.add("openrouter", "primary", b"secret-canary")
.expect("add credential");
assert!(metadata.bound_phases.is_empty());
assert!(metadata.bound_routes.is_empty());
let guard = store
.lock_lifecycle_inventory()
.expect("lifecycle inventory guard");
let phases = vec!["openrouter_api".to_owned()];
let routes = vec!["openrouter-primary".to_owned()];
let expires = current_epoch_seconds().expect("clock") + 60;
let removal = guard
.remove_preview_with_bindings(&metadata.credential_ref, &phases, &routes, expires)
.expect("projected removal preview");
assert_eq!(removal.dependency_impact.bound_phases, phases);
assert_eq!(removal.dependency_impact.bound_routes, routes);
assert_eq!(removal.dependency_impact.binding_count, 2);
assert!(!removal.dependency_impact.apply_allowed);
assert_eq!(
guard.remove_apply_with_bindings(
&metadata.credential_ref,
lifecycle_approval(&removal, REMOVE_CONFIRMATION),
&phases,
&routes,
),
Err(CredentialStoreError::DependencyConflict)
);
let rotation = guard
.rotate_preview_with_bindings(&metadata.credential_ref, &phases, &routes, expires)
.expect("projected rotation preview");
assert_eq!(rotation.dependency_impact.bound_phases, phases);
assert_eq!(rotation.dependency_impact.bound_routes, routes);
assert_eq!(rotation.dependency_impact.binding_count, 2);
assert!(rotation.dependency_impact.apply_allowed);
assert!(matches!(
store.rotate_preview(&metadata.credential_ref, expires),
Err(CredentialStoreError::LockUnavailable)
));
guard
.verify_available(&metadata.credential_ref)
.expect("blocked removal preserves availability");
drop(guard);
assert_eq!(
store
.resolve(&metadata.credential_ref)
.expect("blocked removal preserves secret")
.as_slice(),
b"secret-canary"
);
}
#[test]
fn rejects_newlines_oversize_values_and_secret_symlinks() {
let temporary = tempfile::tempdir().expect("temporary root");
let root = temporary.path().join("provider-credentials-v1");
let store = LocalCredentialStore::at_test_root(&root).expect("open store");
assert_eq!(
store.add("openrouter", "primary", b"secret\n"),
Err(CredentialStoreError::InvalidInput)
);
assert_eq!(
store.add("openrouter", "primary", &vec![b'x'; MAX_SECRET_BYTES + 1]),
Err(CredentialStoreError::InvalidInput)
);
let metadata = store.add("openrouter", "primary", b"safe").expect("add");
let secret = root
.join(ENTRIES_DIRECTORY)
.join(&metadata.id)
.join(SECRET_FILE);
std::fs::remove_file(&secret).expect("remove secret");
symlink("/etc/passwd", &secret).expect("replace with symlink");
assert!(matches!(
store.resolve(&metadata.credential_ref),
Err(CredentialStoreError::UnsafeStorage)
));
}
#[test]
fn rejects_an_intermediate_symlink_in_the_store_root() {
let temporary = tempfile::tempdir().expect("temporary root");
let actual_parent = temporary.path().join("actual");
std::fs::create_dir(&actual_parent).expect("actual parent");
std::fs::set_permissions(&actual_parent, std::fs::Permissions::from_mode(0o700))
.expect("private actual parent");
let linked_parent = temporary.path().join("linked");
symlink(&actual_parent, &linked_parent).expect("intermediate symlink");
assert!(matches!(
LocalCredentialStore::at_test_root(&linked_parent.join("credentials-v1")),
Err(CredentialStoreError::UnsafeStorage)
));
assert!(
!actual_parent.join("credentials-v1").exists(),
"rejection must occur before any escaped directory mutation"
);
}
#[test]
fn rejects_relaxed_permissions_and_hardlinked_secret() {
let temporary = tempfile::tempdir().expect("temporary root");
let root = temporary.path().join("provider-credentials-v1");
let store = LocalCredentialStore::at_test_root(&root).expect("open store");
let metadata = store.add("ollama", "cloud", b"safe").expect("add");
let secret = root
.join(ENTRIES_DIRECTORY)
.join(&metadata.id)
.join(SECRET_FILE);
let hardlink = temporary.path().join("copied-link");
std::fs::hard_link(&secret, &hardlink).expect("hardlink");
assert!(matches!(
store.resolve(&metadata.credential_ref),
Err(CredentialStoreError::UnsafeStorage)
));
std::fs::remove_file(hardlink).expect("remove hardlink");
std::fs::set_permissions(&secret, std::fs::Permissions::from_mode(0o644))
.expect("relax mode");
assert!(store.resolve(&metadata.credential_ref).is_err());
}
#[test]
fn rejects_hardlinked_metadata() {
let temporary = tempfile::tempdir().expect("temporary root");
let root = temporary.path().join("credentials-v1");
let store = LocalCredentialStore::at_test_root(&root).expect("open store");
let metadata = store.add("ollama", "cloud", b"safe").expect("add");
let metadata_path = std::env::temp_dir()
.canonicalize()
.expect("canonical temporary root")
.join(
root.strip_prefix(std::env::temp_dir())
.expect("temporary relative root"),
)
.join(ENTRIES_DIRECTORY)
.join(&metadata.id)
.join(METADATA_FILE);
let hardlink = temporary.path().join("metadata-link");
std::fs::hard_link(&metadata_path, &hardlink).expect("hardlink metadata");
assert!(matches!(
store.list(),
Err(CredentialStoreError::UnsafeStorage)
));
}
#[test]
fn preview_reports_recovery_required_without_mutating_staging_or_store_bytes() {
let temporary = tempfile::tempdir().expect("temporary root");
let root = temporary.path().join("provider-credentials-v1");
let store = LocalCredentialStore::at_test_root(&root).expect("open store");
let metadata = store
.add_at("openrouter", "primary", b"original", 90)
.expect("add");
let id = "0123456789abcdef0123456789abcdef";
let staging = root.join(ENTRIES_DIRECTORY).join(format!(".staging-{id}"));
std::fs::create_dir(&staging).expect("staging directory");
std::fs::set_permissions(&staging, std::fs::Permissions::from_mode(0o700))
.expect("private staging mode");
std::fs::write(staging.join(SECRET_FILE), b"partial").expect("partial secret");
std::fs::set_permissions(
staging.join(SECRET_FILE),
std::fs::Permissions::from_mode(0o600),
)
.expect("private secret mode");
let before = store_tree_snapshot(&root);
assert!(matches!(
store.rotate_preview_at(&metadata.credential_ref, 100, 200),
Err(CredentialStoreError::RecoveryRequired)
));
assert_eq!(store_tree_snapshot(&root), before);
assert!(staging.exists());
}
#[test]
fn post_rename_failure_cleanup_does_not_delete_committed_files() {
let temporary = tempfile::tempdir().expect("temporary root");
let root = temporary.path().join("credentials-v1");
let store = LocalCredentialStore::at_test_root(&root).expect("open store");
let locked = store.lock().expect("lock store");
let entries = open_or_create_private_directory(&locked.root, ENTRIES_DIRECTORY)
.expect("entries directory");
let id = "0123456789abcdef0123456789abcdef";
let staging = format!(".staging-{id}");
mkdirat(&entries, &staging, Mode::from_raw_mode(0o700)).expect("staging directory");
let staged = open_private_directory(&entries, &staging).expect("open staging");
write_new_file(&staged, SECRET_FILE, b"secret-canary").expect("secret file");
write_new_file(&staged, METADATA_FILE, b"metadata-canary").expect("metadata file");
rename_new_entry(&entries, &staging, id).expect("commit entry");
cleanup_failed_add(&entries, &staged, &staging, true);
let committed = root.join(ENTRIES_DIRECTORY).join(id);
assert_eq!(
std::fs::read(committed.join(SECRET_FILE)).expect("committed secret"),
b"secret-canary"
);
assert_eq!(
std::fs::read(committed.join(METADATA_FILE)).expect("committed metadata"),
b"metadata-canary"
);
}
#[test]
fn duplicate_provider_label_is_rejected_without_replacing_the_secret() {
let temporary = tempfile::tempdir().expect("temporary root");
let root = temporary.path().join("provider-credentials-v1");
let store = LocalCredentialStore::at_test_root(&root).expect("open store");
let metadata = store.add("openrouter", "primary", b"first").expect("add");
assert_eq!(
store.add("openrouter", "primary", b"second"),
Err(CredentialStoreError::InvalidInput)
);
assert_eq!(
store
.resolve(&metadata.credential_ref)
.expect("resolve")
.as_slice(),
b"first"
);
}
#[test]
fn account_label_aliases_store_one_canonical_identity() {
let temporary = tempfile::tempdir().expect("temporary root");
let root = temporary.path().join("provider-credentials-v1");
let store = LocalCredentialStore::at_test_root(&root).expect("open store");
let metadata = store
.add("openrouter", " PRIMARY ", b"first")
.expect("canonicalized add");
assert_eq!(metadata.label, "primary");
assert_eq!(store.list().expect("list")[0].label, "primary");
assert_eq!(
store.add("openrouter", "Primary", b"second"),
Err(CredentialStoreError::InvalidInput)
);
assert_eq!(
store
.resolve(&metadata.credential_ref)
.expect("original secret remains")
.as_slice(),
b"first"
);
}
#[test]
fn malformed_account_labels_fail_before_store_mutation() {
let temporary = tempfile::tempdir().expect("temporary root");
let root = temporary.path().join("provider-credentials-v1");
let store = LocalCredentialStore::at_test_root(&root).expect("open store");
let before = store_tree_snapshot(&root);
let oversized = "a".repeat(MAX_LABEL_BYTES + 1);
for label in [
"",
" ",
".primary",
"_primary",
"-primary",
"primary account",
"primary/backup",
"primary\\backup",
"prÃmary",
"primary\n",
"primary\t",
oversized.as_str(),
] {
assert_eq!(
store.add("openrouter", label, b"secret"),
Err(CredentialStoreError::InvalidInput),
"malformed label must fail closed: {label:?}"
);
assert_eq!(store_tree_snapshot(&root), before);
}
let canonical = store
.add("openrouter", " A.B_C-1 ", b"secret")
.expect("conservative alphabet is accepted");
assert_eq!(canonical.label, "a.b_c-1");
}
#[test]
fn legacy_noncanonical_label_reads_canonically_and_reserves_aliases() {
let temporary = tempfile::tempdir().expect("temporary root");
let root = temporary.path().join("provider-credentials-v1");
let store = LocalCredentialStore::at_test_root(&root).expect("open store");
let metadata = store
.add("openrouter", "legacy", b"legacy-secret")
.expect("add legacy fixture");
rewrite_metadata_for_test(&root, &metadata.id, |record| {
record.label = " PRIMARY ".to_owned();
});
assert_eq!(store.list().expect("legacy list")[0].label, "primary");
assert_eq!(
store
.status(&metadata.credential_ref)
.expect("legacy status")
.metadata
.label,
"primary"
);
assert_eq!(
store
.rotate_preview_at(&metadata.credential_ref, 100, 200)
.expect("legacy preview")
.label,
"primary"
);
let before_duplicate = store_tree_snapshot(&root);
assert_eq!(
store.add("openrouter", "primary", b"replacement"),
Err(CredentialStoreError::InvalidInput)
);
assert_eq!(store_tree_snapshot(&root), before_duplicate);
assert_eq!(
store
.resolve(&metadata.credential_ref)
.expect("legacy secret remains")
.as_slice(),
b"legacy-secret"
);
}
#[test]
fn ambiguous_legacy_normalized_labels_fail_closed_without_mutation() {
let temporary = tempfile::tempdir().expect("temporary root");
let root = temporary.path().join("provider-credentials-v1");
let store = LocalCredentialStore::at_test_root(&root).expect("open store");
let first = store
.add("openrouter", "first", b"first-secret")
.expect("first fixture");
let second = store
.add("openrouter", "second", b"second-secret")
.expect("second fixture");
rewrite_metadata_for_test(&root, &first.id, |record| {
record.label = " PRIMARY ".to_owned();
});
rewrite_metadata_for_test(&root, &second.id, |record| {
record.label = "primary".to_owned();
});
let before = store_tree_snapshot(&root);
assert_eq!(store.list(), Err(CredentialStoreError::CorruptState));
assert_eq!(
store.status(&first.credential_ref),
Err(CredentialStoreError::CorruptState)
);
assert_eq!(
store.rotate_preview_at(&first.credential_ref, 100, 200),
Err(CredentialStoreError::CorruptState)
);
assert_eq!(
store.remove_preview_at(&first.credential_ref, 100, 200),
Err(CredentialStoreError::CorruptState)
);
assert_eq!(
store.resolve(&first.credential_ref),
Err(CredentialStoreError::CorruptState)
);
assert_eq!(
store.add("openrouter", "third", b"third-secret"),
Err(CredentialStoreError::CorruptState)
);
assert_eq!(store_tree_snapshot(&root), before);
}
#[test]
fn prepared_rotation_collision_scan_is_concurrent_and_byte_preserving() {
let temporary = tempfile::tempdir().expect("temporary root");
let root = temporary.path().join("provider-credentials-v1");
let store = LocalCredentialStore::at_test_root(&root).expect("open store");
let primary = store
.add_at("openrouter", "primary", b"rotation-old", 90)
.expect("primary fixture");
let alias = store
.add_at("openrouter", "secondary", b"alias-secret", 90)
.expect("alias fixture");
let preview = store
.rotate_preview_at(&primary.credential_ref, 100, 200)
.expect("rotation preview");
set_lifecycle_failpoint(Some("rotate_before_secret_replace"));
assert_eq!(
store.rotate_apply_at(
&primary.credential_ref,
b"rotation-new",
lifecycle_approval(&preview, ROTATE_CONFIRMATION),
110,
),
Err(CredentialStoreError::CommitAmbiguous)
);
set_lifecycle_failpoint(None);
rewrite_metadata_for_test(&root, &alias.id, |record| {
record.label = " PRIMARY ".to_owned();
});
let before = store_tree_snapshot(&root);
let barrier = Arc::new(Barrier::new(3));
let mut workers = Vec::new();
for _ in 0..2 {
let store = store.clone();
let barrier = Arc::clone(&barrier);
let credential_ref = primary.credential_ref.clone();
let preview = preview.clone();
workers.push(std::thread::spawn(move || {
barrier.wait();
store.reconcile_rotate(&credential_ref, lifecycle_request_identity(&preview))
}));
}
barrier.wait();
let results = workers
.into_iter()
.map(|worker| worker.join().expect("collision worker"))
.collect::<Vec<_>>();
assert!(results.contains(&Err(CredentialStoreError::CorruptState)));
assert!(results.iter().all(|result| matches!(
result,
Err(CredentialStoreError::CorruptState | CredentialStoreError::LockUnavailable)
)));
assert_eq!(store_tree_snapshot(&root), before);
assert_eq!(
store.reconcile_rotate(
&primary.credential_ref,
lifecycle_request_identity(&preview),
),
Err(CredentialStoreError::CorruptState)
);
assert_eq!(
store.rotate_apply_at(
&primary.credential_ref,
b"must-not-stage",
CredentialLifecycleApproval {
expected_preview: &preview.preview_digest,
expected_generation: preview.expected_generation,
confirmation: "",
expires_at_epoch_seconds: preview.expires_at_epoch_seconds,
},
500,
),
Err(CredentialStoreError::CorruptState)
);
assert_eq!(store_tree_snapshot(&root), before);
}
#[test]
fn prepared_removal_collisions_never_reconcile_pre_or_post_truncate() {
for (phase, failpoint, expected_initial) in [
(
"pretruncate",
"remove_before_secret_truncate",
CredentialStoreError::CommitAmbiguous,
),
(
"posttruncate",
"remove_after_secret_truncate",
CredentialStoreError::RecoveryRequired,
),
] {
let temporary = tempfile::tempdir().expect("temporary root");
let root = temporary.path().join(format!("credentials-{phase}"));
let store = LocalCredentialStore::at_test_root(&root).expect("open store");
let primary = store
.add_at("openrouter", "primary", b"removal-primary", 90)
.expect("primary fixture");
let alias = store
.add_at("openrouter", "secondary", b"removal-alias", 90)
.expect("alias fixture");
let preview = store
.remove_preview_at(&primary.credential_ref, 100, 200)
.expect("removal preview");
set_lifecycle_failpoint(Some(failpoint));
assert_eq!(
store.remove_apply_at(
&primary.credential_ref,
lifecycle_approval(&preview, REMOVE_CONFIRMATION),
110,
),
Err(expected_initial)
);
set_lifecycle_failpoint(None);
rewrite_metadata_for_test(&root, &alias.id, |record| {
record.label = " PRIMARY ".to_owned();
});
let before = store_tree_snapshot(&root);
assert_eq!(
store.reconcile_remove(
&primary.credential_ref,
lifecycle_request_identity(&preview),
),
Err(CredentialStoreError::CorruptState)
);
assert_eq!(store_tree_snapshot(&root), before);
assert_eq!(
store.remove_apply_at(
&primary.credential_ref,
CredentialLifecycleApproval {
expected_preview: &preview.preview_digest,
expected_generation: preview.expected_generation,
confirmation: "",
expires_at_epoch_seconds: preview.expires_at_epoch_seconds,
},
500,
),
Err(CredentialStoreError::CorruptState)
);
assert_eq!(store_tree_snapshot(&root), before);
}
}
#[test]
fn concurrent_duplicate_add_has_exactly_one_committed_winner() {
let temporary = tempfile::tempdir().expect("temporary root");
let root = temporary.path().join("credentials-v1");
let store = LocalCredentialStore::at_test_root(&root).expect("open store");
let barrier = Arc::new(Barrier::new(3));
let mut workers = Vec::new();
for (label, secret) in [
("PRIMARY", b"first".as_slice()),
(" primary ", b"second".as_slice()),
] {
let store = store.clone();
let barrier = Arc::clone(&barrier);
let secret = secret.to_vec();
workers.push(std::thread::spawn(move || {
barrier.wait();
store.add("openrouter", label, &secret)
}));
}
barrier.wait();
let results = workers
.into_iter()
.map(|worker| worker.join().expect("worker result"))
.collect::<Vec<_>>();
assert_eq!(results.iter().filter(|result| result.is_ok()).count(), 1);
let records = store.list().expect("list committed records");
assert_eq!(records.len(), 1);
assert_eq!(records[0].label, "primary");
let secret = store
.resolve(&records[0].credential_ref)
.expect("resolve winner");
assert!(matches!(secret.as_slice(), b"first" | b"second"));
}
#[test]
fn provider_capacity_is_bounded_to_sixteen_credentials() {
let temporary = tempfile::tempdir().expect("temporary root");
let root = temporary.path().join("provider-credentials-v1");
let store = LocalCredentialStore::at_test_root(&root).expect("open store");
for index in 0..MAX_CREDENTIALS_PER_PROVIDER {
store
.add("openrouter", &format!("account-{index}"), b"secret")
.expect("credential within provider capacity");
}
assert_eq!(
store.add("openrouter", "account-overflow", b"secret"),
Err(CredentialStoreError::CapacityExhausted)
);
}
#[test]
fn device_capacity_is_bounded_to_sixty_four_credentials() {
let temporary = tempfile::tempdir().expect("temporary root");
let root = temporary.path().join("credentials-v1");
let store = LocalCredentialStore::at_test_root(&root).expect("open store");
let mut first = None;
for provider in ["provider-a", "provider-b", "provider-c", "provider-d"] {
for index in 0..MAX_CREDENTIALS_PER_PROVIDER {
let metadata = store
.add_at(provider, &format!("account-{index}"), b"secret", 90)
.expect("credential within device capacity");
first.get_or_insert(metadata);
}
}
assert_eq!(store.list().expect("full store").len(), MAX_CREDENTIALS);
assert_eq!(
store.add_at("provider-e", "overflow", b"secret", 90),
Err(CredentialStoreError::CapacityExhausted)
);
let first = first.expect("first credential");
let preview = store
.remove_preview_at(&first.credential_ref, 100, 200)
.expect("removal preview at device capacity");
store
.remove_apply_at(
&first.credential_ref,
lifecycle_approval(&preview, REMOVE_CONFIRMATION),
110,
)
.expect("remove to bounded tombstone");
assert_eq!(
store.list().expect("tombstone-inclusive store").len(),
MAX_CREDENTIALS
);
assert_eq!(
store.add_at("provider-e", "still-overflow", b"secret", 120),
Err(CredentialStoreError::CapacityExhausted)
);
}
#[test]
fn lifecycle_preview_is_deterministic_complete_and_no_write() {
let temporary = tempfile::tempdir().expect("temporary root");
let root = temporary.path().join("credentials-v1");
let store = LocalCredentialStore::at_test_root(&root).expect("open store");
let metadata = store
.add_at("openrouter", "primary", b"secret-canary", 90)
.expect("add");
let before_metadata = std::fs::read(
root.join(ENTRIES_DIRECTORY)
.join(&metadata.id)
.join(METADATA_FILE),
)
.expect("metadata before preview");
let before_secret = store
.resolve(&metadata.credential_ref)
.expect("secret before");
let first = store
.rotate_preview_at(&metadata.credential_ref, 100, 200)
.expect("first preview");
let second = store
.rotate_preview_at(&metadata.credential_ref, 100, 200)
.expect("second preview");
assert_eq!(first, second);
assert_eq!(first.provider, "openrouter");
assert_eq!(first.label, "primary");
assert_eq!(first.credential_ref, metadata.credential_ref);
assert_eq!(first.expected_generation, 1);
assert_eq!(first.expires_at_epoch_seconds, 200);
assert_eq!(first.required_confirmation, ROTATE_CONFIRMATION);
assert_eq!(first.dependency_impact.policy, "bindings_preserved");
assert_eq!(first.dependency_impact.binding_count, 0);
assert!(first.preview_digest.starts_with("sha256:"));
assert_eq!(
std::fs::read(
root.join(ENTRIES_DIRECTORY)
.join(&metadata.id)
.join(METADATA_FILE)
)
.expect("metadata after preview"),
before_metadata
);
assert_eq!(
store
.resolve(&metadata.credential_ref)
.expect("secret after preview")
.as_slice(),
before_secret.as_slice()
);
let encoded = serde_json::to_string(&first).expect("preview JSON");
assert!(encoded.contains("\"provider\":\"openrouter\""));
assert!(encoded.contains("\"label\":\"primary\""));
assert!(encoded.contains("\"bound_phases\":[]"));
assert!(encoded.contains("\"bound_routes\":[]"));
assert!(encoded.contains("\"required_confirmation\""));
assert!(!encoded.contains("secret-canary"));
assert!(!encoded.contains(root.to_string_lossy().as_ref()));
}
#[test]
fn slice_a_metadata_remains_readable_with_redacted_lifecycle_defaults() {
let id = "0123456789abcdef0123456789abcdef";
let encoded = format!(
"{{\"schema_version\":\"1.0.0\",\"id\":\"{id}\",\"credential_ref\":\"local-file-v1:{id}\",\"provider\":\"openrouter\",\"label\":\"primary\"}}"
);
let metadata: CredentialMetadata =
serde_json::from_str(&encoded).expect("Slice A metadata remains compatible");
metadata.validate().expect("defaulted metadata valid");
assert_eq!(metadata.generation, 1);
assert_eq!(metadata.lifecycle_state, CredentialLifecycleState::Active);
assert_eq!(metadata.health, CredentialHealthState::LocallyVerified);
assert_eq!(metadata.last_verified_at_epoch_seconds, None);
assert!(metadata.bound_phases.is_empty());
assert!(metadata.bound_routes.is_empty());
}
#[test]
fn rotate_apply_is_atomic_generation_bound_and_removes_old_secret() {
let temporary = tempfile::tempdir().expect("temporary root");
let root = temporary.path().join("credentials-v1");
let store = LocalCredentialStore::at_test_root(&root).expect("open store");
let metadata = store
.add_at("ollama", "cloud", b"old-secret-canary", 90)
.expect("add");
let preview = store
.rotate_preview_at(&metadata.credential_ref, 100, 200)
.expect("preview");
let outcome = store
.rotate_apply_at(
&metadata.credential_ref,
b"new-secret-canary",
lifecycle_approval(&preview, ROTATE_CONFIRMATION),
110,
)
.expect("rotate");
assert_eq!(outcome.credential_ref, metadata.credential_ref);
assert_eq!(outcome.previous_generation, 1);
assert_eq!(outcome.generation, Some(2));
let current = store.status(&metadata.credential_ref).expect("status");
assert_eq!(current.metadata.generation, 2);
assert_eq!(current.metadata.previous_generation, Some(1));
assert_eq!(current.metadata.rotated_at_epoch_seconds, Some(110));
assert_eq!(
store
.resolve(&metadata.credential_ref)
.expect("new secret")
.as_slice(),
b"new-secret-canary"
);
assert_no_file_contains(&root, b"old-secret-canary");
let replay = store
.rotate_apply_at(
&metadata.credential_ref,
b"replay-secret",
lifecycle_approval(&preview, ROTATE_CONFIRMATION),
120,
)
.expect("committed replay is idempotent");
assert_eq!(replay.commit_status, "committed");
assert!(!replay.reconciliation_required);
assert_eq!(
store
.resolve(&metadata.credential_ref)
.expect("replay did not replace secret")
.as_slice(),
b"new-secret-canary"
);
}
#[test]
fn lifecycle_apply_rejects_digest_confirmation_expiry_and_storage_attacks_without_mutation() {
let temporary = tempfile::tempdir().expect("temporary root");
let root = temporary.path().join("credentials-v1");
let store = LocalCredentialStore::at_test_root(&root).expect("open store");
let metadata = store
.add_at("openrouter", "primary", b"original-secret", 90)
.expect("add");
let preview = store
.rotate_preview_at(&metadata.credential_ref, 100, 200)
.expect("preview");
for result in [
store.rotate_apply_at(
&metadata.credential_ref,
b"replacement",
CredentialLifecycleApproval {
expected_preview: "sha256:0000000000000000000000000000000000000000000000000000000000000000",
expected_generation: 1,
confirmation: ROTATE_CONFIRMATION,
expires_at_epoch_seconds: 200,
},
110,
),
store.rotate_apply_at(
&metadata.credential_ref,
b"replacement",
lifecycle_approval(&preview, "NO"),
110,
),
store.rotate_apply_at(
&metadata.credential_ref,
b"replacement",
lifecycle_approval(&preview, ROTATE_CONFIRMATION),
200,
),
] {
let error = result.expect_err("apply rejected");
assert_eq!(
error.to_string(),
"device-local credential operation failed"
);
}
assert_eq!(
store
.resolve(&metadata.credential_ref)
.expect("original preserved")
.as_slice(),
b"original-secret"
);
let secret_path = root
.join(ENTRIES_DIRECTORY)
.join(&metadata.id)
.join(SECRET_FILE);
let hardlink = temporary.path().join("hardlink");
std::fs::hard_link(&secret_path, &hardlink).expect("hardlink attack");
assert!(matches!(
store.rotate_apply_at(
&metadata.credential_ref,
b"replacement",
lifecycle_approval(&preview, ROTATE_CONFIRMATION),
110,
),
Err(CredentialStoreError::UnsafeStorage)
));
assert_no_file_contains(&root, b"replacement");
}
#[test]
fn remove_is_atomic_and_fails_closed_for_any_binding() {
let temporary = tempfile::tempdir().expect("temporary root");
let root = temporary.path().join("credentials-v1");
let store = LocalCredentialStore::at_test_root(&root).expect("open store");
let metadata = store
.add_at("openrouter", "primary", b"remove-canary", 90)
.expect("add");
let before_preview = store_tree_snapshot(&root);
let preview = store
.remove_preview_at(&metadata.credential_ref, 100, 200)
.expect("remove preview");
assert_eq!(store_tree_snapshot(&root), before_preview);
assert!(preview.dependency_impact.apply_allowed);
assert_eq!(preview.dependency_impact.policy, "zero_binding_removal");
let outcome = store
.remove_apply_at(
&metadata.credential_ref,
lifecycle_approval(&preview, REMOVE_CONFIRMATION),
110,
)
.expect("remove");
assert_eq!(outcome.lifecycle_state, "removed");
let removed = store
.status(&metadata.credential_ref)
.expect("removed status");
assert_eq!(removed.status, "removed");
assert_eq!(
removed.metadata.lifecycle_state,
CredentialLifecycleState::Removed
);
assert_eq!(removed.metadata.health, CredentialHealthState::Unavailable);
assert!(matches!(
store.resolve(&metadata.credential_ref),
Err(CredentialStoreError::NotFound)
));
assert_no_file_contains(&root, b"remove-canary");
let bound = store
.add_at("ollama", "cloud", b"bound-canary", 120)
.expect("bound add");
rewrite_metadata_for_test(&root, &bound.id, |metadata| {
metadata.bound_phases = vec!["phase-2".to_owned()];
metadata.bound_routes = vec!["route-ollama-cloud".to_owned()];
});
let blocked = store
.remove_preview_at(&bound.credential_ref, 130, 200)
.expect("blocked preview");
assert!(!blocked.dependency_impact.apply_allowed);
assert_eq!(blocked.dependency_impact.binding_count, 2);
assert_eq!(blocked.dependency_impact.bound_phases, ["phase-2"]);
assert_eq!(
blocked.dependency_impact.bound_routes,
["route-ollama-cloud"]
);
assert_eq!(
blocked.dependency_impact.policy,
"blocked_until_zero_bindings"
);
assert!(matches!(
store.remove_apply_at(
&bound.credential_ref,
lifecycle_approval(&blocked, REMOVE_CONFIRMATION),
140,
),
Err(CredentialStoreError::DependencyConflict)
));
assert_eq!(
store
.resolve(&bound.credential_ref)
.expect("bound secret preserved")
.as_slice(),
b"bound-canary"
);
}
#[test]
fn every_rotation_post_commit_failure_is_successful_reconcileable_and_has_no_old_secret() {
for edge in [
"rotate_secret_sync",
"rotate_metadata_replace",
"rotate_metadata_sync",
"rotate_journal_commit",
] {
let temporary = tempfile::tempdir().expect("temporary root");
let root = temporary.path().join(format!("credentials-{edge}"));
let store = LocalCredentialStore::at_test_root(&root).expect("open store");
let metadata = store
.add_at("openrouter", "primary", b"old-crash-canary", 90)
.expect("add");
let preview = store
.rotate_preview_at(&metadata.credential_ref, 100, 200)
.expect("preview");
set_lifecycle_failpoint(Some(edge));
let outcome = store
.rotate_apply_at(
&metadata.credential_ref,
b"new-crash-canary",
lifecycle_approval(&preview, ROTATE_CONFIRMATION),
110,
)
.expect("post-commit failure is a committed outcome");
set_lifecycle_failpoint(None);
assert_eq!(outcome.commit_status, "committed_reconciliation_required");
assert!(outcome.reconciliation_required);
assert_no_file_contains(&root, b"old-crash-canary");
let reconciled = store
.rotate_apply_at(
&metadata.credential_ref,
b"ignored-retry-secret",
lifecycle_approval(&preview, ROTATE_CONFIRMATION),
120,
)
.expect("restart/retry reconciles");
assert_eq!(reconciled.commit_status, "committed");
assert!(!reconciled.reconciliation_required);
assert_eq!(
store
.resolve(&metadata.credential_ref)
.expect("new secret survives")
.as_slice(),
b"new-crash-canary"
);
assert_no_file_contains(&root, b"old-crash-canary");
assert_no_file_contains(&root, b"ignored-retry-secret");
}
}
#[test]
fn rotation_recovery_classifies_exact_inode_before_and_after_replace() {
let temporary = tempfile::tempdir().expect("temporary root");
let root = temporary.path().join("credentials-before-replace");
let store = LocalCredentialStore::at_test_root(&root).expect("open store");
let metadata = store
.add_at("openrouter", "primary", b"old-before-canary", 90)
.expect("add");
let preview = store
.rotate_preview_at(&metadata.credential_ref, 100, 200)
.expect("preview");
set_lifecycle_failpoint(Some("rotate_before_secret_replace"));
assert!(matches!(
store.rotate_apply_at(
&metadata.credential_ref,
b"staged-before-canary",
lifecycle_approval(&preview, ROTATE_CONFIRMATION),
110,
),
Err(CredentialStoreError::CommitAmbiguous)
));
set_lifecycle_failpoint(None);
let journal_path = root.join(LIFECYCLE_DIRECTORY).join(&metadata.id);
let journal_bytes = std::fs::read(&journal_path).expect("prepared journal");
let journal: CredentialLifecycleJournal =
serde_json::from_slice(&journal_bytes).expect("decode journal");
assert_ne!(
journal.old_secret_identity,
journal.staged_secret_identity.expect("staged identity")
);
assert!(
!journal_bytes
.windows(b"old-before-canary".len())
.any(|window| window == b"old-before-canary")
);
assert!(
!journal_bytes
.windows(b"staged-before-canary".len())
.any(|window| window == b"staged-before-canary")
);
let reconciled = store
.reconcile_rotate(
&metadata.credential_ref,
lifecycle_request_identity(&preview),
)
.expect("precommit rollback");
assert_eq!(
reconciled,
CredentialRotationReconciliation::RolledBackFreshPreviewRequired
);
assert_eq!(
store
.resolve(&metadata.credential_ref)
.expect("old secret remains")
.as_slice(),
b"old-before-canary"
);
assert_no_file_contains(&root, b"staged-before-canary");
let after_root = temporary.path().join("credentials-after-replace");
let after_store =
LocalCredentialStore::at_test_root(&after_root).expect("open after store");
let after_metadata = after_store
.add_at("openrouter", "secondary", b"old-after-canary", 90)
.expect("after add");
let after_preview = after_store
.rotate_preview_at(&after_metadata.credential_ref, 100, 101)
.expect("short preview");
set_lifecycle_failpoint(Some("rotate_after_secret_replace"));
let pending = after_store
.rotate_apply_at(
&after_metadata.credential_ref,
b"new-after-canary",
lifecycle_approval(&after_preview, ROTATE_CONFIRMATION),
100,
)
.expect("post-commit outcome");
set_lifecycle_failpoint(None);
assert!(pending.reconciliation_required);
let after_reconciled = after_store
.reconcile_rotate(
&after_metadata.credential_ref,
lifecycle_request_identity(&after_preview),
)
.expect("expired journal authorization remains durable");
let CredentialRotationReconciliation::Committed(outcome) = after_reconciled else {
panic!("committed rotation reconciliation expected")
};
assert_eq!(outcome.generation, Some(2));
assert_eq!(outcome.commit_status, "committed");
assert_eq!(
after_store
.resolve(&after_metadata.credential_ref)
.expect("new secret remains")
.as_slice(),
b"new-after-canary"
);
assert_no_file_contains(&after_root, b"old-after-canary");
}
#[test]
fn missing_staged_secret_with_old_inode_live_rolls_back_without_inference() {
let temporary = tempfile::tempdir().expect("temporary root");
let root = temporary.path().join("credentials-v1");
let store = LocalCredentialStore::at_test_root(&root).expect("open store");
let metadata = store
.add_at("openrouter", "primary", b"old-live-canary", 90)
.expect("add");
let preview = store
.rotate_preview_at(&metadata.credential_ref, 100, 200)
.expect("preview");
set_lifecycle_failpoint(Some("rotate_before_secret_replace"));
let _ = store.rotate_apply_at(
&metadata.credential_ref,
b"missing-stage-canary",
lifecycle_approval(&preview, ROTATE_CONFIRMATION),
110,
);
set_lifecycle_failpoint(None);
std::fs::remove_file(
root.join(ENTRIES_DIRECTORY)
.join(&metadata.id)
.join(NEXT_SECRET_FILE),
)
.expect("remove staged secret");
assert_eq!(
store
.reconcile_rotate(
&metadata.credential_ref,
lifecycle_request_identity(&preview),
)
.expect("old inode proves precommit"),
CredentialRotationReconciliation::RolledBackFreshPreviewRequired
);
assert_eq!(
store
.resolve(&metadata.credential_ref)
.expect("old secret preserved")
.as_slice(),
b"old-live-canary"
);
}
#[test]
fn foreign_or_missing_live_secret_identity_is_ambiguous_and_fails_closed() {
for missing in [false, true] {
let temporary = tempfile::tempdir().expect("temporary root");
let root = temporary
.path()
.join(format!("credentials-missing-{missing}"));
let store = LocalCredentialStore::at_test_root(&root).expect("open store");
let metadata = store
.add_at("openrouter", "primary", b"old-ambiguous-canary", 90)
.expect("add");
let preview = store
.rotate_preview_at(&metadata.credential_ref, 100, 200)
.expect("preview");
set_lifecycle_failpoint(Some("rotate_before_secret_replace"));
let _ = store.rotate_apply_at(
&metadata.credential_ref,
b"staged-ambiguous-canary",
lifecycle_approval(&preview, ROTATE_CONFIRMATION),
110,
);
set_lifecycle_failpoint(None);
let live_secret = root
.join(ENTRIES_DIRECTORY)
.join(&metadata.id)
.join(SECRET_FILE);
std::fs::remove_file(&live_secret).expect("remove recorded old inode");
if !missing {
std::fs::write(&live_secret, b"foreign-identity").expect("foreign live secret");
std::fs::set_permissions(&live_secret, std::fs::Permissions::from_mode(0o600))
.expect("foreign secret mode");
}
assert!(matches!(
store.reconcile_rotate(
&metadata.credential_ref,
lifecycle_request_identity(&preview),
),
Err(CredentialStoreError::RecoveryRequired)
));
assert!(root.join(LIFECYCLE_DIRECTORY).join(&metadata.id).exists());
}
}
#[test]
fn every_removal_post_durability_failure_is_successful_and_replay_safe() {
for edge in [
"remove_metadata_stage",
"remove_metadata_replace",
"remove_metadata_sync",
"remove_journal_commit",
] {
let temporary = tempfile::tempdir().expect("temporary root");
let root = temporary.path().join(format!("credentials-{edge}"));
let store = LocalCredentialStore::at_test_root(&root).expect("open store");
let metadata = store
.add_at("openrouter", "primary", b"remove-crash-canary", 90)
.expect("add");
let preview = store
.remove_preview_at(&metadata.credential_ref, 100, 200)
.expect("preview");
set_lifecycle_failpoint(Some(edge));
let outcome = store
.remove_apply_at(
&metadata.credential_ref,
lifecycle_approval(&preview, REMOVE_CONFIRMATION),
110,
)
.expect("post-commit failure is a committed outcome");
set_lifecycle_failpoint(None);
assert_eq!(outcome.commit_status, "committed_reconciliation_required");
assert!(outcome.reconciliation_required);
assert_no_file_contains(&root, b"remove-crash-canary");
let reconciled = store
.remove_apply_at(
&metadata.credential_ref,
lifecycle_approval(&preview, REMOVE_CONFIRMATION),
120,
)
.expect("restart/retry reconciles");
assert_eq!(reconciled.commit_status, "committed");
assert!(!reconciled.reconciliation_required);
assert_no_file_contains(&root, b"remove-crash-canary");
}
}
#[test]
fn removal_never_reports_committed_before_sync_and_post_sync_verification() {
for edge in [
"remove_after_secret_truncate",
"remove_before_secret_sync",
"remove_after_secret_sync",
] {
let temporary = tempfile::tempdir().expect("temporary root");
let root = temporary.path().join(format!("credentials-{edge}"));
let store = LocalCredentialStore::at_test_root(&root).expect("open store");
let metadata = store
.add_at("openrouter", "primary", b"remove-pending-canary", 90)
.expect("add");
let preview = store
.remove_preview_at(&metadata.credential_ref, 100, 101)
.expect("short preview");
set_lifecycle_failpoint(Some(edge));
assert_eq!(
store.remove_apply_at(
&metadata.credential_ref,
lifecycle_approval(&preview, REMOVE_CONFIRMATION),
100,
),
Err(CredentialStoreError::RecoveryRequired),
"{edge} must not report committed"
);
set_lifecycle_failpoint(None);
let journal_path = root.join(LIFECYCLE_DIRECTORY).join(&metadata.id);
let journal_bytes = std::fs::read(&journal_path).expect("prepared removal journal");
let journal: CredentialLifecycleJournal =
serde_json::from_slice(&journal_bytes).expect("decode journal");
assert_eq!(journal.state, CredentialLifecycleJournalState::Prepared);
assert_eq!(
std::fs::metadata(
root.join(ENTRIES_DIRECTORY)
.join(&metadata.id)
.join(SECRET_FILE),
)
.expect("pending tombstone")
.len(),
0
);
let reconciled = store
.remove_apply_at(
&metadata.credential_ref,
CredentialLifecycleApproval {
expected_preview: &preview.preview_digest,
expected_generation: preview.expected_generation,
confirmation: "",
expires_at_epoch_seconds: preview.expires_at_epoch_seconds,
},
500,
)
.expect("exact expired retry durably reconciles without confirmation");
assert_eq!(reconciled.commit_status, "committed");
assert!(!reconciled.reconciliation_required);
assert_no_file_contains(&root, b"remove-pending-canary");
}
}
#[test]
fn descriptor_sync_failure_preserves_prepared_removal_journal() {
let temporary = tempfile::tempdir().expect("temporary root");
let root = temporary.path().join("credentials-v1");
let store = LocalCredentialStore::at_test_root(&root).expect("open store");
let metadata = store
.add_at("openrouter", "primary", b"remove-sync-failure-canary", 90)
.expect("add");
let preview = store
.remove_preview_at(&metadata.credential_ref, 100, 200)
.expect("preview");
set_lifecycle_failpoint(Some("remove_secret_sync_failure"));
assert_eq!(
store.remove_apply_at(
&metadata.credential_ref,
lifecycle_approval(&preview, REMOVE_CONFIRMATION),
110,
),
Err(CredentialStoreError::RecoveryRequired)
);
set_lifecycle_failpoint(None);
let journal_path = root.join(LIFECYCLE_DIRECTORY).join(&metadata.id);
let journal_bytes = std::fs::read(&journal_path).expect("prepared removal journal");
let journal: CredentialLifecycleJournal =
serde_json::from_slice(&journal_bytes).expect("decode journal");
assert_eq!(journal.state, CredentialLifecycleJournalState::Prepared);
set_lifecycle_failpoint(Some("remove_secret_sync_failure"));
assert_eq!(
store.reconcile_remove(
&metadata.credential_ref,
lifecycle_request_identity(&preview),
),
Err(CredentialStoreError::RecoveryRequired)
);
set_lifecycle_failpoint(None);
assert_eq!(
std::fs::read(&journal_path).expect("failed recovery preserves prepared journal"),
journal_bytes
);
assert!(matches!(
store
.reconcile_remove(
&metadata.credential_ref,
lifecycle_request_identity(&preview),
)
.expect("exact retry completes descriptor sync"),
CredentialRemovalReconciliation::Committed(_)
));
assert_no_file_contains(&root, b"remove-sync-failure-canary");
}
#[test]
fn pending_zero_size_removal_reverted_to_nonzero_rolls_back_for_fresh_preview() {
let temporary = tempfile::tempdir().expect("temporary root");
let root = temporary.path().join("credentials-v1");
let store = LocalCredentialStore::at_test_root(&root).expect("open store");
let original_secret = b"remove-reverted-canary";
let metadata = store
.add_at("openrouter", "primary", original_secret, 90)
.expect("add");
let preview = store
.remove_preview_at(&metadata.credential_ref, 100, 200)
.expect("preview");
set_lifecycle_failpoint(Some("remove_after_secret_truncate"));
assert_eq!(
store.remove_apply_at(
&metadata.credential_ref,
lifecycle_approval(&preview, REMOVE_CONFIRMATION),
110,
),
Err(CredentialStoreError::RecoveryRequired)
);
set_lifecycle_failpoint(None);
let secret_path = root
.join(ENTRIES_DIRECTORY)
.join(&metadata.id)
.join(SECRET_FILE);
std::fs::write(&secret_path, original_secret).expect("simulate crash rollback bytes");
assert_eq!(
store.remove_apply_at(
&metadata.credential_ref,
CredentialLifecycleApproval {
expected_preview: &preview.preview_digest,
expected_generation: preview.expected_generation,
confirmation: "",
expires_at_epoch_seconds: preview.expires_at_epoch_seconds,
},
500,
),
Err(CredentialStoreError::FreshPreviewRequired)
);
assert_eq!(
store
.resolve(&metadata.credential_ref)
.expect("precommit secret remains active")
.as_slice(),
original_secret
);
assert!(!root.join(LIFECYCLE_DIRECTORY).join(&metadata.id).exists());
}
#[test]
fn expired_post_truncate_removal_retry_reconciles_from_durable_authorization() {
let temporary = tempfile::tempdir().expect("temporary root");
let root = temporary.path().join("credentials-v1");
let store = LocalCredentialStore::at_test_root(&root).expect("open store");
let metadata = store
.add_at("openrouter", "primary", b"remove-expired-canary", 90)
.expect("add");
let preview = store
.remove_preview_at(&metadata.credential_ref, 100, 101)
.expect("short preview");
set_lifecycle_failpoint(Some("remove_after_secret_truncate"));
assert_eq!(
store.remove_apply_at(
&metadata.credential_ref,
lifecycle_approval(&preview, REMOVE_CONFIRMATION),
100,
),
Err(CredentialStoreError::RecoveryRequired)
);
set_lifecycle_failpoint(None);
let journal_bytes = std::fs::read(root.join(LIFECYCLE_DIRECTORY).join(&metadata.id))
.expect("prepared removal journal");
let journal: CredentialLifecycleJournal =
serde_json::from_slice(&journal_bytes).expect("decode journal");
assert_eq!(journal.required_confirmation, REMOVE_CONFIRMATION);
assert!(
!journal_bytes
.windows(b"remove-expired-canary".len())
.any(|window| window == b"remove-expired-canary")
);
let reconciled = store
.reconcile_remove(
&metadata.credential_ref,
lifecycle_request_identity(&preview),
)
.expect("expired authorized removal reconciles");
let CredentialRemovalReconciliation::Committed(outcome) = reconciled else {
panic!("committed removal expected")
};
assert_eq!(outcome.commit_status, "committed");
assert_eq!(outcome.lifecycle_state, "removed");
assert_no_file_contains(&root, b"remove-expired-canary");
}
#[test]
fn expired_precommit_removal_with_exact_old_inode_rolls_back_for_fresh_preview() {
let temporary = tempfile::tempdir().expect("temporary root");
let root = temporary.path().join("credentials-v1");
let store = LocalCredentialStore::at_test_root(&root).expect("open store");
let metadata = store
.add_at("openrouter", "primary", b"remove-old-live-canary", 90)
.expect("add");
let preview = store
.remove_preview_at(&metadata.credential_ref, 100, 101)
.expect("short preview");
set_lifecycle_failpoint(Some("remove_before_secret_truncate"));
assert!(matches!(
store.remove_apply_at(
&metadata.credential_ref,
lifecycle_approval(&preview, REMOVE_CONFIRMATION),
100,
),
Err(CredentialStoreError::CommitAmbiguous)
));
set_lifecycle_failpoint(None);
let result = store.remove_apply_at(
&metadata.credential_ref,
CredentialLifecycleApproval {
expected_preview: &preview.preview_digest,
expected_generation: preview.expected_generation,
confirmation: "",
expires_at_epoch_seconds: preview.expires_at_epoch_seconds,
},
500,
);
assert_eq!(result, Err(CredentialStoreError::FreshPreviewRequired));
assert_eq!(
store
.resolve(&metadata.credential_ref)
.expect("old secret remains live")
.as_slice(),
b"remove-old-live-canary"
);
let fresh = store
.remove_preview_at(&metadata.credential_ref, 500, 600)
.expect("fresh preview is available after rollback");
assert_eq!(
store.remove_apply_at(
&metadata.credential_ref,
lifecycle_approval(&fresh, REMOVE_CONFIRMATION),
600,
),
Err(CredentialStoreError::PreviewExpired)
);
}
#[test]
fn mismatched_removal_reconciliation_fails_closed_then_exact_retry_succeeds() {
let temporary = tempfile::tempdir().expect("temporary root");
let root = temporary.path().join("credentials-v1");
let store = LocalCredentialStore::at_test_root(&root).expect("open store");
let metadata = store
.add_at("openrouter", "primary", b"remove-mismatch-canary", 90)
.expect("add");
let preview = store
.remove_preview_at(&metadata.credential_ref, 100, 200)
.expect("preview");
set_lifecycle_failpoint(Some("remove_after_secret_truncate"));
let _ = store.remove_apply_at(
&metadata.credential_ref,
lifecycle_approval(&preview, REMOVE_CONFIRMATION),
110,
);
set_lifecycle_failpoint(None);
let before_mismatch = store_tree_snapshot(&root);
assert!(matches!(
store.reconcile_remove(
&metadata.credential_ref,
CredentialLifecycleRequestIdentity {
expected_preview: "sha256:0000000000000000000000000000000000000000000000000000000000000000",
expected_generation: preview.expected_generation,
expires_at_epoch_seconds: preview.expires_at_epoch_seconds,
},
),
Err(CredentialStoreError::RecoveryRequired)
));
assert_eq!(store_tree_snapshot(&root), before_mismatch);
assert!(root.join(LIFECYCLE_DIRECTORY).join(&metadata.id).exists());
assert!(matches!(
store
.reconcile_remove(
&metadata.credential_ref,
lifecycle_request_identity(&preview),
)
.expect("exact retry reconciles"),
CredentialRemovalReconciliation::Committed(_)
));
assert_no_file_contains(&root, b"remove-mismatch-canary");
}
#[test]
fn pre_truncate_removal_mismatch_preserves_store_and_journal_byte_for_byte() {
let temporary = tempfile::tempdir().expect("temporary root");
let root = temporary.path().join("credentials-v1");
let store = LocalCredentialStore::at_test_root(&root).expect("open store");
let metadata = store
.add_at("openrouter", "primary", b"remove-preunlink-canary", 90)
.expect("add");
let preview = store
.remove_preview_at(&metadata.credential_ref, 100, 200)
.expect("preview");
set_lifecycle_failpoint(Some("remove_before_secret_truncate"));
assert_eq!(
store.remove_apply_at(
&metadata.credential_ref,
lifecycle_approval(&preview, REMOVE_CONFIRMATION),
110,
),
Err(CredentialStoreError::CommitAmbiguous)
);
set_lifecycle_failpoint(None);
for mismatch in [
CredentialLifecycleRequestIdentity {
expected_preview: "sha256:0000000000000000000000000000000000000000000000000000000000000000",
expected_generation: preview.expected_generation,
expires_at_epoch_seconds: preview.expires_at_epoch_seconds,
},
CredentialLifecycleRequestIdentity {
expected_preview: &preview.preview_digest,
expected_generation: preview.expected_generation + 1,
expires_at_epoch_seconds: preview.expires_at_epoch_seconds,
},
CredentialLifecycleRequestIdentity {
expected_preview: &preview.preview_digest,
expected_generation: preview.expected_generation,
expires_at_epoch_seconds: preview.expires_at_epoch_seconds + 1,
},
] {
let before = store_tree_snapshot(&root);
assert_eq!(
store.reconcile_remove(&metadata.credential_ref, mismatch),
Err(CredentialStoreError::RecoveryRequired)
);
assert_eq!(store_tree_snapshot(&root), before);
}
}
#[test]
fn durable_removal_ref_and_literal_mismatch_are_recovery_only_and_non_mutating() {
for field in ["credential_ref", "required_confirmation"] {
let temporary = tempfile::tempdir().expect("temporary root");
let root = temporary.path().join(format!("credentials-{field}"));
let store = LocalCredentialStore::at_test_root(&root).expect("open store");
let metadata = store
.add_at("openrouter", "primary", b"remove-binding-canary", 90)
.expect("add");
let preview = store
.remove_preview_at(&metadata.credential_ref, 100, 200)
.expect("preview");
set_lifecycle_failpoint(Some("remove_before_secret_truncate"));
assert_eq!(
store.remove_apply_at(
&metadata.credential_ref,
lifecycle_approval(&preview, REMOVE_CONFIRMATION),
110,
),
Err(CredentialStoreError::CommitAmbiguous)
);
set_lifecycle_failpoint(None);
let journal_path = root.join(LIFECYCLE_DIRECTORY).join(&metadata.id);
let mut journal: CredentialLifecycleJournal =
serde_json::from_slice(&std::fs::read(&journal_path).expect("prepared journal"))
.expect("decode journal");
match field {
"credential_ref" => {
journal.credential_ref =
"local-file-v1:00000000000000000000000000000000".to_owned();
}
"required_confirmation" => {
journal.required_confirmation = ROTATE_CONFIRMATION.to_owned();
}
_ => unreachable!("bounded test field"),
}
std::fs::write(
&journal_path,
serde_json::to_vec(&journal).expect("encode tampered journal"),
)
.expect("write tampered journal");
let before = store_tree_snapshot(&root);
assert_eq!(
store.reconcile_remove(
&metadata.credential_ref,
lifecycle_request_identity(&preview),
),
Err(CredentialStoreError::RecoveryRequired)
);
assert_eq!(store_tree_snapshot(&root), before);
}
}
#[test]
fn same_user_substitute_path_is_untouched_by_descriptor_erasure() {
let temporary = tempfile::tempdir().expect("temporary root");
let root = temporary.path().join("credentials-v1");
let store = LocalCredentialStore::at_test_root(&root).expect("open store");
let metadata = store
.add_at("openrouter", "primary", b"remove-race-original", 90)
.expect("add");
let preview = store
.remove_preview_at(&metadata.credential_ref, 100, 200)
.expect("preview");
set_lifecycle_failpoint(Some("remove_substitute_path_after_fd_validation"));
assert_eq!(
store.remove_apply_at(
&metadata.credential_ref,
lifecycle_approval(&preview, REMOVE_CONFIRMATION),
110,
),
Err(CredentialStoreError::RecoveryRequired)
);
set_lifecycle_failpoint(None);
assert_eq!(
std::fs::read(
root.join(ENTRIES_DIRECTORY)
.join(&metadata.id)
.join(SECRET_FILE),
)
.expect("substitute remains live"),
b"same-user-substitute"
);
assert_eq!(
std::fs::metadata(
root.join(ENTRIES_DIRECTORY)
.join(&metadata.id)
.join(".test-original-secret"),
)
.expect("descriptor-bound original remains inspectable")
.len(),
0
);
assert!(root.join(LIFECYCLE_DIRECTORY).join(&metadata.id).exists());
assert!(
root.join(ENTRIES_DIRECTORY)
.join(&metadata.id)
.join(METADATA_FILE)
.exists()
);
}
#[test]
fn removal_recovery_rejects_missing_or_foreign_secret_identity() {
for missing in [true, false] {
let temporary = tempfile::tempdir().expect("temporary root");
let root = temporary.path().join(format!("removal-foreign-{missing}"));
let store = LocalCredentialStore::at_test_root(&root).expect("open store");
let metadata = store
.add_at("openrouter", "primary", b"removal-identity-old", 90)
.expect("add");
let preview = store
.remove_preview_at(&metadata.credential_ref, 100, 200)
.expect("preview");
set_lifecycle_failpoint(Some("remove_before_secret_truncate"));
let _ = store.remove_apply_at(
&metadata.credential_ref,
lifecycle_approval(&preview, REMOVE_CONFIRMATION),
110,
);
set_lifecycle_failpoint(None);
let secret_path = root
.join(ENTRIES_DIRECTORY)
.join(&metadata.id)
.join(SECRET_FILE);
std::fs::remove_file(&secret_path).expect("remove journaled inode");
if !missing {
std::fs::write(&secret_path, b"same-user-foreign").expect("foreign secret");
std::fs::set_permissions(&secret_path, std::fs::Permissions::from_mode(0o600))
.expect("foreign secret mode");
}
let before = store_tree_snapshot(&root);
assert_eq!(
store.reconcile_remove(
&metadata.credential_ref,
lifecycle_request_identity(&preview),
),
Err(CredentialStoreError::RecoveryRequired)
);
assert_eq!(store_tree_snapshot(&root), before);
}
}
#[test]
fn rotation_request_mismatch_is_pre_reconciliation_and_byte_preserving() {
for phase in ["precommit", "postcommit"] {
for mismatch in ["digest", "generation", "expiry"] {
let temporary = tempfile::tempdir().expect("temporary root");
let root = temporary
.path()
.join(format!("rotation-{phase}-{mismatch}"));
let store = LocalCredentialStore::at_test_root(&root).expect("open store");
let metadata = store
.add_at("openrouter", "primary", b"rotation-old-canary", 90)
.expect("add");
let preview = store
.rotate_preview_at(&metadata.credential_ref, 100, 200)
.expect("preview");
set_lifecycle_failpoint(Some(if phase == "precommit" {
"rotate_before_secret_replace"
} else {
"rotate_after_secret_replace"
}));
let _ = store.rotate_apply_at(
&metadata.credential_ref,
b"rotation-new-canary",
lifecycle_approval(&preview, ROTATE_CONFIRMATION),
110,
);
set_lifecycle_failpoint(None);
let request = match mismatch {
"digest" => CredentialLifecycleRequestIdentity {
expected_preview: "sha256:0000000000000000000000000000000000000000000000000000000000000000",
expected_generation: preview.expected_generation,
expires_at_epoch_seconds: preview.expires_at_epoch_seconds,
},
"generation" => CredentialLifecycleRequestIdentity {
expected_preview: &preview.preview_digest,
expected_generation: preview.expected_generation + 1,
expires_at_epoch_seconds: preview.expires_at_epoch_seconds,
},
"expiry" => CredentialLifecycleRequestIdentity {
expected_preview: &preview.preview_digest,
expected_generation: preview.expected_generation,
expires_at_epoch_seconds: preview.expires_at_epoch_seconds + 1,
},
_ => unreachable!("bounded mismatch"),
};
let before = store_tree_snapshot(&root);
assert_eq!(
store.reconcile_rotate(&metadata.credential_ref, request),
Err(CredentialStoreError::RecoveryRequired)
);
assert_eq!(store_tree_snapshot(&root), before);
}
}
}
#[test]
fn rotation_durable_ref_and_literal_mismatch_are_non_mutating() {
for phase in ["precommit", "postcommit"] {
for field in ["credential_ref", "required_confirmation"] {
let temporary = tempfile::tempdir().expect("temporary root");
let root = temporary.path().join(format!("rotation-{phase}-{field}"));
let store = LocalCredentialStore::at_test_root(&root).expect("open store");
let metadata = store
.add_at("openrouter", "primary", b"rotation-binding-old", 90)
.expect("add");
let preview = store
.rotate_preview_at(&metadata.credential_ref, 100, 200)
.expect("preview");
set_lifecycle_failpoint(Some(if phase == "precommit" {
"rotate_before_secret_replace"
} else {
"rotate_after_secret_replace"
}));
let _ = store.rotate_apply_at(
&metadata.credential_ref,
b"rotation-binding-new",
lifecycle_approval(&preview, ROTATE_CONFIRMATION),
110,
);
set_lifecycle_failpoint(None);
let journal_path = root.join(LIFECYCLE_DIRECTORY).join(&metadata.id);
let mut journal: CredentialLifecycleJournal = serde_json::from_slice(
&std::fs::read(&journal_path).expect("prepared journal"),
)
.expect("decode journal");
if field == "credential_ref" {
journal.credential_ref =
"local-file-v1:00000000000000000000000000000000".to_owned();
} else {
journal.required_confirmation = REMOVE_CONFIRMATION.to_owned();
}
std::fs::write(
&journal_path,
serde_json::to_vec(&journal).expect("encode tampered journal"),
)
.expect("write tampered journal");
let before = store_tree_snapshot(&root);
assert_eq!(
store.reconcile_rotate(
&metadata.credential_ref,
lifecycle_request_identity(&preview),
),
Err(CredentialStoreError::RecoveryRequired)
);
assert_eq!(store_tree_snapshot(&root), before);
}
}
}
#[test]
fn removed_tombstone_reserves_label_and_provider_capacity() {
let temporary = tempfile::tempdir().expect("temporary root");
let root = temporary.path().join("credentials-v1");
let store = LocalCredentialStore::at_test_root(&root).expect("open store");
let removed = store
.add_at("openrouter", "primary", b"tombstone-canary", 90)
.expect("add");
let preview = store
.remove_preview_at(&removed.credential_ref, 100, 200)
.expect("preview");
store
.remove_apply_at(
&removed.credential_ref,
lifecycle_approval(&preview, REMOVE_CONFIRMATION),
110,
)
.expect("remove to tombstone");
assert_eq!(
store.add_at("openrouter", " PRIMARY ", b"replacement", 120),
Err(CredentialStoreError::InvalidInput)
);
for index in 1..MAX_CREDENTIALS_PER_PROVIDER {
store
.add_at(
"openrouter",
&format!("account-{index}"),
b"active-secret",
120,
)
.expect("active record within tombstone-inclusive capacity");
}
assert_eq!(
store.add_at("openrouter", "overflow", b"secret", 120),
Err(CredentialStoreError::CapacityExhausted)
);
let listed = store.list().expect("tombstone-visible list");
assert_eq!(listed.len(), MAX_CREDENTIALS_PER_PROVIDER);
assert_eq!(
listed
.iter()
.filter(|metadata| metadata.lifecycle_state == CredentialLifecycleState::Removed)
.count(),
1
);
assert_eq!(
std::fs::metadata(
root.join(ENTRIES_DIRECTORY)
.join(&removed.id)
.join(SECRET_FILE),
)
.expect("zero-byte tombstone")
.len(),
0
);
assert_eq!(
store.remove_preview_at(&removed.credential_ref, 120, 200),
Err(CredentialStoreError::NotFound)
);
}
#[test]
fn concurrent_rotation_has_one_generation_winner() {
let temporary = tempfile::tempdir().expect("temporary root");
let root = temporary.path().join("credentials-v1");
let store = LocalCredentialStore::at_test_root(&root).expect("open store");
let metadata = store
.add_at("openrouter", "primary", b"original", 90)
.expect("add");
let preview = store
.rotate_preview_at(&metadata.credential_ref, 100, 200)
.expect("preview");
let barrier = Arc::new(Barrier::new(3));
let mut workers = Vec::new();
for secret in [b"first".to_vec(), b"second".to_vec()] {
let store = store.clone();
let preview = preview.clone();
let credential_ref = metadata.credential_ref.clone();
let barrier = Arc::clone(&barrier);
workers.push(std::thread::spawn(move || {
barrier.wait();
store.rotate_apply_at(
&credential_ref,
&secret,
lifecycle_approval(&preview, ROTATE_CONFIRMATION),
110,
)
}));
}
barrier.wait();
let results = workers
.into_iter()
.map(|worker| worker.join().expect("worker result"))
.collect::<Vec<_>>();
let success_count = results.iter().filter(|result| result.is_ok()).count();
assert!((1..=2).contains(&success_count));
assert!(
results
.iter()
.filter(|result| result.is_err())
.all(|result| matches!(result, Err(CredentialStoreError::LockUnavailable)))
);
assert_eq!(
store
.status(&metadata.credential_ref)
.expect("status")
.metadata
.generation,
2
);
let secret = store
.resolve(&metadata.credential_ref)
.expect("winning secret");
assert!(matches!(secret.as_slice(), b"first" | b"second"));
}
fn rewrite_metadata_for_test(
root: &Path,
id: &str,
mutate: impl FnOnce(&mut CredentialMetadata),
) {
let path = root.join(ENTRIES_DIRECTORY).join(id).join(METADATA_FILE);
let bytes = std::fs::read(&path).expect("read metadata");
let mut metadata: CredentialMetadata =
serde_json::from_slice(&bytes).expect("decode metadata");
mutate(&mut metadata);
metadata.validate().expect("valid mutated metadata");
std::fs::write(
&path,
serde_json::to_vec(&metadata).expect("encode metadata"),
)
.expect("write metadata");
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600))
.expect("metadata mode");
}
fn lifecycle_approval<'a>(
preview: &'a CredentialLifecyclePreview,
confirmation: &'a str,
) -> CredentialLifecycleApproval<'a> {
CredentialLifecycleApproval {
expected_preview: &preview.preview_digest,
expected_generation: preview.expected_generation,
confirmation,
expires_at_epoch_seconds: preview.expires_at_epoch_seconds,
}
}
fn lifecycle_request_identity(
preview: &CredentialLifecyclePreview,
) -> CredentialLifecycleRequestIdentity<'_> {
CredentialLifecycleRequestIdentity {
expected_preview: &preview.preview_digest,
expected_generation: preview.expected_generation,
expires_at_epoch_seconds: preview.expires_at_epoch_seconds,
}
}
fn set_lifecycle_failpoint(edge: Option<&'static str>) {
LIFECYCLE_FAILPOINT.with(|configured| configured.set(edge));
}
fn store_tree_snapshot(root: &Path) -> BTreeMap<PathBuf, (u32, Option<Vec<u8>>)> {
fn visit(
root: &Path,
path: &Path,
snapshot: &mut BTreeMap<PathBuf, (u32, Option<Vec<u8>>)>,
) {
let metadata = std::fs::symlink_metadata(path).expect("snapshot metadata");
let relative = path.strip_prefix(root).expect("snapshot relative path");
let bytes = if metadata.is_file() {
Some(std::fs::read(path).expect("snapshot file"))
} else {
None
};
snapshot.insert(relative.to_path_buf(), (metadata.mode() & 0o777, bytes));
if metadata.is_dir() {
for entry in std::fs::read_dir(path).expect("snapshot directory") {
visit(root, &entry.expect("snapshot entry").path(), snapshot);
}
}
}
let mut snapshot = BTreeMap::new();
visit(root, root, &mut snapshot);
snapshot
}
fn assert_no_file_contains(root: &Path, canary: &[u8]) {
fn visit(path: &Path, canary: &[u8]) {
for entry in std::fs::read_dir(path).expect("read directory") {
let entry = entry.expect("directory entry");
let file_type = entry.file_type().expect("file type");
if file_type.is_dir() {
visit(&entry.path(), canary);
} else if file_type.is_file() {
let bytes = std::fs::read(entry.path()).expect("read file");
assert!(!bytes.windows(canary.len()).any(|window| window == canary));
}
}
}
visit(root, canary);
}
#[cfg(target_os = "linux")]
#[test]
fn linux_default_production_root_uses_account_home_xdg_layout() {
let home = Path::new("/srv/steadymux-owner");
assert_eq!(
linux_production_root_path(home, None).expect("default Linux root"),
home.join(".local")
.join("share")
.join("SteadyMux")
.join("credentials-v1")
);
assert!(linux_production_root_path(home, Some(std::ffi::OsStr::new("relative"))).is_err());
}
#[cfg(target_os = "linux")]
#[test]
fn linux_default_store_creates_private_xdg_tree_and_reopens() {
let temporary = tempfile::tempdir().expect("temporary root");
let home = temporary.path().join("home");
std::fs::create_dir(&home).expect("account home");
std::fs::set_permissions(&home, std::fs::Permissions::from_mode(0o700))
.expect("private account home");
let (root_path, root) =
ensure_linux_production_store(&home, None).expect("ensure default Linux store");
drop(root);
assert_eq!(
root_path,
home.join(".local")
.join("share")
.join("SteadyMux")
.join("credentials-v1")
);
for directory in [
home.join(".local"),
home.join(".local").join("share"),
home.join(".local").join("share").join("SteadyMux"),
root_path.clone(),
] {
assert_eq!(
std::fs::metadata(directory)
.expect("Linux store directory metadata")
.mode()
& 0o777,
0o700
);
}
let (reopened_path, reopened) =
open_linux_production_store(&home, None).expect("reopen default Linux store");
drop(reopened);
assert_eq!(reopened_path, root_path);
}
#[cfg(target_os = "linux")]
#[test]
fn linux_xdg_override_store_is_private_and_reopens_without_creation() {
let temporary = tempfile::tempdir().expect("temporary root");
let home = temporary.path().join("home");
let data_home = temporary.path().join("xdg-data");
std::fs::create_dir(&home).expect("account home");
std::fs::create_dir(&data_home).expect("XDG data home");
std::fs::set_permissions(&home, std::fs::Permissions::from_mode(0o700))
.expect("private account home");
std::fs::set_permissions(&data_home, std::fs::Permissions::from_mode(0o700))
.expect("private XDG data home");
let (root_path, root) = ensure_linux_production_store(&home, Some(data_home.as_os_str()))
.expect("ensure Linux store");
drop(root);
assert_eq!(
root_path,
data_home.join("SteadyMux").join("credentials-v1")
);
assert_eq!(
std::fs::metadata(data_home.join("SteadyMux"))
.expect("product metadata")
.mode()
& 0o777,
0o700
);
assert_eq!(
std::fs::metadata(&root_path).expect("root metadata").mode() & 0o777,
0o700
);
let (reopened_path, reopened) =
open_linux_production_store(&home, Some(data_home.as_os_str()))
.expect("reopen Linux store");
drop(reopened);
assert_eq!(reopened_path, root_path);
std::fs::set_permissions(&data_home, std::fs::Permissions::from_mode(0o777))
.expect("relax XDG data home");
assert!(matches!(
open_linux_production_store(&home, Some(data_home.as_os_str())),
Err(CredentialStoreError::UnsafeStorage)
));
}
#[cfg(target_os = "linux")]
#[test]
fn linux_account_record_home_parser_requires_matching_uid_and_absolute_home() {
let record = b"owner:x:1001:1001:Owner:/srv/owner:/bin/bash\n";
assert_eq!(
parse_linux_account_home(record, 1001).expect("valid account record"),
PathBuf::from("/srv/owner")
);
assert!(parse_linux_account_home(record, 1002).is_err());
assert!(
parse_linux_account_home(b"owner:x:1001:1001:Owner:relative:/bin/sh\n", 1001).is_err()
);
assert!(
parse_linux_account_home(
b"owner:x:1001:1001:Owner:/a:/bin/sh\nsecond:x:1001:1001:Owner:/b:/bin/sh\n",
1001
)
.is_err()
);
}
#[cfg(target_os = "linux")]
#[test]
fn linux_current_user_home_resolves_from_effective_uid_account_record() {
let home = current_user_home_from_account_record().expect("effective-UID account home");
validate_absolute_path(&home).expect("absolute account home");
let home_directory = open_path_directory(&home).expect("open account home");
validate_owned_directory(
&fstat(&home_directory).expect("effective-UID account home metadata"),
)
.expect("owned account home");
}
#[cfg(target_os = "macos")]
#[test]
fn account_record_home_parser_requires_one_matching_uid_and_absolute_home() {
let record = b"name: owner\nuid: 501\ndir: /Users/owner\n";
assert_eq!(
parse_account_home(record, 501).expect("valid account record"),
PathBuf::from("/Users/owner")
);
assert!(parse_account_home(record, 502).is_err());
assert!(parse_account_home(b"uid: 501\ndir: relative\n", 501).is_err());
assert!(parse_account_home(b"uid: 501\ndir: /Users/a\ndir: /Users/b\n", 501).is_err());
}
#[cfg(target_os = "macos")]
fn account_lookup_test_command(script: &str) -> std::process::Command {
use std::os::unix::process::CommandExt as _;
use std::process::Stdio;
let mut command = std::process::Command::new("/bin/sh");
command
.args(["-c", script])
.env_clear()
.stdin(Stdio::null())
.stderr(Stdio::null())
.stdout(Stdio::piped())
.process_group(0);
command
}
#[cfg(target_os = "macos")]
#[test]
fn account_lookup_runner_is_bounded_by_output_and_wall_clock() {
let mut valid = account_lookup_test_command("printf 'uid: 501\\ndir: /Users/owner\\n'");
assert_eq!(
run_bounded_account_lookup(&mut valid, Duration::from_secs(1), 128)
.expect("bounded local output"),
b"uid: 501\ndir: /Users/owner\n"
);
let mut oversized = account_lookup_test_command("printf '0123456789abcdef'");
let error = run_bounded_account_lookup(&mut oversized, Duration::from_secs(1), 8)
.expect_err("oversized output rejected");
assert_eq!(
format!("{error:?} {error}"),
"UnsafeStorage device-local credential operation failed"
);
let mut stalled = account_lookup_test_command("sleep 2; printf 'late-canary'");
let started = Instant::now();
let error = run_bounded_account_lookup(&mut stalled, Duration::from_millis(25), 128)
.expect_err("stalled lookup rejected");
assert!(started.elapsed() < Duration::from_secs(1));
let diagnostic = format!("{error:?} {error}");
assert_eq!(
diagnostic,
"UnsafeStorage device-local credential operation failed"
);
assert!(!diagnostic.contains("late-canary"));
}
}