Something went wrong. Try again.
An OIDC provider for AT Protocol OAuth. Rust port of apenwarr/atlogin. forked from quilling.dev/ratlogin
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164use std::path::{Path, PathBuf};
use crate::client_key::{self, ClientKey};use crate::database::{self, Database, DatabaseError};use crate::keys::{self, KeyError, SigningKeys};
const REQUIRED: [&str; 4] = [ database::FILE_NAME, keys::FILE_NAME, keys::EC_FILE_NAME, client_key::FILE_NAME,];const KNOWN: [&str; 9] = [ database::FILE_NAME, "ratlogin.sqlite3-wal", "ratlogin.sqlite3-shm", keys::FILE_NAME, keys::EC_FILE_NAME, keys::PREVIOUS_FILE_NAME, keys::EC_PREVIOUS_FILE_NAME, client_key::FILE_NAME, client_key::PREVIOUS_FILE_NAME,];
pub struct StateKeys { pub signing: SigningKeys, pub client: ClientKey,}
#[derive(Debug, Clone, Eq, PartialEq)]pub enum Contents { Empty, Complete, Partial { present: Vec<&'static str>, missing: Vec<&'static str>, },}
#[derive(Debug, thiserror::Error)]pub enum StateError { #[error("state directory {0} does not exist")] Missing(PathBuf), #[error("state directory: {0}")] Io(#[from] std::io::Error), #[error(transparent)] Key(#[from] KeyError), #[error(transparent)] Database(#[from] DatabaseError),}
pub fn inspect(state_dir: &Path) -> Result<Contents, StateError> { if !state_dir.is_dir() { return Err(StateError::Missing(state_dir.to_path_buf())); } let exists = |name: &str| state_dir.join(name).exists(); if !KNOWN.iter().any(|name| exists(name)) { return Ok(Contents::Empty); } let (present, missing): (Vec<_>, Vec<_>) = REQUIRED.iter().partition(|name| exists(name)); if missing.is_empty() { Ok(Contents::Complete) } else { Ok(Contents::Partial { present: present.into_iter().copied().collect(), missing: missing.into_iter().copied().collect(), }) }}
pub fn initialize(state_dir: &Path) -> Result<StateKeys, StateError> { let signing = SigningKeys::ensure(state_dir)?; let client = ClientKey::ensure(state_dir)?; Database::open(&state_dir.join(database::FILE_NAME))?; Ok(StateKeys { signing, client })}
pub fn rotate(state_dir: &Path) -> Result<StateKeys, StateError> { Ok(StateKeys { signing: SigningKeys::rotate(state_dir)?, client: ClientKey::rotate(state_dir)?, })}
pub fn open(state_dir: &Path) -> Result<StateKeys, StateError> { match inspect(state_dir)? { Contents::Empty => { tracing::info!(state_dir = %state_dir.display(), "initializing empty state directory"); } Contents::Complete => {} Contents::Partial { present, missing } => { tracing::warn!( state_dir = %state_dir.display(), present = present.join(", "), missing = missing.join(", "), "state directory is incomplete; generating what is missing" ); } } initialize(state_dir)}
#[cfg(test)]mod tests { use super::*; use crate::keys::SigningAlgorithm;
#[test] fn an_empty_state_directory_is_initialized_once() { let dir = tempfile::tempdir().expect("tempdir"); std::fs::create_dir(dir.path().join("lost+found")).expect("stray directory"); assert_eq!(inspect(dir.path()).expect("inspect"), Contents::Empty);
let first = open(dir.path()).expect("initialize"); assert_eq!(inspect(dir.path()).expect("inspect"), Contents::Complete); let second = open(dir.path()).expect("reopen"); for algorithm in SigningAlgorithm::ALL { assert_eq!( first.signing.current(algorithm).kid(), second.signing.current(algorithm).kid() ); } assert_eq!(first.client.kid(), second.client.kid()); }
#[test] fn a_partial_state_directory_is_completed_without_touching_what_it_holds() { let dir = tempfile::tempdir().expect("tempdir"); Database::open(&dir.path().join(database::FILE_NAME)).expect("database"); assert!(matches!( inspect(dir.path()).expect("inspect"), Contents::Partial { ref present, ref missing } if present == &[database::FILE_NAME] && missing == &[keys::FILE_NAME, keys::EC_FILE_NAME, client_key::FILE_NAME] )); let repaired = open(dir.path()).expect("keys are generated"); assert_eq!(inspect(dir.path()).expect("inspect"), Contents::Complete); let reopened = open(dir.path()).expect("reopen"); assert_eq!( repaired.signing.current(SigningAlgorithm::Es256).kid(), reopened.signing.current(SigningAlgorithm::Es256).kid() ); assert_eq!(repaired.client.kid(), reopened.client.kid());
let dir = tempfile::tempdir().expect("tempdir"); let keys = SigningKeys::ensure(dir.path()).expect("keys"); std::fs::remove_file(dir.path().join(keys::EC_FILE_NAME)).expect("remove"); let repaired = open(dir.path()).expect("the database and the missing keys are generated"); assert_eq!( repaired.signing.current(SigningAlgorithm::Rs256).kid(), keys.current(SigningAlgorithm::Rs256).kid() ); assert!(dir.path().join(database::FILE_NAME).exists()); assert!(dir.path().join(client_key::FILE_NAME).exists()); }
#[test] fn a_missing_state_directory_is_refused() { let dir = tempfile::tempdir().expect("tempdir"); let path = dir.path().join("elsewhere"); assert!(matches!(open(&path), Err(StateError::Missing(missing)) if missing == path)); }}