car-auth 0.51.0

Shared Parslee OAuth2 PKCE + token/keychain logic for the CAR CLI and daemon
Documentation
use serde::{Deserialize, Serialize};
use std::io;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};

const AUTHORITY_HINT_FILE: &str = "parslee-auth-authority.json";
static FORCE_UNKNOWN: AtomicBool = AtomicBool::new(false);

/// Non-secret presentation state for passive auth surfaces.
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum CredentialAuthorityState {
    Unknown,
    SignedOut,
    Configured,
}

/// Non-secret routing/presentation metadata. `Configured` is never proof of
/// authenticated identity; authoritative operations still read the OS store.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct CredentialAuthorityHint {
    pub state: CredentialAuthorityState,
    pub generation: u64,
    pub updated_at_unix_ms: u64,
}

impl CredentialAuthorityHint {
    pub fn unknown() -> Self {
        Self {
            state: CredentialAuthorityState::Unknown,
            generation: 0,
            updated_at_unix_ms: 0,
        }
    }

    pub fn signed_out(generation: u64, updated_at_unix_ms: u64) -> Self {
        Self {
            state: CredentialAuthorityState::SignedOut,
            generation,
            updated_at_unix_ms,
        }
    }

    pub fn configured(generation: u64, updated_at_unix_ms: u64) -> Self {
        Self {
            state: CredentialAuthorityState::Configured,
            generation,
            updated_at_unix_ms,
        }
    }
}

pub(crate) struct AuthorityHintStore {
    root: PathBuf,
}

impl AuthorityHintStore {
    pub(crate) fn new(root: PathBuf) -> Self {
        Self { root }
    }

    pub(crate) fn path(&self) -> PathBuf {
        self.root.join(AUTHORITY_HINT_FILE)
    }

    pub(crate) fn load(&self) -> CredentialAuthorityHint {
        let path = self.path();
        let metadata = match std::fs::symlink_metadata(&path) {
            Ok(metadata) if metadata.is_file() && !metadata.file_type().is_symlink() => metadata,
            _ => return CredentialAuthorityHint::unknown(),
        };
        if metadata.len() > 16 * 1024 {
            return CredentialAuthorityHint::unknown();
        }
        std::fs::read(&path)
            .ok()
            .and_then(|raw| serde_json::from_slice(&raw).ok())
            .unwrap_or_else(CredentialAuthorityHint::unknown)
    }

    pub(crate) fn publish(&self, hint: CredentialAuthorityHint) -> io::Result<()> {
        use std::io::Write;

        std::fs::create_dir_all(&self.root)?;
        let temp_path = self.root.join(format!(
            ".{AUTHORITY_HINT_FILE}.{}.tmp",
            uuid::Uuid::new_v4().simple()
        ));
        let result = (|| {
            let mut file = open_private_temp(&temp_path)?;
            let body = serde_json::to_vec(&hint)
                .map_err(|error| io::Error::other(format!("serialize authority hint: {error}")))?;
            file.write_all(&body)?;
            file.sync_all()?;
            atomic_replace(&temp_path, &self.path())?;
            sync_directory(&self.root)?;
            Ok(())
        })();
        if result.is_err() {
            let _ = std::fs::remove_file(&temp_path);
        }
        result
    }

    pub(crate) fn discard(&self) -> io::Result<()> {
        match std::fs::remove_file(self.path()) {
            Ok(()) => sync_directory(&self.root),
            Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(()),
            Err(error) => Err(error),
        }
    }
}

#[cfg(unix)]
fn open_private_temp(path: &Path) -> io::Result<std::fs::File> {
    use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};

    let file = std::fs::OpenOptions::new()
        .write(true)
        .create_new(true)
        .mode(0o600)
        .open(path)?;
    // `OpenOptionsExt::mode` is filtered through the process umask. Reset the
    // final mode explicitly so even an unusually restrictive inherited umask
    // cannot violate the persisted 0600 contract.
    file.set_permissions(std::fs::Permissions::from_mode(0o600))?;
    Ok(file)
}

