1use base64::Engine as _;
26use hmac::{Hmac, Mac};
27use sha2::Sha256;
28use std::sync::atomic::Ordering;
29
30pub const NONCE_BYTES: usize = 16;
33
34const TOKEN_ID_HEX_CHARS: usize = 16;
36
37const MAC_FIELD_SEPARATOR: u8 = 0x1f;
40
41pub const B64: base64::engine::general_purpose::GeneralPurpose =
44 base64::engine::general_purpose::URL_SAFE_NO_PAD;
45
46type HmacSha256 = Hmac<Sha256>;
47
48pub struct MintKey([u8; 32]);
55
56impl MintKey {
57 pub fn generate() -> Result<Self, String> {
59 let mut bytes = [0u8; 32];
60 getrandom::fill(&mut bytes)
61 .map_err(|error| format!("failed to generate a mint key: {error}"))?;
62 Ok(Self(bytes))
63 }
64
65 fn mac(&self) -> HmacSha256 {
67 HmacSha256::new_from_slice(&self.0).expect("HMAC-SHA256 accepts a key of any length")
68 }
69
70 #[allow(unsafe_code)]
75 fn zeroize(&mut self) {
76 for byte in self.0.iter_mut() {
77 unsafe { std::ptr::write_volatile(byte, 0) };
80 }
81 std::sync::atomic::compiler_fence(Ordering::SeqCst);
82 }
83}
84
85impl Drop for MintKey {
86 fn drop(&mut self) {
87 self.zeroize();
88 }
89}
90
91impl std::fmt::Debug for MintKey {
92 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
94 f.write_str("MintKey(<redacted>)")
95 }
96}
97
98#[derive(Debug, Clone, Copy, PartialEq, Eq)]
104pub enum MacTokenError {
105 Malformed,
107 NotValid,
109}
110
111pub fn token_id(token: &str) -> String {
116 murmur_artifact::sha256_hex(token.as_bytes())[..TOKEN_ID_HEX_CHARS].to_string()
117}
118
119pub fn random_hex(bytes: usize) -> Result<String, String> {
121 let mut buffer = vec![0u8; bytes];
122 getrandom::fill(&mut buffer).map_err(|error| format!("failed to generate a nonce: {error}"))?;
123 Ok(buffer
124 .iter()
125 .map(|byte| format!("{byte:02x}"))
126 .collect::<String>())
127}
128
129pub fn seal(
134 key: &MintKey,
135 tag: &str,
136 domain: &[u8],
137 payload_json: &[u8],
138 bound_to: &[&str],
139) -> String {
140 let payload_b64 = B64.encode(payload_json);
141 let mut mac = key.mac();
142 mac.update(&mac_input(domain, &payload_b64, bound_to));
143 let signature = B64.encode(mac.finalize().into_bytes());
144 format!("{tag}.{payload_b64}.{signature}")
145}
146
147pub fn open(
152 key: &MintKey,
153 tag: &str,
154 domain: &[u8],
155 token: &str,
156 bound_to: &[&str],
157) -> Result<Vec<u8>, MacTokenError> {
158 let (payload_b64, payload, signature) = split_token(tag, token)?;
159 let mut mac = key.mac();
160 mac.update(&mac_input(domain, payload_b64, bound_to));
161 mac.verify_slice(&signature)
162 .map_err(|_| MacTokenError::NotValid)?;
163 Ok(payload)
164}
165
166pub fn payload_segment(tag: &str, token: &str) -> Result<Vec<u8>, MacTokenError> {
172 split_token(tag, token).map(|(_, payload, _)| payload)
173}
174
175fn mac_input(domain: &[u8], payload_b64: &str, bound_to: &[&str]) -> Vec<u8> {
178 let mut input = Vec::with_capacity(domain.len() + payload_b64.len() + 1);
179 input.extend_from_slice(domain);
180 input.push(MAC_FIELD_SEPARATOR);
181 input.extend_from_slice(payload_b64.as_bytes());
182 for bound in bound_to {
183 input.push(MAC_FIELD_SEPARATOR);
184 input.extend_from_slice(bound.as_bytes());
185 }
186 input
187}
188
189fn split_token<'a>(
193 expected_tag: &str,
194 token: &'a str,
195) -> Result<(&'a str, Vec<u8>, Vec<u8>), MacTokenError> {
196 let mut segments = token.split('.');
197 let tag = segments.next().ok_or(MacTokenError::Malformed)?;
198 let payload_b64 = segments.next().ok_or(MacTokenError::Malformed)?;
199 let signature_b64 = segments.next().ok_or(MacTokenError::Malformed)?;
200 if segments.next().is_some() || tag != expected_tag {
201 return Err(MacTokenError::Malformed);
202 }
203 let payload = B64
204 .decode(payload_b64)
205 .map_err(|_| MacTokenError::Malformed)?;
206 let signature = B64
207 .decode(signature_b64)
208 .map_err(|_| MacTokenError::Malformed)?;
209 if payload.is_empty() || signature.is_empty() {
210 return Err(MacTokenError::Malformed);
211 }
212 Ok((payload_b64, payload, signature))
213}
214
215#[cfg(test)]
216mod tests {
217 use super::*;
218
219 const TAG: &str = "tst1";
220 const DOMAIN: &[u8] = b"murmur-mac-token-test";
221
222 #[test]
225 fn zeroizing_clears_every_key_byte() {
226 let mut key = MintKey([0xAB; 32]);
227 assert_ne!(key.0, [0u8; 32]);
228 key.zeroize();
229 assert_eq!(key.0, [0u8; 32]);
230 }
231
232 #[test]
235 fn a_generated_key_is_random() {
236 let one = MintKey::generate().unwrap();
237 let two = MintKey::generate().unwrap();
238 assert_ne!(one.0, [0u8; 32]);
239 assert_ne!(one.0, two.0);
240 }
241
242 #[test]
243 fn a_key_prints_no_material() {
244 assert_eq!(format!("{:?}", MintKey([0xAB; 32])), "MintKey(<redacted>)");
245 }
246
247 #[test]
248 fn a_sealed_token_opens_with_the_key_that_sealed_it() {
249 let key = MintKey::generate().unwrap();
250 let token = seal(&key, TAG, DOMAIN, b"{\"a\":1}", &[]);
251 assert!(token.starts_with("tst1."));
252 assert_eq!(open(&key, TAG, DOMAIN, &token, &[]).unwrap(), b"{\"a\":1}");
253 }
254
255 #[test]
256 fn another_key_does_not_open_it() {
257 let token = seal(&MintKey::generate().unwrap(), TAG, DOMAIN, b"{}", &[]);
258 assert_eq!(
259 open(&MintKey::generate().unwrap(), TAG, DOMAIN, &token, &[]),
260 Err(MacTokenError::NotValid)
261 );
262 }
263
264 #[test]
267 fn a_token_of_another_family_does_not_open() {
268 let key = MintKey::generate().unwrap();
269 let token = seal(&key, TAG, DOMAIN, b"{}", &[]);
270 assert_eq!(
271 open(&key, "oth1", DOMAIN, &token, &[]),
272 Err(MacTokenError::Malformed)
273 );
274 assert_eq!(
275 open(&key, TAG, b"murmur-other-domain", &token, &[]),
276 Err(MacTokenError::NotValid)
277 );
278 }
279
280 #[test]
283 fn a_bound_value_is_required_but_not_carried() {
284 let key = MintKey::generate().unwrap();
285 let token = seal(&key, TAG, DOMAIN, b"{}", &["reporter@localhost:1"]);
286 assert!(!token.contains("reporter"));
287 assert!(open(&key, TAG, DOMAIN, &token, &["reporter@localhost:1"]).is_ok());
288 assert_eq!(
289 open(&key, TAG, DOMAIN, &token, &["reporter@localhost:2"]),
290 Err(MacTokenError::NotValid)
291 );
292 assert_eq!(
293 open(&key, TAG, DOMAIN, &token, &[]),
294 Err(MacTokenError::NotValid)
295 );
296 }
297
298 #[test]
299 fn a_token_that_is_not_three_base64_segments_is_malformed() {
300 let key = MintKey::generate().unwrap();
301 for token in [
302 "",
303 "tst1",
304 "tst1.abc",
305 "tst1.abc.def.ghi",
306 "tst1..def",
307 "tst1.abc.",
308 "tst1.not base64!.def",
309 ] {
310 assert_eq!(
311 open(&key, TAG, DOMAIN, token, &[]),
312 Err(MacTokenError::Malformed),
313 "{token}"
314 );
315 }
316 }
317
318 #[test]
319 fn a_payload_segment_reads_without_the_key() {
320 let token = seal(
321 &MintKey::generate().unwrap(),
322 TAG,
323 DOMAIN,
324 b"{\"a\":1}",
325 &[],
326 );
327 assert_eq!(payload_segment(TAG, &token).unwrap(), b"{\"a\":1}");
328 assert_eq!(
329 payload_segment("oth1", &token),
330 Err(MacTokenError::Malformed)
331 );
332 }
333
334 #[test]
335 fn a_token_id_is_stable_and_distinguishing() {
336 let id = token_id("tst1.abc.def");
337 assert_eq!(id.len(), TOKEN_ID_HEX_CHARS);
338 assert_eq!(id, token_id("tst1.abc.def"));
339 assert_ne!(id, token_id("tst1.abc.deg"));
340 }
341
342 #[test]
343 fn a_nonce_is_hex_of_the_requested_width() {
344 let nonce = random_hex(NONCE_BYTES).unwrap();
345 assert_eq!(nonce.len(), NONCE_BYTES * 2);
346 assert!(nonce.chars().all(|c| c.is_ascii_hexdigit()));
347 assert_ne!(nonce, random_hex(NONCE_BYTES).unwrap());
348 }
349}