openlatch-provider 0.0.0

Self-service onboarding CLI + runtime daemon for OpenLatch Editors and Providers
//! Local-only admin token used to authenticate `POST /v1/admin/reload`.
//!
//! Generated on first daemon startup at `~/.openlatch/provider/admin.token`
//! with mode 0600 (Unix). Bearer-auth comparison is constant-time. Anyone
//! with read access to that file can hit the admin endpoint — which is why
//! the endpoint binds to `127.0.0.1` only and requires the user to have
//! already authenticated as the local user.

use std::path::PathBuf;

use base64::engine::general_purpose::STANDARD_NO_PAD;
use base64::Engine;
use rand_compat::random_32_bytes;
use subtle::ConstantTimeEq;

use crate::config;
use crate::error::{OlError, OL_4272_XDG_DIR_UNWRITABLE};

/// Filename inside the provider dir.
const FILENAME: &str = "admin.token";

pub fn admin_token_path() -> PathBuf {
    config::provider_dir().join(FILENAME)
}

/// Read the existing admin token from disk, or mint + persist a fresh one.
/// Returns the token string (base64url, 32 random bytes).
pub fn load_or_create() -> Result<String, OlError> {
    let path = admin_token_path();
    if let Ok(raw) = std::fs::read_to_string(&path) {
        let trimmed = raw.trim();
        if !trimmed.is_empty() {
            return Ok(trimmed.to_string());
        }
    }
    let token = mint();
    if let Some(parent) = path.parent() {
        std::fs::create_dir_all(parent).map_err(|e| {
            OlError::new(
                OL_4272_XDG_DIR_UNWRITABLE,
                format!("cannot create '{}': {e}", parent.display()),
            )
        })?;
    }
    let tmp = path.with_extension("token.tmp");
    std::fs::write(&tmp, token.as_bytes()).map_err(|e| {
        OlError::new(
            OL_4272_XDG_DIR_UNWRITABLE,
            format!("cannot write '{}': {e}", tmp.display()),
        )
    })?;
    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        let _ = std::fs::set_permissions(&tmp, std::fs::Permissions::from_mode(0o600));
    }
    std::fs::rename(&tmp, &path).map_err(|e| {
        OlError::new(
            OL_4272_XDG_DIR_UNWRITABLE,
            format!("cannot rename '{}' into place: {e}", tmp.display()),
        )
    })?;
    Ok(token)
}

fn mint() -> String {
    let bytes = random_32_bytes();
    STANDARD_NO_PAD.encode(bytes)
}

/// Constant-time check of a bearer header against the stored token. Returns
/// `true` only on a length-match + byte-for-byte match.
pub fn matches(stored: &str, presented: &str) -> bool {
    let a = stored.as_bytes();
    let b = presented.as_bytes();
    a.len() == b.len() && bool::from(a.ct_eq(b))
}

mod rand_compat {
    /// 32 bytes of cryptographically-random entropy without pulling the `rand`
    /// crate. We tap the seeds the OS kernel exposes through stdlib's
    /// `std::time::SystemTime` mixed with `getrandom`-style availability via
    /// `uuid::Uuid::new_v4`.
    ///
    /// `uuid` is already a dependency. v4 UUIDs draw from `getrandom`
    /// internally; concatenating two of them yields 32 random bytes.
    pub fn random_32_bytes() -> [u8; 32] {
        let a = uuid::Uuid::new_v4();
        let b = uuid::Uuid::new_v4();
        let mut out = [0u8; 32];
        out[..16].copy_from_slice(a.as_bytes());
        out[16..].copy_from_slice(b.as_bytes());
        out
    }
}

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

    #[test]
    fn matches_returns_true_for_equal_strings() {
        assert!(matches("abc", "abc"));
    }

    #[test]
    fn matches_returns_false_for_different_lengths() {
        assert!(!matches("abc", "abcd"));
    }

    #[test]
    fn matches_returns_false_for_different_bytes() {
        assert!(!matches("abc", "xyz"));
    }

    #[test]
    fn load_or_create_round_trips_via_provider_dir_override() {
        let tmp = TempDir::new().unwrap();
        std::env::set_var("OPENLATCH_PROVIDER_CONFIG_DIR", tmp.path());
        let first = load_or_create().expect("mint succeeds");
        let second = load_or_create().expect("re-load succeeds");
        assert_eq!(first, second, "second call returns the same token");
        std::env::remove_var("OPENLATCH_PROVIDER_CONFIG_DIR");
    }
}