1mod config;
2mod error;
3mod signer;
4
5pub use config::EncryptConfig;
6pub use error::EncryptError;
7pub use signer::Signer;
8
9use std::time::Duration;
10
11use aes_gcm::{
12 aead::{Aead, KeyInit},
13 Aes256Gcm, Key, Nonce,
14};
15use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
16use serde::{Deserialize, Serialize};
17use sha2::{Digest, Sha256};
18
19#[derive(Serialize, Deserialize)]
22struct Payload {
23 v: String,
25 #[serde(skip_serializing_if = "Option::is_none")]
27 p: Option<String>,
28 #[serde(skip_serializing_if = "Option::is_none")]
30 e: Option<i64>,
31}
32
33fn derive_key(secret: &str) -> [u8; 32] {
36 Sha256::digest(secret.as_bytes()).into()
37}
38
39#[derive(Clone)]
72pub struct Encrypter {
73 primary_key: [u8; 32],
74 old_keys: Vec<[u8; 32]>,
75}
76
77impl Encrypter {
78 pub fn from_config(config: EncryptConfig) -> Self {
80 Self {
81 primary_key: derive_key(&config.key),
82 old_keys: config.old_keys.iter().map(|k| derive_key(k)).collect(),
83 }
84 }
85
86 fn cipher(key: &[u8; 32]) -> Aes256Gcm {
89 Aes256Gcm::new(Key::<Aes256Gcm>::from_slice(key))
90 }
91
92 fn encrypt_payload(&self, payload: &Payload) -> String {
93 let json = serde_json::to_vec(payload).expect("Payload is always serialisable");
94 let nonce_bytes: [u8; 12] = rand::random();
95 let nonce = Nonce::from_slice(&nonce_bytes);
96 let mut ct = Self::cipher(&self.primary_key)
97 .encrypt(nonce, json.as_slice())
98 .expect("AES-256-GCM encryption is infallible for valid keys");
99 let mut out = nonce_bytes.to_vec();
100 out.append(&mut ct);
101 URL_SAFE_NO_PAD.encode(out)
102 }
103
104 fn decrypt_token(&self, token: &str) -> Result<Payload, EncryptError> {
105 let bytes = URL_SAFE_NO_PAD
106 .decode(token)
107 .map_err(|_| EncryptError::InvalidFormat)?;
108 if bytes.len() <= 12 {
109 return Err(EncryptError::InvalidFormat);
110 }
111 let (nonce_bytes, ciphertext) = bytes.split_at(12);
112 let nonce = Nonce::from_slice(nonce_bytes);
113
114 let keys = std::iter::once(&self.primary_key).chain(self.old_keys.iter());
116 for key in keys {
117 if let Ok(plaintext) = Self::cipher(key).decrypt(nonce, ciphertext) {
118 return serde_json::from_slice(&plaintext).map_err(|_| EncryptError::InvalidFormat);
119 }
120 }
121 Err(EncryptError::DecryptionFailed)
122 }
123
124 fn check_expiry(payload: &Payload) -> Result<(), EncryptError> {
125 if let Some(exp) = payload.e {
126 if chrono::Utc::now().timestamp() > exp {
127 return Err(EncryptError::Expired);
128 }
129 }
130 Ok(())
131 }
132
133 pub fn seal(&self, value: &str) -> String {
137 self.encrypt_payload(&Payload {
138 v: value.to_string(),
139 p: None,
140 e: None,
141 })
142 }
143
144 pub fn open(&self, token: &str) -> Result<String, EncryptError> {
148 let payload = self.decrypt_token(token)?;
149 Self::check_expiry(&payload)?;
150 Ok(payload.v)
151 }
152
153 pub fn try_open(&self, token: &str) -> Option<String> {
155 self.open(token).ok()
156 }
157
158 pub fn seal_for(&self, purpose: &str, value: &str) -> String {
165 self.encrypt_payload(&Payload {
166 v: value.to_string(),
167 p: Some(purpose.to_string()),
168 e: None,
169 })
170 }
171
172 pub fn open_for(&self, expected_purpose: &str, token: &str) -> Result<String, EncryptError> {
177 let payload = self.decrypt_token(token)?;
178 let actual = payload.p.as_deref().unwrap_or("(none)");
179 if actual != expected_purpose {
180 return Err(EncryptError::WrongPurpose {
181 expected: expected_purpose.to_string(),
182 actual: actual.to_string(),
183 });
184 }
185 Self::check_expiry(&payload)?;
186 Ok(payload.v)
187 }
188
189 pub fn seal_expiring(&self, value: &str, ttl: Duration) -> String {
195 let expires_at = chrono::Utc::now().timestamp() + ttl.as_secs() as i64;
196 self.encrypt_payload(&Payload {
197 v: value.to_string(),
198 p: None,
199 e: Some(expires_at),
200 })
201 }
202
203 pub fn seal_for_expiring(&self, purpose: &str, value: &str, ttl: Duration) -> String {
205 let expires_at = chrono::Utc::now().timestamp() + ttl.as_secs() as i64;
206 self.encrypt_payload(&Payload {
207 v: value.to_string(),
208 p: Some(purpose.to_string()),
209 e: Some(expires_at),
210 })
211 }
212}