use std::io;
use std::path::PathBuf;
const DIR_NAME: &str = "ai.parslee.car";
const FILE_NAME: &str = "auth-token";
pub fn default_path() -> io::Result<PathBuf> {
let dir = default_dir()?;
Ok(dir.join(FILE_NAME))
}
fn default_dir() -> io::Result<PathBuf> {
#[cfg(target_os = "macos")]
{
if let Some(home) = std::env::var_os("HOME") {
return Ok(PathBuf::from(home)
.join("Library")
.join("Application Support")
.join(DIR_NAME));
}
}
#[cfg(target_os = "linux")]
{
if let Some(rt) = std::env::var_os("XDG_RUNTIME_DIR") {
return Ok(PathBuf::from(rt).join(DIR_NAME));
}
if let Some(home) = std::env::var_os("HOME") {
return Ok(PathBuf::from(home).join(".config").join(DIR_NAME));
}
}
#[cfg(target_os = "windows")]
{
if let Some(local) = std::env::var_os("LOCALAPPDATA") {
return Ok(PathBuf::from(local).join(DIR_NAME));
}
}
if let Some(home) = std::env::var_os("HOME") {
return Ok(PathBuf::from(home).join(".car"));
}
Err(io::Error::new(
io::ErrorKind::NotFound,
"no HOME / XDG_RUNTIME_DIR / LOCALAPPDATA — can't resolve auth-token directory",
))
}
pub fn read() -> io::Result<Option<String>> {
read_at(&default_path()?)
}
pub fn read_at(path: &std::path::Path) -> io::Result<Option<String>> {
match std::fs::read_to_string(path) {
Ok(s) => Ok(Some(s.trim().to_string())),
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(e),
}
}
pub fn write(token: &str) -> io::Result<PathBuf> {
let path = default_path()?;
write_at(&path, token)?;
Ok(path)
}
pub fn write_at(path: &std::path::Path, token: &str) -> io::Result<()> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let _ = std::fs::set_permissions(parent, std::fs::Permissions::from_mode(0o700));
}
}
let tmp = path.with_extension("tmp");
std::fs::write(&tmp, token)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&tmp, std::fs::Permissions::from_mode(0o600))?;
}
std::fs::rename(&tmp, path)?;
#[cfg(target_os = "windows")]
{
harden_windows_acl(path);
}
Ok(())
}
#[cfg(target_os = "windows")]
fn harden_windows_acl(path: &std::path::Path) {
use std::process::Command;
let path_str = match path.to_str() {
Some(s) => s,
None => {
tracing::warn!(
?path,
"auth-token ACL hardening skipped: path is not valid UTF-8"
);
return;
}
};
let _ = run_icacls(&[path_str, "/grant:r", "*S-1-5-18:F"]);
if let Some(username) = std::env::var_os("USERNAME") {
if let Some(u) = username.to_str() {
let grant = format!("{u}:F");
let _ = run_icacls(&[path_str, "/grant:r", &grant]);
}
}
if let Err(e) = run_icacls(&[path_str, "/inheritance:r"]) {
tracing::warn!(?e, ?path, "auth-token ACL hardening: /inheritance:r failed");
}
fn run_icacls(args: &[&str]) -> io::Result<()> {
let status = Command::new("icacls").args(args).status()?;
if !status.success() {
return Err(io::Error::new(
io::ErrorKind::Other,
format!("icacls {args:?} exited with status {status}"),
));
}
Ok(())
}
}
pub fn remove() -> io::Result<()> {
let path = default_path()?;
match std::fs::remove_file(&path) {
Ok(()) => Ok(()),
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(()),
Err(e) => Err(e),
}
}
pub fn generate() -> String {
use base64::Engine as _;
let a = uuid::Uuid::new_v4();
let b = uuid::Uuid::new_v4();
let mut bytes = [0u8; 32];
bytes[..16].copy_from_slice(a.as_bytes());
bytes[16..].copy_from_slice(b.as_bytes());
base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(bytes)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn generated_token_is_43_chars_base64url() {
let t = generate();
assert_eq!(t.len(), 43);
for c in t.chars() {
assert!(
c.is_ascii_alphanumeric() || c == '-' || c == '_',
"non-base64url char: {c:?}"
);
}
}
#[test]
fn read_at_missing_returns_none() {
let tmp = tempfile::TempDir::new().unwrap();
let path = tmp.path().join("nonexistent.token");
let result = read_at(&path).unwrap();
assert_eq!(result, None);
}
#[test]
fn write_at_then_read_at_round_trips() {
let tmp = tempfile::TempDir::new().unwrap();
let path = tmp.path().join("ai.parslee.car").join("auth-token");
let token = generate();
write_at(&path, &token).unwrap();
assert_eq!(read_at(&path).unwrap().as_deref(), Some(token.as_str()));
assert!(path.exists());
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mode = std::fs::metadata(&path).unwrap().permissions().mode();
assert_eq!(mode & 0o777, 0o600, "token must be 0600");
}
}
#[test]
fn write_at_overwrites_atomically() {
let tmp = tempfile::TempDir::new().unwrap();
let path = tmp.path().join("auth-token");
write_at(&path, "first").unwrap();
write_at(&path, "second").unwrap();
assert_eq!(read_at(&path).unwrap().as_deref(), Some("second"));
}
}