use hmac::{Hmac, Mac};
use sha2::Sha256;
use subtle::ConstantTimeEq;
use crate::types::{CapPayload, Key, SealedTag};
type HmacSha256 = Hmac<Sha256>;
#[allow(dead_code)] #[allow(clippy::expect_used)] pub(crate) fn seal_payload(key: &Key, payload: &CapPayload) -> SealedTag {
let mut mac = HmacSha256::new_from_slice(key.as_bytes()).expect("HMAC accepts any key length");
let data = serialize_payload(payload);
mac.update(&data);
let result = mac.finalize();
SealedTag::from_bytes(result.into_bytes().into())
}
#[allow(dead_code)] #[allow(clippy::expect_used)] pub(crate) fn verify_seal(key: &Key, payload: &CapPayload, tag: &SealedTag) -> bool {
let mut mac = HmacSha256::new_from_slice(key.as_bytes()).expect("HMAC accepts any key length");
let data = serialize_payload(payload);
mac.update(&data);
let result = mac.finalize();
constant_time_eq(&result.into_bytes(), tag.as_bytes())
}
#[inline]
fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
if a.len() != b.len() {
return false;
}
a.ct_eq(b).into()
}
#[allow(clippy::expect_used)] fn serialize_payload(payload: &CapPayload) -> [u8; 64] {
let mut data = [0u8; 64];
data[0..16].copy_from_slice(&payload.holder().to_le_bytes());
data[16..32].copy_from_slice(&payload.target().to_le_bytes());
data[32..40].copy_from_slice(&payload.epoch().to_le_bytes());
let parent_val = match payload.parent() {
None => 0u128,
Some(p) => p
.checked_add(1)
.expect("parent_id must be < u128::MAX for unique HMAC domain separation"),
};
data[40..56].copy_from_slice(&parent_val.to_le_bytes());
data[56..64].copy_from_slice(&u64::from(payload.rights().to_bits()).to_le_bytes());
data
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::{Right, Rights};
#[test]
fn test_seal_verify_roundtrip() {
let key = Key::from_bytes([1u8; 32]);
let payload = CapPayload::new(1, 100, Rights::singleton(Right::Read), None, 0);
let tag = seal_payload(&key, &payload);
assert!(verify_seal(&key, &payload, &tag));
}
#[test]
fn test_wrong_key_fails() {
let key1 = Key::from_bytes([1u8; 32]);
let key2 = Key::from_bytes([2u8; 32]);
let payload = CapPayload::new(1, 100, Rights::singleton(Right::Read), None, 0);
let tag = seal_payload(&key1, &payload);
assert!(!verify_seal(&key2, &payload, &tag));
}
#[test]
fn test_modified_payload_fails() {
let key = Key::from_bytes([1u8; 32]);
let payload1 = CapPayload::new(1, 100, Rights::singleton(Right::Read), None, 0);
let payload2 = CapPayload::new(1, 101, Rights::singleton(Right::Read), None, 0);
let tag = seal_payload(&key, &payload1);
assert!(!verify_seal(&key, &payload2, &tag));
}
#[test]
fn test_different_payloads_different_tags() {
let key = Key::from_bytes([1u8; 32]);
let p1 = CapPayload::new(1, 100, Rights::singleton(Right::Read), None, 0);
let p2 = CapPayload::new(2, 100, Rights::singleton(Right::Read), None, 0);
let t1 = seal_payload(&key, &p1);
let t2 = seal_payload(&key, &p2);
assert_ne!(t1.as_bytes(), t2.as_bytes());
}
#[test]
fn test_serialize_payload_length_stable() {
let payload = CapPayload::new(1, 100, Rights::singleton(Right::Read), None, 0);
let data = serialize_payload(&payload);
assert_eq!(
data.len(),
64,
"Payload serialization must be exactly 64 bytes (holder:16 + target:16 + epoch:8 + parent:16 + rights:8)"
);
}
#[test]
fn test_parent_none_vs_some_zero_distinct() {
let key = Key::from_bytes([1u8; 32]);
let p_none = CapPayload::new(1, 100, Rights::singleton(Right::Read), None, 0);
let p_zero = CapPayload::new(1, 100, Rights::singleton(Right::Read), Some(0), 0);
let t_none = seal_payload(&key, &p_none);
let t_zero = seal_payload(&key, &p_zero);
assert_ne!(
t_none.as_bytes(),
t_zero.as_bytes(),
"parent=None and parent=Some(0) must produce distinct seals"
);
}
#[test]
fn test_parent_adjacent_ids_distinct() {
let key = Key::from_bytes([1u8; 32]);
let p1 = CapPayload::new(1, 100, Rights::singleton(Right::Read), Some(42), 0);
let p2 = CapPayload::new(1, 100, Rights::singleton(Right::Read), Some(43), 0);
let t1 = seal_payload(&key, &p1);
let t2 = seal_payload(&key, &p2);
assert_ne!(t1.as_bytes(), t2.as_bytes());
}
#[test]
#[should_panic(expected = "parent_id must be < u128::MAX")]
fn test_parent_u128_max_panics() {
let payload = CapPayload::new(1, 100, Rights::singleton(Right::Read), Some(u128::MAX), 0);
serialize_payload(&payload);
}
}