use hmac::{Hmac, Mac};
use sha2::Sha256;
use crate::application::service::chatter_acl::MessagingIdentity;
type HmacSha256 = Hmac<Sha256>;
pub const SESSION_TTL_SECS: i64 = 3600;
pub fn mint_session_token(secret: &[u8], identity: &MessagingIdentity, ttl_secs: i64) -> String {
let id = identity_key(identity);
let exp = chrono::Utc::now().timestamp() + ttl_secs;
let payload = format!("{id}|{exp}");
let sig = mac(secret, payload.as_bytes());
format!("{}.{}.{}", b64(id.as_bytes()), b64(exp.to_string().as_bytes()), b64(&sig))
}
pub fn verify_session_token(secret: &[u8], token: &str, identity: &MessagingIdentity) -> bool {
let mut parts = token.split('.');
let (Some(id_b64), Some(exp_b64), Some(sig_b64), None) =
(parts.next(), parts.next(), parts.next(), parts.next())
else {
return false;
};
let (Ok(id_raw), Ok(exp_raw), Ok(sig_given)) =
(ub64(id_b64), ub64(exp_b64), ub64(sig_b64))
else {
return false;
};
let (Ok(id), Ok(exp)) = (String::from_utf8(id_raw), String::from_utf8(exp_raw)) else {
return false;
};
if id != identity_key(identity) {
return false;
}
let Ok(exp) = exp.parse::<i64>() else { return false };
if chrono::Utc::now().timestamp() > exp {
return false;
}
let payload = format!("{id}|{exp}");
let expected = mac(secret, payload.as_bytes());
const_eq(&expected, &sig_given)
}
fn identity_key(identity: &MessagingIdentity) -> String {
match identity {
MessagingIdentity::User { partner_id } => format!("p:{partner_id}"),
MessagingIdentity::Guest { guest_id } => format!("g:{guest_id}"),
}
}
fn mac(secret: &[u8], payload: &[u8]) -> [u8; 32] {
let mut m = HmacSha256::new_from_slice(secret)
.unwrap_or_else(|_| unreachable!("HMAC-SHA256 accepts any key length"));
m.update(payload);
m.finalize().into_bytes().into()
}
fn const_eq(a: &[u8], b: &[u8]) -> bool {
if a.len() != b.len() {
return false;
}
let mut diff = 0u8;
for (x, y) in a.iter().zip(b.iter()) {
diff |= x ^ y;
}
diff == 0
}
fn b64(bytes: &[u8]) -> String {
use base64::Engine;
base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(bytes)
}
fn ub64(s: &str) -> Result<Vec<u8>, base64::DecodeError> {
use base64::Engine;
base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(s)
}
#[cfg(test)]
mod tests {
use super::*;
use uuid::Uuid;
fn user() -> MessagingIdentity {
MessagingIdentity::User { partner_id: Uuid::new_v4() }
}
#[test]
fn roundtrip_and_identity_binding() {
let id = user();
let tok = mint_session_token(b"s", &id, 60);
assert!(verify_session_token(b"s", &tok, &id));
assert!(!verify_session_token(b"s", &tok, &user()));
assert!(!verify_session_token(b"t", &tok, &id));
}
#[test]
fn expiry_enforced() {
let id = user();
let tok = mint_session_token(b"s", &id, -1); assert!(!verify_session_token(b"s", &tok, &id));
}
#[test]
fn malformed_tokens_fail_closed() {
let id = user();
for bad in ["", "a", "a.b", "a.b.c.d", "!!!.?.#"] {
assert!(!verify_session_token(b"s", bad, &id), "{bad:?}");
}
let tok = mint_session_token(b"s", &id, 60);
let mut parts = tok.split('.').collect::<Vec<_>>();
parts[2] = "AAAA";
assert!(!verify_session_token(b"s", &parts.join("."), &id));
}
}