1use std::fmt::Display;
6use std::time::Duration;
7
8use axum::extract::FromRequestParts;
9use axum::http::request::Parts;
10use hmac::{Hmac, KeyInit, Mac};
11use sha2::Sha256;
12
13use crate::crypto::constant_time_eq;
14use crate::{AppState, Error, Result};
15
16fn now() -> u64 {
17 crate::clock::unix_secs().max(0) as u64
18}
19
20pub(crate) fn signature(state: &AppState, payload: &str) -> String {
21 let mut mac = <Hmac<Sha256> as KeyInit>::new_from_slice(state.key.signing())
22 .expect("HMAC accepts keys of any length");
23 mac.update(payload.as_bytes());
24 mac.finalize()
25 .into_bytes()
26 .iter()
27 .map(|b| format!("{b:02x}"))
28 .collect()
29}
30
31impl AppState {
32 pub fn signed_url(&self, name: &str, params: &[&dyn Display], ttl: Duration) -> Result<String> {
34 let path = self.url(name, params)?;
35 self.sign_path(&path, ttl)
36 }
37
38 pub fn sign_path(&self, path: &str, ttl: Duration) -> Result<String> {
40 let unsigned = format!("{path}?expires={}", now().saturating_add(ttl.as_secs()));
41 let signature = signature(self, &unsigned);
42 Ok(format!(
43 "{}{unsigned}&signature={signature}",
44 self.config.url.trim_end_matches('/')
45 ))
46 }
47
48 pub fn absolute_url(&self, name: &str, params: &[&dyn Display]) -> Result<String> {
50 Ok(format!(
51 "{}{}",
52 self.config.url.trim_end_matches('/'),
53 self.url(name, params)?
54 ))
55 }
56}
57
58pub struct ValidSignature;
60
61impl<S: Send + Sync> FromRequestParts<S> for ValidSignature {
62 type Rejection = Error;
63
64 async fn from_request_parts(parts: &mut Parts, _: &S) -> Result<Self> {
65 let state = parts
66 .extensions
67 .get::<AppState>()
68 .ok_or_else(|| anyhow::anyhow!("the auth middleware is not installed"))?;
69 if verify(state, &parts.uri) {
70 Ok(Self)
71 } else {
72 Err(Error::Forbidden)
73 }
74 }
75}
76
77pub(crate) fn verify(state: &AppState, uri: &axum::http::Uri) -> bool {
79 let query = uri.query().unwrap_or_default();
80 let Some((unsigned_query, given)) = query.rsplit_once("&signature=") else {
81 return false;
82 };
83 let Some(expires) = unsigned_query
84 .strip_prefix("expires=")
85 .and_then(|v| v.parse::<u64>().ok())
86 else {
87 return false;
88 };
89 let unsigned = format!("{}?{unsigned_query}", uri.path());
90 expires >= now() && constant_time_eq(&signature(state, &unsigned), given)
91}