All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.github.twitch4j.eventsub.util.EventSubVerifier Maven / Gradle / Ivy

The newest version!
package com.github.twitch4j.eventsub.util;

import com.github.twitch4j.common.util.CryptoUtils;
import io.github.xanthic.cache.api.Cache;
import io.github.xanthic.cache.api.domain.ExpiryType;
import io.github.xanthic.cache.core.CacheApi;
import lombok.experimental.UtilityClass;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.Duration;
import java.time.Instant;
import java.time.format.DateTimeParseException;

@Slf4j
@UtilityClass
public class EventSubVerifier {

    /**
     * Twitch recommends a buffer of 10 minutes for resiliency against replay attacks
     *
     * @see Official docs
     */
    private final Duration RECENT_EVENT = Duration.ofMinutes(10L);

    /**
     * The Twitch-Eventsub-Message-Id's that have been observed during {@link #RECENT_EVENT}
     */
    private final Cache RECENT_MESSAGE_IDS = CacheApi.create(spec -> {
        spec.expiryType(ExpiryType.POST_WRITE);
        spec.expiryTime(RECENT_EVENT);
        spec.maxSize(65_536L);
    });

    /**
     * Twitch's prefix for Twitch-Eventsub-Message-Signature
     */
    private final String SIGNATURE_HASH_PREFIX = "sha256=";

    /**
     * Java algorithm name that corresponds to {@link #SIGNATURE_HASH_PREFIX}
     */
    public final String JAVA_HMAC_ALGORITHM = "HmacSHA256";

    /**
     * The number of characters in hashes produced by {@link #JAVA_HMAC_ALGORITHM}
     */
    private final int HASH_LENGTH = 256 / 4;

    /**
     * A thread-local {@link Mac} instance of {@link #JAVA_HMAC_ALGORITHM}
     */
    private final ThreadLocal HMAC_FUNCTION = ThreadLocal.withInitial(() -> {
        try {
            return Mac.getInstance(JAVA_HMAC_ALGORITHM);
        } catch (NoSuchAlgorithmException e) {
            return null;
        }
    });

    /**
     * @param messageId Twitch-Eventsub-Message-Id
     * @return whether the message id has not been observed recently
     */
    public boolean verifyMessageId(String messageId) {
        return messageId != null && !messageId.isEmpty() && RECENT_MESSAGE_IDS.putIfAbsent(messageId, Boolean.TRUE) == null;
    }

    /**
     * @param messageTimestamp Twitch-Eventsub-Message-Timestamp
     * @return whether the event occurred recently
     */
    public boolean verifyTimestamp(String messageTimestamp) {
        if (messageTimestamp == null || messageTimestamp.isEmpty())
            return false;

        final Instant timestamp;
        try {
            timestamp = Instant.parse(messageTimestamp);
        } catch (DateTimeParseException e) {
            log.debug("Failed to parse timestamp: " + messageTimestamp, e);
            return false;
        }

        return Duration.between(timestamp, Instant.now()).compareTo(RECENT_EVENT) < 0;
    }

    /**
     * @param secret            The subscription secret.
     * @param messageId         Twitch-Eventsub-Message-Id
     * @param messageTimestamp  Twitch-Eventsub-Message-Timestamp
     * @param requestBody       The raw bytes of the request body.
     * @param expectedSignature Twitch-Eventsub-Message-Signature
     * @return whether the signature indicates an authentic event
     */
    public boolean verifySignature(SecretKeySpec secret, String messageId, String messageTimestamp, byte[] requestBody, String expectedSignature) {
        if (secret == null || expectedSignature == null || messageId == null || messageTimestamp == null || requestBody == null) {
            log.warn("Could not verify eventsub signature with null argument");
            return false;
        }

        if (expectedSignature.length() - SIGNATURE_HASH_PREFIX.length() != HASH_LENGTH || !StringUtils.startsWithIgnoreCase(expectedSignature, SIGNATURE_HASH_PREFIX)) {
            log.debug("Could not verify unknown eventsub signature hash scheme; " + expectedSignature);
            return false;
        }

        final Mac mac = HMAC_FUNCTION.get();
        if (mac == null) {
            log.error("Unable to prepare hash function for eventsub signature verification!");
            return false;
        }

        try {
            mac.init(secret);
        } catch (InvalidKeyException e) {
            log.error("Unable to initialize secret for eventsub signature verification!", e);
            return false;
        }

        final byte[] id = messageId.getBytes(StandardCharsets.UTF_8);
        final byte[] timestamp = messageTimestamp.getBytes(StandardCharsets.UTF_8);
        final byte[] message = new byte[id.length + timestamp.length + requestBody.length];
        System.arraycopy(id, 0, message, 0, id.length);
        System.arraycopy(timestamp, 0, message, id.length, timestamp.length);
        System.arraycopy(requestBody, 0, message, id.length + timestamp.length, requestBody.length);
        final byte[] computedHmac = mac.doFinal(message);
        mac.reset(); // Clean-up
        final byte[] expectedHmac = CryptoUtils.hexToBytes(expectedSignature.substring(SIGNATURE_HASH_PREFIX.length()));
        return MessageDigest.isEqual(computedHmac, expectedHmac); // constant-time comparison
    }

    /**
     * @see #verifySignature(SecretKeySpec, String, String, byte[], String)
     */
    public boolean verifySignature(byte[] secret, String messageId, String messageTimestamp, byte[] requestBody, String expectedSignature) {
        return verifySignature(new SecretKeySpec(secret, JAVA_HMAC_ALGORITHM), messageId, messageTimestamp, requestBody, expectedSignature);
    }

    /**
     * @see #verifySignature(SecretKeySpec, String, String, byte[], String)
     */
    public boolean verifySignature(String secret, String messageId, String messageTimestamp, byte[] requestBody, String expectedSignature) {
        return verifySignature(secret.getBytes(StandardCharsets.UTF_8), messageId, messageTimestamp, requestBody, expectedSignature);
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy