use base64::Engine as _;
use hmac::{Hmac, Mac};
use sha2::Sha256;
use std::sync::atomic::Ordering;
pub const NONCE_BYTES: usize = 16;
const TOKEN_ID_HEX_CHARS: usize = 16;
const MAC_FIELD_SEPARATOR: u8 = 0x1f;
pub const B64: base64::engine::general_purpose::GeneralPurpose =
base64::engine::general_purpose::URL_SAFE_NO_PAD;
type HmacSha256 = Hmac<Sha256>;
pub struct MintKey([u8; 32]);
impl MintKey {
pub fn generate() -> Result<Self, String> {
let mut bytes = [0u8; 32];
getrandom::fill(&mut bytes)
.map_err(|error| format!("failed to generate a mint key: {error}"))?;
Ok(Self(bytes))
}
fn mac(&self) -> HmacSha256 {
HmacSha256::new_from_slice(&self.0).expect("HMAC-SHA256 accepts a key of any length")
}
fn zeroize(&mut self) {
zeroize(&mut self.0);
}
}
#[allow(unsafe_code)]
pub(crate) fn zeroize(bytes: &mut [u8]) {
for byte in bytes.iter_mut() {
unsafe { std::ptr::write_volatile(byte, 0) };
}
std::sync::atomic::compiler_fence(Ordering::SeqCst);
}
impl Drop for MintKey {
fn drop(&mut self) {
self.zeroize();
}
}
impl std::fmt::Debug for MintKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("MintKey(<redacted>)")
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MacTokenError {
Malformed,
NotValid,
}
pub fn token_id(token: &str) -> String {
murmur_artifact::sha256_hex(token.as_bytes())[..TOKEN_ID_HEX_CHARS].to_string()
}
pub fn random_hex(bytes: usize) -> Result<String, String> {
let mut buffer = vec![0u8; bytes];
getrandom::fill(&mut buffer).map_err(|error| format!("failed to generate a nonce: {error}"))?;
Ok(buffer
.iter()
.map(|byte| format!("{byte:02x}"))
.collect::<String>())
}
pub fn seal(
key: &MintKey,
tag: &str,
domain: &[u8],
payload_json: &[u8],
bound_to: &[&str],
) -> String {
let payload_b64 = B64.encode(payload_json);
let mut mac = key.mac();
mac.update(&mac_input(domain, &payload_b64, bound_to));
let signature = B64.encode(mac.finalize().into_bytes());
format!("{tag}.{payload_b64}.{signature}")
}
pub fn open(
key: &MintKey,
tag: &str,
domain: &[u8],
token: &str,
bound_to: &[&str],
) -> Result<Vec<u8>, MacTokenError> {
let (payload_b64, payload, signature) = split_token(tag, token)?;
let mut mac = key.mac();
mac.update(&mac_input(domain, payload_b64, bound_to));
mac.verify_slice(&signature)
.map_err(|_| MacTokenError::NotValid)?;
Ok(payload)
}
pub fn payload_segment(tag: &str, token: &str) -> Result<Vec<u8>, MacTokenError> {
split_token(tag, token).map(|(_, payload, _)| payload)
}
fn mac_input(domain: &[u8], payload_b64: &str, bound_to: &[&str]) -> Vec<u8> {
let mut input = Vec::with_capacity(domain.len() + payload_b64.len() + 1);
input.extend_from_slice(domain);
input.push(MAC_FIELD_SEPARATOR);
input.extend_from_slice(payload_b64.as_bytes());
for bound in bound_to {
input.push(MAC_FIELD_SEPARATOR);
input.extend_from_slice(bound.as_bytes());
}
input
}
fn split_token<'a>(
expected_tag: &str,
token: &'a str,
) -> Result<(&'a str, Vec<u8>, Vec<u8>), MacTokenError> {
let mut segments = token.split('.');
let tag = segments.next().ok_or(MacTokenError::Malformed)?;
let payload_b64 = segments.next().ok_or(MacTokenError::Malformed)?;
let signature_b64 = segments.next().ok_or(MacTokenError::Malformed)?;
if segments.next().is_some() || tag != expected_tag {
return Err(MacTokenError::Malformed);
}
let payload = B64
.decode(payload_b64)
.map_err(|_| MacTokenError::Malformed)?;
let signature = B64
.decode(signature_b64)
.map_err(|_| MacTokenError::Malformed)?;
if payload.is_empty() || signature.is_empty() {
return Err(MacTokenError::Malformed);
}
Ok((payload_b64, payload, signature))
}
#[cfg(test)]
mod tests {
use super::*;
const TAG: &str = "tst1";
const DOMAIN: &[u8] = b"murmur-mac-token-test";
#[test]
fn zeroizing_clears_every_key_byte() {
let mut key = MintKey([0xAB; 32]);
assert_ne!(key.0, [0u8; 32]);
key.zeroize();
assert_eq!(key.0, [0u8; 32]);
}
#[test]
fn a_generated_key_is_random() {
let one = MintKey::generate().unwrap();
let two = MintKey::generate().unwrap();
assert_ne!(one.0, [0u8; 32]);
assert_ne!(one.0, two.0);
}
#[test]
fn a_key_prints_no_material() {
assert_eq!(format!("{:?}", MintKey([0xAB; 32])), "MintKey(<redacted>)");
}
#[test]
fn a_sealed_token_opens_with_the_key_that_sealed_it() {
let key = MintKey::generate().unwrap();
let token = seal(&key, TAG, DOMAIN, b"{\"a\":1}", &[]);
assert!(token.starts_with("tst1."));
assert_eq!(open(&key, TAG, DOMAIN, &token, &[]).unwrap(), b"{\"a\":1}");
}
#[test]
fn another_key_does_not_open_it() {
let token = seal(&MintKey::generate().unwrap(), TAG, DOMAIN, b"{}", &[]);
assert_eq!(
open(&MintKey::generate().unwrap(), TAG, DOMAIN, &token, &[]),
Err(MacTokenError::NotValid)
);
}
#[test]
fn a_token_of_another_family_does_not_open() {
let key = MintKey::generate().unwrap();
let token = seal(&key, TAG, DOMAIN, b"{}", &[]);
assert_eq!(
open(&key, "oth1", DOMAIN, &token, &[]),
Err(MacTokenError::Malformed)
);
assert_eq!(
open(&key, TAG, b"murmur-other-domain", &token, &[]),
Err(MacTokenError::NotValid)
);
}
#[test]
fn a_bound_value_is_required_but_not_carried() {
let key = MintKey::generate().unwrap();
let token = seal(&key, TAG, DOMAIN, b"{}", &["reporter@localhost:1"]);
assert!(!token.contains("reporter"));
assert!(open(&key, TAG, DOMAIN, &token, &["reporter@localhost:1"]).is_ok());
assert_eq!(
open(&key, TAG, DOMAIN, &token, &["reporter@localhost:2"]),
Err(MacTokenError::NotValid)
);
assert_eq!(
open(&key, TAG, DOMAIN, &token, &[]),
Err(MacTokenError::NotValid)
);
}
#[test]
fn a_token_that_is_not_three_base64_segments_is_malformed() {
let key = MintKey::generate().unwrap();
for token in [
"",
"tst1",
"tst1.abc",
"tst1.abc.def.ghi",
"tst1..def",
"tst1.abc.",
"tst1.not base64!.def",
] {
assert_eq!(
open(&key, TAG, DOMAIN, token, &[]),
Err(MacTokenError::Malformed),
"{token}"
);
}
}
#[test]
fn a_payload_segment_reads_without_the_key() {
let token = seal(
&MintKey::generate().unwrap(),
TAG,
DOMAIN,
b"{\"a\":1}",
&[],
);
assert_eq!(payload_segment(TAG, &token).unwrap(), b"{\"a\":1}");
assert_eq!(
payload_segment("oth1", &token),
Err(MacTokenError::Malformed)
);
}
#[test]
fn a_token_id_is_stable_and_distinguishing() {
let id = token_id("tst1.abc.def");
assert_eq!(id.len(), TOKEN_ID_HEX_CHARS);
assert_eq!(id, token_id("tst1.abc.def"));
assert_ne!(id, token_id("tst1.abc.deg"));
}
#[test]
fn a_nonce_is_hex_of_the_requested_width() {
let nonce = random_hex(NONCE_BYTES).unwrap();
assert_eq!(nonce.len(), NONCE_BYTES * 2);
assert!(nonce.chars().all(|c| c.is_ascii_hexdigit()));
assert_ne!(nonce, random_hex(NONCE_BYTES).unwrap());
}
}