shopify-client 0.19.0

Modular Shopify library for Rust: API client, type-safe models, and webhook parsing utilities
Documentation
use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
use hmac::{Hmac, Mac};
use sha2::Sha256;

use super::types::VerificationError;

type HmacSha256 = Hmac<Sha256>;

pub fn verify_hmac(
    hmac_header: &str,
    raw_body: &[u8],
    client_secret: &str,
) -> Result<(), VerificationError> {
    let received_hmac = BASE64
        .decode(hmac_header)
        .map_err(|_| VerificationError::InvalidBase64)?;

    let mut mac = HmacSha256::new_from_slice(client_secret.as_bytes())
        .expect("HMAC can take key of any size");
    mac.update(raw_body);

    mac.verify_slice(&received_hmac)
        .map_err(|_| VerificationError::InvalidHmac)
}

pub fn verify_hmac_from_headers(
    headers: &std::collections::HashMap<String, String>,
    raw_body: &[u8],
    client_secret: &str,
) -> Result<(), VerificationError> {
    let hmac_header = headers
        .get("x-shopify-hmac-sha256")
        .or_else(|| headers.get("X-Shopify-Hmac-SHA256"))
        .ok_or(VerificationError::MissingHmacHeader)?;

    verify_hmac(hmac_header, raw_body, client_secret)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_verify_hmac_valid() {
        let client_secret = "test_secret";
        let body = b"{\"test\":\"data\"}";

        let mut mac = HmacSha256::new_from_slice(client_secret.as_bytes()).unwrap();
        mac.update(body);
        let result = mac.finalize();
        let expected_hmac = BASE64.encode(result.into_bytes());

        let result = verify_hmac(&expected_hmac, body, client_secret);
        assert!(result.is_ok());
    }

    #[test]
    fn test_verify_hmac_invalid() {
        let client_secret = "test_secret";
        let body = b"{\"test\":\"data\"}";
        let wrong_hmac = BASE64.encode(b"wrong_hmac_value_here_123456");

        let result = verify_hmac(&wrong_hmac, body, client_secret);
        assert!(matches!(result, Err(VerificationError::InvalidHmac)));
    }

    #[test]
    fn test_verify_hmac_invalid_base64() {
        let client_secret = "test_secret";
        let body = b"{\"test\":\"data\"}";
        let invalid_base64 = "not_valid_base64!@#$%";

        let result = verify_hmac(invalid_base64, body, client_secret);
        assert!(matches!(result, Err(VerificationError::InvalidBase64)));
    }

    #[test]
    fn test_verify_hmac_from_headers_valid() {
        let client_secret = "test_secret";
        let body = b"{\"test\":\"data\"}";

        let mut mac = HmacSha256::new_from_slice(client_secret.as_bytes()).unwrap();
        mac.update(body);
        let result = mac.finalize();
        let expected_hmac = BASE64.encode(result.into_bytes());

        let mut headers = std::collections::HashMap::new();
        headers.insert("x-shopify-hmac-sha256".to_string(), expected_hmac);

        let result = verify_hmac_from_headers(&headers, body, client_secret);
        assert!(result.is_ok());
    }

    #[test]
    fn test_verify_hmac_from_headers_case_insensitive() {
        let client_secret = "test_secret";
        let body = b"{\"test\":\"data\"}";

        let mut mac = HmacSha256::new_from_slice(client_secret.as_bytes()).unwrap();
        mac.update(body);
        let result = mac.finalize();
        let expected_hmac = BASE64.encode(result.into_bytes());

        let mut headers = std::collections::HashMap::new();
        headers.insert("X-Shopify-Hmac-SHA256".to_string(), expected_hmac);

        let result = verify_hmac_from_headers(&headers, body, client_secret);
        assert!(result.is_ok());
    }

    #[test]
    fn test_verify_hmac_from_headers_missing() {
        let client_secret = "test_secret";
        let body = b"{\"test\":\"data\"}";
        let headers = std::collections::HashMap::new();

        let result = verify_hmac_from_headers(&headers, body, client_secret);
        assert!(matches!(result, Err(VerificationError::MissingHmacHeader)));
    }

    #[test]
    fn test_shopify_example() {
        let client_secret = "hush";
        let body = b"{\"example\":\"webhook_data\"}";

        let mut mac = HmacSha256::new_from_slice(client_secret.as_bytes()).unwrap();
        mac.update(body);
        let result = mac.finalize();
        let hmac = BASE64.encode(result.into_bytes());

        let verification = verify_hmac(&hmac, body, client_secret);
        assert!(verification.is_ok());

        let wrong_verification = verify_hmac(&hmac, b"different_body", client_secret);
        assert!(matches!(
            wrong_verification,
            Err(VerificationError::InvalidHmac)
        ));
    }
}