use base64::Engine; use hmac::{KeyInit, Mac}; use sha2::Sha256; use uuid::Uuid; use crate::{ ChannelBus, ChannelError, ChannelResult, security::require_cluster, }; type HmacSha256 = hmac::Hmac; const VERSION: u8 = 0; pub const TOKEN_TTL_SECS: u64 = 600; const SESSION_TTL_SECS: u64 = 1800; const MAX_LIFETIME_SECS: i64 = 3000; const TOKEN_PREFIX: &str = "token:access:"; const SESSION_PREFIX: &str = "channel:session:"; const MAX_TOKEN_BASE64_LEN: usize = 256; #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] pub struct ChannelAccessToken { pub access_token: String, } #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] pub struct ChannelTokenApply { pub client_id: String, pub device_id: String, } #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] pub struct ChannelTokenContext { pub user_id: Uuid, pub device_id: String, pub client_id: String, } struct TokenPayload { user_id: Uuid, created_at: i64, } impl TokenPayload { const LEN: usize = 57; fn encode(&self, signing_key: &[u8]) -> ChannelResult> { let mut buf = Vec::with_capacity(Self::LEN); buf.push(VERSION); buf.extend_from_slice(self.user_id.as_bytes()); buf.extend_from_slice(&self.created_at.to_be_bytes()); let tag = hmac_sign(signing_key, &buf)?; buf.extend_from_slice(&tag); Ok(buf) } fn decode(bytes: &[u8], signing_key: &[u8]) -> ChannelResult { if bytes.len() != Self::LEN || bytes[0] != VERSION { return Err(ChannelError::TokenInvalidOrExpired); } let expected_tag = hmac_sign(signing_key, &bytes[..25])?; if !constant_time_eq(&expected_tag, &bytes[25..]) { return Err(ChannelError::TokenInvalidOrExpired); } let user_id_bytes: [u8; 16] = bytes[1..17].try_into().map_err( |_: std::array::TryFromSliceError| { ChannelError::TokenInvalidOrExpired }, )?; let user_id = Uuid::from_bytes(user_id_bytes); let created_at_bytes: [u8; 8] = bytes[17..25].try_into().map_err( |_: std::array::TryFromSliceError| { ChannelError::TokenInvalidOrExpired }, )?; let created_at = i64::from_be_bytes(created_at_bytes); Ok(TokenPayload { user_id, created_at, }) } } impl ChannelBus { fn signing_key(&self) -> ChannelResult<[u8; 32]> { let secret = self.inner.config.signing_secret.as_deref().ok_or( ChannelError::Internal("no signing secret".to_string()), )?; let mut mac = HmacSha256::new_from_slice(secret.as_bytes()).map_err(|_| { ChannelError::Internal("hmac init failed".to_string()) })?; mac.update(b"channel-access-token-signing-key"); let result = mac.finalize().into_bytes(); Ok(result.into()) } fn session_hash_key(&self, user_id: &Uuid, created_at: i64) -> String { format!("{}{}:{}", SESSION_PREFIX, user_id, created_at) } fn token_redis_key(&self, token_str: &str) -> String { format!("{}{}", TOKEN_PREFIX, token_str) } pub async fn apply_access_token( &self, user_id: Uuid, apply: ChannelTokenApply, ) -> ChannelResult { let created_at = chrono::Utc::now().timestamp(); let signing_key = self.signing_key()?; let payload = TokenPayload { user_id, created_at, }; let token_bytes = payload.encode(&signing_key)?; let access_token = base64::engine::general_purpose::URL_SAFE_NO_PAD .encode(&token_bytes); let session_key = self.session_hash_key(&user_id, created_at); let token_key = self.token_redis_key(&access_token); let cluster = require_cluster(&self.inner.cache)?; let mut conn = cluster.conn(); let mut pipe = redis::Pipeline::new(); pipe.hset(&session_key, "device_id", &apply.device_id) .hset(&session_key, "client_id", &apply.client_id) .expire(&session_key, SESSION_TTL_SECS as i64); pipe.query_async::<()>(&mut conn) .await .map_err(|e| ChannelError::Cache(cache::CacheError::Redis(e)))?; redis::Cmd::set_ex(&token_key, &session_key, TOKEN_TTL_SECS) .query_async::<()>(&mut conn) .await .map_err(|e| ChannelError::Cache(cache::CacheError::Redis(e)))?; Ok(ChannelAccessToken { access_token }) } pub async fn check_access_token( &self, access_token: String, ) -> ChannelResult { let token_bytes = decode_token_bytes(&access_token)?; let signing_key = self.signing_key()?; let payload = TokenPayload::decode(&token_bytes, &signing_key)?; let elapsed = chrono::Utc::now().timestamp() - payload.created_at; if elapsed > MAX_LIFETIME_SECS { return Err(ChannelError::TokenInvalidOrExpired); } let session_key = self.session_hash_key(&payload.user_id, payload.created_at); let cluster = require_cluster(&self.inner.cache)?; let mut conn = cluster.conn(); let token_key = self.token_redis_key(&access_token); let token_exists: bool = redis::Cmd::new() .arg("EXISTS") .arg(&token_key) .query_async(&mut conn) .await .map_err(|e| ChannelError::Cache(cache::CacheError::Redis(e)))?; if !token_exists { return Err(ChannelError::TokenInvalidOrExpired); } let hash_data: std::collections::HashMap = redis::Cmd::new() .arg("HGETALL") .arg(&session_key) .query_async(&mut conn) .await .map_err(|e| { ChannelError::Cache(cache::CacheError::Redis(e)) })?; let device_id = hash_data .get("device_id") .cloned() .ok_or(ChannelError::TokenInvalidOrExpired)?; let client_id = hash_data .get("client_id") .cloned() .ok_or(ChannelError::TokenInvalidOrExpired)?; Ok(ChannelTokenContext { user_id: payload.user_id, device_id, client_id, }) } pub async fn renew_access_token( &self, access_token: String, ) -> ChannelResult { let token_bytes = decode_token_bytes(&access_token)?; let signing_key = self.signing_key()?; let payload = TokenPayload::decode(&token_bytes, &signing_key)?; let elapsed = chrono::Utc::now().timestamp() - payload.created_at; if elapsed > MAX_LIFETIME_SECS { return Err(ChannelError::RenewalLimitExceeded); } let session_key = self.session_hash_key(&payload.user_id, payload.created_at); let token_key = self.token_redis_key(&access_token); let cluster = require_cluster(&self.inner.cache)?; let mut conn = cluster.conn(); let hash_data: std::collections::HashMap = redis::Cmd::new() .arg("HGETALL") .arg(&session_key) .query_async(&mut conn) .await .map_err(|e| { ChannelError::Cache(cache::CacheError::Redis(e)) })?; let device_id = hash_data .get("device_id") .cloned() .ok_or(ChannelError::TokenInvalidOrExpired)?; let client_id = hash_data .get("client_id") .cloned() .ok_or(ChannelError::TokenInvalidOrExpired)?; let _: () = redis::Cmd::new() .arg("DEL") .arg(&token_key) .query_async(&mut conn) .await .map_err(|e| ChannelError::Cache(cache::CacheError::Redis(e)))?; let created_at = chrono::Utc::now().timestamp(); let new_payload = TokenPayload { user_id: payload.user_id, created_at, }; let new_token_bytes = new_payload.encode(&signing_key)?; let new_access_token = base64::engine::general_purpose::URL_SAFE_NO_PAD .encode(&new_token_bytes); let new_session_key = self.session_hash_key(&payload.user_id, created_at); let new_token_key = self.token_redis_key(&new_access_token); let mut pipe = redis::Pipeline::new(); pipe.hset(&new_session_key, "device_id", &device_id) .hset(&new_session_key, "client_id", &client_id) .expire(&new_session_key, SESSION_TTL_SECS as i64); pipe.query_async::<()>(&mut conn) .await .map_err(|e| ChannelError::Cache(cache::CacheError::Redis(e)))?; redis::Cmd::set_ex(&new_token_key, &new_session_key, TOKEN_TTL_SECS) .query_async::<()>(&mut conn) .await .map_err(|e| ChannelError::Cache(cache::CacheError::Redis(e)))?; Ok(ChannelAccessToken { access_token: new_access_token, }) } } fn decode_token_bytes(token: &str) -> ChannelResult> { if token.len() > MAX_TOKEN_BASE64_LEN { return Err(ChannelError::TokenInvalidOrExpired); } base64::engine::general_purpose::URL_SAFE_NO_PAD .decode(token) .map_err(|_| ChannelError::TokenInvalidOrExpired) } fn hmac_sign(key: &[u8], payload: &[u8]) -> ChannelResult<[u8; 32]> { let mut mac = HmacSha256::new_from_slice(key) .map_err(|_| ChannelError::Internal("hmac sign failed".to_string()))?; mac.update(payload); Ok(mac.finalize().into_bytes().into()) } fn constant_time_eq(expected: &[u8; 32], actual: &[u8]) -> bool { if actual.len() != 32 { return false; } let mut diff = 0u8; for i in 0..32 { diff |= expected[i] ^ actual[i]; } diff == 0 }