1use crate::types::{ExecutionError, RiskLevel, TokenError};
6use hmac::{Hmac, KeyInit, Mac};
7use secrecy::{ExposeSecret, SecretBox};
8use serde::{Deserialize, Serialize};
9use sha2::Sha256;
10use uuid::Uuid;
11
12type HmacSha256 = Hmac<Sha256>;
13
14pub struct TokenSecret(SecretBox<[u8]>);
39
40impl TokenSecret {
49 pub fn new(secret: impl Into<Vec<u8>>) -> Self {
53 let bytes: Vec<u8> = secret.into();
54 Self(SecretBox::new(Box::from(bytes.as_slice())))
55 }
56
57 pub fn from_env(var: &str) -> Result<Self, std::env::VarError> {
60 let val = std::env::var(var)?;
61 Ok(Self::new(val.into_bytes()))
62 }
63
64 pub fn expose_secret(&self) -> &[u8] {
67 self.0.expose_secret()
68 }
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct ApprovalToken {
74 pub request_id: String,
76
77 pub code_hash: String,
79
80 pub user_id: String,
82
83 pub session_id: String,
85
86 pub server_id: String,
88
89 pub context_hash: String,
91
92 pub risk_level: RiskLevel,
94
95 pub created_at: i64,
97
98 pub expires_at: i64,
100
101 pub signature: String,
103}
104
105impl ApprovalToken {
106 pub fn encode(&self) -> Result<String, serde_json::Error> {
108 let json = serde_json::to_string(self)?;
109 Ok(base64::Engine::encode(
110 &base64::engine::general_purpose::URL_SAFE_NO_PAD,
111 json.as_bytes(),
112 ))
113 }
114
115 pub fn decode(encoded: &str) -> Result<Self, TokenDecodeError> {
117 let bytes =
118 base64::Engine::decode(&base64::engine::general_purpose::URL_SAFE_NO_PAD, encoded)
119 .map_err(|_| TokenDecodeError::InvalidBase64)?;
120
121 let json = String::from_utf8(bytes).map_err(|_| TokenDecodeError::InvalidUtf8)?;
122
123 serde_json::from_str(&json).map_err(|_| TokenDecodeError::InvalidJson)
124 }
125
126 fn payload_bytes(&self) -> Vec<u8> {
134 format!(
135 "{}|{}|{}|{}|{}|{}|{}|{}|{}",
136 self.request_id,
137 self.code_hash,
138 self.user_id,
139 self.session_id,
140 self.server_id,
141 self.context_hash,
142 self.risk_level,
143 self.created_at,
144 self.expires_at,
145 )
146 .into_bytes()
147 }
148}
149
150#[derive(Debug, thiserror::Error)]
152pub enum TokenDecodeError {
153 #[error(
154 "Token is not valid base64 — it may have been truncated or corrupted during transport"
155 )]
156 InvalidBase64,
157 #[error("Token contains invalid UTF-8 bytes after base64 decoding")]
158 InvalidUtf8,
159 #[error("Token decoded to invalid JSON — the token string may have been truncated, double-encoded, or is not an approval token")]
160 InvalidJson,
161}
162
163pub trait TokenGenerator: Send + Sync {
165 fn generate(
167 &self,
168 code: &str,
169 user_id: &str,
170 session_id: &str,
171 server_id: &str,
172 context_hash: &str,
173 risk_level: RiskLevel,
174 ttl_seconds: i64,
175 ) -> ApprovalToken;
176
177 fn verify(&self, token: &ApprovalToken) -> Result<(), ExecutionError>;
179
180 fn verify_code(&self, code: &str, token: &ApprovalToken) -> Result<(), ExecutionError>;
182}
183
184pub struct HmacTokenGenerator {
186 secret: TokenSecret,
187}
188
189impl HmacTokenGenerator {
190 pub const MIN_SECRET_LEN: usize = 16;
195
196 pub fn new(secret: TokenSecret) -> Result<Self, TokenError> {
203 if secret.expose_secret().len() < Self::MIN_SECRET_LEN {
204 return Err(TokenError::SecretTooShort {
205 minimum: Self::MIN_SECRET_LEN,
206 actual: secret.expose_secret().len(),
207 });
208 }
209 Ok(Self { secret })
210 }
211
212 pub fn new_from_bytes(bytes: impl Into<Vec<u8>>) -> Result<Self, TokenError> {
222 Self::new(TokenSecret::new(bytes))
223 }
224
225 pub fn from_env(env_var: &str) -> Result<Self, Box<dyn std::error::Error>> {
232 let secret = TokenSecret::from_env(env_var)?;
233 Ok(Self::new(secret)?)
234 }
235
236 fn sign(&self, payload: &[u8]) -> String {
238 let mut mac = HmacSha256::new_from_slice(self.secret.expose_secret())
239 .expect("HMAC can take key of any size");
240 mac.update(payload);
241 hex::encode(mac.finalize().into_bytes())
242 }
243
244 fn verify_signature(&self, payload: &[u8], signature: &str) -> bool {
246 let mut mac = HmacSha256::new_from_slice(self.secret.expose_secret())
247 .expect("HMAC can take key of any size");
248 mac.update(payload);
249
250 let expected = hex::decode(signature).unwrap_or_default();
251 mac.verify_slice(&expected).is_ok()
252 }
253}
254
255impl TokenGenerator for HmacTokenGenerator {
256 fn generate(
257 &self,
258 code: &str,
259 user_id: &str,
260 session_id: &str,
261 server_id: &str,
262 context_hash: &str,
263 risk_level: RiskLevel,
264 ttl_seconds: i64,
265 ) -> ApprovalToken {
266 let now = chrono::Utc::now().timestamp();
267
268 let mut token = ApprovalToken {
269 request_id: Uuid::new_v4().to_string(),
270 code_hash: hash_code(code),
271 user_id: user_id.to_string(),
272 session_id: session_id.to_string(),
273 server_id: server_id.to_string(),
274 context_hash: context_hash.to_string(),
275 risk_level,
276 created_at: now,
277 expires_at: now + ttl_seconds,
278 signature: String::new(),
279 };
280
281 token.signature = self.sign(&token.payload_bytes());
282 token
283 }
284
285 fn verify(&self, token: &ApprovalToken) -> Result<(), ExecutionError> {
286 let now = chrono::Utc::now().timestamp();
287 if now > token.expires_at {
288 return Err(ExecutionError::TokenExpired);
289 }
290
291 if !self.verify_signature(&token.payload_bytes(), &token.signature) {
292 return Err(ExecutionError::TokenInvalid(
293 "signature verification failed".into(),
294 ));
295 }
296
297 Ok(())
298 }
299
300 fn verify_code(&self, code: &str, token: &ApprovalToken) -> Result<(), ExecutionError> {
301 let current_hash = hash_code(code);
302 if current_hash != token.code_hash {
303 let expected_prefix = if token.code_hash.len() >= 12 {
304 &token.code_hash[..12]
305 } else {
306 &token.code_hash
307 };
308 let actual_prefix = if current_hash.len() >= 12 {
309 ¤t_hash[..12]
310 } else {
311 ¤t_hash
312 };
313 return Err(ExecutionError::CodeMismatch {
314 expected_hash: expected_prefix.to_string(),
315 actual_hash: actual_prefix.to_string(),
316 });
317 }
318 Ok(())
319 }
320}
321
322pub fn hash_code(code: &str) -> String {
327 use sha2::Digest;
328 let mut hasher = Sha256::new();
329 hasher.update(canonicalize_code(code).as_bytes());
330 hex::encode(hasher.finalize())
331}
332
333pub fn canonicalize_code(code: &str) -> String {
342 let mut result = String::new();
343 for line in code.trim().lines() {
344 let trimmed = line.trim();
345 if !trimmed.is_empty() {
346 if !result.is_empty() {
347 result.push('\n');
348 }
349 result.push_str(trimmed);
350 }
351 }
352 result
353}
354
355pub fn compute_context_hash(schema_hash: &str, permissions_hash: &str) -> String {
357 use sha2::Digest;
358 let mut hasher = Sha256::new();
359 hasher.update(schema_hash.as_bytes());
360 hasher.update(b"|");
361 hasher.update(permissions_hash.as_bytes());
362 hex::encode(hasher.finalize())
363}
364
365#[cfg(test)]
366mod tests {
367 use super::*;
368
369 #[test]
370 fn test_token_generation_and_verification() {
371 let generator =
372 HmacTokenGenerator::new(TokenSecret::new(b"test-secret-key!".to_vec())).unwrap();
373
374 let token = generator.generate(
375 "query { users { id } }",
376 "user-123",
377 "session-456",
378 "server-789",
379 "context-hash",
380 RiskLevel::Low,
381 300,
382 );
383
384 assert!(generator.verify(&token).is_ok());
386
387 assert!(generator
389 .verify_code("query { users { id } }", &token)
390 .is_ok());
391 }
392
393 #[test]
394 fn test_code_mismatch() {
395 let generator =
396 HmacTokenGenerator::new(TokenSecret::new(b"test-secret-key!".to_vec())).unwrap();
397
398 let token = generator.generate(
399 "query { users { id } }",
400 "user-123",
401 "session-456",
402 "server-789",
403 "context-hash",
404 RiskLevel::Low,
405 300,
406 );
407
408 let result = generator.verify_code("query { orders { id } }", &token);
410 assert!(matches!(result, Err(ExecutionError::CodeMismatch { .. })));
411 }
412
413 #[test]
414 fn test_token_encode_decode() {
415 let generator =
416 HmacTokenGenerator::new(TokenSecret::new(b"test-secret-key!".to_vec())).unwrap();
417
418 let token = generator.generate(
419 "query { users { id } }",
420 "user-123",
421 "session-456",
422 "server-789",
423 "context-hash",
424 RiskLevel::Low,
425 300,
426 );
427
428 let encoded = token.encode().unwrap();
429 let decoded = ApprovalToken::decode(&encoded).unwrap();
430
431 assert_eq!(token.request_id, decoded.request_id);
432 assert_eq!(token.code_hash, decoded.code_hash);
433 assert_eq!(token.signature, decoded.signature);
434 }
435
436 #[test]
437 fn test_canonicalize_code() {
438 let code1 = "query { users { id } }";
439 let code2 = " query { users { id } } ";
440 let code3 = "query {\n users {\n id\n }\n}";
441
442 assert_eq!(canonicalize_code(code1), canonicalize_code(code2));
444
445 let canonical = canonicalize_code(code3);
447 assert!(canonical.contains("query {"));
448 assert!(canonical.contains("users {"));
449 }
450
451 #[test]
452 fn test_empty_secret_rejected() {
453 let result = HmacTokenGenerator::new(TokenSecret::new(b"".to_vec()));
454 assert!(matches!(
455 result,
456 Err(TokenError::SecretTooShort {
457 minimum: 16,
458 actual: 0
459 })
460 ));
461 }
462
463 #[test]
464 fn test_short_secret_rejected() {
465 let result = HmacTokenGenerator::new(TokenSecret::new(b"short".to_vec()));
466 assert!(matches!(
467 result,
468 Err(TokenError::SecretTooShort {
469 minimum: 16,
470 actual: 5
471 })
472 ));
473 }
474}