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 fn zeroize(&mut self) {
72 zeroize(&mut self.0);
73 }
74}
75
76#[allow(unsafe_code)]
81pub(crate) fn zeroize(bytes: &mut [u8]) {
82 for byte in bytes.iter_mut() {
83 unsafe { std::ptr::write_volatile(byte, 0) };
86 }
87 std::sync::atomic::compiler_fence(Ordering::SeqCst);
88}
89
90impl Drop for MintKey {
91 fn drop(&mut self) {
92 self.zeroize();
93 }
94}
95
96impl std::fmt::Debug for MintKey {
97 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
99 f.write_str("MintKey(<redacted>)")
100 }
101}
102
103#[derive(Debug, Clone, Copy, PartialEq, Eq)]
109pub enum MacTokenError {
110 Malformed,
112 NotValid,
114}
115
116pub fn token_id(token: &str) -> String {
121 murmur_artifact::sha256_hex(token.as_bytes())[..TOKEN_ID_HEX_CHARS].to_string()
122}
123
124pub fn random_hex(bytes: usize) -> Result<String, String> {
126 let mut buffer = vec![0u8; bytes];
127 getrandom::fill(&mut buffer).map_err(|error| format!("failed to generate a nonce: {error}"))?;
128 Ok(buffer
129 .iter()
130 .map(|byte| format!("{byte:02x}"))
131 .collect::<String>())
132}
133
134pub fn seal(
139 key: &MintKey,
140 tag: &str,
141 domain: &[u8],
142 payload_json: &[u8],
143 bound_to: &[&str],
144) -> String {
145 let payload_b64 = B64.encode(payload_json);
146 let mut mac = key.mac();
147 mac.update(&mac_input(domain, &payload_b64, bound_to));
148 let signature = B64.encode(mac.finalize().into_bytes());
149 format!("{tag}.{payload_b64}.{signature}")
150}
151
152pub fn open(
157 key: &MintKey,
158 tag: &str,
159 domain: &[u8],
160 token: &str,
161 bound_to: &[&str],
162) -> Result<Vec<u8>, MacTokenError> {
163 let (payload_b64, payload, signature) = split_token(tag, token)?;
164 let mut mac = key.mac();
165 mac.update(&mac_input(domain, payload_b64, bound_to));
166 mac.verify_slice(&signature)
167 .map_err(|_| MacTokenError::NotValid)?;
168 Ok(payload)
169}
170
171pub fn payload_segment(tag: &str, token: &str) -> Result<Vec<u8>, MacTokenError> {
177 split_token(tag, token).map(|(_, payload, _)| payload)
178}
179
180fn mac_input(domain: &[u8], payload_b64: &str, bound_to: &[&str]) -> Vec<u8> {
183 let mut input = Vec::with_capacity(domain.len() + payload_b64.len() + 1);
184 input.extend_from_slice(domain);
185 input.push(MAC_FIELD_SEPARATOR);
186 input.extend_from_slice(payload_b64.as_bytes());
187 for bound in bound_to {
188 input.push(MAC_FIELD_SEPARATOR);
189 input.extend_from_slice(bound.as_bytes());
190 }
191 input
192}
193
194pub(crate) fn split_token<'a>(
198 expected_tag: &str,
199 token: &'a str,
200) -> Result<(&'a str, Vec<u8>, Vec<u8>), MacTokenError> {
201 let mut segments = token.split('.');
202 let tag = segments.next().ok_or(MacTokenError::Malformed)?;
203 let payload_b64 = segments.next().ok_or(MacTokenError::Malformed)?;
204 let signature_b64 = segments.next().ok_or(MacTokenError::Malformed)?;
205 if segments.next().is_some() || tag != expected_tag {
206 return Err(MacTokenError::Malformed);
207 }
208 let payload = B64
209 .decode(payload_b64)
210 .map_err(|_| MacTokenError::Malformed)?;
211 let signature = B64
212 .decode(signature_b64)
213 .map_err(|_| MacTokenError::Malformed)?;
214 if payload.is_empty() || signature.is_empty() {
215 return Err(MacTokenError::Malformed);
216 }
217 Ok((payload_b64, payload, signature))
218}
219
220#[cfg(test)]
221mod tests {
222 use super::*;
223
224 const TAG: &str = "tst1";
225 const DOMAIN: &[u8] = b"murmur-mac-token-test";
226
227 #[test]
230 fn zeroizing_clears_every_key_byte() {
231 let mut key = MintKey([0xAB; 32]);
232 assert_ne!(key.0, [0u8; 32]);
233 key.zeroize();
234 assert_eq!(key.0, [0u8; 32]);
235 }
236
237 #[test]
240 fn a_generated_key_is_random() {
241 let one = MintKey::generate().unwrap();
242 let two = MintKey::generate().unwrap();
243 assert_ne!(one.0, [0u8; 32]);
244 assert_ne!(one.0, two.0);
245 }
246
247 #[test]
248 fn a_key_prints_no_material() {
249 assert_eq!(format!("{:?}", MintKey([0xAB; 32])), "MintKey(<redacted>)");
250 }
251
252 #[test]
253 fn a_sealed_token_opens_with_the_key_that_sealed_it() {
254 let key = MintKey::generate().unwrap();
255 let token = seal(&key, TAG, DOMAIN, b"{\"a\":1}", &[]);
256 assert!(token.starts_with("tst1."));
257 assert_eq!(open(&key, TAG, DOMAIN, &token, &[]).unwrap(), b"{\"a\":1}");
258 }
259
260 #[test]
261 fn another_key_does_not_open_it() {
262 let token = seal(&MintKey::generate().unwrap(), TAG, DOMAIN, b"{}", &[]);
263 assert_eq!(
264 open(&MintKey::generate().unwrap(), TAG, DOMAIN, &token, &[]),
265 Err(MacTokenError::NotValid)
266 );
267 }
268
269 #[test]
272 fn a_token_of_another_family_does_not_open() {
273 let key = MintKey::generate().unwrap();
274 let token = seal(&key, TAG, DOMAIN, b"{}", &[]);
275 assert_eq!(
276 open(&key, "oth1", DOMAIN, &token, &[]),
277 Err(MacTokenError::Malformed)
278 );
279 assert_eq!(
280 open(&key, TAG, b"murmur-other-domain", &token, &[]),
281 Err(MacTokenError::NotValid)
282 );
283 }
284
285 #[test]
288 fn a_bound_value_is_required_but_not_carried() {
289 let key = MintKey::generate().unwrap();
290 let token = seal(&key, TAG, DOMAIN, b"{}", &["reporter@localhost:1"]);
291 assert!(!token.contains("reporter"));
292 assert!(open(&key, TAG, DOMAIN, &token, &["reporter@localhost:1"]).is_ok());
293 assert_eq!(
294 open(&key, TAG, DOMAIN, &token, &["reporter@localhost:2"]),
295 Err(MacTokenError::NotValid)
296 );
297 assert_eq!(
298 open(&key, TAG, DOMAIN, &token, &[]),
299 Err(MacTokenError::NotValid)
300 );
301 }
302
303 #[test]
304 fn a_token_that_is_not_three_base64_segments_is_malformed() {
305 let key = MintKey::generate().unwrap();
306 for token in [
307 "",
308 "tst1",
309 "tst1.abc",
310 "tst1.abc.def.ghi",
311 "tst1..def",
312 "tst1.abc.",
313 "tst1.not base64!.def",
314 ] {
315 assert_eq!(
316 open(&key, TAG, DOMAIN, token, &[]),
317 Err(MacTokenError::Malformed),
318 "{token}"
319 );
320 }
321 }
322
323 #[test]
324 fn a_payload_segment_reads_without_the_key() {
325 let token = seal(
326 &MintKey::generate().unwrap(),
327 TAG,
328 DOMAIN,
329 b"{\"a\":1}",
330 &[],
331 );
332 assert_eq!(payload_segment(TAG, &token).unwrap(), b"{\"a\":1}");
333 assert_eq!(
334 payload_segment("oth1", &token),
335 Err(MacTokenError::Malformed)
336 );
337 }
338
339 #[test]
340 fn a_token_id_is_stable_and_distinguishing() {
341 let id = token_id("tst1.abc.def");
342 assert_eq!(id.len(), TOKEN_ID_HEX_CHARS);
343 assert_eq!(id, token_id("tst1.abc.def"));
344 assert_ne!(id, token_id("tst1.abc.deg"));
345 }
346
347 #[test]
348 fn a_nonce_is_hex_of_the_requested_width() {
349 let nonce = random_hex(NONCE_BYTES).unwrap();
350 assert_eq!(nonce.len(), NONCE_BYTES * 2);
351 assert!(nonce.chars().all(|c| c.is_ascii_hexdigit()));
352 assert_ne!(nonce, random_hex(NONCE_BYTES).unwrap());
353 }
354}