1use std::fmt::Display;
7use std::time::Duration;
8
9use axum::extract::FromRequestParts;
10use axum::http::request::Parts;
11use hmac::{Hmac, KeyInit, Mac};
12use sha2::Sha256;
13
14use crate::crypto::constant_time_eq;
15use crate::{AppState, Error, Result};
16
17fn now() -> u64 {
18 crate::clock::unix_secs().max(0) as u64
19}
20
21pub(crate) fn signature(state: &AppState, payload: &str) -> String {
22 hmac_hex(state.key.signing(), payload)
23}
24
25pub(crate) fn hmac_hex(key: &[u8], payload: &str) -> String {
27 let mut mac =
28 <Hmac<Sha256> as KeyInit>::new_from_slice(key).expect("HMAC accepts keys of any length");
29 mac.update(payload.as_bytes());
30 mac.finalize()
31 .into_bytes()
32 .iter()
33 .map(|b| format!("{b:02x}"))
34 .collect()
35}
36
37impl AppState {
38 pub fn signed_url(&self, name: &str, params: &[&dyn Display], ttl: Duration) -> Result<String> {
40 let path = self.url(name, params)?;
41 self.sign_path(&path, ttl)
42 }
43
44 pub fn sign_path(&self, path: &str, ttl: Duration) -> Result<String> {
46 let unsigned = format!("{path}?expires={}", now().saturating_add(ttl.as_secs()));
47 let signature = signature(self, &unsigned);
48 Ok(format!(
49 "{}{unsigned}&signature={signature}",
50 self.config.url.trim_end_matches('/')
51 ))
52 }
53
54 pub fn absolute_url(&self, name: &str, params: &[&dyn Display]) -> Result<String> {
56 Ok(format!(
57 "{}{}",
58 self.config.url.trim_end_matches('/'),
59 self.url(name, params)?
60 ))
61 }
62}
63
64pub struct ValidSignature;
90
91impl<S: Send + Sync> FromRequestParts<S> for ValidSignature {
92 type Rejection = Error;
93
94 async fn from_request_parts(parts: &mut Parts, _: &S) -> Result<Self> {
95 if signed_request(parts)? {
96 Ok(Self)
97 } else {
98 Err(Error::Forbidden)
99 }
100 }
101}
102
103impl<S: Send + Sync> axum::extract::OptionalFromRequestParts<S> for ValidSignature {
104 type Rejection = Error;
105
106 async fn from_request_parts(parts: &mut Parts, _: &S) -> Result<Option<Self>> {
107 Ok(signed_request(parts)?.then_some(Self))
108 }
109}
110
111fn signed_request(parts: &Parts) -> Result<bool> {
114 let state = parts
115 .extensions
116 .get::<AppState>()
117 .ok_or_else(|| anyhow::anyhow!("the auth middleware is not installed"))?;
118 let uri = parts
119 .extensions
120 .get::<axum::extract::OriginalUri>()
121 .map_or(&parts.uri, |original| &original.0);
122 Ok(verify(state, uri))
123}
124
125pub fn verify(state: &AppState, uri: &axum::http::Uri) -> bool {
141 let query = uri.query().unwrap_or_default();
142 let Some((unsigned_query, given)) = query.rsplit_once("&signature=") else {
143 return false;
144 };
145 let Some(expires) = unsigned_query
146 .strip_prefix("expires=")
147 .and_then(|v| v.parse::<u64>().ok())
148 else {
149 return false;
150 };
151 let unsigned = format!("{}?{unsigned_query}", uri.path());
152 expires >= now() && constant_time_eq(&signature(state, &unsigned), given)
153}