use hmac::{Hmac, Mac};
use sha2::Sha256;
use std::collections::{HashSet, VecDeque};
type HmacSha256 = Hmac<Sha256>;
const MAC_LEN: usize = 32;
pub const DEFAULT_TOLERANCE_SECS: u64 = 300;
#[must_use]
pub fn compute_hmac_hex(key: &[u8], data: &[u8]) -> String {
let mut mac = HmacSha256::new_from_slice(key).expect("HMAC accepts any key length");
mac.update(data);
to_hex(&mac.finalize().into_bytes())
}
#[must_use]
pub fn verify_hex(key: &[u8], data: &[u8], provided_hex: &str) -> bool {
let Some(provided) = decode_hex(provided_hex) else {
return false;
};
let mut mac = HmacSha256::new_from_slice(key).expect("HMAC accepts any key length");
mac.update(data);
mac.verify_slice(&provided).is_ok()
}
#[must_use]
pub fn canonical_payload(timestamp: i64, method: &str, path: &str, body: &[u8]) -> Vec<u8> {
let mut out = Vec::with_capacity(body.len() + method.len() + path.len() + 32);
out.extend_from_slice(format!("t={timestamp}\n").as_bytes());
out.extend_from_slice(method.as_bytes());
out.push(b'\n');
out.extend_from_slice(path.as_bytes());
out.push(b'\n');
out.extend_from_slice(body);
out
}
#[must_use]
pub fn sign_request(
secret: &[u8],
method: &str,
path: &str,
body: &[u8],
timestamp: i64,
) -> String {
let signed = canonical_payload(timestamp, method, path, body);
format!("t={timestamp},v1={}", compute_hmac_hex(secret, &signed))
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct SignatureHeader {
pub timestamp: Option<i64>,
pub v1: Vec<String>,
}
impl SignatureHeader {
#[must_use]
pub fn has_v1(&self) -> bool {
!self.v1.is_empty()
}
}
#[must_use]
pub fn parse_forjar_signature(raw: &str) -> SignatureHeader {
let mut out = SignatureHeader::default();
for element in raw.split(',') {
let Some((k, v)) = element.split_once('=') else {
continue;
};
match k.trim() {
"t" => out.timestamp = v.trim().parse::<i64>().ok(),
"v1" => out.v1.push(v.trim().to_string()),
_ => {}
}
}
out
}
#[must_use]
pub fn parse_github_signature(raw: &str) -> Option<String> {
raw.trim()
.strip_prefix("sha256=")
.map(|h| h.trim().to_string())
}
#[must_use]
pub fn timestamp_is_fresh(timestamp: i64, now: i64, tolerance_secs: u64) -> bool {
let skew = now.saturating_sub(timestamp).unsigned_abs();
skew <= tolerance_secs
}
#[must_use]
pub fn unix_now() -> i64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| i64::try_from(d.as_secs()).unwrap_or(i64::MAX))
.unwrap_or(0)
}
#[derive(Debug)]
pub struct ReplayGuard {
seen: HashSet<String>,
order: VecDeque<(i64, String)>,
tolerance_secs: u64,
capacity: usize,
}
impl ReplayGuard {
#[must_use]
pub fn new(tolerance_secs: u64, capacity: usize) -> Self {
Self {
seen: HashSet::new(),
order: VecDeque::new(),
tolerance_secs,
capacity: capacity.max(1),
}
}
pub fn admit(&mut self, signature: &str, now: i64) -> bool {
self.expire(now);
if !self.seen.insert(signature.to_string()) {
return false;
}
self.order.push_back((now, signature.to_string()));
while self.order.len() > self.capacity {
if let Some((_, old)) = self.order.pop_front() {
self.seen.remove(&old);
}
}
true
}
#[must_use]
pub fn len(&self) -> usize {
self.seen.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.seen.is_empty()
}
#[must_use]
pub fn seen_contains(&self, signature: &str) -> bool {
self.seen.contains(signature)
}
fn expire(&mut self, now: i64) {
let cutoff = now.saturating_sub(i64::try_from(self.tolerance_secs).unwrap_or(i64::MAX));
while let Some((seen_at, _)) = self.order.front() {
if *seen_at >= cutoff {
break;
}
if let Some((_, old)) = self.order.pop_front() {
self.seen.remove(&old);
}
}
}
}
fn to_hex(bytes: &[u8]) -> String {
let mut s = String::with_capacity(bytes.len() * 2);
for b in bytes {
use std::fmt::Write;
let _ = write!(s, "{b:02x}");
}
s
}
fn decode_hex(s: &str) -> Option<Vec<u8>> {
let s = s.trim();
if s.len() != MAC_LEN * 2 {
return None;
}
let bytes = s.as_bytes();
let mut out = Vec::with_capacity(MAC_LEN);
for pair in bytes.chunks(2) {
let hi = hex_val(pair[0])?;
let lo = hex_val(pair[1])?;
out.push((hi << 4) | lo);
}
Some(out)
}
fn hex_val(c: u8) -> Option<u8> {
match c {
b'0'..=b'9' => Some(c - b'0'),
b'a'..=b'f' => Some(c - b'a' + 10),
b'A'..=b'F' => Some(c - b'A' + 10),
_ => None,
}
}