use std::time::{SystemTime, UNIX_EPOCH};
use data_encoding::BASE64URL_NOPAD;
use hmac::{Hmac, KeyInit, Mac};
use sha2::Sha256;
use topcoat::context::Cx;
const PURPOSE: &[u8] = b"tablo upload url v1";
pub(crate) const WINDOW: u64 = 3600;
const EXPIRES: &str = "expires";
const SIGNATURE: &str = "signature";
pub(crate) fn now() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_or(0, |elapsed| elapsed.as_secs())
}
fn mac(cx: &Cx, path: &str, expires: u64) -> Hmac<Sha256> {
let mut mac = <Hmac<Sha256> as KeyInit>::new_from_slice(&super::key::derived(cx, PURPOSE))
.expect("HMAC takes a key of any length");
mac.update(path.as_bytes());
mac.update(b"\n");
mac.update(expires.to_string().as_bytes());
mac
}
pub(crate) fn sign(cx: &Cx, path: &str, now: u64) -> String {
let expires = (now / WINDOW + 2) * WINDOW;
let signature = BASE64URL_NOPAD.encode(&mac(cx, path, expires).finalize().into_bytes());
format!("{path}?{EXPIRES}={expires}&{SIGNATURE}={signature}")
}
pub(crate) fn verify(cx: &Cx, path: &str, query: Option<&str>, now: u64) -> bool {
let (mut expires, mut signature) = (None, None);
for (key, value) in form_urlencoded::parse(query.unwrap_or_default().as_bytes()) {
match &*key {
EXPIRES => expires = value.parse::<u64>().ok(),
SIGNATURE => signature = BASE64URL_NOPAD.decode(value.as_bytes()).ok(),
_ => {}
}
}
let (Some(expires), Some(signature)) = (expires, signature) else {
return false;
};
expires > now && mac(cx, path, expires).verify_slice(&signature).is_ok()
}
#[cfg(test)]
mod tests;