use sha2::{Digest, Sha256};
use uuid::Uuid;
const BLOCK: usize = 64;
fn hmac_sha256(key: &[u8], message: &[u8]) -> [u8; 32] {
let mut block = [0u8; BLOCK];
if key.len() > BLOCK {
block[..32].copy_from_slice(&Sha256::digest(key));
} else {
block[..key.len()].copy_from_slice(key);
}
let mut ipad = [0x36u8; BLOCK];
let mut opad = [0x5cu8; BLOCK];
for i in 0..BLOCK {
ipad[i] ^= block[i];
opad[i] ^= block[i];
}
let mut inner = Sha256::new();
inner.update(ipad);
inner.update(message);
let inner = inner.finalize();
let mut outer = Sha256::new();
outer.update(opad);
outer.update(inner);
outer.finalize().into()
}
fn eq_ct(a: &[u8], b: &[u8]) -> bool {
if a.len() != b.len() {
return false;
}
a.iter().zip(b).fold(0u8, |acc, (x, y)| acc | (x ^ y)) == 0
}
pub fn issue(secret: &[u8], team_id: Uuid, now_unix: i64, ttl_secs: i64) -> String {
let payload = format!("{team_id}.{}", now_unix + ttl_secs);
let tag = hmac_sha256(secret, payload.as_bytes());
format!("{payload}.{}", hex::encode(tag))
}
pub fn verify(secret: &[u8], grant: &str, now_unix: i64) -> Option<Uuid> {
let (payload, tag_hex) = grant.rsplit_once('.')?;
let (team, expiry) = payload.rsplit_once('.')?;
let expected = hmac_sha256(secret, payload.as_bytes());
let presented = hex::decode(tag_hex).ok()?;
if !eq_ct(&expected, &presented) {
return None;
}
if expiry.parse::<i64>().ok()? <= now_unix {
return None;
}
Uuid::parse_str(team).ok()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hmac_matches_rfc4231_vectors() {
let tag = hmac_sha256(&[0x0b; 20], b"Hi There");
assert_eq!(
hex::encode(tag),
"b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7"
);
let tag = hmac_sha256(b"Jefe", b"what do ya want for nothing?");
assert_eq!(
hex::encode(tag),
"5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843"
);
let tag = hmac_sha256(
&[0xaa; 131],
b"Test Using Larger Than Block-Size Key - Hash Key First",
);
assert_eq!(
hex::encode(tag),
"60e431591ee0b67f0d8a26aacbf5b77f8e0bc6213728c5140546040f0ee37f54"
);
}
#[test]
fn round_trips_within_its_lifetime() {
let team = Uuid::new_v4();
let g = issue(b"secret", team, 1_000, 60);
assert_eq!(verify(b"secret", &g, 1_030), Some(team));
}
#[test]
fn rejects_expired_grants() {
let g = issue(b"secret", Uuid::new_v4(), 1_000, 60);
assert_eq!(verify(b"secret", &g, 1_061), None, "past its expiry");
}
#[test]
fn rejects_a_different_secret() {
let g = issue(b"secret", Uuid::new_v4(), 1_000, 60);
assert_eq!(verify(b"other", &g, 1_010), None);
}
#[test]
fn rejects_tampering() {
let team = Uuid::new_v4();
let g = issue(b"secret", team, 1_000, 60);
let (payload, tag) = g.rsplit_once('.').expect("shape");
let (team_part, _) = payload.rsplit_once('.').expect("shape");
let forged = format!("{team_part}.9999999999.{tag}");
assert_eq!(verify(b"secret", &forged, 1_010), None);
let other = Uuid::new_v4();
let forged = format!("{other}.{}", g.split_once('.').expect("shape").1);
assert_eq!(verify(b"secret", &forged, 1_010), None);
assert_eq!(verify(b"secret", "garbage", 1_010), None);
assert_eq!(verify(b"secret", "", 1_010), None);
}
}