use std::collections::VecDeque;
use std::path::PathBuf;
use std::sync::Mutex;
const MAX: usize = 16;
#[derive(Default)]
pub struct Capabilities(Mutex<VecDeque<String>>, Option<PathBuf>);
impl Capabilities {
pub fn load(path: PathBuf) -> Self {
let live: VecDeque<String> = std::fs::read_to_string(&path)
.unwrap_or_default()
.lines()
.map(str::trim)
.filter(|l| l.len() == 64 && l.chars().all(|c| c.is_ascii_hexdigit()))
.map(str::to_string)
.collect();
let skip = live.len().saturating_sub(MAX);
Self(
Mutex::new(live.into_iter().skip(skip).collect()),
Some(path),
)
}
pub fn mint(&self) -> anyhow::Result<String> {
let cap = random_hex()?;
let mut live = self.0.lock().unwrap_or_else(|e| e.into_inner());
if live.len() >= MAX {
live.pop_front();
}
live.push_back(cap.clone());
if let Some(path) = &self.1 {
save(path, &live)?;
}
Ok(cap)
}
pub fn verify(&self, given: &str) -> bool {
let live = self.0.lock().unwrap_or_else(|e| e.into_inner());
live.iter()
.fold(false, |found, cap| constant_eq(given, cap) | found)
}
#[cfg(test)]
fn len(&self) -> usize {
self.0.lock().unwrap().len()
}
}
fn save(path: &std::path::Path, live: &VecDeque<String>) -> anyhow::Result<()> {
use std::io::Write;
if let Some(dir) = path.parent() {
std::fs::create_dir_all(dir)?;
}
let tmp = path.with_extension("tmp");
let mut o = std::fs::OpenOptions::new();
o.write(true).create(true).truncate(true);
#[cfg(unix)]
std::os::unix::fs::OpenOptionsExt::mode(&mut o, 0o600);
let mut f = o.open(&tmp)?;
for cap in live {
writeln!(f, "{cap}")?;
}
f.sync_all()?;
std::fs::rename(&tmp, path)?;
Ok(())
}
fn random_hex() -> anyhow::Result<String> {
let mut buf = [0u8; 32];
getrandom::fill(&mut buf)
.map_err(|e| anyhow::anyhow!("reading random bytes for the capability: {e}"))?;
Ok(buf.iter().map(|b| format!("{b:02x}")).collect())
}
fn constant_eq(a: &str, b: &str) -> bool {
a.len() == b.len()
&& a.bytes()
.zip(b.bytes())
.fold(0u8, |acc, (x, y)| acc | (x ^ y))
== 0
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_minted_capability_verifies_and_a_made_up_one_does_not() {
let caps = Capabilities::default();
let cap = caps.mint().unwrap();
assert!(caps.verify(&cap));
assert!(!caps.verify(&"0".repeat(64)));
assert!(!caps.verify(""));
}
#[test]
fn a_capability_is_thirty_two_bytes_of_hex_and_never_repeats() {
let caps = Capabilities::default();
let a = caps.mint().unwrap();
let b = caps.mint().unwrap();
assert_eq!(a.len(), 64);
assert!(a.chars().all(|c| c.is_ascii_hexdigit()));
assert_ne!(a, b);
}
#[test]
fn a_capability_outlives_the_daemon_that_minted_it() {
let dir = std::env::temp_dir().join(format!("snyvi-caps-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
let path = dir.join("capabilities");
let cap = Capabilities::load(path.clone()).mint().unwrap();
let next = Capabilities::load(path.clone());
assert!(next.verify(&cap));
assert!(!next.verify(&"0".repeat(64)));
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mode = std::fs::metadata(&path).unwrap().permissions().mode();
assert_eq!(mode & 0o777, 0o600, "only its owner reads it");
}
std::fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn several_windows_are_live_at_once_and_the_oldest_falls_out_past_the_cap() {
let caps = Capabilities::default();
let first = caps.mint().unwrap();
let rest: Vec<_> = (0..MAX).map(|_| caps.mint().unwrap()).collect();
assert_eq!(caps.len(), MAX);
assert!(!caps.verify(&first), "the oldest was evicted");
assert!(rest.iter().all(|c| caps.verify(c)), "the rest still hold");
}
}