1use base64::engine::general_purpose::URL_SAFE_NO_PAD;
4use base64::Engine;
5use hmac::{Hmac, Mac};
6use serde::{Deserialize, Serialize};
7use serde_json::json;
8use sha2::Sha256;
9
10use crate::error::Error;
11use crate::jcs::canonical_json;
12
13pub const VERSION: &str = "agent-pay/0.1";
14
15type HmacSha256 = Hmac<Sha256>;
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct TokenPayload {
19 pub v: String,
20 pub payment_hash: String,
21 pub expires_at: String,
22}
23
24fn b64url(data: &[u8]) -> String {
25 URL_SAFE_NO_PAD.encode(data)
26}
27
28fn b64url_decode(s: &str) -> Result<Vec<u8>, Error> {
29 URL_SAFE_NO_PAD
30 .decode(s)
31 .map_err(|e| Error::Token(format!("base64url: {e}")))
32}
33
34pub async fn issue_token(
35 payment_hash: &str,
36 expires_at: &str,
37 secret: &[u8],
38) -> Result<String, Error> {
39 let payload = json!({
40 "v": VERSION,
41 "payment_hash": payment_hash,
42 "expires_at": expires_at,
43 });
44 let payload_bytes = canonical_json(&payload)?;
45 let mut mac =
46 HmacSha256::new_from_slice(secret).map_err(|e| Error::Token(format!("hmac key: {e}")))?;
47 mac.update(&payload_bytes);
48 let sig = mac.finalize().into_bytes();
49 Ok(format!("{}.{}", b64url(&payload_bytes), b64url(&sig)))
50}
51
52pub async fn verify_token(token: &str, secret: &[u8]) -> Result<TokenPayload, Error> {
53 let parts: Vec<&str> = token.split('.').collect();
54 if parts.len() != 2 {
55 return Err(Error::Token("token must have 2 parts".into()));
56 }
57 let payload_bytes = b64url_decode(parts[0])?;
58 let got = b64url_decode(parts[1])?;
59 let mut mac =
60 HmacSha256::new_from_slice(secret).map_err(|e| Error::Token(format!("hmac key: {e}")))?;
61 mac.update(&payload_bytes);
62 mac.verify_slice(&got)
63 .map_err(|_| Error::Token("token HMAC verification failed".into()))?;
64 let payload: TokenPayload = serde_json::from_slice(&payload_bytes)
65 .map_err(|e| Error::Token(format!("invalid token payload: {e}")))?;
66 if payload.v != VERSION {
67 return Err(Error::Token(format!(
68 "unsupported token version: {}",
69 payload.v
70 )));
71 }
72 Ok(payload)
73}