zlicenser-server 0.1.2

Server library for the zlicenser hardware-bound software licensing framework.
Documentation
use std::sync::Arc;

use axum::{
    body::Bytes,
    extract::State,
    http::{HeaderMap, StatusCode},
    response::{IntoResponse, Response},
};

use crate::issuance::handlers::HandlerContext;
use crate::storage::Storage;

pub async fn stripe_handler<S: Storage + Clone + Send + Sync + 'static>(
    State(ctx): State<Arc<HandlerContext<S>>>,
    headers: HeaderMap,
    body: Bytes,
) -> Response {
    let Some(secret) = ctx.config.stripe_webhook_secret.as_deref() else {
        return StatusCode::NOT_IMPLEMENTED.into_response();
    };

    let Some(sig_header) = headers
        .get("stripe-signature")
        .and_then(|v| v.to_str().ok())
    else {
        return (StatusCode::BAD_REQUEST, "missing stripe-signature").into_response();
    };

    if !verify_stripe_signature(sig_header, &body, secret) {
        return (StatusCode::UNAUTHORIZED, "invalid stripe-signature").into_response();
    }

    let event: serde_json::Value = match serde_json::from_slice(&body) {
        Ok(v) => v,
        Err(_) => return (StatusCode::BAD_REQUEST, "invalid json").into_response(),
    };

    let event_type = event["type"].as_str().unwrap_or("");
    if event_type == "payment_intent.payment_failed" {
        let intent_id = event["data"]["object"]["id"]
            .as_str()
            .unwrap_or_default()
            .to_owned();
        if !intent_id.is_empty() {
            let storage = ctx.storage.clone();
            tokio::spawn(async move {
                if let Ok(Some(session)) = storage.get_session_by_payment_intent(&intent_id).await {
                    crate::issuance::handlers::abandon(
                        &storage,
                        session.id,
                        crate::storage::types::abandon_reason::PAYMENT_FAILED,
                    )
                    .await;
                }
            });
        }
    }

    StatusCode::OK.into_response()
}

fn hmac_sha256(key: &[u8], data: &[u8]) -> [u8; 32] {
    use sha2::{Digest, Sha256};
    const BLOCK: usize = 64;
    let mut k = [0u8; BLOCK];
    if key.len() > BLOCK {
        let h = Sha256::digest(key);
        k[..32].copy_from_slice(&h);
    } else {
        k[..key.len()].copy_from_slice(key);
    }
    let mut ipad = [0u8; BLOCK];
    let mut opad = [0u8; BLOCK];
    for i in 0..BLOCK {
        ipad[i] = k[i] ^ 0x36;
        opad[i] = k[i] ^ 0x5c;
    }
    let inner: [u8; 32] = Sha256::new()
        .chain_update(ipad)
        .chain_update(data)
        .finalize()
        .into();
    Sha256::new()
        .chain_update(opad)
        .chain_update(inner)
        .finalize()
        .into()
}

fn hex_to_bytes(s: &str) -> Option<Vec<u8>> {
    if !s.len().is_multiple_of(2) {
        return None;
    }
    (0..s.len())
        .step_by(2)
        .map(|i| u8::from_str_radix(&s[i..i + 2], 16).ok())
        .collect()
}

fn verify_stripe_signature(header: &str, body: &[u8], secret: &str) -> bool {
    let mut timestamp = None;
    let mut signature_hex: Option<&str> = None;

    for part in header.split(',') {
        if let Some(t) = part.strip_prefix("t=") {
            timestamp = Some(t);
        } else if let Some(v) = part.strip_prefix("v1=") {
            signature_hex = Some(v);
        }
    }

    let (Some(ts), Some(sig_hex)) = (timestamp, signature_hex) else {
        return false;
    };

    let Some(expected_bytes) = hex_to_bytes(sig_hex) else {
        return false;
    };

    let signed_payload = format!("{}.{}", ts, String::from_utf8_lossy(body));
    let computed = hmac_sha256(secret.as_bytes(), signed_payload.as_bytes());

    computed.as_ref() == expected_bytes.as_slice()
}