1use crate::types::{ExecutionError, RiskLevel};
6use hmac::{Hmac, Mac};
7use serde::{Deserialize, Serialize};
8use sha2::Sha256;
9use uuid::Uuid;
10
11type HmacSha256 = Hmac<Sha256>;
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct ApprovalToken {
16 pub request_id: String,
18
19 pub code_hash: String,
21
22 pub user_id: String,
24
25 pub session_id: String,
27
28 pub server_id: String,
30
31 pub context_hash: String,
33
34 pub risk_level: RiskLevel,
36
37 pub created_at: i64,
39
40 pub expires_at: i64,
42
43 pub signature: String,
45}
46
47impl ApprovalToken {
48 pub fn encode(&self) -> Result<String, serde_json::Error> {
50 let json = serde_json::to_string(self)?;
51 Ok(base64::Engine::encode(
52 &base64::engine::general_purpose::URL_SAFE_NO_PAD,
53 json.as_bytes(),
54 ))
55 }
56
57 pub fn decode(encoded: &str) -> Result<Self, TokenDecodeError> {
59 let bytes =
60 base64::Engine::decode(&base64::engine::general_purpose::URL_SAFE_NO_PAD, encoded)
61 .map_err(|_| TokenDecodeError::InvalidBase64)?;
62
63 let json = String::from_utf8(bytes).map_err(|_| TokenDecodeError::InvalidUtf8)?;
64
65 serde_json::from_str(&json).map_err(|_| TokenDecodeError::InvalidJson)
66 }
67
68 fn payload_bytes(&self) -> Vec<u8> {
70 format!(
71 "{}|{}|{}|{}|{}|{}|{:?}|{}|{}",
72 self.request_id,
73 self.code_hash,
74 self.user_id,
75 self.session_id,
76 self.server_id,
77 self.context_hash,
78 self.risk_level,
79 self.created_at,
80 self.expires_at,
81 )
82 .into_bytes()
83 }
84}
85
86#[derive(Debug, thiserror::Error)]
88pub enum TokenDecodeError {
89 #[error(
90 "Token is not valid base64 — it may have been truncated or corrupted during transport"
91 )]
92 InvalidBase64,
93 #[error("Token contains invalid UTF-8 bytes after base64 decoding")]
94 InvalidUtf8,
95 #[error("Token decoded to invalid JSON — the token string may have been truncated, double-encoded, or is not an approval token")]
96 InvalidJson,
97}
98
99pub trait TokenGenerator: Send + Sync {
101 fn generate(
103 &self,
104 code: &str,
105 user_id: &str,
106 session_id: &str,
107 server_id: &str,
108 context_hash: &str,
109 risk_level: RiskLevel,
110 ttl_seconds: i64,
111 ) -> ApprovalToken;
112
113 fn verify(&self, token: &ApprovalToken) -> Result<(), ExecutionError>;
115
116 fn verify_code(&self, code: &str, token: &ApprovalToken) -> Result<(), ExecutionError>;
118}
119
120pub struct HmacTokenGenerator {
122 secret: Vec<u8>,
123}
124
125impl HmacTokenGenerator {
126 pub fn new(secret: impl Into<Vec<u8>>) -> Self {
128 Self {
129 secret: secret.into(),
130 }
131 }
132
133 pub fn from_env(env_var: &str) -> Result<Self, std::env::VarError> {
135 let secret = std::env::var(env_var)?;
136 Ok(Self::new(secret.into_bytes()))
137 }
138
139 fn sign(&self, payload: &[u8]) -> String {
141 let mut mac =
142 HmacSha256::new_from_slice(&self.secret).expect("HMAC can take key of any size");
143 mac.update(payload);
144 hex::encode(mac.finalize().into_bytes())
145 }
146
147 fn verify_signature(&self, payload: &[u8], signature: &str) -> bool {
149 let mut mac =
150 HmacSha256::new_from_slice(&self.secret).expect("HMAC can take key of any size");
151 mac.update(payload);
152
153 let expected = hex::decode(signature).unwrap_or_default();
154 mac.verify_slice(&expected).is_ok()
155 }
156}
157
158impl TokenGenerator for HmacTokenGenerator {
159 fn generate(
160 &self,
161 code: &str,
162 user_id: &str,
163 session_id: &str,
164 server_id: &str,
165 context_hash: &str,
166 risk_level: RiskLevel,
167 ttl_seconds: i64,
168 ) -> ApprovalToken {
169 let now = chrono::Utc::now().timestamp();
170
171 let mut token = ApprovalToken {
172 request_id: Uuid::new_v4().to_string(),
173 code_hash: hash_code(code),
174 user_id: user_id.to_string(),
175 session_id: session_id.to_string(),
176 server_id: server_id.to_string(),
177 context_hash: context_hash.to_string(),
178 risk_level,
179 created_at: now,
180 expires_at: now + ttl_seconds,
181 signature: String::new(),
182 };
183
184 token.signature = self.sign(&token.payload_bytes());
186 token
187 }
188
189 fn verify(&self, token: &ApprovalToken) -> Result<(), ExecutionError> {
190 let now = chrono::Utc::now().timestamp();
192 if now > token.expires_at {
193 return Err(ExecutionError::TokenExpired);
194 }
195
196 if !self.verify_signature(&token.payload_bytes(), &token.signature) {
198 return Err(ExecutionError::TokenInvalid(
199 "signature verification failed".into(),
200 ));
201 }
202
203 Ok(())
204 }
205
206 fn verify_code(&self, code: &str, token: &ApprovalToken) -> Result<(), ExecutionError> {
207 let current_hash = hash_code(code);
208 if current_hash != token.code_hash {
209 return Err(ExecutionError::CodeMismatch {
210 expected_hash: token.code_hash[..12].to_string(),
211 actual_hash: current_hash[..12].to_string(),
212 });
213 }
214 Ok(())
215 }
216}
217
218pub fn hash_code(code: &str) -> String {
223 use sha2::Digest;
224 let mut hasher = Sha256::new();
225 hasher.update(canonicalize_code(code).as_bytes());
226 hex::encode(hasher.finalize())
227}
228
229pub fn canonicalize_code(code: &str) -> String {
238 code.trim()
239 .lines()
240 .map(|line| line.trim())
241 .filter(|line| !line.is_empty())
242 .collect::<Vec<_>>()
243 .join("\n")
244}
245
246pub fn compute_context_hash(schema_hash: &str, permissions_hash: &str) -> String {
248 use sha2::Digest;
249 let mut hasher = Sha256::new();
250 hasher.update(schema_hash.as_bytes());
251 hasher.update(b"|");
252 hasher.update(permissions_hash.as_bytes());
253 hex::encode(hasher.finalize())
254}
255
256#[cfg(test)]
257mod tests {
258 use super::*;
259
260 #[test]
261 fn test_token_generation_and_verification() {
262 let generator = HmacTokenGenerator::new(b"test-secret-key".to_vec());
263
264 let token = generator.generate(
265 "query { users { id } }",
266 "user-123",
267 "session-456",
268 "server-789",
269 "context-hash",
270 RiskLevel::Low,
271 300,
272 );
273
274 assert!(generator.verify(&token).is_ok());
276
277 assert!(generator
279 .verify_code("query { users { id } }", &token)
280 .is_ok());
281 }
282
283 #[test]
284 fn test_code_mismatch() {
285 let generator = HmacTokenGenerator::new(b"test-secret-key".to_vec());
286
287 let token = generator.generate(
288 "query { users { id } }",
289 "user-123",
290 "session-456",
291 "server-789",
292 "context-hash",
293 RiskLevel::Low,
294 300,
295 );
296
297 let result = generator.verify_code("query { orders { id } }", &token);
299 assert!(matches!(result, Err(ExecutionError::CodeMismatch { .. })));
300 }
301
302 #[test]
303 fn test_token_encode_decode() {
304 let generator = HmacTokenGenerator::new(b"test-secret-key".to_vec());
305
306 let token = generator.generate(
307 "query { users { id } }",
308 "user-123",
309 "session-456",
310 "server-789",
311 "context-hash",
312 RiskLevel::Low,
313 300,
314 );
315
316 let encoded = token.encode().unwrap();
317 let decoded = ApprovalToken::decode(&encoded).unwrap();
318
319 assert_eq!(token.request_id, decoded.request_id);
320 assert_eq!(token.code_hash, decoded.code_hash);
321 assert_eq!(token.signature, decoded.signature);
322 }
323
324 #[test]
325 fn test_canonicalize_code() {
326 let code1 = "query { users { id } }";
327 let code2 = " query { users { id } } ";
328 let code3 = "query {\n users {\n id\n }\n}";
329
330 assert_eq!(canonicalize_code(code1), canonicalize_code(code2));
332
333 let canonical = canonicalize_code(code3);
335 assert!(canonical.contains("query {"));
336 assert!(canonical.contains("users {"));
337 }
338}