//! Cached session token stored next to the state file. //! //! File format (no password): two lines — username and hex-encoded session token. //! File format (with password): QPCE-encrypted version of the above. //! The token has a server-side 24h TTL; no client-side expiry tracking. use std::path::{Path, PathBuf}; use anyhow::Context; use super::state::{decrypt_state, encrypt_state, is_encrypted_state}; pub struct CachedSession { pub username: String, pub token_hex: String, } /// Derive the session cache path: `{state_path}.session`. fn session_cache_path(state_path: &Path) -> PathBuf { state_path.with_extension("session") } /// Parse the two-line format (username + token_hex) from plaintext bytes. fn parse_session_lines(text: &str) -> Option { let mut lines = text.lines(); let username = lines.next()?.trim().to_string(); let token_hex = lines.next()?.trim().to_string(); if username.is_empty() || token_hex.is_empty() { return None; } if hex::decode(&token_hex).is_err() { return None; } Some(CachedSession { username, token_hex }) } /// Load a cached session token. Returns None if file is missing or malformed. /// Decrypts if the file is QPCE-encrypted (requires `password`). pub fn load_cached_session(state_path: &Path, password: Option<&str>) -> Option { let path = session_cache_path(state_path); let raw = std::fs::read(&path).ok()?; if is_encrypted_state(&raw) { let pw = password?; let plaintext = decrypt_state(pw, &raw).ok()?; let text = String::from_utf8(plaintext).ok()?; parse_session_lines(&text) } else { let text = String::from_utf8(raw).ok()?; parse_session_lines(&text) } } /// Save a session token to the cache file (mode 0o600 on Unix). /// Encrypts with QPCE if `password` is provided. pub fn save_cached_session( state_path: &Path, username: &str, token_hex: &str, password: Option<&str>, ) -> anyhow::Result<()> { let path = session_cache_path(state_path); let contents = format!("{username}\n{token_hex}\n"); let bytes = match password { Some(pw) => encrypt_state(pw, contents.as_bytes())?, None => { #[cfg(not(unix))] tracing::warn!( "storing session token as plaintext (no password set); \ file permissions cannot be restricted on this platform" ); contents.into_bytes() } }; std::fs::write(&path, bytes).with_context(|| format!("write session cache {path:?}"))?; #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; let perms = std::fs::Permissions::from_mode(0o600); std::fs::set_permissions(&path, perms).ok(); } Ok(()) } /// Remove the cached session file. pub fn clear_cached_session(state_path: &Path) { let path = session_cache_path(state_path); std::fs::remove_file(&path).ok(); }