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};
const FILENAME: &str = "admin.token";
pub fn admin_token_path() -> PathBuf {
config::provider_dir().join(FILENAME)
}
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)
}
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 {
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");
}
}