use chrono::{DateTime, Utc};
use hmac::{Hmac, Mac};
use serde::{Deserialize, Serialize};
use sha2::Sha256;
use subtle::ConstantTimeEq;
use super::event_error::EventError;
pub const PURPOSE_ICS: &str = "event-ics-access";
pub const PURPOSE_TICKET_REPORT: &str = "event-registration-ticket-report-access";
const CAPABILITY_CONTEXT: &str = "event-capability-v1";
type HmacSha256 = Hmac<Sha256>;
pub const EVENT_CAPABILITY_SECRET_ENV: &str = "EVENT_CAPABILITY_SECRET";
pub const ICS_TOKEN_TTL_SECS: i64 = 24 * 60 * 60;
pub fn capability_secret_from_env() -> String {
std::env::var(EVENT_CAPABILITY_SECRET_ENV).unwrap_or_default()
}
fn b64url_encode(bytes: &[u8]) -> String {
const ALPHA: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
let mut out = String::with_capacity(bytes.len().div_ceil(3) * 4);
for chunk in bytes.chunks(3) {
let b = [chunk[0], *chunk.get(1).unwrap_or(&0), *chunk.get(2).unwrap_or(&0)];
let n = ((b[0] as u32) << 16) | ((b[1] as u32) << 8) | (b[2] as u32);
out.push(ALPHA[(n >> 18) as usize & 63] as char);
out.push(ALPHA[(n >> 12) as usize & 63] as char);
out.push(if chunk.len() > 1 { ALPHA[(n >> 6) as usize & 63] as char } else { '=' });
out.push(if chunk.len() > 2 { ALPHA[n as usize & 63] as char } else { '=' });
}
while out.ends_with('=') {
out.pop();
}
out
}
fn b64url_decode(text: &str) -> Option<Vec<u8>> {
const ALPHA: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
let mut bits: u32 = 0;
let mut nbits: u32 = 0;
let mut out = Vec::with_capacity(text.len() * 3 / 4);
for ch in text.bytes() {
let v = ALPHA.iter().position(|&a| a == ch)? as u32;
bits = (bits << 6) | v;
nbits += 6;
if nbits >= 8 {
nbits -= 8;
out.push(((bits >> nbits) & 0xff) as u8);
}
}
Some(out)
}
fn sign(secret: &str, purpose: &str, payload_b64: &str) -> Result<Vec<u8>, EventError> {
if secret.is_empty() {
return Err(EventError::EventCapabilitySecretNotConfigured);
}
let mut mac = HmacSha256::new_from_slice(secret.as_bytes())
.map_err(|e| EventError::Internal(format!("capability secret rejected by HMAC: {e}")))?;
mac.update(CAPABILITY_CONTEXT.as_bytes());
mac.update(b"\n");
mac.update(purpose.as_bytes());
mac.update(b"\n");
mac.update(payload_b64.as_bytes());
Ok(mac.finalize().into_bytes().to_vec())
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CapabilityClaims {
pub purpose: String,
pub exp: i64,
pub data: Vec<String>,
}
impl CapabilityClaims {
pub fn mint(&self, secret: &str) -> Result<String, EventError> {
let payload = serde_json::to_vec(self)
.map_err(|e| EventError::Internal(format!("capability payload encode: {e}")))?;
let payload_b64 = b64url_encode(&payload);
let sig = sign(secret, &self.purpose, &payload_b64)?;
Ok(format!("v1.{payload_b64}.{}", b64url_encode(&sig)))
}
pub fn verify(
secret: &str,
expected_purpose: &str,
token: &str,
now: DateTime<Utc>,
) -> Result<Self, EventError> {
if secret.is_empty() {
return Err(EventError::EventCapabilitySecretNotConfigured);
}
let bad = || EventError::EventNotPublished;
let mut parts = token.splitn(3, '.');
let version = parts.next().unwrap_or_default();
let payload_b64 = parts.next().unwrap_or_default();
let sig_b64 = parts.next().unwrap_or_default();
if version != "v1" || payload_b64.is_empty() || sig_b64.is_empty() {
return Err(bad());
}
let expected_sig = sign(secret, expected_purpose, payload_b64)?;
let given_sig = b64url_decode(sig_b64).ok_or_else(bad)?;
if expected_sig.len() != given_sig.len() || expected_sig.ct_eq(&given_sig).unwrap_u8() == 0
{
return Err(bad());
}
let payload = b64url_decode(payload_b64).ok_or_else(bad)?;
let claims: Self =
serde_json::from_slice(&payload).map_err(|_| bad())?;
if claims.purpose != expected_purpose {
return Err(bad());
}
if claims.exp != 0 && now.timestamp() >= claims.exp {
return Err(bad());
}
Ok(claims)
}
}
pub fn mint_ics_capability(
secret: &str,
event_id: &uuid::Uuid,
slot_id: Option<&uuid::Uuid>,
now: DateTime<Utc>,
ttl_secs: i64,
) -> Result<String, EventError> {
let mut data = vec![event_id.to_string()];
if let Some(slot) = slot_id {
data.push(slot.to_string());
}
CapabilityClaims {
purpose: PURPOSE_ICS.to_string(),
exp: now.timestamp() + ttl_secs,
data,
}
.mint(secret)
}
pub fn mint_ticket_report_capability(
secret: &str,
event_id: &uuid::Uuid,
registration_ids: &[uuid::Uuid],
) -> Result<String, EventError> {
let mut ids: Vec<String> = registration_ids.iter().map(|id| id.to_string()).collect();
ids.sort();
let mut data = vec![event_id.to_string()];
data.append(&mut ids);
CapabilityClaims {
purpose: PURPOSE_TICKET_REPORT.to_string(),
exp: 0,
data,
}
.mint(secret)
}