#[cfg(not(unix))]
fn open_private_temp(path: &Path) -> io::Result<std::fs::File> {
    std::fs::OpenOptions::new()
        .write(true)
        .create_new(true)
        .open(path)
}

#[cfg(not(windows))]
fn atomic_replace(source: &Path, destination: &Path) -> io::Result<()> {
    std::fs::rename(source, destination)
}

#[cfg(windows)]
fn atomic_replace(source: &Path, destination: &Path) -> io::Result<()> {
    use std::os::windows::ffi::OsStrExt;

    const MOVEFILE_REPLACE_EXISTING: u32 = 0x1;
    const MOVEFILE_WRITE_THROUGH: u32 = 0x8;
    #[link(name = "kernel32")]
    unsafe extern "system" {
        fn MoveFileExW(existing: *const u16, new: *const u16, flags: u32) -> i32;
    }
    let source: Vec<u16> = source.as_os_str().encode_wide().chain(Some(0)).collect();
    let destination: Vec<u16> = destination
        .as_os_str()
        .encode_wide()
        .chain(Some(0))
        .collect();
    // SAFETY: both pointers address NUL-terminated UTF-16 buffers for the
    // duration of the call, and the flags request atomic replacement.
    let replaced = unsafe {
        MoveFileExW(
            source.as_ptr(),
            destination.as_ptr(),
            MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH,
        )
    };
    if replaced == 0 {
        Err(io::Error::last_os_error())
    } else {
        Ok(())
    }
}

#[cfg(unix)]
fn sync_directory(path: &Path) -> io::Result<()> {
    std::fs::File::open(path)?.sync_all()
}

#[cfg(not(unix))]
fn sync_directory(_path: &Path) -> io::Result<()> {
    Ok(())
}

/// Read the passive, non-authoritative auth hint from `CAR_HOME`.
pub fn credential_authority_hint() -> CredentialAuthorityHint {
    if FORCE_UNKNOWN.load(Ordering::Acquire) {
        return CredentialAuthorityHint::unknown();
    }
    let Some(root) = car_home::root() else {
        return CredentialAuthorityHint::unknown();
    };
    AuthorityHintStore::new(root).load()
}

pub(crate) fn publish_for_state(state: &crate::state::AuthStateV2) -> io::Result<()> {
    let root = car_home::root().ok_or_else(|| {
        FORCE_UNKNOWN.store(true, Ordering::Release);
        io::Error::new(io::ErrorKind::NotFound, "CAR state root is unavailable")
    })?;
    let now = crate::epoch_millis();
    let hint = if state.active.is_some() {
        CredentialAuthorityHint::configured(state.generation, now)
    } else {
        CredentialAuthorityHint::signed_out(state.generation, now)
    };
    match AuthorityHintStore::new(root).publish(hint) {
        Ok(()) => {
            FORCE_UNKNOWN.store(false, Ordering::Release);
            Ok(())
        }
        Err(error) => {
            FORCE_UNKNOWN.store(true, Ordering::Release);
            Err(error)
        }
    }
}

