#[cfg(any(feature = "sqlite", feature = "postgres", feature = "mysql"))]
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
use crate::session::{
crypto::{BoundFormat, SessionCrypto},
data::SessionData,
id::SessionId,
};
#[cfg(any(feature = "sqlite", feature = "postgres", feature = "mysql"))]
use std::time::Duration;
#[derive(Clone)]
pub(crate) struct SessionCodec {
crypto: Option<SessionCrypto>,
}
impl SessionCodec {
pub fn encrypted(crypto: SessionCrypto) -> Self {
Self {
crypto: Some(crypto),
}
}
pub fn plaintext() -> Self {
Self { crypto: None }
}
#[cfg(all(test, feature = "sqlite"))]
pub fn encode_text(&self, id: &SessionId, data: &SessionData) -> Result<String, SqlStoreError> {
Ok(BASE64.encode(self.encode_bytes(id, data)?))
}
#[cfg(any(feature = "sqlite", feature = "postgres", feature = "mysql"))]
pub fn decode_row(
&self,
id: &SessionId,
binary: Option<&[u8]>,
text: &str,
) -> Result<SessionData, SqlStoreError> {
match binary {
Some(bytes) => self.decode_bytes(id, bytes),
None => self.decode(id, text),
}
}
#[cfg(any(feature = "sqlite", feature = "postgres", feature = "mysql"))]
pub fn decode(&self, id: &SessionId, stored: &str) -> Result<SessionData, SqlStoreError> {
if self.crypto.is_none() && stored.starts_with('{') {
return Ok(serde_json::from_str(stored)?);
}
let payload = BASE64
.decode(stored)
.map_err(|_| SqlStoreError::Crypto(crate::session::crypto::CryptoError))?;
self.decode_bytes(id, &payload)
}
pub fn encode_bytes(
&self,
id: &SessionId,
data: &SessionData,
) -> Result<Vec<u8>, SqlStoreError> {
let bytes = rmp_serde::to_vec_named(data)?;
Ok(match &self.crypto {
Some(crypto) => crypto.seal_bound(BoundFormat::SESSION, id.as_bytes(), &bytes)?,
None => bytes,
})
}
pub fn decode_bytes(
&self,
id: &SessionId,
payload: &[u8],
) -> Result<SessionData, SqlStoreError> {
let opened;
let plaintext: &[u8] = match &self.crypto {
Some(crypto) => {
opened = crypto.open_bound(BoundFormat::SESSION, id.as_bytes(), payload)?;
if opened.legacy {
tracing::debug!("session blob predates id binding; re-sealed on next save");
}
&opened.plaintext
}
None => payload,
};
match rmp_serde::from_slice(plaintext) {
Ok(data) => Ok(data),
Err(_) => Ok(serde_json::from_slice(plaintext)?),
}
}
}
#[cfg(any(feature = "sqlite", feature = "postgres", feature = "mysql"))]
pub(crate) fn expires_at(clock: &dyn axess_clock::Clock, ttl: Duration) -> i64 {
clock
.now()
.timestamp()
.saturating_add(ttl.as_secs().min(i64::MAX as u64) as i64)
}
#[derive(Debug, thiserror::Error)]
pub enum SqlStoreError {
#[cfg(any(feature = "sqlite", feature = "postgres", feature = "mysql"))]
#[error("database error: {0}")]
Db(#[from] sqlx::Error),
#[error("session JSON deserialisation failed: {0}")]
Serialization(#[from] serde_json::Error),
#[error("session MessagePack encoding failed: {0}")]
Encode(#[from] rmp_serde::encode::Error),
#[error("session MessagePack decoding failed: {0}")]
Decode(#[from] rmp_serde::decode::Error),
#[error("encryption/decryption error: {0}")]
Crypto(#[from] crate::session::crypto::CryptoError),
}
#[cfg(test)]
mod tests;