| 1 | package edu.ucsb.cs156.frontiers.utilities; | |
| 2 | ||
| 3 | import java.nio.charset.StandardCharsets; | |
| 4 | import java.security.InvalidKeyException; | |
| 5 | import java.security.MessageDigest; | |
| 6 | import java.security.NoSuchAlgorithmException; | |
| 7 | import java.util.regex.Pattern; | |
| 8 | import javax.crypto.Mac; | |
| 9 | import javax.crypto.spec.SecretKeySpec; | |
| 10 | import lombok.extern.slf4j.Slf4j; | |
| 11 | ||
| 12 | /** Utility class for webhook security validation */ | |
| 13 | @Slf4j | |
| 14 | public class WebhookSecurityUtils { | |
| 15 | ||
| 16 | private WebhookSecurityUtils() { | |
| 17 | // Utility Class | |
| 18 | } | |
| 19 | ||
| 20 | private static final String HMAC_SHA256 = "HmacSHA256"; | |
| 21 | private static final Pattern GITHUB_SIGNATURE_PATTERN = | |
| 22 | Pattern.compile("^sha256=[a-fA-F0-9]{64}$"); | |
| 23 | ||
| 24 | /** | |
| 25 | * Validates GitHub webhook signature using HMAC-SHA256 | |
| 26 | * | |
| 27 | * @param payload the raw payload body as string | |
| 28 | * @param signature the GitHub signature header (e.g., "sha256=abc123...") | |
| 29 | * @param secret the webhook secret | |
| 30 | * @return true if signature is valid, false otherwise | |
| 31 | */ | |
| 32 | public static boolean validateGitHubSignature(String payload, String signature, String secret) | |
| 33 | throws NoSuchAlgorithmException, InvalidKeyException { | |
| 34 |
3
1. validateGitHubSignature : negated conditional → KILLED 2. validateGitHubSignature : negated conditional → KILLED 3. validateGitHubSignature : negated conditional → KILLED |
if (payload == null || signature == null || secret == null) { |
| 35 | log.warn("Null values provided for webhook validation"); | |
| 36 |
1
1. validateGitHubSignature : replaced boolean return with true for edu/ucsb/cs156/frontiers/utilities/WebhookSecurityUtils::validateGitHubSignature → KILLED |
return false; |
| 37 | } | |
| 38 | ||
| 39 |
1
1. validateGitHubSignature : negated conditional → KILLED |
if (!GITHUB_SIGNATURE_PATTERN.matcher(signature).matches()) { |
| 40 | log.warn("Invalid signature format"); | |
| 41 |
1
1. validateGitHubSignature : replaced boolean return with true for edu/ucsb/cs156/frontiers/utilities/WebhookSecurityUtils::validateGitHubSignature → KILLED |
return false; |
| 42 | } | |
| 43 | ||
| 44 | String expectedSignature = "sha256=" + calculateHmacSha256(payload, secret); | |
| 45 | boolean isValid = safeEquals(signature, expectedSignature); | |
| 46 | ||
| 47 |
1
1. validateGitHubSignature : negated conditional → KILLED |
if (!isValid) { |
| 48 | log.warn("Webhook signature validation failed"); | |
| 49 |
1
1. validateGitHubSignature : replaced boolean return with true for edu/ucsb/cs156/frontiers/utilities/WebhookSecurityUtils::validateGitHubSignature → KILLED |
return false; |
| 50 | } | |
| 51 | ||
| 52 |
1
1. validateGitHubSignature : replaced boolean return with false for edu/ucsb/cs156/frontiers/utilities/WebhookSecurityUtils::validateGitHubSignature → KILLED |
return true; |
| 53 | } | |
| 54 | ||
| 55 | /** | |
| 56 | * Calculates HMAC-SHA256 signature for given payload and secret | |
| 57 | * | |
| 58 | * @param payload the payload to sign | |
| 59 | * @param secret the secret key | |
| 60 | * @return hex-encoded signature | |
| 61 | */ | |
| 62 | private static String calculateHmacSha256(String payload, String secret) | |
| 63 | throws NoSuchAlgorithmException, InvalidKeyException { | |
| 64 | Mac mac = Mac.getInstance(HMAC_SHA256); | |
| 65 | SecretKeySpec secretKeySpec = | |
| 66 | new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), HMAC_SHA256); | |
| 67 |
1
1. calculateHmacSha256 : removed call to javax/crypto/Mac::init → KILLED |
mac.init(secretKeySpec); |
| 68 | byte[] signature = mac.doFinal(payload.getBytes(StandardCharsets.UTF_8)); | |
| 69 |
1
1. calculateHmacSha256 : replaced return value with "" for edu/ucsb/cs156/frontiers/utilities/WebhookSecurityUtils::calculateHmacSha256 → KILLED |
return bytesToHex(signature); |
| 70 | } | |
| 71 | ||
| 72 | /** | |
| 73 | * Converts byte array to hexadecimal string | |
| 74 | * | |
| 75 | * @param bytes the byte array | |
| 76 | * @return hex string | |
| 77 | */ | |
| 78 | private static String bytesToHex(byte[] bytes) { | |
| 79 | StringBuilder result = new StringBuilder(); | |
| 80 | for (byte b : bytes) { | |
| 81 | result.append(String.format("%02x", b)); | |
| 82 | } | |
| 83 |
1
1. bytesToHex : replaced return value with "" for edu/ucsb/cs156/frontiers/utilities/WebhookSecurityUtils::bytesToHex → KILLED |
return result.toString(); |
| 84 | } | |
| 85 | ||
| 86 | /** | |
| 87 | * Constant-time string comparison to prevent timing attacks | |
| 88 | * | |
| 89 | * @param a first string | |
| 90 | * @param b second string | |
| 91 | * @return true if strings are equal | |
| 92 | */ | |
| 93 | private static boolean safeEquals(String a, String b) { | |
| 94 |
2
1. safeEquals : replaced boolean return with false for edu/ucsb/cs156/frontiers/utilities/WebhookSecurityUtils::safeEquals → KILLED 2. safeEquals : replaced boolean return with true for edu/ucsb/cs156/frontiers/utilities/WebhookSecurityUtils::safeEquals → KILLED |
return MessageDigest.isEqual( |
| 95 | a.getBytes(StandardCharsets.UTF_8), b.getBytes(StandardCharsets.UTF_8)); | |
| 96 | } | |
| 97 | } | |
Mutations | ||
| 34 |
1.1 2.2 3.3 |
|
| 36 |
1.1 |
|
| 39 |
1.1 |
|
| 41 |
1.1 |
|
| 47 |
1.1 |
|
| 49 |
1.1 |
|
| 52 |
1.1 |
|
| 67 |
1.1 |
|
| 69 |
1.1 |
|
| 83 |
1.1 |
|
| 94 |
1.1 2.2 |