#[cfg(not(target_os = "macos"))]
use keyring::Entry;
use serde::{Deserialize, Serialize};
use thiserror::Error;
pub mod secure_path;
pub use secure_path::{
atomic_replace_private_file, create_private_file, create_private_file_with_failure_injector,
ensure_private_dir, ensure_private_dir_with_failure_injector, harden_owner_only,
harden_owner_only_fallible, harden_private_tree, open_private_append,
open_private_append_with_failure_injector, open_private_read, open_private_truncate,
revalidate_private_file, revalidate_private_path, PrivatePathDurabilityFailureInjector,
PrivatePathDurabilityFailurePoint, PrivateTree, PrivateTreePolicy, PrivateTreeReport,
};
pub const DEFAULT_SERVICE: &str = "car";
pub const OPENROUTER_OAUTH_KEY: &str = "OPENROUTER_OAUTH_API_KEY";
pub const PARSLEE_ACCESS_TOKEN_KEY: &str = "PARSLEE_ACCESS_TOKEN";
pub const PARSLEE_REFRESH_TOKEN_KEY: &str = "PARSLEE_REFRESH_TOKEN";
pub const PARSLEE_EXPIRES_AT_KEY: &str = "PARSLEE_ACCESS_TOKEN_EXPIRES_AT";
pub const PARSLEE_API_BASE_KEY: &str = "PARSLEE_API_BASE";
pub const PARSLEE_ACCOUNTS_KEY: &str = "PARSLEE_ACCOUNTS";
pub const PARSLEE_TOKENS_PREFIX: &str = "PARSLEE_TOKENS_";
pub const PARSLEE_AUTH_GENERATION_KEY: &str = "PARSLEE_AUTH_GENERATION";
pub const PARSLEE_AUTH_COMPLETION_KEY: &str = "PARSLEE_AUTH_COMPLETION";
pub const PARSLEE_ACTIVE_ACCOUNT_ID_KEY: &str = "PARSLEE_ACTIVE_ACCOUNT_ID";
pub const PARSLEE_AUTH_STATE_V2_KEY: &str = "PARSLEE_AUTH_STATE_V2";
fn is_private_chunk_derivative(key: &str, root: &str) -> bool {
key.strip_prefix(root)
.is_some_and(|suffix| suffix.starts_with("#chunk"))
}
pub fn is_daemon_private_secret(service: &str, key: &str) -> bool {
service == DEFAULT_SERVICE
&& (matches!(
key,
OPENROUTER_OAUTH_KEY
| PARSLEE_ACCESS_TOKEN_KEY
| PARSLEE_REFRESH_TOKEN_KEY
| PARSLEE_EXPIRES_AT_KEY
| PARSLEE_API_BASE_KEY
| PARSLEE_ACCOUNTS_KEY
| PARSLEE_AUTH_GENERATION_KEY
| PARSLEE_AUTH_COMPLETION_KEY
| PARSLEE_ACTIVE_ACCOUNT_ID_KEY
| PARSLEE_AUTH_STATE_V2_KEY
) || key.starts_with(PARSLEE_TOKENS_PREFIX)
|| [
OPENROUTER_OAUTH_KEY,
PARSLEE_ACCESS_TOKEN_KEY,
PARSLEE_REFRESH_TOKEN_KEY,
PARSLEE_EXPIRES_AT_KEY,
PARSLEE_API_BASE_KEY,
PARSLEE_ACCOUNTS_KEY,
PARSLEE_AUTH_GENERATION_KEY,
PARSLEE_AUTH_COMPLETION_KEY,
PARSLEE_ACTIVE_ACCOUNT_ID_KEY,
PARSLEE_AUTH_STATE_V2_KEY,
]
.iter()
.any(|root| is_private_chunk_derivative(key, root)))
}
pub fn resolve_env_or_keychain(env_var: &str) -> Option<String> {
if let Ok(v) = std::env::var(env_var) {
if !v.is_empty() {
return Some(v);
}
}
let store = SecretStore::new();
if !store.is_available() {
return None;
}
let secret_ref = SecretRef::new(DEFAULT_SERVICE, env_var);
match store.get(&secret_ref) {
Ok(v) if !v.is_empty() => {
tracing::debug!(env_var = %env_var, "resolved API key from OS keychain");
Some(v)
}
Ok(_) => None, Err(SecretError::NotFound { .. }) => None,
Err(e) => {
tracing::warn!(env_var = %env_var, error = %e, "keychain lookup failed");
None
}
}
}
#[derive(Debug, Error)]
pub enum SecretError {
#[error("secret store unavailable: {0}")]
Unavailable(String),
#[error("no entry for service={service:?} key={key:?}")]
NotFound { service: String, key: String },
#[error("secret store access denied: {message}")]
AccessDenied { message: String },
#[error("secret store access cancelled: {message}")]
UserCancelled { message: String },
#[error("secret store helper timed out during {operation}")]
HelperTimedOut { operation: String },
#[error("secret store error: {0}")]
Backend(String),
#[error("stored value is not valid JSON: {0}")]
InvalidJson(String),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SecretStatus {
pub service: String,
pub key: String,
pub exists: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AvailabilityCheck {
pub available: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct SecretStoreActivity {
pub get_attempts: u64,
pub status_attempts: u64,
pub availability_attempts: u64,
pub write_attempts: u64,
pub delete_attempts: u64,
}
static GET_ATTEMPTS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
static STATUS_ATTEMPTS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
static AVAILABILITY_ATTEMPTS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
static WRITE_ATTEMPTS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
static DELETE_ATTEMPTS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
pub fn secret_store_activity() -> SecretStoreActivity {
use std::sync::atomic::Ordering;
SecretStoreActivity {
get_attempts: GET_ATTEMPTS.load(Ordering::Relaxed),
status_attempts: STATUS_ATTEMPTS.load(Ordering::Relaxed),
availability_attempts: AVAILABILITY_ATTEMPTS.load(Ordering::Relaxed),
write_attempts: WRITE_ATTEMPTS.load(Ordering::Relaxed),
delete_attempts: DELETE_ATTEMPTS.load(Ordering::Relaxed),
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct SecretRef {
pub service: String,
pub key: String,
}
impl SecretRef {
pub fn new(service: impl Into<String>, key: impl Into<String>) -> Self {
Self {
service: service.into(),
key: key.into(),
}
}
pub fn with_default_service(key: impl Into<String>) -> Self {
Self {
service: DEFAULT_SERVICE.to_string(),
key: key.into(),
}
}
}
#[derive(Debug, Default, Clone, Copy)]
pub struct SecretStore;
impl SecretStore {
pub fn new() -> Self {
Self
}
pub fn put(&self, r: &SecretRef, value: &str) -> Result<(), SecretError> {
WRITE_ATTEMPTS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
platform_put(self, r, value)
}
pub fn publish(&self, r: &SecretRef, value: &str) -> Result<(), SecretError> {
WRITE_ATTEMPTS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
platform_publish(self, r, value)
}
pub fn put_json<T: Serialize>(&self, r: &SecretRef, value: &T) -> Result<(), SecretError> {
let s = serde_json::to_string(value)
.map_err(|e| SecretError::Backend(format!("serialize: {}", e)))?;
self.put(r, &s)
}
pub fn get(&self, r: &SecretRef) -> Result<String, SecretError> {
GET_ATTEMPTS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
platform_get(self, r)
}
pub fn get_json<T: for<'de> Deserialize<'de>>(&self, r: &SecretRef) -> Result<T, SecretError> {
let raw = self.get(r)?;
serde_json::from_str(&raw).map_err(|e| SecretError::InvalidJson(e.to_string()))
}
pub fn delete(&self, r: &SecretRef) -> Result<(), SecretError> {
DELETE_ATTEMPTS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
platform_delete(self, r)
}
pub fn status(&self, r: &SecretRef) -> Result<SecretStatus, SecretError> {
STATUS_ATTEMPTS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
platform_status(self, r)
}
const PROBE_SERVICE: &'static str = "car-internal";
const PROBE_KEY: &'static str = "__availability_probe__";
#[cfg(target_os = "macos")]
const PROBE_VALUE: &'static str = "car-availability-probe";
pub fn is_available(&self) -> bool {
self.availability().available
}
pub fn availability(&self) -> AvailabilityCheck {
AVAILABILITY_ATTEMPTS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
if file_backend_dir().is_some() {
return AvailabilityCheck {
available: true,
reason: None,
};
}
platform_availability(self)
}
#[cfg(not(target_os = "macos"))]
fn entry(&self, r: &SecretRef) -> Result<Entry, SecretError> {
Entry::new(&r.service, &r.key).map_err(|e| classify(e, "entry"))
}
}
fn file_backend_dir() -> Option<std::path::PathBuf> {
if !cfg!(debug_assertions) {
return None;
}
match std::env::var_os("CAR_SECRETS_FILE_DIR") {
Some(d) if !d.is_empty() => {
static WARNED: std::sync::Once = std::sync::Once::new();
WARNED.call_once(|| {
tracing::warn!(
"CAR_SECRETS_FILE_DIR set — secrets are PLAINTEXT ON DISK; \
test-only, never production"
);
});
Some(std::path::PathBuf::from(d))
}
_ => None,
}
}
fn file_backend_path(dir: &std::path::Path, r: &SecretRef) -> std::path::PathBuf {
let sanitize = |s: &str| s.replace(['/', '\\', '.'], "_");
dir.join(format!("{}.{}", sanitize(&r.service), sanitize(&r.key)))
}
fn file_backend_put(dir: &std::path::Path, r: &SecretRef, value: &str) -> Result<(), SecretError> {
std::fs::create_dir_all(dir)
.map_err(|e| SecretError::Backend(format!("file backend mkdir: {e}")))?;
std::fs::write(file_backend_path(dir, r), value)
.map_err(|e| SecretError::Backend(format!("file backend write: {e}")))
}
fn file_backend_publish(
dir: &std::path::Path,
r: &SecretRef,
value: &str,
) -> Result<(), SecretError> {
use std::io::Write;
std::fs::create_dir_all(dir)
.map_err(|e| SecretError::Backend(format!("file backend mkdir: {e}")))?;
let destination = file_backend_path(dir, r);
let nonce = publication_nonce();
let staging = destination.with_extension(format!("stage-{nonce}"));
let mut options = std::fs::OpenOptions::new();
options.create_new(true).write(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
options.mode(0o600);
}
let mut file = options
.open(&staging)
.map_err(|e| SecretError::Backend(format!("file backend stage: {e}")))?;
file.write_all(value.as_bytes())
.and_then(|_| file.sync_all())
.map_err(|e| SecretError::Backend(format!("file backend stage write: {e}")))?;
drop(file);
if let Err(error) = std::fs::rename(&staging, &destination) {
let _ = std::fs::remove_file(&staging);
return Err(SecretError::Backend(format!(
"file backend publish rename: {error}"
)));
}
Ok(())
}
fn file_backend_entry_is_merely_absent(dir: &std::path::Path) -> bool {
match std::fs::metadata(dir) {
Ok(metadata) => metadata.is_dir(),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
for ancestor in dir.ancestors().skip(1) {
match std::fs::metadata(ancestor) {
Ok(metadata) => return metadata.is_dir(),
Err(ancestor_error)
if ancestor_error.kind() == std::io::ErrorKind::NotFound => {}
Err(_) => return false,
}
}
false
}
Err(_) => false,
}
}
fn file_backend_get(dir: &std::path::Path, r: &SecretRef) -> Result<String, SecretError> {
match std::fs::read_to_string(file_backend_path(dir, r)) {
Ok(v) => Ok(v),
Err(e)
if e.kind() == std::io::ErrorKind::NotFound
&& file_backend_entry_is_merely_absent(dir) =>
{
Err(SecretError::NotFound {
service: r.service.clone(),
key: r.key.clone(),
})
}
Err(e) => Err(SecretError::Backend(format!("file backend read: {e}"))),
}
}
fn file_backend_delete(dir: &std::path::Path, r: &SecretRef) -> Result<(), SecretError> {
match std::fs::remove_file(file_backend_path(dir, r)) {
Ok(()) => Ok(()),
Err(e)
if e.kind() == std::io::ErrorKind::NotFound
&& file_backend_entry_is_merely_absent(dir) =>
{
Ok(())
}
Err(e) => Err(SecretError::Backend(format!("file backend delete: {e}"))),
}
}
fn file_backend_status(dir: &std::path::Path, r: &SecretRef) -> SecretStatus {
SecretStatus {
service: r.service.clone(),
key: r.key.clone(),
exists: file_backend_path(dir, r).exists(),
}
}
#[cfg(target_os = "macos")]
fn platform_put(_store: &SecretStore, r: &SecretRef, value: &str) -> Result<(), SecretError> {
if let Some(dir) = file_backend_dir() {
return file_backend_put(&dir, r, value);
}
mac_put_via_security_cli(&r.service, &r.key, value)
}
#[cfg(target_os = "macos")]
fn platform_publish(_store: &SecretStore, r: &SecretRef, value: &str) -> Result<(), SecretError> {
if let Some(dir) = file_backend_dir() {
return file_backend_publish(&dir, r, value);
}
mac_publish_via_security_cli(&r.service, &r.key, value)
}
#[cfg(any(not(target_os = "macos"), test))]
const CHUNK_SENTINEL: &str = "__car_secrets_chunked_v1__:";
#[cfg(any(target_os = "windows", test))]
const CHUNK_SENTINEL_V2: &str = "__car_secrets_chunked_v2__:";
#[cfg(any(target_os = "windows", test))]
const CHUNK_SENTINEL_V3: &str = "__car_secrets_chunked_v3__:";
#[cfg(any(target_os = "windows", test))]
const CHUNK_VALUE_V3: &str = "__car_secrets_chunk_v3__:";
#[cfg(any(not(target_os = "macos"), test))]
const CHUNK_THRESHOLD_UTF16: usize = 2000;
#[cfg(any(not(target_os = "macos"), test))]
const CHUNK_CHARS: usize = 1000;
#[cfg(any(target_os = "windows", test))]
const WINDOWS_MAX_CHUNKS: usize = 1024;
#[cfg(any(target_os = "windows", test))]
const WINDOWS_READ_ATTEMPTS: usize = 4;
#[cfg(not(target_os = "macos"))]
fn chunk_ref(r: &SecretRef, i: usize) -> SecretRef {
SecretRef::new(r.service.clone(), format!("{}#chunk{}", r.key, i))
}
#[cfg(target_os = "windows")]
fn chunk_v2_ref(r: &SecretRef, nonce: &str, i: usize) -> SecretRef {
SecretRef::new(r.service.clone(), format!("{}#chunkv2#{nonce}#{i}", r.key))
}
#[cfg(target_os = "windows")]
fn chunk_v3_ref(r: &SecretRef, generation: ChunkGeneration, i: usize) -> SecretRef {
SecretRef::new(
r.service.clone(),
format!("{}#chunkv3#{}#{i}", r.key, generation.label()),
)
}
#[cfg(target_os = "windows")]
fn chunk_v3_manifest_ref(r: &SecretRef, generation: ChunkGeneration) -> SecretRef {
SecretRef::new(
r.service.clone(),
format!("{}#chunkv3#{}#manifest", r.key, generation.label()),
)
}
#[cfg(target_os = "windows")]
fn chunk_v3_retired_v2_ref(r: &SecretRef) -> SecretRef {
SecretRef::new(r.service.clone(), format!("{}#chunkv3#retired-v2", r.key))
}
#[cfg(any(not(target_os = "macos"), test))]
fn split_on_chars(s: &str, n: usize) -> Vec<String> {
let mut out = Vec::new();
let mut cur = String::new();
let mut count = 0usize;
for ch in s.chars() {
cur.push(ch);
count += 1;
if count == n {
out.push(std::mem::take(&mut cur));
count = 0;
}
}
if !cur.is_empty() {
out.push(cur);
}
out
}
fn publication_nonce() -> String {
static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
let sequence = NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|duration| duration.as_nanos())
.unwrap_or_default();
format!("{:x}-{:x}-{:x}", std::process::id(), nanos, sequence)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
#[cfg(any(target_os = "windows", test))]
enum ChunkGeneration {
A,
B,
}
#[cfg(any(target_os = "windows", test))]
impl ChunkGeneration {
fn label(self) -> &'static str {
match self {
Self::A => "a",
Self::B => "b",
}
}
fn inactive(self) -> Self {
match self {
Self::A => Self::B,
Self::B => Self::A,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg(any(target_os = "windows", test))]
struct ChunkPublicationPlan {
generation: ChunkGeneration,
revision: String,
chunks: Vec<String>,
root: String,
}
#[cfg(any(target_os = "windows", test))]
fn chunk_publication_plan(
value: &str,
generation: ChunkGeneration,
revision: &str,
) -> Result<ChunkPublicationPlan, SecretError> {
if revision.is_empty() || revision.contains(':') {
return Err(SecretError::Backend(
"invalid Windows credential publication revision".to_string(),
));
}
let mut chunks = split_on_chars(value, CHUNK_CHARS);
if chunks.is_empty() {
chunks.push(String::new());
}
if chunks.len() > WINDOWS_MAX_CHUNKS {
return Err(SecretError::Backend(format!(
"Windows credential publication requires {} chunks; maximum is {WINDOWS_MAX_CHUNKS}",
chunks.len()
)));
}
Ok(ChunkPublicationPlan {
generation,
revision: revision.to_string(),
root: format!(
"{CHUNK_SENTINEL_V3}{}:{revision}:{}",
generation.label(),
chunks.len()
),
chunks,
})
}
#[cfg(any(target_os = "windows", test))]
fn encode_v3_chunk(revision: &str, value: &str) -> String {
format!("{CHUNK_VALUE_V3}{revision}:{value}")
}
#[cfg(any(target_os = "windows", test))]
fn decode_v3_chunk<'a>(raw: &'a str, revision: &str) -> Result<&'a str, SecretError> {
let payload = raw.strip_prefix(CHUNK_VALUE_V3).ok_or_else(|| {
SecretError::Backend("invalid Windows v3 credential chunk metadata".to_string())
})?;
let (stored_revision, value) = payload.split_once(':').ok_or_else(|| {
SecretError::Backend("invalid Windows v3 credential chunk metadata".to_string())
})?;
if stored_revision != revision {
return Err(SecretError::Backend(
"Windows credential chunk revision changed during read".to_string(),
));
}
Ok(value)
}
#[cfg(any(target_os = "windows", test))]
fn parse_v2_sentinel(raw: &str) -> Option<(&str, usize)> {
let payload = raw.strip_prefix(CHUNK_SENTINEL_V2)?;
let (nonce, count) = payload.rsplit_once(':')?;
let count = count.parse::<usize>().ok()?;
if nonce.is_empty() || count == 0 || count > WINDOWS_MAX_CHUNKS {
return None;
}
Some((nonce, count))
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg(any(target_os = "windows", test))]
enum WindowsRootLayout {
Inline,
LegacyV1 {
count: usize,
},
LegacyV2 {
nonce: String,
count: usize,
},
V3 {
generation: ChunkGeneration,
revision: String,
count: usize,
},
}
#[cfg(any(target_os = "windows", test))]
fn windows_root_layout(raw: &str) -> Result<WindowsRootLayout, SecretError> {
if let Some(payload) = raw.strip_prefix(CHUNK_SENTINEL_V3) {
let (publication, count) = payload.rsplit_once(':').ok_or_else(|| {
SecretError::Backend("invalid Windows v3 credential root metadata".to_string())
})?;
let (generation, revision) = publication.split_once(':').ok_or_else(|| {
SecretError::Backend("invalid Windows v3 credential root metadata".to_string())
})?;
let generation = match generation {
"a" => ChunkGeneration::A,
"b" => ChunkGeneration::B,
_ => {
return Err(SecretError::Backend(
"invalid Windows v3 credential generation".to_string(),
))
}
};
if revision.is_empty() {
return Err(SecretError::Backend(
"invalid Windows v3 credential publication revision".to_string(),
));
}
let count = count
.parse::<usize>()
.ok()
.filter(|count| *count > 0 && *count <= WINDOWS_MAX_CHUNKS);
return count
.map(|count| WindowsRootLayout::V3 {
generation,
revision: revision.to_string(),
count,
})
.ok_or_else(|| {
SecretError::Backend("invalid Windows v3 credential chunk count".to_string())
});
}
if raw.starts_with(CHUNK_SENTINEL_V2) {
return parse_v2_sentinel(raw)
.map(|(nonce, count)| WindowsRootLayout::LegacyV2 {
nonce: nonce.to_string(),
count,
})
.ok_or_else(|| {
SecretError::Backend("invalid Windows v2 credential root metadata".to_string())
});
}
if let Some(count) = raw.strip_prefix(CHUNK_SENTINEL) {
return count
.parse::<usize>()
.ok()
.filter(|count| *count > 0 && *count <= WINDOWS_MAX_CHUNKS)
.map(|count| WindowsRootLayout::LegacyV1 { count })
.ok_or_else(|| {
SecretError::Backend("invalid Windows v1 credential chunk count".to_string())
});
}
Ok(WindowsRootLayout::Inline)
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
#[cfg(any(target_os = "windows", test))]
enum WindowsCredentialSlot {
Root,
LegacyV1Chunk(usize),
LegacyV2Chunk {
nonce: String,
index: usize,
},
V3Chunk {
generation: ChunkGeneration,
index: usize,
},
V3Manifest(ChunkGeneration),
RetiredV2Manifest,
}
#[cfg(any(target_os = "windows", test))]
trait WindowsCredentialBackend {
fn read(&mut self, slot: &WindowsCredentialSlot) -> Result<Option<String>, SecretError>;
fn write(&mut self, slot: &WindowsCredentialSlot, value: &str) -> Result<(), SecretError>;
fn delete(&mut self, slot: &WindowsCredentialSlot) -> Result<(), SecretError>;
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
#[cfg(any(target_os = "windows", test))]
struct WindowsCleanupReport {
failures: usize,
}
#[cfg(any(target_os = "windows", test))]
fn cleanup_windows_slot(
backend: &mut impl WindowsCredentialBackend,
slot: WindowsCredentialSlot,
report: &mut WindowsCleanupReport,
) {
if backend.delete(&slot).is_err() {
report.failures += 1;
}
}
#[cfg(any(target_os = "windows", test))]
fn read_generation_manifest(
backend: &mut impl WindowsCredentialBackend,
generation: ChunkGeneration,
) -> Result<usize, SecretError> {
let Some(raw) = backend.read(&WindowsCredentialSlot::V3Manifest(generation))? else {
return Ok(0);
};
raw.parse::<usize>()
.ok()
.filter(|count| *count <= WINDOWS_MAX_CHUNKS)
.ok_or_else(|| {
SecretError::Backend("invalid Windows credential generation manifest".to_string())
})
}
#[cfg(any(target_os = "windows", test))]
fn read_retired_v2_manifest(
backend: &mut impl WindowsCredentialBackend,
) -> Result<Option<(String, usize)>, SecretError> {
let Some(raw) = backend.read(&WindowsCredentialSlot::RetiredV2Manifest)? else {
return Ok(None);
};
match windows_root_layout(&raw)? {
WindowsRootLayout::LegacyV2 { nonce, count } => Ok(Some((nonce, count))),
_ => Err(SecretError::Backend(
"invalid retired Windows v2 credential manifest".to_string(),
)),
}
}
#[cfg(any(target_os = "windows", test))]
fn cleanup_retired_v2(
backend: &mut impl WindowsCredentialBackend,
nonce: &str,
count: usize,
report: &mut WindowsCleanupReport,
) {
let failures_before = report.failures;
for index in 0..count {
cleanup_windows_slot(
backend,
WindowsCredentialSlot::LegacyV2Chunk {
nonce: nonce.to_string(),
index,
},
report,
);
}
if report.failures == failures_before {
cleanup_windows_slot(backend, WindowsCredentialSlot::RetiredV2Manifest, report);
}
}
#[cfg(any(target_os = "windows", test))]
fn publish_windows_value(
backend: &mut impl WindowsCredentialBackend,
value: &str,
) -> Result<WindowsCleanupReport, SecretError> {
let previous_root = backend.read(&WindowsCredentialSlot::Root)?;
let previous_layout = previous_root
.as_deref()
.map(windows_root_layout)
.transpose()?;
let retired_v2_before = read_retired_v2_manifest(backend)?;
let newly_retired_v2 = match previous_layout.as_ref() {
Some(WindowsRootLayout::LegacyV2 { nonce, count }) => {
let root = previous_root
.as_deref()
.expect("a parsed legacy root came from a present credential");
backend.write(&WindowsCredentialSlot::RetiredV2Manifest, root)?;
Some((nonce.clone(), *count))
}
_ => None,
};
let generation = match previous_layout {
Some(WindowsRootLayout::V3 { generation, .. }) => generation.inactive(),
_ => ChunkGeneration::A,
};
let plan = chunk_publication_plan(value, generation, &publication_nonce())?;
let previous_bound = read_generation_manifest(backend, generation)?;
let high_water = previous_bound.max(plan.chunks.len());
backend.write(
&WindowsCredentialSlot::V3Manifest(generation),
&high_water.to_string(),
)?;
let mut staged = 0;
for (index, chunk) in plan.chunks.iter().enumerate() {
let slot = WindowsCredentialSlot::V3Chunk { generation, index };
if let Err(error) = backend.write(&slot, &encode_v3_chunk(&plan.revision, chunk)) {
let mut ignored_cleanup = WindowsCleanupReport::default();
for staged_index in 0..staged {
cleanup_windows_slot(
backend,
WindowsCredentialSlot::V3Chunk {
generation,
index: staged_index,
},
&mut ignored_cleanup,
);
}
return Err(error);
}
staged += 1;
}
if let Err(error) = backend.write(&WindowsCredentialSlot::Root, &plan.root) {
let mut ignored_cleanup = WindowsCleanupReport::default();
for staged_index in 0..staged {
cleanup_windows_slot(
backend,
WindowsCredentialSlot::V3Chunk {
generation,
index: staged_index,
},
&mut ignored_cleanup,
);
}
return Err(error);
}
let mut cleanup = WindowsCleanupReport::default();
let tail_failures_before = cleanup.failures;
for index in plan.chunks.len()..high_water {
cleanup_windows_slot(
backend,
WindowsCredentialSlot::V3Chunk { generation, index },
&mut cleanup,
);
}
if cleanup.failures == tail_failures_before
&& backend
.write(
&WindowsCredentialSlot::V3Manifest(generation),
&plan.chunks.len().to_string(),
)
.is_err()
{
cleanup.failures += 1;
}
if let Some((nonce, count)) = retired_v2_before {
if newly_retired_v2.as_ref() != Some(&(nonce.clone(), count)) {
cleanup_retired_v2(backend, &nonce, count, &mut cleanup);
}
}
Ok(cleanup)
}
#[cfg(not(target_os = "macos"))]
fn clear_chunks(store: &SecretStore, r: &SecretRef) {
for i in 0..1024 {
let cr = chunk_ref(r, i);
let Ok(entry) = store.entry(&cr) else { break };
match entry.delete_credential() {
Ok(_) => {}
Err(keyring::Error::NoEntry) => break,
Err(_) => break,
}
}
}
#[cfg(any(target_os = "windows", test))]
fn read_windows_value(
backend: &mut impl WindowsCredentialBackend,
) -> Result<Option<String>, SecretError> {
for attempt in 0..WINDOWS_READ_ATTEMPTS {
let Some(root) = backend.read(&WindowsCredentialSlot::Root)? else {
return Ok(None);
};
let (slots, expected_revision) = match windows_root_layout(&root)? {
WindowsRootLayout::Inline => return Ok(Some(root)),
WindowsRootLayout::LegacyV1 { count } => (
(0..count)
.map(WindowsCredentialSlot::LegacyV1Chunk)
.collect::<Vec<_>>(),
None,
),
WindowsRootLayout::LegacyV2 { nonce, count } => (
(0..count)
.map(|index| WindowsCredentialSlot::LegacyV2Chunk {
nonce: nonce.clone(),
index,
})
.collect::<Vec<_>>(),
None,
),
WindowsRootLayout::V3 {
generation,
revision,
count,
} => (
(0..count)
.map(|index| WindowsCredentialSlot::V3Chunk { generation, index })
.collect::<Vec<_>>(),
Some(revision),
),
};
let mut value = String::new();
let mut chunk_error = None;
for slot in slots {
match backend.read(&slot) {
Ok(Some(chunk)) => {
if let Some(revision) = expected_revision.as_deref() {
match decode_v3_chunk(&chunk, revision) {
Ok(chunk) => value.push_str(chunk),
Err(error) => {
chunk_error = Some(error);
break;
}
}
} else {
value.push_str(&chunk);
}
}
Ok(None) => {
chunk_error = Some(SecretError::Backend(
"Windows credential publication is incomplete".to_string(),
));
break;
}
Err(error) => {
chunk_error = Some(error);
break;
}
}
}
let root_after = backend.read(&WindowsCredentialSlot::Root);
if matches!(&root_after, Ok(Some(current)) if current != &root) {
if chunk_error.is_none() {
return Ok(Some(value));
}
if attempt + 1 < WINDOWS_READ_ATTEMPTS {
continue;
}
return Err(SecretError::Backend(
"Windows credential root changed during every read attempt".to_string(),
));
}
if let Some(error) = chunk_error {
return Err(error);
}
match root_after {
Ok(Some(current)) if current == root => return Ok(Some(value)),
Ok(_) if attempt + 1 < WINDOWS_READ_ATTEMPTS => continue,
Ok(_) => {
return Err(SecretError::Backend(
"Windows credential root changed during every read attempt".to_string(),
))
}
Err(error) => return Err(error),
}
}
Err(SecretError::Backend(
"Windows credential read retry limit reached".to_string(),
))
}
#[cfg(any(target_os = "windows", test))]
fn delete_windows_value(
backend: &mut impl WindowsCredentialBackend,
) -> Result<WindowsCleanupReport, SecretError> {
let root = backend.read(&WindowsCredentialSlot::Root)?;
let layout = root.as_deref().map(windows_root_layout).transpose()?;
let retired_v2 = read_retired_v2_manifest(backend)?;
let mut generation_bounds = [
(
ChunkGeneration::A,
read_generation_manifest(backend, ChunkGeneration::A)?,
),
(
ChunkGeneration::B,
read_generation_manifest(backend, ChunkGeneration::B)?,
),
];
if let Some(WindowsRootLayout::V3 {
generation, count, ..
}) = layout.as_ref()
{
let (_, bound) = generation_bounds
.iter_mut()
.find(|(candidate, _)| candidate == generation)
.expect("both deterministic generations are present");
*bound = (*bound).max(*count);
}
backend.delete(&WindowsCredentialSlot::Root)?;
let mut cleanup = WindowsCleanupReport::default();
for (generation, bound) in generation_bounds {
let failures_before = cleanup.failures;
for index in 0..bound {
cleanup_windows_slot(
backend,
WindowsCredentialSlot::V3Chunk { generation, index },
&mut cleanup,
);
}
if cleanup.failures == failures_before {
cleanup_windows_slot(
backend,
WindowsCredentialSlot::V3Manifest(generation),
&mut cleanup,
);
}
}
match layout {
Some(WindowsRootLayout::LegacyV1 { count }) => {
for index in 0..count {
cleanup_windows_slot(
backend,
WindowsCredentialSlot::LegacyV1Chunk(index),
&mut cleanup,
);
}
}
Some(WindowsRootLayout::LegacyV2 { nonce, count }) => {
for index in 0..count {
cleanup_windows_slot(
backend,
WindowsCredentialSlot::LegacyV2Chunk {
nonce: nonce.clone(),
index,
},
&mut cleanup,
);
}
}
_ => {}
}
if let Some((nonce, count)) = retired_v2 {
cleanup_retired_v2(backend, &nonce, count, &mut cleanup);
}
Ok(cleanup)
}
#[cfg(not(target_os = "macos"))]
fn platform_put(store: &SecretStore, r: &SecretRef, value: &str) -> Result<(), SecretError> {
if let Some(dir) = file_backend_dir() {
return file_backend_put(&dir, r, value);
}
if cfg!(windows) {
clear_chunks(store, r);
if value.encode_utf16().count() > CHUNK_THRESHOLD_UTF16 {
let parts = split_on_chars(value, CHUNK_CHARS);
for (i, part) in parts.iter().enumerate() {
let cr = chunk_ref(r, i);
store
.entry(&cr)?
.set_password(part)
.map_err(|e| classify(e, "set_password(chunk)"))?;
}
let sentinel = format!("{CHUNK_SENTINEL}{}", parts.len());
return store
.entry(r)?
.set_password(&sentinel)
.map_err(|e| classify(e, "set_password(sentinel)"));
}
}
let entry = store.entry(r)?;
entry
.set_password(value)
.map_err(|e| classify(e, "set_password"))
}
#[cfg(target_os = "windows")]
struct KeyringWindowsBackend<'a> {
store: &'a SecretStore,
root: &'a SecretRef,
}
#[cfg(target_os = "windows")]
impl KeyringWindowsBackend<'_> {
fn secret_ref(&self, slot: &WindowsCredentialSlot) -> SecretRef {
match slot {
WindowsCredentialSlot::Root => self.root.clone(),
WindowsCredentialSlot::LegacyV1Chunk(index) => chunk_ref(self.root, *index),
WindowsCredentialSlot::LegacyV2Chunk { nonce, index } => {
chunk_v2_ref(self.root, nonce, *index)
}
WindowsCredentialSlot::V3Chunk { generation, index } => {
chunk_v3_ref(self.root, *generation, *index)
}
WindowsCredentialSlot::V3Manifest(generation) => {
chunk_v3_manifest_ref(self.root, *generation)
}
WindowsCredentialSlot::RetiredV2Manifest => chunk_v3_retired_v2_ref(self.root),
}
}
}
#[cfg(target_os = "windows")]
impl WindowsCredentialBackend for KeyringWindowsBackend<'_> {
fn read(&mut self, slot: &WindowsCredentialSlot) -> Result<Option<String>, SecretError> {
match self.store.entry(&self.secret_ref(slot))?.get_password() {
Ok(value) => Ok(Some(value)),
Err(keyring::Error::NoEntry) => Ok(None),
Err(error) => Err(classify(error, "get_password(windows-publish)")),
}
}
fn write(&mut self, slot: &WindowsCredentialSlot, value: &str) -> Result<(), SecretError> {
self.store
.entry(&self.secret_ref(slot))?
.set_password(value)
.map_err(|error| classify(error, "set_password(windows-publish)"))
}
fn delete(&mut self, slot: &WindowsCredentialSlot) -> Result<(), SecretError> {
match self
.store
.entry(&self.secret_ref(slot))?
.delete_credential()
{
Ok(()) | Err(keyring::Error::NoEntry) => Ok(()),
Err(error) => Err(classify(error, "delete_credential(windows-publish)")),
}
}
}
#[cfg(target_os = "windows")]
fn platform_publish(store: &SecretStore, r: &SecretRef, value: &str) -> Result<(), SecretError> {
if let Some(dir) = file_backend_dir() {
return file_backend_publish(&dir, r, value);
}
let mut backend = KeyringWindowsBackend { store, root: r };
let cleanup = publish_windows_value(&mut backend, value)?;
if cleanup.failures > 0 {
tracing::warn!(
cleanup_failures = cleanup.failures,
"Windows credential publication committed; bounded cleanup deferred"
);
}
Ok(())
}
#[cfg(all(not(target_os = "macos"), not(target_os = "windows")))]
fn platform_publish(store: &SecretStore, r: &SecretRef, value: &str) -> Result<(), SecretError> {
if let Some(dir) = file_backend_dir() {
return file_backend_publish(&dir, r, value);
}
store
.entry(r)?
.set_password(value)
.map_err(|error| classify(error, "publish_password"))
}
#[cfg(target_os = "macos")]
fn platform_get(_store: &SecretStore, r: &SecretRef) -> Result<String, SecretError> {
if let Some(dir) = file_backend_dir() {
return file_backend_get(&dir, r);
}
mac_get_via_security_cli(r)
}
#[cfg(target_os = "windows")]
fn platform_get(store: &SecretStore, r: &SecretRef) -> Result<String, SecretError> {
if let Some(dir) = file_backend_dir() {
return file_backend_get(&dir, r);
}
let mut backend = KeyringWindowsBackend { store, root: r };
match read_windows_value(&mut backend)? {
Some(value) => Ok(value),
None => Err(SecretError::NotFound {
service: r.service.clone(),
key: r.key.clone(),
}),
}
}
#[cfg(all(not(target_os = "macos"), not(target_os = "windows")))]
fn platform_get(store: &SecretStore, r: &SecretRef) -> Result<String, SecretError> {
if let Some(dir) = file_backend_dir() {
return file_backend_get(&dir, r);
}
match store.entry(r)?.get_password() {
Ok(value) => Ok(value),
Err(keyring::Error::NoEntry) => Err(SecretError::NotFound {
service: r.service.clone(),
key: r.key.clone(),
}),
Err(error) => Err(classify(error, "get_password")),
}
}
#[cfg(target_os = "macos")]
fn platform_delete(_store: &SecretStore, r: &SecretRef) -> Result<(), SecretError> {
if let Some(dir) = file_backend_dir() {
return file_backend_delete(&dir, r);
}
mac_delete_via_security_cli(r)
}
#[cfg(target_os = "windows")]
fn platform_delete(store: &SecretStore, r: &SecretRef) -> Result<(), SecretError> {
if let Some(dir) = file_backend_dir() {
return file_backend_delete(&dir, r);
}
let mut backend = KeyringWindowsBackend { store, root: r };
let cleanup = delete_windows_value(&mut backend)?;
if cleanup.failures > 0 {
tracing::warn!(
cleanup_failures = cleanup.failures,
"Windows credential root deleted; bounded cleanup deferred"
);
}
Ok(())
}
#[cfg(all(not(target_os = "macos"), not(target_os = "windows")))]
fn platform_delete(store: &SecretStore, r: &SecretRef) -> Result<(), SecretError> {
if let Some(dir) = file_backend_dir() {
return file_backend_delete(&dir, r);
}
match store.entry(r)?.delete_credential() {
Ok(_) | Err(keyring::Error::NoEntry) => Ok(()),
Err(error) => Err(classify(error, "delete_credential")),
}
}
#[cfg(target_os = "macos")]
fn platform_status(_store: &SecretStore, r: &SecretRef) -> Result<SecretStatus, SecretError> {
if let Some(dir) = file_backend_dir() {
return Ok(file_backend_status(&dir, r));
}
mac_status_via_security_cli(r)
}
#[cfg(not(target_os = "macos"))]
fn platform_status(store: &SecretStore, r: &SecretRef) -> Result<SecretStatus, SecretError> {
if let Some(dir) = file_backend_dir() {
return Ok(file_backend_status(&dir, r));
}
let entry = store.entry(r)?;
let exists = match entry.get_password() {
Ok(_) => true,
Err(keyring::Error::NoEntry) => false,
Err(other) => return Err(classify(other, "status")),
};
Ok(SecretStatus {
service: r.service.clone(),
key: r.key.clone(),
exists,
})
}
#[cfg(target_os = "macos")]
fn platform_availability(_store: &SecretStore) -> AvailabilityCheck {
mac_availability_via_security_cli_with(&SystemSecurityCli)
}
#[cfg(target_os = "macos")]
fn mac_availability_via_security_cli_with(cli: &impl SecurityCli) -> AvailabilityCheck {
let probe = SecretRef::new(SecretStore::PROBE_SERVICE, SecretStore::PROBE_KEY);
let result = mac_exists_via_security_cli_with(&probe, cli).and_then(|_| {
mac_put_via_security_cli_with(&probe.service, &probe.key, SecretStore::PROBE_VALUE, cli)
.and_then(|()| mac_delete_via_security_cli_with(&probe, cli))
});
match result {
Ok(()) => AvailabilityCheck {
available: true,
reason: None,
},
Err(error) => AvailabilityCheck {
available: false,
reason: Some(error.to_string()),
},
}
}
#[cfg(not(target_os = "macos"))]
fn platform_availability(store: &SecretStore) -> AvailabilityCheck {
let probe = SecretRef::new(SecretStore::PROBE_SERVICE, SecretStore::PROBE_KEY);
match store.entry(&probe) {
Ok(entry) => match entry.get_password() {
Ok(_) | Err(keyring::Error::NoEntry) => AvailabilityCheck {
available: true,
reason: None,
},
Err(keyring::Error::PlatformFailure(e)) => AvailabilityCheck {
available: false,
reason: Some(format!("platform failure: {e}")),
},
Err(keyring::Error::NoStorageAccess(e)) => AvailabilityCheck {
available: false,
reason: Some(format!("no storage access: {e}")),
},
Err(_) => AvailabilityCheck {
available: true,
reason: None,
},
},
Err(SecretError::Unavailable(reason)) => AvailabilityCheck {
available: false,
reason: Some(reason),
},
Err(other) => AvailabilityCheck {
available: false,
reason: Some(other.to_string()),
},
}
}
#[cfg(target_os = "macos")]
fn mac_put_via_security_cli(service: &str, account: &str, value: &str) -> Result<(), SecretError> {
mac_put_via_security_cli_with(service, account, value, &SystemSecurityCli)
}
#[cfg(target_os = "macos")]
fn mac_publish_via_security_cli(
service: &str,
account: &str,
value: &str,
) -> Result<(), SecretError> {
mac_publish_via_security_cli_with(service, account, value, &SystemSecurityCli)
}
#[cfg(target_os = "macos")]
fn mac_publish_via_security_cli_with(
service: &str,
account: &str,
value: &str,
cli: &impl SecurityCli,
) -> Result<(), SecretError> {
mac_write_via_security_cli(service, account, value, cli)
}
#[cfg(target_os = "macos")]
fn mac_put_via_security_cli_with(
service: &str,
account: &str,
value: &str,
cli: &impl SecurityCli,
) -> Result<(), SecretError> {
mac_write_via_security_cli(service, account, value, cli)
}
#[cfg(target_os = "macos")]
fn mac_write_via_security_cli(
service: &str,
account: &str,
value: &str,
cli: &impl SecurityCli,
) -> Result<(), SecretError> {
let output = cli
.output(&[
"add-generic-password",
"-U", "-A", "-s",
service,
"-a",
account,
"-w",
value,
])
.map_err(|e| security_cli_spawn_error("add-generic-password", e))?;
if output.success {
return Ok(());
}
Err(security_cli_backend_error("add-generic-password", output))
}
#[cfg(target_os = "macos")]
const SECURITY_ERR_SEC_ITEM_NOT_FOUND: i32 = 44;
#[cfg(target_os = "macos")]
#[derive(Debug)]
struct SecurityCliOutput {
success: bool,
code: Option<i32>,
stdout: Vec<u8>,
stderr: Vec<u8>,
prompted: bool,
timed_out: bool,
}
#[cfg(target_os = "macos")]
trait SecurityCli {
fn output(&self, args: &[&str]) -> std::io::Result<SecurityCliOutput>;
}
#[cfg(target_os = "macos")]
struct SystemSecurityCli;
#[cfg(target_os = "macos")]
impl SecurityCli for SystemSecurityCli {
fn output(&self, args: &[&str]) -> std::io::Result<SecurityCliOutput> {
let mut command = std::process::Command::new("/usr/bin/security");
command.args(args);
if let Some(keychain_path) = selected_keychain_path()? {
command.arg(keychain_path);
}
let run = bounded_command_output(&mut command, SECURITY_CLI_TIMEOUT, &describe_item(args))?;
Ok(SecurityCliOutput {
success: run.output.status.success(),
code: run.output.status.code(),
stdout: run.output.stdout,
stderr: run.output.stderr,
prompted: run.prompted,
timed_out: run.timed_out,
})
}
}
#[cfg(target_os = "macos")]
const KEYCHAIN_PATH_ENV: &str = "CAR_KEYCHAIN_PATH";
#[cfg(target_os = "macos")]
const KEYCHAIN_PROOF_ROOT_ENV: &str = "CAR_KEYCHAIN_PROOF_ROOT";
#[cfg(target_os = "macos")]
fn selected_keychain_path() -> std::io::Result<Option<std::path::PathBuf>> {
let Some(path) = std::env::var_os(KEYCHAIN_PATH_ENV).filter(|value| !value.is_empty()) else {
return Ok(None);
};
let proof_root = std::env::var_os(KEYCHAIN_PROOF_ROOT_ENV)
.filter(|value| !value.is_empty())
.ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("{KEYCHAIN_PATH_ENV} requires {KEYCHAIN_PROOF_ROOT_ENV}"),
)
})?;
validate_keychain_path(
std::path::Path::new(&path),
std::path::Path::new(&proof_root),
)
.map(Some)
}
#[cfg(target_os = "macos")]
fn validate_keychain_path(
path: &std::path::Path,
proof_root: &std::path::Path,
) -> std::io::Result<std::path::PathBuf> {
use std::os::unix::fs::{MetadataExt, PermissionsExt};
if !path.is_absolute() || !proof_root.is_absolute() {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"isolated Keychain path and proof root must be absolute",
));
}
let expected_uid = current_effective_uid();
let root_metadata = std::fs::symlink_metadata(proof_root)?;
if root_metadata.file_type().is_symlink()
|| !root_metadata.is_dir()
|| root_metadata.uid() != expected_uid
|| root_metadata.permissions().mode() & 0o077 != 0
{
return Err(std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
"Keychain proof root must be an owner-private, non-symlink directory owned by the current user",
));
}
let path_metadata = std::fs::symlink_metadata(path)?;
if path_metadata.file_type().is_symlink()
|| !path_metadata.is_file()
|| path_metadata.uid() != expected_uid
|| path_metadata.permissions().mode() & 0o077 != 0
{
return Err(std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
"isolated Keychain must be an owner-private, non-symlink regular file owned by the current user",
));
}
let canonical_root = std::fs::canonicalize(proof_root)?;
let canonical_path = std::fs::canonicalize(path)?;
if !canonical_path.starts_with(&canonical_root) || canonical_path == canonical_root {
return Err(std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
"isolated Keychain must be canonically contained by its proof root",
));
}
Ok(canonical_path)
}
#[cfg(target_os = "macos")]
fn current_effective_uid() -> u32 {
unsafe extern "C" {
fn geteuid() -> u32;
}
unsafe { geteuid() }
}
#[cfg(target_os = "macos")]
const SECURITY_CLI_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(15);
#[cfg(target_os = "macos")]
const SECURITY_CLI_INTERACTIVE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(180);
#[cfg(target_os = "macos")]
fn security_agent_is_prompting() -> bool {
std::process::Command::new("/usr/bin/pgrep")
.arg("-x")
.arg("SecurityAgent")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
}
#[cfg(target_os = "macos")]
const PROMPT_EVIDENCE_MIN: std::time::Duration = std::time::Duration::from_millis(500);
#[cfg(target_os = "macos")]
fn dialog_is_evidence_for_this_read(dialog_on_screen: bool, elapsed: std::time::Duration) -> bool {
dialog_on_screen && elapsed >= PROMPT_EVIDENCE_MIN
}
#[cfg(target_os = "macos")]
#[derive(Debug)]
struct BoundedRun {
output: std::process::Output,
prompted: bool,
timed_out: bool,
}
#[cfg(target_os = "macos")]
fn describe_item(args: &[&str]) -> String {
let flag = |name: &str| {
args.iter()
.position(|a| *a == name)
.and_then(|i| args.get(i + 1))
.copied()
};
match (flag("-s"), flag("-a")) {
(Some(service), Some(account)) => format!("{service}/{account}"),
(Some(service), None) => service.to_string(),
(None, Some(account)) => account.to_string(),
(None, None) => args.first().copied().unwrap_or("security").to_string(),
}
}
#[cfg(target_os = "macos")]
fn keychain_prompt_notice(item: &str) -> String {
format!(
"waiting on a macOS keychain prompt for \"{item}\" (up to {}s) — CAR is not \
hung. Click \"Always Allow\" on the dialog (it may be behind another \
window), or grant the \"car\" service access in Keychain Access.",
SECURITY_CLI_INTERACTIVE_TIMEOUT.as_secs()
)
}
#[cfg(target_os = "macos")]
fn bounded_command_output(
command: &mut std::process::Command,
timeout: std::time::Duration,
item: &str,
) -> std::io::Result<BoundedRun> {
bounded_command_output_with(command, timeout, security_agent_is_prompting, || {
tracing::warn!("{}", keychain_prompt_notice(item));
})
}
#[cfg(target_os = "macos")]
fn bounded_command_output_with(
command: &mut std::process::Command,
timeout: std::time::Duration,
dialog_probe: impl Fn() -> bool,
on_waiting_for_user: impl Fn(),
) -> std::io::Result<BoundedRun> {
use std::io::Read;
use std::process::Stdio;
use std::time::Instant;
command.stdout(Stdio::piped()).stderr(Stdio::piped());
let mut child = command.spawn()?;
let stdout = child
.stdout
.take()
.ok_or_else(|| std::io::Error::other("keychain helper stdout was not piped"))?;
let stderr = child
.stderr
.take()
.ok_or_else(|| std::io::Error::other("keychain helper stderr was not piped"))?;
let stdout_reader = std::thread::spawn(move || {
let mut bytes = Vec::new();
let mut stdout = stdout;
stdout.read_to_end(&mut bytes)?;
Ok::<_, std::io::Error>(bytes)
});
let stderr_reader = std::thread::spawn(move || {
let mut bytes = Vec::new();
let mut stderr = stderr;
stderr.read_to_end(&mut bytes)?;
Ok::<_, std::io::Error>(bytes)
});
let started = Instant::now();
let mut prompted = false;
let (status, timed_out) = loop {
if let Some(status) = child.try_wait()? {
break (status, false);
}
let dialog_on_screen = dialog_probe();
if !prompted && dialog_is_evidence_for_this_read(dialog_on_screen, started.elapsed()) {
prompted = true;
on_waiting_for_user();
}
let deadline = if dialog_on_screen {
SECURITY_CLI_INTERACTIVE_TIMEOUT
} else {
timeout
};
if started.elapsed() >= deadline {
let _ = child.kill();
break (child.wait()?, true);
}
std::thread::sleep(std::time::Duration::from_millis(10));
};
let join_reader = |reader: std::thread::JoinHandle<std::io::Result<Vec<u8>>>,
stream: &str|
-> std::io::Result<Vec<u8>> {
reader.join().map_err(|_| {
std::io::Error::other(format!("keychain helper {stream} reader panicked"))
})?
};
let stdout = join_reader(stdout_reader, "stdout")?;
let mut stderr = join_reader(stderr_reader, "stderr")?;
if timed_out {
stderr.extend_from_slice(
format!(
"\nCAR killed the keychain helper after {}ms. This usually means a macOS \
keychain prompt is open and waiting: click \"Always Allow\" (or grant access \
to the \"car\" service in Keychain Access). Until it is answered, CAR cannot \
read your saved credentials and will report that no account is signed in.",
timeout.as_millis()
)
.as_bytes(),
);
}
Ok(BoundedRun {
output: std::process::Output {
status,
stdout,
stderr,
},
prompted,
timed_out,
})
}
#[cfg(target_os = "macos")]
fn mac_get_via_security_cli(r: &SecretRef) -> Result<String, SecretError> {
mac_get_via_security_cli_with(r, &SystemSecurityCli)
}
#[cfg(target_os = "macos")]
fn mac_get_via_security_cli_with(
r: &SecretRef,
cli: &impl SecurityCli,
) -> Result<String, SecretError> {
let output = cli
.output(&[
"find-generic-password",
"-s",
&r.service,
"-a",
&r.key,
"-g",
])
.map_err(|e| security_cli_spawn_error("find-generic-password", e))?;
if !output.success {
return security_cli_not_found_or_backend("find-generic-password", r, output);
}
if output.prompted {
tracing::debug!(
service = %r.service,
key = %r.key,
"keychain read completed after user approval; preserving the item and its persisted grant"
);
}
mac_parse_security_cli_password(&output)
}
#[cfg(target_os = "macos")]
fn mac_parse_security_cli_password(output: &SecurityCliOutput) -> Result<String, SecretError> {
let line = mac_security_cli_text(&output.stderr, "stderr")?
.lines()
.find(|line| line.starts_with("password:"))
.or_else(|| {
mac_security_cli_text(&output.stdout, "stdout")
.ok()
.and_then(|stdout| stdout.lines().find(|line| line.starts_with("password:")))
})
.ok_or_else(|| {
SecretError::Backend(
"/usr/bin/security find-generic-password -g did not print a password line"
.to_string(),
)
})?;
let payload = line
.strip_prefix("password:")
.expect("password line prefix was checked")
.trim_start();
if payload.is_empty() {
return Ok(String::new());
}
let bytes = if let Some(hex_and_preview) = payload.strip_prefix("0x") {
mac_decode_security_cli_hex_password(hex_and_preview)?
} else {
mac_decode_security_cli_quoted_password(payload)?
};
String::from_utf8(bytes).map_err(|e| {
SecretError::Backend(format!(
"/usr/bin/security find-generic-password password was not valid utf-8: {}",
e
))
})
}
#[cfg(target_os = "macos")]
fn mac_security_cli_text<'a>(bytes: &'a [u8], stream: &str) -> Result<&'a str, SecretError> {
std::str::from_utf8(bytes).map_err(|e| {
SecretError::Backend(format!(
"/usr/bin/security find-generic-password {stream} was not valid utf-8: {e}"
))
})
}
#[cfg(target_os = "macos")]
fn mac_decode_security_cli_hex_password(hex_and_preview: &str) -> Result<Vec<u8>, SecretError> {
let hex: String = hex_and_preview
.chars()
.take_while(|c| c.is_ascii_hexdigit())
.collect();
if hex.is_empty() || !hex.len().is_multiple_of(2) {
return Err(SecretError::Backend(format!(
"/usr/bin/security find-generic-password printed invalid password hex: {hex:?}"
)));
}
(0..hex.len())
.step_by(2)
.map(|i| {
u8::from_str_radix(&hex[i..i + 2], 16).map_err(|e| {
SecretError::Backend(format!(
"/usr/bin/security find-generic-password printed invalid password hex: {e}"
))
})
})
.collect()
}
#[cfg(target_os = "macos")]
fn mac_decode_security_cli_quoted_password(payload: &str) -> Result<Vec<u8>, SecretError> {
let quoted = payload.strip_prefix('"').and_then(|s| s.strip_suffix('"'));
match quoted {
Some(value) => Ok(value.as_bytes().to_vec()),
None => Err(SecretError::Backend(
"/usr/bin/security find-generic-password printed an unrecognized password line"
.to_string(),
)),
}
}
#[cfg(target_os = "macos")]
fn mac_status_via_security_cli(r: &SecretRef) -> Result<SecretStatus, SecretError> {
mac_status_via_security_cli_with(r, &SystemSecurityCli)
}
#[cfg(target_os = "macos")]
fn mac_status_via_security_cli_with(
r: &SecretRef,
cli: &impl SecurityCli,
) -> Result<SecretStatus, SecretError> {
let exists = mac_exists_via_security_cli_with(r, cli)?;
Ok(SecretStatus {
service: r.service.clone(),
key: r.key.clone(),
exists,
})
}
#[cfg(target_os = "macos")]
fn mac_exists_via_security_cli_with(
r: &SecretRef,
cli: &impl SecurityCli,
) -> Result<bool, SecretError> {
let output = cli
.output(&["find-generic-password", "-s", &r.service, "-a", &r.key])
.map_err(|e| security_cli_spawn_error("find-generic-password", e))?;
if output.success {
return Ok(true);
}
if output.code == Some(SECURITY_ERR_SEC_ITEM_NOT_FOUND) {
return Ok(false);
}
Err(security_cli_backend_error("find-generic-password", output))
}
#[cfg(target_os = "macos")]
fn mac_delete_via_security_cli(r: &SecretRef) -> Result<(), SecretError> {
mac_delete_via_security_cli_with(r, &SystemSecurityCli)
}
#[cfg(target_os = "macos")]
fn mac_delete_via_security_cli_with(
r: &SecretRef,
cli: &impl SecurityCli,
) -> Result<(), SecretError> {
let output = cli
.output(&["delete-generic-password", "-s", &r.service, "-a", &r.key])
.map_err(|e| security_cli_spawn_error("delete-generic-password", e))?;
if output.success || output.code == Some(SECURITY_ERR_SEC_ITEM_NOT_FOUND) {
return Ok(());
}
Err(security_cli_backend_error(
"delete-generic-password",
output,
))
}
#[cfg(target_os = "macos")]
fn security_cli_not_found_or_backend<T>(
command: &str,
r: &SecretRef,
output: SecurityCliOutput,
) -> Result<T, SecretError> {
if output.code == Some(SECURITY_ERR_SEC_ITEM_NOT_FOUND) {
return Err(SecretError::NotFound {
service: r.service.clone(),
key: r.key.clone(),
});
}
Err(security_cli_backend_error(command, output))
}
#[cfg(target_os = "macos")]
fn security_cli_spawn_error(command: &str, e: std::io::Error) -> SecretError {
SecretError::Backend(format!("/usr/bin/security {command} spawn: {e}"))
}
#[cfg(target_os = "macos")]
fn security_cli_backend_error(command: &str, output: SecurityCliOutput) -> SecretError {
let stderr = String::from_utf8_lossy(&output.stderr);
if output.timed_out {
return classify_helper_timeout(command);
}
let code = output.code.unwrap_or(-1);
match classify_security_error(code, stderr.trim()) {
SecretError::Backend(_) => SecretError::Backend(format!(
"/usr/bin/security {command} failed: code={code} {}",
stderr.trim()
)),
typed => typed,
}
}
#[cfg(target_os = "macos")]
fn classify_security_error(code: i32, detail: &str) -> SecretError {
let normalized = detail.to_ascii_lowercase();
if code == -128 || (code == 128 && normalized.contains("cancel")) {
return SecretError::UserCancelled {
message: detail.to_string(),
};
}
if code == -25293
|| code == 51
|| normalized.contains("authorization denied")
|| normalized.contains("auth denied")
|| normalized.contains("interaction is not allowed")
{
return SecretError::AccessDenied {
message: detail.to_string(),
};
}
SecretError::Backend(format!("macOS security error: code={code} {detail}"))
}
#[cfg(target_os = "macos")]
fn classify_helper_timeout(operation: &str) -> SecretError {
SecretError::HelperTimedOut {
operation: operation.to_string(),
}
}
#[cfg(not(target_os = "macos"))]
fn classify(e: keyring::Error, op: &str) -> SecretError {
use keyring::Error as K;
match e {
K::NoEntry => SecretError::NotFound {
service: String::new(),
key: String::new(),
},
K::PlatformFailure(inner) => SecretError::Unavailable(format!("{}: {}", op, inner)),
K::NoStorageAccess(inner) => SecretError::Unavailable(format!("{}: {}", op, inner)),
K::BadEncoding(_) => SecretError::Backend(format!("{}: value encoding", op)),
other => SecretError::Backend(format!("{}: {}", op, other)),
}
}
#[cfg(test)]
mod chunk_tests {
use super::*;
use std::collections::BTreeMap;
#[test]
fn split_on_chars_covers_boundaries() {
assert_eq!(split_on_chars("", 3), Vec::<String>::new());
assert_eq!(split_on_chars("abc", 3), vec!["abc"]);
assert_eq!(split_on_chars("abcd", 3), vec!["abc", "d"]);
assert_eq!(split_on_chars("abcdef", 2), vec!["ab", "cd", "ef"]);
let big: String = "x".repeat(4000);
let joined: String = split_on_chars(&big, CHUNK_CHARS).concat();
assert_eq!(joined, big);
}
#[test]
fn sentinel_round_trips_the_chunk_count() {
let n = split_on_chars(&"y".repeat(3300), CHUNK_CHARS).len();
let sentinel = format!("{CHUNK_SENTINEL}{n}");
let parsed = sentinel
.strip_prefix(CHUNK_SENTINEL)
.and_then(|s| s.parse::<usize>().ok());
assert_eq!(parsed, Some(4)); assert!("eyJhbGciOi.reallongjwt"
.strip_prefix(CHUNK_SENTINEL)
.is_none());
}
#[test]
fn threshold_leaves_small_values_inline() {
assert!("short-api-key".encode_utf16().count() <= CHUNK_THRESHOLD_UTF16);
assert!("z".repeat(2001).encode_utf16().count() > CHUNK_THRESHOLD_UTF16);
}
#[derive(Debug, Clone)]
struct FailureRule {
slot: WindowsCredentialSlot,
matches_to_skip: usize,
}
#[derive(Debug, Clone, Default)]
struct MemoryWindowsBackend {
entries: BTreeMap<WindowsCredentialSlot, String>,
mutation_calls: usize,
crash_after_mutation: Option<usize>,
fail_write: Option<FailureRule>,
fail_delete: Option<FailureRule>,
}
impl MemoryWindowsBackend {
fn after_mutation(&mut self) {
self.mutation_calls += 1;
if self.crash_after_mutation == Some(self.mutation_calls) {
panic!("injected Windows credential process crash");
}
}
fn should_fail(rule: &mut Option<FailureRule>, slot: &WindowsCredentialSlot) -> bool {
let Some(candidate) = rule.as_mut() else {
return false;
};
if &candidate.slot != slot {
return false;
}
if candidate.matches_to_skip > 0 {
candidate.matches_to_skip -= 1;
return false;
}
*rule = None;
true
}
fn reset_faults(&mut self) {
self.mutation_calls = 0;
self.crash_after_mutation = None;
self.fail_write = None;
self.fail_delete = None;
}
fn root(&self) -> String {
self.entries
.get(&WindowsCredentialSlot::Root)
.expect("root credential")
.clone()
}
}
impl WindowsCredentialBackend for MemoryWindowsBackend {
fn read(&mut self, slot: &WindowsCredentialSlot) -> Result<Option<String>, SecretError> {
Ok(self.entries.get(slot).cloned())
}
fn write(&mut self, slot: &WindowsCredentialSlot, value: &str) -> Result<(), SecretError> {
if Self::should_fail(&mut self.fail_write, slot) {
return Err(SecretError::Backend(
"injected Windows credential write failure".to_string(),
));
}
self.entries.insert(slot.clone(), value.to_string());
self.after_mutation();
Ok(())
}
fn delete(&mut self, slot: &WindowsCredentialSlot) -> Result<(), SecretError> {
if Self::should_fail(&mut self.fail_delete, slot) {
return Err(SecretError::Backend(
"injected Windows credential cleanup failure".to_string(),
));
}
self.entries.remove(slot);
self.after_mutation();
Ok(())
}
}
fn publish(backend: &mut MemoryWindowsBackend, value: &str) -> WindowsCleanupReport {
publish_windows_value(backend, value).expect("publication")
}
fn read(backend: &mut impl WindowsCredentialBackend) -> String {
read_windows_value(backend)
.expect("read succeeds")
.expect("root exists")
}
fn legacy_v2(value: &str, nonce: &str) -> MemoryWindowsBackend {
let mut backend = MemoryWindowsBackend::default();
let chunks = split_on_chars(value, CHUNK_CHARS);
backend.entries.insert(
WindowsCredentialSlot::Root,
format!("{CHUNK_SENTINEL_V2}{nonce}:{}", chunks.len()),
);
for (index, chunk) in chunks.into_iter().enumerate() {
backend.entries.insert(
WindowsCredentialSlot::LegacyV2Chunk {
nonce: nonce.to_string(),
index,
},
chunk,
);
}
backend
}
fn assert_backend_error(error: SecretError, needle: &str) {
match error {
SecretError::Backend(message) => assert!(message.contains(needle), "{message}"),
other => panic!("expected backend error, got {other:?}"),
}
}
#[test]
fn v3_publication_uses_revisioned_dual_generation_roots() {
let value = "v".repeat(3300);
let plan = chunk_publication_plan(&value, ChunkGeneration::B, "revision-7").unwrap();
assert_eq!(plan.generation, ChunkGeneration::B);
assert_eq!(plan.chunks.concat(), value);
assert_eq!(
windows_root_layout(&plan.root).unwrap(),
WindowsRootLayout::V3 {
generation: ChunkGeneration::B,
revision: "revision-7".to_string(),
count: 4,
}
);
assert!(
plan.chunks
.iter()
.all(|chunk| chunk.encode_utf16().count() <= CHUNK_CHARS),
"every staged credential must remain below the platform cap"
);
}
#[test]
fn reader_capturing_old_root_finishes_after_writer_swaps_root() {
let old = "old-".repeat(900);
let new = "new-".repeat(900);
let mut backend = MemoryWindowsBackend::default();
publish(&mut backend, &old);
let mut reader = InterleavingReader::new(backend, vec![new.as_str()]);
assert_eq!(read(&mut reader), old);
assert_eq!(read(&mut reader.inner), new);
}
#[test]
fn reader_detects_generation_aba_and_retries_latest_root() {
let old = "old-".repeat(900);
let middle = "mid-".repeat(1100);
let latest = "latest-".repeat(700);
let mut backend = MemoryWindowsBackend::default();
publish(&mut backend, &old);
let mut reader = InterleavingReader::new(backend, vec![middle.as_str(), latest.as_str()]);
assert_eq!(read(&mut reader), latest);
assert!(reader.root_reads >= 4, "the ABA path must consume a retry");
}
#[test]
fn legacy_nonce_chunks_survive_the_first_v3_root_swap_then_recover() {
let old = "legacy-".repeat(700);
let replacement = "replacement-".repeat(500);
let followup = "followup-".repeat(500);
let backend = legacy_v2(&old, "legacy-nonce");
let mut reader = InterleavingReader::new(backend, vec![replacement.as_str()]);
assert_eq!(read(&mut reader), old);
assert!(reader
.inner
.entries
.contains_key(&WindowsCredentialSlot::RetiredV2Manifest));
assert!(reader
.inner
.entries
.contains_key(&WindowsCredentialSlot::LegacyV2Chunk {
nonce: "legacy-nonce".to_string(),
index: 0,
}));
publish(&mut reader.inner, &followup);
assert!(!reader
.inner
.entries
.contains_key(&WindowsCredentialSlot::RetiredV2Manifest));
assert!(!reader.inner.entries.keys().any(|slot| matches!(
slot,
WindowsCredentialSlot::LegacyV2Chunk { nonce, .. } if nonce == "legacy-nonce"
)));
}
#[test]
fn crash_after_every_publish_mutation_preserves_a_readable_generation() {
let old = "old-".repeat(1200);
let current = "current-".repeat(900);
let replacement = "replacement-".repeat(300);
let mut base = MemoryWindowsBackend::default();
publish(&mut base, &old);
publish(&mut base, ¤t);
base.reset_faults();
let mut successful = base.clone();
publish(&mut successful, &replacement);
let mutation_count = successful.mutation_calls;
assert!(mutation_count >= 7, "exercise stage, commit, and cleanup");
for crash_after in 1..=mutation_count {
let mut crashed = base.clone();
crashed.crash_after_mutation = Some(crash_after);
let unwind = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let _ = publish_windows_value(&mut crashed, &replacement);
}));
assert!(unwind.is_err(), "mutation {crash_after} must crash");
crashed.reset_faults();
let observed = read(&mut crashed);
assert!(
observed == current || observed == replacement,
"crash {crash_after} exposed neither committed generation"
);
publish(&mut crashed, &replacement);
publish(&mut crashed, "recovery-pass");
publish(&mut crashed, &replacement);
assert_eq!(read(&mut crashed), replacement);
assert!(
crashed.entries.len() <= 20,
"crash {crash_after} leaked unbounded entries: {:?}",
crashed.entries.keys().collect::<Vec<_>>()
);
}
}
#[test]
fn repeated_precommit_crashes_have_bounded_cardinality_and_recover_cleanup() {
let old = "old-".repeat(900);
let attempted = "attempted-".repeat(900);
let recovered = "ok-".repeat(600);
let attempted_chunks = split_on_chars(&attempted, CHUNK_CHARS).len();
let old_chunks = split_on_chars(&old, CHUNK_CHARS).len();
let mut backend = MemoryWindowsBackend::default();
publish(&mut backend, &old);
for crash_index in 0..64 {
backend.reset_faults();
backend.crash_after_mutation = Some(1 + crash_index % attempted_chunks);
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let _ = publish_windows_value(&mut backend, &attempted);
}));
assert!(
backend.entries.len() <= 1 + 2 + old_chunks + attempted_chunks,
"attempt {crash_index} grew deterministic storage"
);
}
backend.reset_faults();
publish(&mut backend, &recovered);
assert_eq!(read(&mut backend), recovered);
let recovered_chunks = split_on_chars(&recovered, CHUNK_CHARS).len();
assert!(!backend.entries.keys().any(|slot| matches!(
slot,
WindowsCredentialSlot::V3Chunk {
generation: ChunkGeneration::B,
index,
} if *index >= recovered_chunks
)));
assert_eq!(
backend
.entries
.get(&WindowsCredentialSlot::V3Manifest(ChunkGeneration::B)),
Some(&recovered_chunks.to_string())
);
}
#[test]
fn staging_and_root_failures_leave_the_only_good_generation_readable() {
let old = "old-".repeat(900);
let replacement = "replacement-".repeat(500);
for failed_slot in [
WindowsCredentialSlot::V3Chunk {
generation: ChunkGeneration::B,
index: 1,
},
WindowsCredentialSlot::Root,
] {
let mut backend = MemoryWindowsBackend::default();
publish(&mut backend, &old);
backend.fail_write = Some(FailureRule {
slot: failed_slot,
matches_to_skip: 0,
});
let error = publish_windows_value(&mut backend, &replacement).unwrap_err();
assert_backend_error(error, "injected");
assert_eq!(read(&mut backend), old);
}
}
#[test]
fn postcommit_cleanup_errors_report_deferred_success_and_recover_later() {
let old = "old-".repeat(1400);
let current = "current-".repeat(900);
let replacement = "replacement-".repeat(200);
let mut backend = MemoryWindowsBackend::default();
publish(&mut backend, &old);
publish(&mut backend, ¤t);
backend.fail_delete = Some(FailureRule {
slot: WindowsCredentialSlot::V3Chunk {
generation: ChunkGeneration::A,
index: 4,
},
matches_to_skip: 0,
});
let cleanup = publish_windows_value(&mut backend, &replacement).unwrap();
assert_eq!(cleanup.failures, 1);
assert_eq!(read(&mut backend), replacement);
assert_eq!(
backend
.entries
.get(&WindowsCredentialSlot::V3Manifest(ChunkGeneration::A)),
Some(&split_on_chars(&old, CHUNK_CHARS).len().to_string()),
"failed cleanup keeps the crash high-water for a later sweep"
);
publish(&mut backend, "rotate-once");
publish(&mut backend, &replacement);
assert!(!backend.entries.keys().any(|slot| matches!(
slot,
WindowsCredentialSlot::V3Chunk {
generation: ChunkGeneration::A,
index,
} if *index >= split_on_chars(&replacement, CHUNK_CHARS).len()
)));
}
#[test]
fn postcommit_manifest_shrink_failure_keeps_recovery_high_water() {
let old = "old-".repeat(1400);
let current = "current-".repeat(900);
let replacement = "replacement-".repeat(200);
let mut backend = MemoryWindowsBackend::default();
publish(&mut backend, &old);
publish(&mut backend, ¤t);
backend.fail_write = Some(FailureRule {
slot: WindowsCredentialSlot::V3Manifest(ChunkGeneration::A),
matches_to_skip: 1,
});
let cleanup = publish_windows_value(&mut backend, &replacement).unwrap();
assert_eq!(cleanup.failures, 1);
assert_eq!(read(&mut backend), replacement);
assert_eq!(
backend
.entries
.get(&WindowsCredentialSlot::V3Manifest(ChunkGeneration::A)),
Some(&split_on_chars(&old, CHUNK_CHARS).len().to_string())
);
}
#[test]
fn delete_cleanup_failure_retains_manifest_for_idempotent_recovery() {
let value = "secret-".repeat(700);
let mut backend = MemoryWindowsBackend::default();
publish(&mut backend, &value);
backend.fail_delete = Some(FailureRule {
slot: WindowsCredentialSlot::V3Chunk {
generation: ChunkGeneration::A,
index: 0,
},
matches_to_skip: 0,
});
let cleanup = delete_windows_value(&mut backend).unwrap();
assert_eq!(cleanup.failures, 1);
assert!(!backend.entries.contains_key(&WindowsCredentialSlot::Root));
assert!(backend
.entries
.contains_key(&WindowsCredentialSlot::V3Manifest(ChunkGeneration::A)));
backend.reset_faults();
assert_eq!(delete_windows_value(&mut backend).unwrap().failures, 0);
assert!(backend.entries.is_empty());
}
#[test]
fn corrupt_cleanup_metadata_fails_before_root_or_chunks_are_deleted() {
let old = "old-".repeat(900);
let mut backend = MemoryWindowsBackend::default();
publish(&mut backend, &old);
let root_before = backend.root();
backend.entries.insert(
WindowsCredentialSlot::V3Manifest(ChunkGeneration::B),
"not-a-count".to_string(),
);
let error = publish_windows_value(&mut backend, "replacement").unwrap_err();
assert_backend_error(error, "manifest");
assert_eq!(backend.root(), root_before);
assert_eq!(read(&mut backend), old);
let error = delete_windows_value(&mut backend).unwrap_err();
assert_backend_error(error, "manifest");
assert_eq!(backend.root(), root_before);
assert_eq!(read(&mut backend), old);
}
#[test]
fn reader_retry_is_bounded_when_root_never_stabilizes() {
let value_a = "a".repeat(2500);
let value_b = "b".repeat(2500);
let mut backend = MemoryWindowsBackend::default();
publish(&mut backend, &value_a);
let root_a = backend.root();
publish(&mut backend, &value_b);
let root_b = backend.root();
backend.entries.remove(&WindowsCredentialSlot::V3Chunk {
generation: ChunkGeneration::A,
index: 0,
});
let mut churning = AlternatingRootBackend {
inner: backend,
roots: [root_a, root_b],
root_reads: 0,
};
let error = read_windows_value(&mut churning).unwrap_err();
assert_backend_error(error, "changed during every read attempt");
assert_eq!(churning.root_reads, WINDOWS_READ_ATTEMPTS * 2);
}
struct InterleavingReader<'a> {
inner: MemoryWindowsBackend,
publications: Vec<&'a str>,
root_reads: usize,
}
impl<'a> InterleavingReader<'a> {
fn new(inner: MemoryWindowsBackend, publications: Vec<&'a str>) -> Self {
Self {
inner,
publications,
root_reads: 0,
}
}
}
impl WindowsCredentialBackend for InterleavingReader<'_> {
fn read(&mut self, slot: &WindowsCredentialSlot) -> Result<Option<String>, SecretError> {
let captured = self.inner.read(slot)?;
if slot == &WindowsCredentialSlot::Root && self.root_reads == 0 {
for value in self.publications.drain(..) {
publish_windows_value(&mut self.inner, value)?;
}
}
if slot == &WindowsCredentialSlot::Root {
self.root_reads += 1;
}
Ok(captured)
}
fn write(&mut self, slot: &WindowsCredentialSlot, value: &str) -> Result<(), SecretError> {
self.inner.write(slot, value)
}
fn delete(&mut self, slot: &WindowsCredentialSlot) -> Result<(), SecretError> {
self.inner.delete(slot)
}
}
struct AlternatingRootBackend {
inner: MemoryWindowsBackend,
roots: [String; 2],
root_reads: usize,
}
impl WindowsCredentialBackend for AlternatingRootBackend {
fn read(&mut self, slot: &WindowsCredentialSlot) -> Result<Option<String>, SecretError> {
if slot == &WindowsCredentialSlot::Root {
let root = self.roots[self.root_reads % self.roots.len()].clone();
self.root_reads += 1;
return Ok(Some(root));
}
self.inner.read(slot)
}
fn write(&mut self, slot: &WindowsCredentialSlot, value: &str) -> Result<(), SecretError> {
self.inner.write(slot, value)
}
fn delete(&mut self, slot: &WindowsCredentialSlot) -> Result<(), SecretError> {
self.inner.delete(slot)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde::{Deserialize, Serialize};
#[test]
fn a_store_that_is_not_a_directory_is_a_backend_error_not_a_missing_secret() {
let parent = tempfile::tempdir().unwrap();
let not_a_dir = parent.path().join("blocked");
std::fs::write(¬_a_dir, b"a regular file where the store should be").unwrap();
let reference = SecretRef::with_default_service("SOME_KEY");
assert!(
!file_backend_entry_is_merely_absent(¬_a_dir),
"the platform-neutral discriminator must reject a regular-file store root"
);
match file_backend_get(¬_a_dir, &reference) {
Err(SecretError::Backend(_)) => {}
other => panic!("unusable store must report a backend error, got {other:?}"),
}
match file_backend_delete(¬_a_dir, &reference) {
Err(SecretError::Backend(_)) => {}
other => panic!("unusable store must not report a successful delete, got {other:?}"),
}
assert!(
!file_backend_status(¬_a_dir, &reference).exists,
"status on an unusable store must not claim knowledge of the entry"
);
}
#[test]
fn a_store_directory_that_does_not_exist_yet_is_still_not_found() {
let parent = tempfile::tempdir().unwrap();
let never_created = parent.path().join("not-created-yet");
assert!(!never_created.exists());
assert!(
file_backend_entry_is_merely_absent(&never_created),
"a missing directory beneath an existing directory is a normal first run"
);
let reference = SecretRef::with_default_service("SOME_KEY");
match file_backend_get(&never_created, &reference) {
Err(SecretError::NotFound { .. }) => {}
other => panic!("a first-run store has no secrets, it is not broken: {other:?}"),
}
assert!(
file_backend_delete(&never_created, &reference).is_ok(),
"deleting from a store that was never written is a no-op success"
);
assert!(!file_backend_status(&never_created, &reference).exists);
}
#[test]
fn a_missing_entry_in_a_real_directory_is_still_not_found() {
let dir = tempfile::tempdir().unwrap();
let reference = SecretRef::with_default_service("ABSENT_KEY");
match file_backend_get(dir.path(), &reference) {
Err(SecretError::NotFound { .. }) => {}
other => panic!("an absent entry in a usable store is NotFound, got {other:?}"),
}
assert!(
file_backend_delete(dir.path(), &reference).is_ok(),
"deleting an absent entry from a usable store is a no-op success"
);
assert!(!file_backend_status(dir.path(), &reference).exists);
}
static STORE_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
fn lock_store() -> std::sync::MutexGuard<'static, ()> {
STORE_LOCK.lock().unwrap_or_else(|e| e.into_inner())
}
struct IsolatedStoreFixture {
_guard: std::sync::MutexGuard<'static, ()>,
_dir: tempfile::TempDir,
previous_dir: Option<std::ffi::OsString>,
}
impl IsolatedStoreFixture {
fn new() -> Self {
let guard = lock_store();
let dir = tempfile::tempdir().expect("isolated secret-store directory");
let previous_dir = std::env::var_os("CAR_SECRETS_FILE_DIR");
std::env::set_var("CAR_SECRETS_FILE_DIR", dir.path());
assert_eq!(
file_backend_dir().as_deref(),
Some(dir.path()),
"contract test must use the isolated file backend"
);
Self {
_guard: guard,
_dir: dir,
previous_dir,
}
}
fn store(&self) -> SecretStore {
SecretStore::new()
}
}
impl Drop for IsolatedStoreFixture {
fn drop(&mut self) {
match self.previous_dir.take() {
Some(value) => std::env::set_var("CAR_SECRETS_FILE_DIR", value),
None => std::env::remove_var("CAR_SECRETS_FILE_DIR"),
}
}
}
#[cfg(target_os = "macos")]
fn test_service() -> String {
format!(
"car-secrets-tests-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0)
)
}
#[cfg(target_os = "macos")]
const NATIVE_KEYCHAIN_LANE: &str = "CAR_TEST_NATIVE_KEYCHAIN";
#[cfg(target_os = "macos")]
fn run_native_keychain_lane() {
assert!(
std::env::var_os("CAR_SECRETS_FILE_DIR").is_none(),
"native lane refuses CAR_SECRETS_FILE_DIR; run it against the provisioned keychain"
);
let store = SecretStore::new();
let availability = store.availability();
assert!(
availability.available,
"native keychain unavailable: {}",
availability
.reason
.unwrap_or_else(|| "no reason reported".to_string())
);
#[derive(Serialize, Deserialize, PartialEq, Debug)]
struct Session {
cookies: Vec<String>,
expires_at: i64,
}
let reference = SecretRef::new(test_service(), "provisioned-native-contracts");
store
.delete(&reference)
.expect("clean native fixture before run");
assert!(matches!(
store.get(&reference),
Err(SecretError::NotFound { .. })
));
store.put(&reference, "abc\n").expect("write native secret");
assert_eq!(store.get(&reference).unwrap(), "abc\n");
let status = store.status(&reference).unwrap();
assert!(status.exists);
assert!(!serde_json::to_string(&status).unwrap().contains("abc"));
let session = Session {
cookies: vec!["a=1".into(), "b=2".into()],
expires_at: 1_700_000_000,
};
store.put_json(&reference, &session).unwrap();
assert_eq!(store.get_json::<Session>(&reference).unwrap(), session);
store
.delete(&reference)
.expect("clean native fixture after run");
store
.delete(&reference)
.expect("native delete is idempotent");
assert!(!store.status(&reference).unwrap().exists);
}
#[derive(Clone)]
struct BufWriter(std::sync::Arc<std::sync::Mutex<Vec<u8>>>);
impl std::io::Write for BufWriter {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.0.lock().unwrap().extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
impl<'a> tracing_subscriber::fmt::MakeWriter<'a> for BufWriter {
type Writer = BufWriter;
fn make_writer(&'a self) -> Self::Writer {
self.clone()
}
}
#[test]
fn file_backend_roundtrip_and_warn_under_debug() {
const CHILD: &str = "CAR_TEST_FILE_BACKEND_WARNING_CHILD";
if std::env::var_os(CHILD).is_none() {
let output =
std::process::Command::new(std::env::current_exe().expect("test executable"))
.args([
"--exact",
"tests::file_backend_roundtrip_and_warn_under_debug",
"--nocapture",
])
.env(CHILD, "1")
.env_remove("CAR_SECRETS_FILE_DIR")
.env_remove("CAR_TEST_PRECONSUME_FILE_BACKEND_WARNING")
.env_remove("CAR_KEYCHAIN_PROOF_ROOT")
.env_remove("CAR_KEYCHAIN_PATH")
.output()
.expect("spawn isolated file-backend warning test");
assert!(
output.status.success(),
"isolated file-backend warning test failed\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
);
return;
}
if std::env::var_os("CAR_TEST_PRECONSUME_FILE_BACKEND_WARNING").is_some() {
let _ = file_backend_dir();
}
let _guard = lock_store();
#[allow(clippy::assertions_on_constants)]
{
assert!(
cfg!(debug_assertions),
"the crate test suite runs in debug; the file backend depends on it"
);
}
let dir = std::env::temp_dir().join(format!(
"car-secrets-filebackend-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0)
));
std::fs::create_dir_all(&dir).unwrap();
std::env::set_var("CAR_SECRETS_FILE_DIR", &dir);
let buf = std::sync::Arc::new(std::sync::Mutex::new(Vec::<u8>::new()));
let subscriber = tracing_subscriber::fmt()
.with_writer(BufWriter(buf.clone()))
.with_max_level(tracing::Level::WARN)
.finish();
tracing::subscriber::with_default(subscriber, || {
assert_eq!(
file_backend_dir().as_deref(),
Some(dir.as_path()),
"CAR_SECRETS_FILE_DIR must be honored under debug_assertions"
);
});
let logged = String::from_utf8(buf.lock().unwrap().clone()).unwrap();
assert!(
logged.contains("PLAINTEXT ON DISK"),
"the file backend must emit the one-time PLAINTEXT warning, got logs: {logged:?}"
);
let store = SecretStore::new();
let check = store.availability();
assert!(check.available, "file backend must report available");
assert!(check.reason.is_none());
let r = SecretRef::new("svc", "key");
store.put(&r, "xoxb-plaintext-value").unwrap();
assert_eq!(store.get(&r).unwrap(), "xoxb-plaintext-value");
let on_disk = std::fs::read_to_string(file_backend_path(&dir, &r)).unwrap();
assert_eq!(on_disk, "xoxb-plaintext-value");
store.delete(&r).unwrap();
match store.get(&r) {
Err(SecretError::NotFound { .. }) => {}
other => panic!("expected NotFound after delete, got {other:?}"),
}
for key in [
OPENROUTER_OAUTH_KEY,
PARSLEE_ACCESS_TOKEN_KEY,
PARSLEE_REFRESH_TOKEN_KEY,
PARSLEE_EXPIRES_AT_KEY,
PARSLEE_API_BASE_KEY,
PARSLEE_ACCOUNTS_KEY,
"PARSLEE_TOKENS_account-1",
PARSLEE_AUTH_GENERATION_KEY,
PARSLEE_AUTH_COMPLETION_KEY,
PARSLEE_ACTIVE_ACCOUNT_ID_KEY,
PARSLEE_AUTH_STATE_V2_KEY,
] {
let private = SecretRef::new(DEFAULT_SERVICE, key);
assert!(is_daemon_private_secret(&private.service, &private.key));
store.put(&private, "internal-test-value").unwrap();
assert_eq!(store.get(&private).unwrap(), "internal-test-value");
store.delete(&private).unwrap();
assert!(matches!(
store.get(&private),
Err(SecretError::NotFound { .. })
));
}
std::env::remove_var("CAR_SECRETS_FILE_DIR");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn every_parslee_auth_slot_is_private_to_the_dedicated_auth_surface() {
for key in [
PARSLEE_ACCESS_TOKEN_KEY,
PARSLEE_REFRESH_TOKEN_KEY,
PARSLEE_EXPIRES_AT_KEY,
PARSLEE_API_BASE_KEY,
PARSLEE_ACCOUNTS_KEY,
"PARSLEE_TOKENS_account-1",
PARSLEE_ACTIVE_ACCOUNT_ID_KEY,
PARSLEE_AUTH_GENERATION_KEY,
PARSLEE_AUTH_COMPLETION_KEY,
PARSLEE_AUTH_STATE_V2_KEY,
] {
assert!(
is_daemon_private_secret(DEFAULT_SERVICE, key),
"{key} must be unreachable through generic secret surfaces"
);
assert!(
!is_daemon_private_secret("other-service", key),
"reservation must remain scoped to the CAR service"
);
}
assert!(!is_daemon_private_secret(
DEFAULT_SERVICE,
"OPENROUTER_API_KEY"
));
for key in [
format!("{PARSLEE_AUTH_STATE_V2_KEY}#chunk0"),
format!("{PARSLEE_AUTH_STATE_V2_KEY}#chunkv2#nonce-1#0"),
format!("{OPENROUTER_OAUTH_KEY}#chunk17"),
format!("{OPENROUTER_OAUTH_KEY}#chunkv2#nonce-2#3"),
] {
assert!(
is_daemon_private_secret(DEFAULT_SERVICE, &key),
"{key} is derived from a daemon-private root"
);
assert!(!is_daemon_private_secret("other-service", &key));
}
}
#[cfg(target_os = "macos")]
#[test]
fn bounded_command_output_large_helper() {
if std::env::var_os("CAR_SECURITY_OUTPUT_HELPER").is_none() {
return;
}
use std::io::Write;
let payload = vec![b'x'; 128 * 1024];
std::io::stdout().write_all(&payload).unwrap();
std::io::stdout().flush().unwrap();
std::io::stderr().write_all(&payload).unwrap();
std::io::stderr().flush().unwrap();
}
#[cfg(target_os = "macos")]
#[test]
fn bounded_command_output_drains_large_stdout_and_stderr() {
let mut command = std::process::Command::new(std::env::current_exe().unwrap());
command
.args([
"--exact",
"tests::bounded_command_output_large_helper",
"--nocapture",
])
.env("CAR_SECURITY_OUTPUT_HELPER", "1");
let output =
bounded_command_output(&mut command, std::time::Duration::from_secs(5), "test")
.unwrap();
assert!(output.output.status.success(), "{output:?}");
assert!(output.output.stdout.len() >= 128 * 1024);
assert!(output.output.stderr.len() >= 128 * 1024);
}
#[cfg(target_os = "macos")]
pub(super) struct FakeSecurityCli {
outputs: std::cell::RefCell<std::collections::VecDeque<std::io::Result<SecurityCliOutput>>>,
calls: std::cell::RefCell<Vec<Vec<String>>>,
}
#[cfg(target_os = "macos")]
impl FakeSecurityCli {
pub(super) fn new(outputs: Vec<std::io::Result<SecurityCliOutput>>) -> Self {
Self {
outputs: std::cell::RefCell::new(outputs.into()),
calls: std::cell::RefCell::new(Vec::new()),
}
}
pub(super) fn calls(&self) -> Vec<Vec<String>> {
self.calls.borrow().clone()
}
}
#[cfg(target_os = "macos")]
impl SecurityCli for FakeSecurityCli {
fn output(&self, args: &[&str]) -> std::io::Result<SecurityCliOutput> {
self.calls
.borrow_mut()
.push(args.iter().map(|arg| (*arg).to_string()).collect());
self.outputs
.borrow_mut()
.pop_front()
.expect("missing fake security output")
}
}
#[cfg(target_os = "macos")]
pub(super) fn security_output(
code: i32,
stdout: impl Into<Vec<u8>>,
stderr: impl Into<Vec<u8>>,
) -> std::io::Result<SecurityCliOutput> {
Ok(SecurityCliOutput {
success: code == 0,
code: Some(code),
stdout: stdout.into(),
stderr: stderr.into(),
prompted: false,
timed_out: false,
})
}
#[cfg(target_os = "macos")]
pub(super) fn security_output_prompted(
code: i32,
stdout: impl Into<Vec<u8>>,
stderr: impl Into<Vec<u8>>,
) -> std::io::Result<SecurityCliOutput> {
let mut out = security_output(code, stdout, stderr)?;
out.prompted = true;
Ok(out)
}
#[cfg(target_os = "macos")]
fn args(values: &[&str]) -> Vec<String> {
values.iter().map(|value| (*value).to_string()).collect()
}
#[cfg(target_os = "macos")]
#[test]
fn availability_probe_goes_through_the_security_helper() {
let cli = FakeSecurityCli::new(vec![
security_output(0, "", ""),
security_output(0, "", ""),
security_output(0, "", ""),
]);
let check = mac_availability_via_security_cli_with(&cli);
assert!(check.available);
assert_eq!(
cli.calls(),
vec![
args(&[
"find-generic-password",
"-s",
"car-internal",
"-a",
"__availability_probe__",
]),
args(&[
"add-generic-password",
"-U",
"-A",
"-s",
"car-internal",
"-a",
"__availability_probe__",
"-w",
"car-availability-probe",
]),
args(&[
"delete-generic-password",
"-s",
"car-internal",
"-a",
"__availability_probe__",
]),
]
);
}
#[cfg(target_os = "macos")]
#[test]
fn availability_probe_absent_cleanup_is_still_available() {
let cli = FakeSecurityCli::new(vec![
security_output(SECURITY_ERR_SEC_ITEM_NOT_FOUND, "", ""),
security_output(0, "", ""),
security_output(SECURITY_ERR_SEC_ITEM_NOT_FOUND, "", ""),
]);
let check = mac_availability_via_security_cli_with(&cli);
assert!(check.available);
assert!(check.reason.is_none(), "{:?}", check.reason);
}
#[cfg(target_os = "macos")]
#[test]
fn availability_probe_reports_unavailable_when_reads_succeed_but_writes_are_denied() {
struct ReadableButWriteDeniedCli;
impl SecurityCli for ReadableButWriteDeniedCli {
fn output(&self, args: &[&str]) -> std::io::Result<SecurityCliOutput> {
match args.first().copied() {
Some("find-generic-password") => security_output(0, "", ""),
Some("add-generic-password") => security_output(
152,
"",
"security: SecKeychainItemCreateFromContent: User interaction is not allowed.",
),
other => panic!("unexpected security command: {other:?}"),
}
}
}
let check = mac_availability_via_security_cli_with(&ReadableButWriteDeniedCli);
assert!(!check.available);
let reason = check
.reason
.expect("write-denied probe must carry a reason");
assert!(
reason.contains("User interaction is not allowed"),
"reason should carry the write error, got {reason:?}"
);
}
#[cfg(target_os = "macos")]
#[test]
fn availability_probe_backend_error_reports_unavailable() {
let cli = FakeSecurityCli::new(vec![security_output(
51,
"",
"security: SecKeychainSearchCopyNext: User interaction is not allowed.",
)]);
let check = mac_availability_via_security_cli_with(&cli);
assert!(!check.available);
let reason = check.reason.expect("unavailable must carry a reason");
assert!(
reason.contains("User interaction is not allowed"),
"reason should carry the helper's stderr, got {reason:?}"
);
}
#[cfg(target_os = "macos")]
#[test]
fn availability_probe_cleanup_error_reports_unavailable() {
let cli = FakeSecurityCli::new(vec![
security_output(0, "", ""),
security_output(0, "", ""),
security_output(
51,
"",
"security: SecKeychainItemDelete: User interaction is not allowed.",
),
]);
let check = mac_availability_via_security_cli_with(&cli);
assert!(!check.available);
let reason = check.reason.expect("cleanup failure must carry a reason");
assert!(
reason.contains("User interaction is not allowed"),
"reason should carry the cleanup error, got {reason:?}"
);
}
#[cfg(target_os = "macos")]
#[test]
fn availability_probe_names_itself_in_the_prompt_notice() {
let cli = FakeSecurityCli::new(vec![security_output(
51,
"",
"security: SecKeychainSearchCopyNext: User interaction is not allowed.",
)]);
let _ = mac_availability_via_security_cli_with(&cli);
let sent = cli.calls().remove(0);
let sent: Vec<&str> = sent.iter().map(String::as_str).collect();
let item = describe_item(&sent);
assert_eq!(item, "car-internal/__availability_probe__");
assert!(
keychain_prompt_notice(&item).contains(&item),
"notice must name the blocking item: {}",
keychain_prompt_notice(&item)
);
}
#[cfg(target_os = "macos")]
fn assert_access_denied_contains(err: SecretError, expected: &str) {
match err {
SecretError::AccessDenied { message } => assert!(
message.contains(expected),
"expected access-denied error to contain {expected:?}, got {message:?}"
),
other => panic!("expected AccessDenied, got {:?}", other),
}
}
#[cfg(target_os = "macos")]
#[test]
fn mac_security_errors_are_typed_for_recovery() {
assert!(matches!(
classify_security_error(-128, "user canceled"),
SecretError::UserCancelled { .. }
));
assert!(matches!(
classify_security_error(-25293, "authorization denied"),
SecretError::AccessDenied { .. }
));
assert!(matches!(
classify_helper_timeout("car/PARSLEE_AUTH_STATE_V2"),
SecretError::HelperTimedOut { .. }
));
let mut timed_out = security_output(9, b"", b"helper killed").unwrap();
timed_out.timed_out = true;
let cli = FakeSecurityCli::new(vec![Ok(timed_out)]);
let secret = SecretRef::new("svc", "key");
assert!(matches!(
mac_get_via_security_cli_with(&secret, &cli),
Err(SecretError::HelperTimedOut { .. })
));
}
#[cfg(target_os = "macos")]
struct IsolatedKeychainFixture {
_temp: tempfile::TempDir,
proof_root: std::path::PathBuf,
valid_path: std::path::PathBuf,
symlink_path: std::path::PathBuf,
outside_path: std::path::PathBuf,
public_path: std::path::PathBuf,
directory_path: std::path::PathBuf,
public_root: std::path::PathBuf,
}
#[cfg(target_os = "macos")]
impl IsolatedKeychainFixture {
fn new() -> Self {
use std::os::unix::fs::{symlink, PermissionsExt};
let temp = tempfile::tempdir().unwrap();
let proof_root = temp.path().join("proof");
std::fs::create_dir(&proof_root).unwrap();
std::fs::set_permissions(&proof_root, std::fs::Permissions::from_mode(0o700)).unwrap();
let valid_path = proof_root.join("valid.keychain-db");
std::fs::write(&valid_path, b"keychain fixture").unwrap();
std::fs::set_permissions(&valid_path, std::fs::Permissions::from_mode(0o600)).unwrap();
let symlink_path = proof_root.join("linked.keychain-db");
symlink(&valid_path, &symlink_path).unwrap();
let outside_path = temp.path().join("outside.keychain-db");
std::fs::write(&outside_path, b"outside fixture").unwrap();
std::fs::set_permissions(&outside_path, std::fs::Permissions::from_mode(0o600))
.unwrap();
let public_path = proof_root.join("public.keychain-db");
std::fs::write(&public_path, b"public fixture").unwrap();
std::fs::set_permissions(&public_path, std::fs::Permissions::from_mode(0o644)).unwrap();
let directory_path = proof_root.join("directory.keychain-db");
std::fs::create_dir(&directory_path).unwrap();
let public_root = temp.path().join("public-proof");
std::fs::create_dir(&public_root).unwrap();
std::fs::set_permissions(&public_root, std::fs::Permissions::from_mode(0o755)).unwrap();
Self {
_temp: temp,
proof_root,
valid_path,
symlink_path,
outside_path,
public_path,
directory_path,
public_root,
}
}
fn proof_root(&self) -> &std::path::Path {
&self.proof_root
}
fn valid_path(&self) -> &std::path::Path {
&self.valid_path
}
fn symlink_path(&self) -> &std::path::Path {
&self.symlink_path
}
fn outside_path(&self) -> &std::path::Path {
&self.outside_path
}
fn public_path(&self) -> &std::path::Path {
&self.public_path
}
fn directory_path(&self) -> &std::path::Path {
&self.directory_path
}
fn public_root(&self) -> &std::path::Path {
&self.public_root
}
}
#[cfg(target_os = "macos")]
#[test]
fn isolated_keychain_must_be_absolute_private_regular_owned_and_under_proof_root() {
let fixture = IsolatedKeychainFixture::new();
assert!(validate_keychain_path(fixture.valid_path(), fixture.proof_root()).is_ok());
assert!(validate_keychain_path(
std::path::Path::new("relative.keychain-db"),
fixture.proof_root()
)
.is_err());
assert!(validate_keychain_path(fixture.symlink_path(), fixture.proof_root()).is_err());
assert!(validate_keychain_path(fixture.outside_path(), fixture.proof_root()).is_err());
assert!(validate_keychain_path(fixture.public_path(), fixture.proof_root()).is_err());
assert!(validate_keychain_path(fixture.directory_path(), fixture.proof_root()).is_err());
assert!(validate_keychain_path(fixture.valid_path(), fixture.public_root()).is_err());
}
#[test]
fn secret_store_activity_counts_only_aggregate_public_operation_attempts() {
let _guard = lock_store();
let dir = tempfile::tempdir().unwrap();
std::env::set_var("CAR_SECRETS_FILE_DIR", dir.path());
let before = secret_store_activity();
let store = SecretStore::new();
let secret = SecretRef::new("activity-test", "credential");
assert!(store.availability().available);
store.put(&secret, "sensitive-value").unwrap();
let _ = store.get(&secret).unwrap();
let _ = store.status(&secret).unwrap();
store.publish(&secret, "replacement-value").unwrap();
store.delete(&secret).unwrap();
let after = secret_store_activity();
assert_eq!(after.get_attempts - before.get_attempts, 1);
assert_eq!(after.status_attempts - before.status_attempts, 1);
assert_eq!(
after.availability_attempts - before.availability_attempts,
1
);
assert_eq!(after.write_attempts - before.write_attempts, 2);
assert_eq!(after.delete_attempts - before.delete_attempts, 1);
let encoded = serde_json::to_string(&after).unwrap();
assert!(!encoded.contains("activity-test"));
assert!(!encoded.contains("credential"));
assert!(!encoded.contains("sensitive-value"));
assert!(!encoded.contains(dir.path().to_string_lossy().as_ref()));
std::env::remove_var("CAR_SECRETS_FILE_DIR");
}
#[test]
fn roundtrip_string() {
#[cfg(target_os = "macos")]
if std::env::var_os(NATIVE_KEYCHAIN_LANE).is_some() {
run_native_keychain_lane();
return;
}
let fixture = IsolatedStoreFixture::new();
let store = fixture.store();
let r = SecretRef::new("isolated-contract", "roundtrip");
store.put(&r, "hello world").unwrap();
assert_eq!(store.get(&r).unwrap(), "hello world");
assert!(store.status(&r).unwrap().exists);
store.delete(&r).unwrap();
assert!(!store.status(&r).unwrap().exists);
}
#[test]
fn roundtrip_string_with_trailing_newline() {
let fixture = IsolatedStoreFixture::new();
let store = fixture.store();
let r = SecretRef::new("isolated-contract", "roundtrip-newline");
let value = "abc\n";
store.put(&r, value).unwrap();
assert_eq!(store.get(&r).unwrap(), value);
store.delete(&r).unwrap();
}
#[test]
fn get_missing_returns_not_found() {
let fixture = IsolatedStoreFixture::new();
let store = fixture.store();
let r = SecretRef::new("isolated-contract", "never-written");
match store.get(&r) {
Err(SecretError::NotFound { .. }) => (),
other => panic!("expected NotFound, got {:?}", other),
}
}
#[test]
fn delete_missing_is_idempotent() {
let fixture = IsolatedStoreFixture::new();
let store = fixture.store();
let r = SecretRef::new("isolated-contract", "missing");
store.delete(&r).unwrap();
store.delete(&r).unwrap();
}
#[test]
fn json_roundtrip() {
let fixture = IsolatedStoreFixture::new();
#[derive(Serialize, Deserialize, PartialEq, Debug)]
struct Session {
cookies: Vec<String>,
expires_at: i64,
}
let store = fixture.store();
let r = SecretRef::new("isolated-contract", "session");
let s = Session {
cookies: vec!["a=1".into(), "b=2".into()],
expires_at: 1_700_000_000,
};
store.put_json(&r, &s).unwrap();
let back: Session = store.get_json(&r).unwrap();
assert_eq!(back, s);
store.delete(&r).unwrap();
}
#[test]
fn status_no_leak() {
let fixture = IsolatedStoreFixture::new();
let store = fixture.store();
let r = SecretRef::new("isolated-contract", "status");
store.put(&r, "secret-payload").unwrap();
let st = store.status(&r).unwrap();
let encoded = serde_json::to_string(&st).unwrap();
assert!(!encoded.contains("secret-payload"));
store.delete(&r).unwrap();
}
#[cfg(target_os = "macos")]
#[test]
fn mac_get_uses_security_cli_and_maps_success() {
let cli = FakeSecurityCli::new(vec![security_output(
0,
b"keychain: \"/Users/example/Library/Keychains/login.keychain-db\"\n",
b"password: \"secret\"\n",
)]);
let r = SecretRef::new("svc", "key");
assert_eq!(mac_get_via_security_cli_with(&r, &cli).unwrap(), "secret");
assert_eq!(
cli.calls(),
vec![args(&[
"find-generic-password",
"-s",
"svc",
"-a",
"key",
"-g"
])]
);
}
#[cfg(target_os = "macos")]
#[test]
fn prompted_read_preserves_the_item_and_persisted_grant() {
let cli = FakeSecurityCli::new(vec![security_output_prompted(
0,
b"keychain: isolated-test.keychain-db\n",
b"password: \"secret\"\n",
)]);
let r = SecretRef::new("car-test-0o9-prompt-persistence", "credential");
assert_eq!(mac_get_via_security_cli_with(&r, &cli).unwrap(), "secret");
assert_eq!(
cli.calls(),
vec![args(&[
"find-generic-password",
"-s",
"car-test-0o9-prompt-persistence",
"-a",
"credential",
"-g",
])],
"an approved read must never rewrite or recreate the item"
);
}
#[cfg(target_os = "macos")]
#[test]
fn mac_get_decodes_hex_password_output_with_trailing_newline() {
let cli = FakeSecurityCli::new(vec![security_output(
0,
b"keychain: \"/Users/example/Library/Keychains/login.keychain-db\"\n",
b"password: 0x6162630A \"abc\\012\"\n",
)]);
let r = SecretRef::new("svc", "key");
assert_eq!(mac_get_via_security_cli_with(&r, &cli).unwrap(), "abc\n");
assert_eq!(
cli.calls(),
vec![args(&[
"find-generic-password",
"-s",
"svc",
"-a",
"key",
"-g"
])]
);
}
#[cfg(target_os = "macos")]
#[test]
fn mac_get_maps_not_found_and_access_denied_without_fallback() {
let r = SecretRef::new("svc", "missing");
let cli = FakeSecurityCli::new(vec![security_output(
SECURITY_ERR_SEC_ITEM_NOT_FOUND,
b"",
b"The specified item could not be found in the keychain.\n",
)]);
match mac_get_via_security_cli_with(&r, &cli) {
Err(SecretError::NotFound { service, key }) => {
assert_eq!(service, "svc");
assert_eq!(key, "missing");
}
other => panic!("expected NotFound, got {:?}", other),
}
assert_eq!(cli.calls().len(), 1);
let cli = FakeSecurityCli::new(vec![security_output(
51,
b"",
b"User interaction is not allowed.\n",
)]);
let err = mac_get_via_security_cli_with(&r, &cli).unwrap_err();
assert_access_denied_contains(err, "User interaction is not allowed.");
assert_eq!(cli.calls().len(), 1);
}
#[cfg(target_os = "macos")]
#[test]
fn mac_status_uses_security_cli_and_maps_results() {
let r = SecretRef::new("svc", "key");
let cli = FakeSecurityCli::new(vec![security_output(0, b"", b"")]);
let status = mac_status_via_security_cli_with(&r, &cli).unwrap();
assert!(status.exists);
assert_eq!(
cli.calls(),
vec![args(&["find-generic-password", "-s", "svc", "-a", "key"])]
);
let cli = FakeSecurityCli::new(vec![security_output(
SECURITY_ERR_SEC_ITEM_NOT_FOUND,
b"",
b"The specified item could not be found in the keychain.\n",
)]);
assert!(!mac_status_via_security_cli_with(&r, &cli).unwrap().exists);
let cli = FakeSecurityCli::new(vec![security_output(128, b"", b"auth denied\n")]);
let err = mac_status_via_security_cli_with(&r, &cli).unwrap_err();
assert_access_denied_contains(err, "auth denied");
}
#[cfg(target_os = "macos")]
#[test]
fn mac_put_surfaces_add_failure_as_access_denied() {
let cli = FakeSecurityCli::new(vec![security_output(
51,
b"",
b"User interaction is not allowed.\n",
)]);
let err =
mac_write_via_security_cli("car-test-0o9-write", "key", "secret", &cli).unwrap_err();
assert_access_denied_contains(err, "User interaction is not allowed.");
assert_eq!(
cli.calls().len(),
1,
"a failed write must not trigger a delete"
);
}
#[cfg(target_os = "macos")]
#[test]
fn mac_put_does_not_pre_delete_and_therefore_cannot_prompt() {
let cli = FakeSecurityCli::new(vec![security_output(0, b"", b"")]);
mac_put_via_security_cli_with("car-test-0o9-update-persistence", "key", "secret", &cli)
.unwrap();
assert_eq!(
cli.calls(),
vec![args(&[
"add-generic-password",
"-U",
"-A",
"-s",
"car-test-0o9-update-persistence",
"-a",
"key",
"-w",
"secret",
])],
"an ordinary write must issue exactly one call, and not a delete"
);
}
#[cfg(target_os = "macos")]
#[test]
fn mac_publish_updates_in_place_without_a_pre_delete_gap() {
let cli = FakeSecurityCli::new(vec![security_output(0, b"", b"")]);
mac_publish_via_security_cli_with("svc", "key", "secret", &cli).unwrap();
assert_eq!(
cli.calls(),
vec![args(&[
"add-generic-password",
"-U",
"-A",
"-s",
"svc",
"-a",
"key",
"-w",
"secret",
])]
);
}
#[cfg(target_os = "macos")]
#[test]
fn mac_security_child_is_killed_and_reaped_at_its_deadline() {
let mut command = std::process::Command::new("/bin/sh");
command.args(["-c", "sleep 5"]);
let started = std::time::Instant::now();
let output = bounded_command_output_with(
&mut command,
std::time::Duration::from_millis(40),
|| false,
|| {},
)
.unwrap();
assert!(!output.output.status.success());
assert!(
started.elapsed() < std::time::Duration::from_secs(1),
"bounded helper must not wait for the child command's natural exit"
);
let stderr = String::from_utf8_lossy(&output.output.stderr);
assert!(stderr.contains("CAR killed the keychain helper"));
assert!(
stderr.contains("keychain prompt"),
"the timeout must name a pending keychain prompt as the likely cause"
);
assert!(
stderr.contains("Always Allow"),
"the timeout must tell the user what action clears it"
);
}
#[cfg(target_os = "macos")]
#[test]
fn a_hung_helper_with_no_dialog_still_dies_at_the_short_deadline() {
assert!(
SECURITY_CLI_INTERACTIVE_TIMEOUT > SECURITY_CLI_TIMEOUT,
"the interactive allowance must be longer than the hang deadline"
);
assert!(
SECURITY_CLI_INTERACTIVE_TIMEOUT >= std::time::Duration::from_secs(60),
"a human needs to find a window, type a password and submit — 15s \
is why entering the correct password repeatedly never worked"
);
let mut command = std::process::Command::new("/bin/sh");
command.args(["-c", "sleep 5"]);
let started = std::time::Instant::now();
let output = bounded_command_output_with(
&mut command,
std::time::Duration::from_millis(40),
|| false,
|| {},
)
.unwrap();
assert!(!output.output.status.success());
assert!(
started.elapsed() < std::time::Duration::from_secs(1),
"a helper with no dialog must not inherit the interactive allowance"
);
}
#[cfg(target_os = "macos")]
#[test]
fn a_dialog_is_not_attributed_to_a_read_that_did_not_wait_for_it() {
let instant = std::time::Duration::from_millis(0);
let quick = std::time::Duration::from_millis(20);
assert!(
!dialog_is_evidence_for_this_read(true, instant),
"a dialog already on screen at spawn belongs to whatever opened it"
);
assert!(
!dialog_is_evidence_for_this_read(true, quick),
"a read that returned in 20ms was never blocked on a human"
);
assert!(
!dialog_is_evidence_for_this_read(false, std::time::Duration::from_secs(60)),
"no dialog is no evidence, however long the helper took"
);
assert!(
dialog_is_evidence_for_this_read(true, PROMPT_EVIDENCE_MIN),
"a call still blocked with a dialog up is the one being authorized"
);
}
#[cfg(target_os = "macos")]
#[test]
fn the_prompt_evidence_threshold_sits_between_a_silent_read_and_a_human() {
assert!(
PROMPT_EVIDENCE_MIN >= std::time::Duration::from_millis(200),
"must be an order of magnitude above a silent `security -g` read, \
which returns in tens of milliseconds"
);
assert!(
PROMPT_EVIDENCE_MIN <= std::time::Duration::from_secs(2),
"must stay below the fastest a human can answer a dialog, or the \
blocking read finishes before CAR can explain what is waiting"
);
assert!(
PROMPT_EVIDENCE_MIN < SECURITY_CLI_TIMEOUT,
"a prompted read must be attributable before any deadline can end it"
);
}
#[cfg(target_os = "macos")]
#[test]
fn a_fast_helper_is_not_attributed_a_dialog_that_is_on_screen_throughout() {
let mut command = std::process::Command::new("/bin/echo");
command.arg("hi");
let notices = std::sync::atomic::AtomicUsize::new(0);
let run = bounded_command_output_with(
&mut command,
SECURITY_CLI_TIMEOUT,
|| true,
|| {
notices.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
},
)
.unwrap();
assert!(run.output.status.success());
assert!(
!run.prompted,
"a helper that exited in milliseconds was not the one being authorized, \
however many dialogs the machine is showing"
);
assert_eq!(
notices.load(std::sync::atomic::Ordering::Relaxed),
0,
"and it must not tell the user to go answer a dialog it never waited on \
(Parslee-ai/car#878 rides on the same attribution rule as #897)"
);
}
#[cfg(target_os = "macos")]
#[test]
fn a_helper_still_blocked_past_the_threshold_is_attributed_the_dialog() {
let mut command = std::process::Command::new("/bin/sh");
command.args(["-c", "sleep 1"]);
let notices = std::sync::atomic::AtomicUsize::new(0);
let run = bounded_command_output_with(
&mut command,
SECURITY_CLI_TIMEOUT,
|| true,
|| {
notices.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
},
)
.unwrap();
assert!(run.output.status.success(), "the child must exit naturally");
assert!(
run.prompted,
"a call still running past PROMPT_EVIDENCE_MIN with a dialog up is \
the call that dialog belongs to"
);
assert_eq!(
notices.load(std::sync::atomic::Ordering::Relaxed),
1,
"a blocked read must explain itself exactly once, promptly"
);
}
#[cfg(target_os = "macos")]
#[test]
fn the_prompt_notice_names_the_wait_and_both_remedies() {
let notice = keychain_prompt_notice("car/parslee_access_token");
assert!(
notice.contains("car/parslee_access_token"),
"must name WHICH item is being asked for — the operator who walked away \
and came back to a stack of prompts cannot read the dialog after the \
fact, and the log is the only record (Parslee-ai/car#897): {notice}"
);
assert!(
notice.contains(&SECURITY_CLI_INTERACTIVE_TIMEOUT.as_secs().to_string()),
"must state how long CAR will wait, or it reads as an indefinite hang: {notice}"
);
assert!(
notice.contains("Always Allow"),
"must name the one click that also prevents the NEXT prompt: {notice}"
);
assert!(
notice.contains("Keychain Access"),
"must name the remedy for someone who already dismissed the dialog: {notice}"
);
assert!(
notice.contains("not hung"),
"the reported failure was reading the silence as a hang and killing it: {notice}"
);
}
#[cfg(target_os = "macos")]
#[test]
fn describe_item_names_the_keychain_item_from_the_argv() {
assert_eq!(
describe_item(&["find-generic-password", "-s", "car", "-a", "token", "-w"]),
"car/token"
);
assert_eq!(
describe_item(&["delete-generic-password", "-s", "car"]),
"car"
);
assert_eq!(
describe_item(&["find-generic-password", "-a", "token"]),
"token"
);
assert_eq!(describe_item(&["unlock-keychain"]), "unlock-keychain");
assert_eq!(describe_item(&[]), "security");
assert_eq!(
describe_item(&["find-generic-password", "-s"]),
"find-generic-password"
);
}
#[cfg(target_os = "macos")]
#[test]
fn mac_delete_uses_security_cli_and_maps_results() {
let r = SecretRef::new("svc", "key");
let cli = FakeSecurityCli::new(vec![security_output(0, b"", b"")]);
mac_delete_via_security_cli_with(&r, &cli).unwrap();
assert_eq!(
cli.calls(),
vec![args(&["delete-generic-password", "-s", "svc", "-a", "key"])]
);
let cli = FakeSecurityCli::new(vec![security_output(
SECURITY_ERR_SEC_ITEM_NOT_FOUND,
b"",
b"The specified item could not be found in the keychain.\n",
)]);
mac_delete_via_security_cli_with(&r, &cli).unwrap();
let cli = FakeSecurityCli::new(vec![security_output(128, b"", b"auth denied\n")]);
let err = mac_delete_via_security_cli_with(&r, &cli).unwrap_err();
assert_access_denied_contains(err, "auth denied");
}
}