twitch_tohell 0.1.1

Twitch EventSub webhook and WebSocket support
Documentation
use hmac::{Hmac, Mac};
use sha2::Sha256;
use subtle::ConstantTimeEq;

use crate::webhook::{
    HeaderAccess, VerificationError,
    types::{MESSAGE_ID, MESSAGE_SIGNATURE, MESSAGE_TIMESTAMP},
};

const HMAC_PREFIX: &str = "sha256=";

type HmacSha256 = Hmac<Sha256>;

pub fn verify_event_message<H: HeaderAccess>(
    headers: &H,
    body: &[u8],
    secret: &str,
) -> Result<(), VerificationError> {
    let signature = headers
        .get_header(MESSAGE_SIGNATURE)
        .ok_or(VerificationError::MissingHeader(MESSAGE_SIGNATURE))?;

    let hmac_signature = hmac_signature(headers, body, secret)?;

    if verify_message(&hmac_signature, signature) {
        Ok(())
    } else {
        Err(VerificationError::InvalidSignature)
    }
}

fn get_hmac_message<H: HeaderAccess>(
    headers: &H,
    body: &[u8],
) -> Result<Vec<u8>, VerificationError> {
    let message_id = headers
        .get_header(MESSAGE_ID)
        .ok_or(VerificationError::MissingHeader(MESSAGE_ID))?;

    let message_timestamp = headers
        .get_header(MESSAGE_TIMESTAMP)
        .ok_or(VerificationError::MissingHeader(MESSAGE_TIMESTAMP))?;

    let mut message = Vec::with_capacity(message_id.len() + message_timestamp.len() + body.len());

    message.extend_from_slice(message_id.as_bytes());
    message.extend_from_slice(message_timestamp.as_bytes());
    message.extend_from_slice(body);
    Ok(message)
}

fn get_hmac(secret: &str, message: &[u8]) -> String {
    let mut mac =
        HmacSha256::new_from_slice(secret.as_bytes()).expect("HMAC can take key of any size");
    mac.update(message);

    format!(
        "{}{}",
        HMAC_PREFIX,
        hex::encode(mac.finalize().into_bytes())
    )
}

fn hmac_signature<H: HeaderAccess>(
    headers: &H,
    body: &[u8],
    secret: &str,
) -> Result<String, VerificationError> {
    Ok(get_hmac(secret, &get_hmac_message(headers, body)?))
}

fn verify_message(hmac: &str, signature: &str) -> bool {
    if hmac.len() != signature.len() {
        return false;
    }

    hmac.as_bytes().ct_eq(signature.as_bytes()).into()
}