1use aes_gcm::aead::{Aead, KeyInit as AeadKeyInit};
11use aes_gcm::{Aes256Gcm, Nonce};
12use base64::engine::general_purpose::URL_SAFE_NO_PAD;
13use base64::Engine;
14use hmac::{Hmac, Mac};
15use rand::RngCore;
16use sha2::Sha256;
17
18const KEY_LEN: usize = 32;
19const NONCE_LEN: usize = 12;
20
21type HmacSha256 = Hmac<Sha256>;
22
23fn derive_key(app_key: &str) -> [u8; KEY_LEN] {
27 let mut out = [0u8; KEY_LEN];
28 let raw = if let Some(stripped) = app_key.strip_prefix("base64:") {
29 stripped
30 } else {
31 app_key
32 };
33
34 if let Ok(decoded) = URL_SAFE_NO_PAD.decode(raw.trim_end_matches('=')) {
35 let n = decoded.len().min(KEY_LEN);
36 out[..n].copy_from_slice(&decoded[..n]);
37 return out;
38 }
39 if let Ok(decoded) = base64::engine::general_purpose::STANDARD.decode(raw) {
40 let n = decoded.len().min(KEY_LEN);
41 out[..n].copy_from_slice(&decoded[..n]);
42 return out;
43 }
44
45 let bytes = raw.as_bytes();
46 let n = bytes.len().min(KEY_LEN);
47 out[..n].copy_from_slice(&bytes[..n]);
48 out
49}
50
51pub fn sign(app_key: &str, body: &[u8]) -> String {
53 let key = derive_key(app_key);
54 let mut mac = <HmacSha256 as Mac>::new_from_slice(&key).expect("hmac key");
55 mac.update(body);
56 let tag = mac.finalize().into_bytes();
57 URL_SAFE_NO_PAD.encode(tag)
58}
59
60pub fn verify(app_key: &str, body: &[u8], expected_b64: &str) -> bool {
62 let key = derive_key(app_key);
63 let mut mac = <HmacSha256 as Mac>::new_from_slice(&key).expect("hmac key");
64 mac.update(body);
65 let Ok(expected) = URL_SAFE_NO_PAD.decode(expected_b64) else {
66 return false;
67 };
68 mac.verify_slice(&expected).is_ok()
69}
70
71pub fn encrypt(app_key: &str, plaintext: &[u8]) -> Vec<u8> {
73 let key = derive_key(app_key);
74 let cipher = Aes256Gcm::new(&key.into());
75 let mut nonce_bytes = [0u8; NONCE_LEN];
76 rand::thread_rng().fill_bytes(&mut nonce_bytes);
77 let nonce = Nonce::from_slice(&nonce_bytes);
78 let ciphertext = cipher.encrypt(nonce, plaintext).expect("aes-gcm encrypt");
79 let mut out = Vec::with_capacity(NONCE_LEN + ciphertext.len());
80 out.extend_from_slice(&nonce_bytes);
81 out.extend_from_slice(&ciphertext);
82 out
83}
84
85pub fn decrypt(app_key: &str, blob: &[u8]) -> Option<Vec<u8>> {
87 if blob.len() < NONCE_LEN + 16 {
88 return None;
89 }
90 let key = derive_key(app_key);
91 let cipher = Aes256Gcm::new(&key.into());
92 let (nonce_bytes, ciphertext) = blob.split_at(NONCE_LEN);
93 let nonce = Nonce::from_slice(nonce_bytes);
94 cipher.decrypt(nonce, ciphertext).ok()
95}
96
97#[cfg(test)]
98mod tests {
99 use super::*;
100
101 const KEY: &str = "test-key-thirty-two-bytes-padded";
102
103 #[test]
104 fn sign_verify_round_trip() {
105 let body = b"hello world";
106 let sig = sign(KEY, body);
107 assert!(verify(KEY, body, &sig));
108 assert!(!verify(KEY, b"different", &sig));
109 }
110
111 #[test]
112 fn aes_gcm_round_trip() {
113 let body = b"some private state";
114 let blob = encrypt(KEY, body);
115 let recovered = decrypt(KEY, &blob).unwrap();
116 assert_eq!(recovered, body);
117 }
118}