use hex::FromHexError;
use hmac::{digest::InvalidLength, Hmac, Mac};
use http::Request;
use sha2::Sha256;
use thiserror::Error;
use tower::filter::Predicate;
#[derive(Debug, Clone)]
pub struct HmacQueryParamValidator {
pub key: String,
}
#[derive(Error, Debug)]
pub enum HmacQueryParamError {
#[error("Request did not have a query string")]
NoQueryString,
#[error("Request did not have an HMAC query param: {query_string}")]
QueryStringButNoHmac { query_string: String },
#[error("Hash key not appropriately set: {error:#?}")]
HashKeyNotSet { error: InvalidLength },
#[error("Failed decoding HMAC as hex string: {error:#?}")]
HexDecodingError { error: FromHexError },
#[error("Computed hash did not match provided hash")]
HashVerificationFailed,
}
impl<T> Predicate<Request<T>> for HmacQueryParamValidator {
type Request = http::Request<T>;
fn check(&mut self, request: Request<T>) -> Result<Self::Request, tower::BoxError> {
let query = request.uri().query().ok_or(HmacQueryParamError::NoQueryString)?;
let (hmac, params): (Vec<_>, Vec<_>) =
form_urlencoded::parse(query.as_bytes()).partition(|(key, _)| key == "hmac");
let hmac = &hmac
.first()
.ok_or(HmacQueryParamError::QueryStringButNoHmac {
query_string: query.to_string(),
})?
.1;
let query_string_without_hmac = form_urlencoded::Serializer::new(String::new())
.extend_pairs(params)
.finish();
let mut hasher = Hmac::<Sha256>::new_from_slice(self.key.as_bytes())
.map_err(|e| HmacQueryParamError::HashKeyNotSet { error: e })?;
hasher.update(&query_string_without_hmac.into_bytes());
let hmac_bytes =
hex::decode(hmac.as_bytes()).map_err(|e| HmacQueryParamError::HexDecodingError { error: e })?;
hasher
.verify(hmac_bytes.as_slice().into())
.map_err(|_| HmacQueryParamError::HashVerificationFailed)?;
Ok(request)
}
}