//! The owner's browser sign-in, shared by Radar and the API (spec ยง8.3): the owner's CLI signs a request for a //! one-time link (`POST /radar/link`, valid 60 s); opening it starts a //! browser session whose events carry that signed request as their proof //! (level 2). The passkey (level 3) is 0.2. Sessions live in memory: a //! restart signs the browser out. use rand::RngCore; use std::collections::HashMap; use std::sync::Mutex; /// How long a sign-in link is good for, and a session. pub const LINK_MS: u64 = 60_000; pub const SESSION_MS: u64 = 12 * 3_600_000; /// What a session holds: the owner's key and the proof that opened it. #[derive(Clone, Debug)] pub struct Session { pub key: [u8; 32], pub proof: Vec, pub csrf: String, pub expires_ms: u64, } #[derive(Debug, Default)] pub struct Sessions { codes: Mutex>, live: Mutex>, } fn random_hex() -> String { let mut b = [0u8; 32]; rand::rngs::OsRng.fill_bytes(&mut b); hex::encode(b) } impl Sessions { /// A one-time code for the owner, from their signed request. pub fn link(&self, key: [u8; 32], proof: Vec, now: u64) -> String { let code = random_hex(); let s = Session { key, proof, csrf: random_hex(), expires_ms: now + SESSION_MS }; let mut codes = self.codes.lock().unwrap(); codes.retain(|_, (_, until)| *until > now); codes.insert(code.clone(), (s, now + LINK_MS)); code } /// Spend a code: the session id, once, within a minute of the link. pub fn open(&self, code: &str, now: u64) -> Option { let (s, until) = self.codes.lock().unwrap().remove(code)?; if until < now { return None; } let id = random_hex(); let mut live = self.live.lock().unwrap(); live.retain(|_, s| s.expires_ms > now); live.insert(id.clone(), s); Some(id) } pub fn get(&self, id: &str, now: u64) -> Option { self.live.lock().unwrap().get(id).filter(|s| s.expires_ms > now).cloned() } pub fn close(&self, id: &str) { self.live.lock().unwrap().remove(id); } } #[cfg(test)] mod tests { use super::*; #[test] fn a_link_opens_one_session_once_and_only_while_fresh() { let s = Sessions::default(); let code = s.link([1; 32], vec![9], 1_000); let id = s.open(&code, 2_000).expect("fresh"); assert!(s.open(&code, 2_000).is_none(), "spent"); assert_eq!(s.get(&id, 3_000).unwrap().key, [1; 32]); assert!(s.get(&id, 1_000 + SESSION_MS + 1).is_none(), "expired"); let stale = s.link([1; 32], vec![9], 1_000); assert!(s.open(&stale, 1_000 + LINK_MS + 1).is_none()); s.close(&id); assert!(s.get(&id, 3_000).is_none()); } }