WebhookSecurityUtils.java
package edu.ucsb.cs156.frontiers.utilities;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import lombok.extern.slf4j.Slf4j;
/** Utility class for webhook security validation */
@Slf4j
public class WebhookSecurityUtils {
private WebhookSecurityUtils() {
// Utility Class
}
private static final String HMAC_SHA256 = "HmacSHA256";
/**
* Validates GitHub webhook signature using HMAC-SHA256
*
* @param payload the raw payload body as string
* @param signature the GitHub signature header (e.g., "sha256=abc123...")
* @param secret the webhook secret
* @return true if signature is valid, false otherwise
*/
public static boolean validateGitHubSignature(String payload, String signature, String secret)
throws NoSuchAlgorithmException, InvalidKeyException {
if (payload == null || signature == null || secret == null) {
log.warn("Null values provided for webhook validation");
return false;
}
if (!signature.startsWith("sha256=")) {
log.warn("Invalid signature format: {}", signature);
return false;
}
String expectedSignature = "sha256=" + calculateHmacSha256(payload, secret);
boolean isValid = safeEquals(signature, expectedSignature);
if (!isValid) {
log.warn("Webhook signature validation failed");
return false;
}
return true;
}
/**
* Calculates HMAC-SHA256 signature for given payload and secret
*
* @param payload the payload to sign
* @param secret the secret key
* @return hex-encoded signature
*/
private static String calculateHmacSha256(String payload, String secret)
throws NoSuchAlgorithmException, InvalidKeyException {
Mac mac = Mac.getInstance(HMAC_SHA256);
SecretKeySpec secretKeySpec =
new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), HMAC_SHA256);
mac.init(secretKeySpec);
byte[] signature = mac.doFinal(payload.getBytes(StandardCharsets.UTF_8));
return bytesToHex(signature);
}
/**
* Converts byte array to hexadecimal string
*
* @param bytes the byte array
* @return hex string
*/
private static String bytesToHex(byte[] bytes) {
StringBuilder result = new StringBuilder();
for (byte b : bytes) {
result.append(String.format("%02x", b));
}
return result.toString();
}
/**
* Constant-time string comparison to prevent timing attacks
*
* @param a first string
* @param b second string
* @return true if strings are equal
*/
private static boolean safeEquals(String a, String b) {
if (a.length() != b.length()) {
return false;
}
int result = 0;
for (int i = 0; i < a.length(); i++) {
result |= a.charAt(i) ^ b.charAt(i);
}
return result == 0;
}
}