pub(crate) fn degrade_to_unknown() -> io::Result<()> {
    let root = car_home::root().ok_or_else(|| {
        FORCE_UNKNOWN.store(true, Ordering::Release);
        io::Error::new(io::ErrorKind::NotFound, "CAR state root is unavailable")
    })?;
    let store = AuthorityHintStore::new(root);
    if let Err(error) = store.publish(CredentialAuthorityHint::unknown()) {
        FORCE_UNKNOWN.store(true, Ordering::Release);
        if store.discard().is_err() {
            FORCE_UNKNOWN.store(true, Ordering::Release);
        }
        return Err(error);
    }
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[cfg(unix)]
    const FAILURE_CHILD_ENV: &str = "CAR_AUTHORITY_HINT_FAILURE_CHILD";

    #[cfg(unix)]
    fn run_failed_degradation_scenario() {
        use std::os::unix::fs::PermissionsExt;

        let dir = tempfile::tempdir().unwrap();
        std::env::set_var(car_home::ENV_VAR, dir.path());
        let store = AuthorityHintStore::new(dir.path().to_path_buf());
        store
            .publish(CredentialAuthorityHint::configured(7, 1234))
            .unwrap();

        std::fs::set_permissions(dir.path(), std::fs::Permissions::from_mode(0o500)).unwrap();
        let mut signed_out = crate::state::AuthStateV2::signed_out();
        signed_out.generation = 8;
        assert!(publish_for_state(&signed_out).is_err());
        assert!(degrade_to_unknown().is_err());
        let during_failure = credential_authority_hint();

        std::fs::set_permissions(dir.path(), std::fs::Permissions::from_mode(0o700)).unwrap();
        signed_out.generation = 9;
        publish_for_state(&signed_out).unwrap();
        let after_recovery = credential_authority_hint();
        std::env::remove_var(car_home::ENV_VAR);

        assert_eq!(during_failure, CredentialAuthorityHint::unknown());
        assert_eq!(after_recovery.state, CredentialAuthorityState::SignedOut);
        assert_eq!(after_recovery.generation, 9);
    }

    #[cfg(unix)]
    #[test]
    fn failed_hint_degradation_forces_public_reads_unknown_until_success() {
        if std::env::var_os(FAILURE_CHILD_ENV).is_some() {
            run_failed_degradation_scenario();
            return;
        }

        let output = std::process::Command::new(std::env::current_exe().unwrap())
            .args([
                "--exact",
                "authority_hint::tests::failed_hint_degradation_forces_public_reads_unknown_until_success",
                "--nocapture",
            ])
            .env(FAILURE_CHILD_ENV, "1")
            .output()
            .unwrap();
        assert!(
            output.status.success(),
            "isolated authority-hint regression failed:\nstdout:\n{}\nstderr:\n{}",
            String::from_utf8_lossy(&output.stdout),
            String::from_utf8_lossy(&output.stderr)
        );
    }

    #[test]
    fn malformed_or_missing_hint_is_unknown_and_contains_no_identity() {
        let dir = tempfile::tempdir().unwrap();
        let store = AuthorityHintStore::new(dir.path().to_path_buf());
        assert_eq!(store.load(), CredentialAuthorityHint::unknown());

        std::fs::write(
            store.path(),
            br#"{"state":"configured","account_id":"forbidden"}"#,
        )
        .unwrap();
        assert_eq!(store.load(), CredentialAuthorityHint::unknown());
    }

    #[test]
    fn published_hint_is_atomic_private_and_token_free() {
        let dir = tempfile::tempdir().unwrap();
        let store = AuthorityHintStore::new(dir.path().to_path_buf());
        store
            .publish(CredentialAuthorityHint::configured(7, 1234))
            .unwrap();

        let body = std::fs::read_to_string(store.path()).unwrap();
        assert!(!body.contains("token"));
        assert!(!body.contains("account"));
        assert_eq!(store.load().generation, 7);

        store
            .publish(CredentialAuthorityHint::signed_out(8, 2345))
            .unwrap();
        assert_eq!(store.load().generation, 8);
        assert_eq!(store.load().state, CredentialAuthorityState::SignedOut);
        #[cfg(unix)]
        {
            use std::os::unix::fs::PermissionsExt;
            assert_eq!(
                std::fs::metadata(store.path())
                    .unwrap()
                    .permissions()
                    .mode()
                    & 0o777,
                0o600
            );
        }
        assert!(
            std::fs::read_dir(dir.path())
                .unwrap()
                .all(|entry| entry.unwrap().path() == store.path()),
            "atomic publication must not leave staging files behind"
        );
    }
}