//! # Cookie handling //! //! cookies are stored as postcard-encoded ed25519-signed tokens, [Protocol Buffer Tokens], which //! this is inspired by. They contain session ids, _not_ the actual access token, which are stored //! in an in-memory session store. //! //! the signing key is generated on server start this does mean server restarts invalidate active //! sessions. this is not a big deal for my current usecase, and we could probably do secret //! handover or have configurable secrets should the need arise. //! //! [Protocol Buffer Tokens]: https://fly.io/blog/api-tokens-a-tedious-survey/ use std::marker::PhantomData; use base64::Engine as _; use base64::prelude::BASE64_STANDARD; use color_eyre::Result; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize)] pub struct Signed { signature: ed25519_dalek::Signature, message: Vec, _kind: PhantomData, } impl Signed { pub fn contents(raw: &str, key: &ed25519_dalek::SigningKey) -> Result { let raw = BASE64_STANDARD.decode(raw).map_err(drop)?; // timing threats: technically, someone could try to discover the appropriate shape of our // tokens by seeing if we early-return from the postcard::from_bytes message. // this doesn't seem like a huge threat, since we're not encrypting our cookies, so the // structure is already pretty obvious let envelope: Self = postcard::from_bytes(&raw).map_err(drop)?; key.verify(&envelope.message, &envelope.signature) .map_err(drop)?; let (version_num, msg_raw): (u64, _) = postcard::take_from_bytes(&envelope.message).map_err(drop)?; // we're past the signature part, so we know this is ours. // worst we're getting here is an attempt to check if old tokens still work, // so we don't need to be as stressed about timing sensitivity if version_num != MSG::VERSION { return Err(()); } postcard::from_bytes(msg_raw).map_err(drop) } } impl Signed { pub fn sign(msg: MSG, key: &ed25519_dalek::SigningKey) -> Result { use ed25519_dalek::ed25519::signature::Signer as _; let raw = { let raw = Vec::new(); let raw = postcard::to_extend(&MSG::VERSION, raw).map_err(drop)?; postcard::to_extend(&msg, raw).map_err(drop)? }; let signature = key.sign(&raw); let envelope = Self { signature, message: raw, _kind: Default::default(), }; let raw = postcard::to_extend(&envelope, Vec::new()).map_err(drop)?; Ok(BASE64_STANDARD.encode(raw)) } } pub trait Versioned { const VERSION: u64; } #[derive(Serialize, Deserialize)] pub struct CookieMessage { // NB: per rfc:draft-ietf-oauth-v2-1#7.1.3.4, since we're signing our cookies and not encrypting // them, we can't store the access token directly. instead we'll store the session id, which // we'll use to look up the token in a session store. pub session_id: u64, } impl Versioned for CookieMessage { const VERSION: u64 = 1; } pub type CookieContents = Signed;