use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
use doido_cache::CacheStore;
use doido_core::Result;
use hmac::{Hmac, KeyInit, Mac};
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use sha2::Sha256;
use std::sync::Arc;
type HmacSha256 = Hmac<Sha256>;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Session {
pub id: String,
pub data: Value,
}
impl Session {
pub fn new() -> Self {
Self {
id: uuid::Uuid::new_v4().to_string(),
data: Value::Object(Default::default()),
}
}
pub fn with_id(id: impl Into<String>) -> Self {
Self {
id: id.into(),
data: Value::Object(Default::default()),
}
}
pub fn set(&mut self, key: &str, value: impl Serialize) {
if !self.data.is_object() {
self.data = Value::Object(Default::default());
}
if let Value::Object(map) = &mut self.data {
map.insert(
key.to_string(),
serde_json::to_value(value).unwrap_or(Value::Null),
);
}
}
pub fn get<T: DeserializeOwned>(&self, key: &str) -> Option<T> {
self.data
.get(key)
.cloned()
.and_then(|v| serde_json::from_value(v).ok())
}
}
impl Default for Session {
fn default() -> Self {
Self::new()
}
}
#[async_trait::async_trait]
pub trait SessionStore: Send + Sync {
async fn load(&self, id: &str) -> Result<Option<Session>>;
async fn save(&self, session: &Session) -> Result<()>;
async fn destroy(&self, id: &str) -> Result<()>;
}
pub struct CookieSessionStore {
secret: Vec<u8>,
}
impl CookieSessionStore {
pub fn new(secret: impl Into<Vec<u8>>) -> Self {
Self {
secret: secret.into(),
}
}
pub fn encode(&self, session: &Session) -> String {
let payload = serde_json::to_vec(session).unwrap_or_default();
let msg = URL_SAFE_NO_PAD.encode(payload);
let sig = self.sign(msg.as_bytes());
format!("{msg}.{}", URL_SAFE_NO_PAD.encode(sig))
}
pub fn decode(&self, raw: &str) -> Option<Session> {
let (msg, sig_b64) = raw.split_once('.')?;
let sig = URL_SAFE_NO_PAD.decode(sig_b64).ok()?;
let mut mac = HmacSha256::new_from_slice(&self.secret).ok()?;
mac.update(msg.as_bytes());
mac.verify_slice(&sig).ok()?;
let payload = URL_SAFE_NO_PAD.decode(msg).ok()?;
serde_json::from_slice(&payload).ok()
}
fn sign(&self, msg: &[u8]) -> Vec<u8> {
let mut mac =
HmacSha256::new_from_slice(&self.secret).expect("HMAC accepts a key of any length");
mac.update(msg);
mac.finalize().into_bytes().to_vec()
}
}
impl Default for CookieSessionStore {
fn default() -> Self {
Self::new(b"doido-dev-insecure-secret".to_vec())
}
}
#[async_trait::async_trait]
impl SessionStore for CookieSessionStore {
async fn load(&self, raw: &str) -> Result<Option<Session>> {
Ok(self.decode(raw))
}
async fn save(&self, _session: &Session) -> Result<()> {
Ok(())
}
async fn destroy(&self, _id: &str) -> Result<()> {
Ok(())
}
}
pub struct EncryptedCookieSessionStore {
secret: Vec<u8>,
}
impl EncryptedCookieSessionStore {
pub fn new(secret: impl Into<Vec<u8>>) -> Self {
Self {
secret: secret.into(),
}
}
pub fn encode(&self, session: &Session) -> String {
let payload = serde_json::to_vec(session).unwrap_or_default();
let blob = doido_core::crypto::encrypt(&self.secret, &payload);
URL_SAFE_NO_PAD.encode(blob)
}
pub fn decode(&self, raw: &str) -> Option<Session> {
let blob = URL_SAFE_NO_PAD.decode(raw).ok()?;
let payload = doido_core::crypto::decrypt(&self.secret, &blob)?;
serde_json::from_slice(&payload).ok()
}
}
impl Default for EncryptedCookieSessionStore {
fn default() -> Self {
Self::new(crate::secret::key_base())
}
}
#[async_trait::async_trait]
impl SessionStore for EncryptedCookieSessionStore {
async fn load(&self, raw: &str) -> Result<Option<Session>> {
Ok(self.decode(raw))
}
async fn save(&self, _session: &Session) -> Result<()> {
Ok(())
}
async fn destroy(&self, _id: &str) -> Result<()> {
Ok(())
}
}
pub struct CacheSessionStore {
store: Arc<dyn CacheStore>,
ttl_secs: Option<u64>,
}
impl CacheSessionStore {
pub fn new(store: Arc<dyn CacheStore>) -> Self {
Self {
store,
ttl_secs: None,
}
}
pub fn with_ttl(mut self, secs: u64) -> Self {
self.ttl_secs = Some(secs);
self
}
fn key(id: &str) -> String {
format!("session:{id}")
}
}
#[async_trait::async_trait]
impl SessionStore for CacheSessionStore {
async fn load(&self, id: &str) -> Result<Option<Session>> {
match self.store.get(&Self::key(id)).await? {
Some(value) => Ok(serde_json::from_value(value).ok()),
None => Ok(None),
}
}
async fn save(&self, session: &Session) -> Result<()> {
let value = serde_json::to_value(session)?;
self.store
.set(&Self::key(&session.id), value, self.ttl_secs)
.await
}
async fn destroy(&self, id: &str) -> Result<()> {
self.store.delete(&Self::key(id)).await
}
}