use http::HeaderMap;
use crate::error::{Error, Result};
use crate::signature::Signature;
use super::{WebhookVerifier, header_str, hmac_compare};
pub struct InacashCashoutVerifier {
pub secret: String,
}
impl WebhookVerifier for InacashCashoutVerifier {
fn canonical_payload(&self, _headers: &HeaderMap, body: &[u8]) -> Result<Vec<u8>> {
Ok(body.to_vec())
}
fn extract_signature(&self, headers: &HeaderMap) -> Result<Signature> {
Signature::from_base64(header_str(headers, "X-Signature")?)
}
fn compare(&self, sig: &Signature, canonical: &[u8]) -> Result<()> {
hmac_compare(self.secret.as_bytes(), sig, canonical)
}
}
pub struct InacashQrisVerifier {
pub secret: String,
}
impl WebhookVerifier for InacashQrisVerifier {
fn canonical_payload(&self, _headers: &HeaderMap, body: &[u8]) -> Result<Vec<u8>> {
Ok(body.to_vec())
}
fn extract_signature(&self, headers: &HeaderMap) -> Result<Signature> {
Signature::from_base64(header_str(headers, "X-Signature")?)
}
fn compare(&self, sig: &Signature, canonical: &[u8]) -> Result<()> {
hmac_compare(self.secret.as_bytes(), sig, canonical)
}
}
pub struct BriVaPaidVerifier {
pub secret: String,
}
impl WebhookVerifier for BriVaPaidVerifier {
fn canonical_payload(&self, _headers: &HeaderMap, _body: &[u8]) -> Result<Vec<u8>> {
Err(Error::Webhook(
"BriVaPaidVerifier cannot verify from the body alone: BRI VA signs the SNAP BI \
service stringToSign (method:path:accessToken:lowercaseHex(SHA256(body)):timestamp), \
not the raw body. Use kamu-snap-crypto-actix / kamu-snap-crypto-axum `verify_request`, \
or implement WebhookVerifier with the request context and override canonical_payload."
.to_owned(),
))
}
fn extract_signature(&self, headers: &HeaderMap) -> Result<Signature> {
Signature::from_base64(header_str(headers, "X-SIGNATURE")?)
}
fn compare(&self, sig: &Signature, canonical: &[u8]) -> Result<()> {
hmac_compare(self.secret.as_bytes(), sig, canonical)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::HmacSigner;
fn signed_headers(name: &str, secret: &str, body: &[u8]) -> HeaderMap {
let sig = HmacSigner::new(secret.as_bytes()).unwrap().sign(body).to_base64();
let mut h = HeaderMap::new();
let hn = http::HeaderName::from_bytes(name.as_bytes()).unwrap();
h.insert(hn, http::HeaderValue::from_str(&sig).unwrap());
h
}
#[test]
fn inacash_cashout_round_trip() {
let secret = "inacash-cashout-secret";
let body = br#"{"status":"PAID"}"#;
let v = InacashCashoutVerifier { secret: secret.to_owned() };
let headers = signed_headers("X-Signature", secret, body);
assert!(v.verify(&headers, body).is_ok());
}
#[test]
fn inacash_cashout_rejects_tampered_body() {
let secret = "inacash-cashout-secret";
let v = InacashCashoutVerifier { secret: secret.to_owned() };
let headers = signed_headers("X-Signature", secret, b"original-body");
assert!(v.verify(&headers, b"tampered-body").is_err());
}
#[test]
fn inacash_qris_round_trip_with_independent_secret() {
let secret = "inacash-qris-secret";
let body = br#"{"qris":"ok"}"#;
let v = InacashQrisVerifier { secret: secret.to_owned() };
let headers = signed_headers("X-Signature", secret, body);
assert!(v.verify(&headers, body).is_ok());
}
#[test]
fn missing_signature_header_errors() {
let v = InacashCashoutVerifier { secret: "s".to_owned() };
assert!(v.verify(&HeaderMap::new(), b"body").is_err());
}
#[test]
fn bri_va_verifier_refuses_body_only_verification() {
let secret = "bri-secret";
let v = BriVaPaidVerifier { secret: secret.to_owned() };
let headers = signed_headers("X-SIGNATURE", secret, b"body");
match v.verify(&headers, b"body") {
Err(Error::Webhook(msg)) => assert!(msg.contains("stringToSign")),
other => panic!("expected fail-loud Webhook error, got {other:?}"),
}
}
}