1use chrono::{DateTime, Duration, Utc};
13use serde::{Deserialize, Serialize};
14
15use super::sign::{IdentityKeypair, PublicKey};
16use crate::error::JoyError;
17
18const TOKEN_PREFIX: &str = "joy_t_";
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct DelegationClaims {
24 pub ai_member: String,
25 pub delegated_by: String,
26 pub project_id: String,
27 pub created: DateTime<Utc>,
28 pub expires: Option<DateTime<Utc>>,
29}
30
31#[derive(Debug, Serialize, Deserialize)]
33pub struct DelegationToken {
34 pub claims: DelegationClaims,
35 pub delegator_signature: String,
37 pub binding_signature: String,
39 pub token_public_key: String,
41}
42
43pub struct CreateTokenResult {
45 pub token: DelegationToken,
46 pub token_public_key: String,
47}
48
49pub fn create_token(
53 delegator_keypair: &IdentityKeypair,
54 ai_member: &str,
55 human: &str,
56 project_id: &str,
57 ttl: Option<Duration>,
58) -> CreateTokenResult {
59 let now = Utc::now();
60 let claims = DelegationClaims {
61 ai_member: ai_member.to_string(),
62 delegated_by: human.to_string(),
63 project_id: project_id.to_string(),
64 created: now,
65 expires: ttl.map(|d| now + d),
66 };
67 let claims_json = serde_json::to_string(&claims).expect("claims serialize");
68
69 let delegator_sig = delegator_keypair.sign(claims_json.as_bytes());
71
72 let token_keypair = IdentityKeypair::from_random();
74 let binding_sig = token_keypair.sign(claims_json.as_bytes());
75 let token_pk = token_keypair.public_key();
76
77 CreateTokenResult {
78 token: DelegationToken {
79 claims,
80 delegator_signature: hex::encode(delegator_sig),
81 binding_signature: hex::encode(binding_sig),
82 token_public_key: token_pk.to_hex(),
83 },
84 token_public_key: token_pk.to_hex(),
85 }
86}
87
88pub fn validate_token(
90 token: &DelegationToken,
91 delegator_pk: &PublicKey,
92 token_pk: &PublicKey,
93 project_id: &str,
94) -> Result<DelegationClaims, JoyError> {
95 if token.claims.project_id != project_id {
97 return Err(JoyError::AuthFailed(
98 "token belongs to a different project".into(),
99 ));
100 }
101
102 if let Some(expires) = token.claims.expires {
104 if Utc::now() > expires {
105 return Err(JoyError::AuthFailed("delegation token expired".into()));
106 }
107 }
108
109 let claims_json = serde_json::to_string(&token.claims).expect("claims serialize");
110
111 let delegator_sig = hex::decode(&token.delegator_signature)
113 .map_err(|e| JoyError::AuthFailed(format!("{e}")))?;
114 delegator_pk.verify(claims_json.as_bytes(), &delegator_sig)?;
115
116 let binding_sig =
118 hex::decode(&token.binding_signature).map_err(|e| JoyError::AuthFailed(format!("{e}")))?;
119 token_pk.verify(claims_json.as_bytes(), &binding_sig)?;
120
121 Ok(token.claims.clone())
122}
123
124pub fn encode_token(token: &DelegationToken) -> String {
126 let json = serde_json::to_string(token).expect("token serialize");
127 let encoded = base64_encode(json.as_bytes());
128 format!("{TOKEN_PREFIX}{encoded}")
129}
130
131pub fn decode_token(s: &str) -> Result<DelegationToken, JoyError> {
133 let data = s.strip_prefix(TOKEN_PREFIX).ok_or_else(|| {
134 JoyError::AuthFailed("invalid token format (missing joy_t_ prefix)".into())
135 })?;
136 let json = base64_decode(data)?;
137 let token: DelegationToken = serde_json::from_slice(&json)
138 .map_err(|e| JoyError::AuthFailed(format!("invalid token: {e}")))?;
139 Ok(token)
140}
141
142pub fn is_token(s: &str) -> bool {
144 s.starts_with(TOKEN_PREFIX)
145}
146
147fn base64_encode(data: &[u8]) -> String {
148 use base64ct::{Base64, Encoding};
149 Base64::encode_string(data)
150}
151
152fn base64_decode(s: &str) -> Result<Vec<u8>, JoyError> {
153 use base64ct::{Base64, Encoding};
154 Base64::decode_vec(s).map_err(|e| JoyError::AuthFailed(format!("base64 decode: {e}")))
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160 use crate::auth::{derive, sign};
161 use chrono::Duration;
162
163 const TEST_PASSPHRASE: &str = "correct horse battery staple extra words";
164
165 fn test_keypair() -> (sign::IdentityKeypair, sign::PublicKey) {
166 let salt = derive::Salt::from_hex(
167 "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
168 )
169 .unwrap();
170 let key = derive::derive_key(TEST_PASSPHRASE, &salt).unwrap();
171 let kp = sign::IdentityKeypair::from_derived_key(&key);
172 let pk = kp.public_key();
173 (kp, pk)
174 }
175
176 #[test]
177 fn create_and_validate_token() {
178 let (kp, pk) = test_keypair();
179 let result = create_token(&kp, "ai:claude@joy", "human@example.com", "TST", None);
180 let token_pk = sign::PublicKey::from_hex(&result.token_public_key).unwrap();
181 let claims = validate_token(&result.token, &pk, &token_pk, "TST").unwrap();
182 assert_eq!(claims.ai_member, "ai:claude@joy");
183 assert_eq!(claims.delegated_by, "human@example.com");
184 }
185
186 #[test]
187 fn token_with_expiry() {
188 let (kp, pk) = test_keypair();
189 let result = create_token(
190 &kp,
191 "ai:claude@joy",
192 "human@example.com",
193 "TST",
194 Some(Duration::hours(8)),
195 );
196 let token_pk = sign::PublicKey::from_hex(&result.token_public_key).unwrap();
197 let claims = validate_token(&result.token, &pk, &token_pk, "TST").unwrap();
198 assert!(claims.expires.is_some());
199 }
200
201 #[test]
202 fn expired_token_rejected() {
203 let (kp, pk) = test_keypair();
204 let result = create_token(
205 &kp,
206 "ai:claude@joy",
207 "human@example.com",
208 "TST",
209 Some(Duration::seconds(-1)),
210 );
211 let token_pk = sign::PublicKey::from_hex(&result.token_public_key).unwrap();
212 assert!(validate_token(&result.token, &pk, &token_pk, "TST").is_err());
213 }
214
215 #[test]
216 fn wrong_project_rejected() {
217 let (kp, pk) = test_keypair();
218 let result = create_token(&kp, "ai:claude@joy", "human@example.com", "TST", None);
219 let token_pk = sign::PublicKey::from_hex(&result.token_public_key).unwrap();
220 assert!(validate_token(&result.token, &pk, &token_pk, "OTHER").is_err());
221 }
222
223 #[test]
224 fn tampered_claims_rejected() {
225 let (kp, pk) = test_keypair();
226 let result = create_token(&kp, "ai:claude@joy", "human@example.com", "TST", None);
227 let token_pk = sign::PublicKey::from_hex(&result.token_public_key).unwrap();
228 let mut token = result.token;
229 token.claims.ai_member = "ai:attacker@evil".into();
230 assert!(validate_token(&token, &pk, &token_pk, "TST").is_err());
231 }
232
233 #[test]
234 fn wrong_delegator_key_rejected() {
235 let (kp, _) = test_keypair();
236 let result = create_token(&kp, "ai:claude@joy", "human@example.com", "TST", None);
237 let token_pk = sign::PublicKey::from_hex(&result.token_public_key).unwrap();
238
239 let other_salt = derive::generate_salt();
241 let other_key =
242 derive::derive_key("alpha bravo charlie delta echo foxtrot", &other_salt).unwrap();
243 let other_kp = sign::IdentityKeypair::from_derived_key(&other_key);
244 let other_pk = other_kp.public_key();
245
246 assert!(validate_token(&result.token, &other_pk, &token_pk, "TST").is_err());
247 }
248
249 #[test]
250 fn wrong_token_key_rejected() {
251 let (kp, pk) = test_keypair();
252 let result = create_token(&kp, "ai:claude@joy", "human@example.com", "TST", None);
253
254 let wrong_token_kp = sign::IdentityKeypair::from_random();
256 let wrong_token_pk = wrong_token_kp.public_key();
257
258 assert!(validate_token(&result.token, &pk, &wrong_token_pk, "TST").is_err());
259 }
260
261 #[test]
262 fn encode_decode_roundtrip() {
263 let (kp, pk) = test_keypair();
264 let result = create_token(&kp, "ai:claude@joy", "human@example.com", "TST", None);
265 let encoded = encode_token(&result.token);
266 assert!(encoded.starts_with("joy_t_"));
267 let decoded = decode_token(&encoded).unwrap();
268 let token_pk = sign::PublicKey::from_hex(&result.token_public_key).unwrap();
269 let claims = validate_token(&decoded, &pk, &token_pk, "TST").unwrap();
270 assert_eq!(claims.ai_member, "ai:claude@joy");
271 }
272
273 #[test]
274 fn invalid_prefix_rejected() {
275 assert!(decode_token("invalid_prefix_data").is_err());
276 }
277}