diff --git a/src/main/java/com/abnov/infisicalbridge/infisical/InfisicalSignatureVerifier.java b/src/main/java/com/abnov/infisicalbridge/infisical/InfisicalSignatureVerifier.java new file mode 100644 index 0000000..1c17bb6 --- /dev/null +++ b/src/main/java/com/abnov/infisicalbridge/infisical/InfisicalSignatureVerifier.java @@ -0,0 +1,151 @@ +package com.abnov.infisicalbridge.infisical; + +import org.springframework.stereotype.Component; + +import lombok.extern.slf4j.Slf4j; + +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.Instant; + +/** + * Service to verify Infisical webhook signatures + */ +@Component +@Slf4j +public class InfisicalSignatureVerifier { + + private static final String HMAC_SHA256 = "HmacSHA256"; + private static final int DEFAULT_TOLERANCE_SECONDS = 300; // 5 minutes + + /** + * Verifies the signature of an Infisical webhook request + * + * @param payload The raw request body as string + * @param signatureHeader The value of x-infisical-signature header + * @param secretKey Your secret key from Infisical + * @return true if signature is valid, false otherwise + */ + public boolean verifySignature(String payload, String signatureHeader, String secretKey) { + return verifySignature(payload, signatureHeader, secretKey, DEFAULT_TOLERANCE_SECONDS); + } + + /** + * Verifies the signature with custom tolerance + * + * @param payload The raw request body as string + * @param signatureHeader The value of x-infisical-signature header (format: + * t=;) + * @param secretKey Your secret key from Infisical + * @param toleranceInSeconds Time tolerance for replay attacks + * @return true if signature is valid, false otherwise + */ + public boolean verifySignature(String payload, String signatureHeader, String secretKey, int toleranceInSeconds) { + try { + // Parse the signature header: t=; + String[] parts = signatureHeader.split(";"); + if (parts.length != 2) { + log.error("Invalid signature header format"); + return false; + } + + String timestamp = parts[0].substring(2); // Remove "t=" + String receivedSignature = parts[1]; + + // Check timestamp to prevent replay attacks + long webhookTime = Long.parseLong(timestamp); + long currentTime; + + // Detect if timestamp is in milliseconds or seconds + if (timestamp.length() > 10) { + // Timestamp is in milliseconds + currentTime = Instant.now().toEpochMilli(); + long toleranceInMillis = (long) toleranceInSeconds * 1000; + if (currentTime - webhookTime > toleranceInMillis) { + log.error("Webhook timestamp is too old"); + return false; + } + } else { + // Timestamp is in seconds + currentTime = Instant.now().getEpochSecond(); + if (currentTime - webhookTime > toleranceInSeconds) { + log.error("Webhook timestamp is too old"); + return false; + } + } + + // Try different signature formats that Infisical might use + + // Format 1: timestamp.payload (most common) + String signedPayload1 = timestamp + "." + payload; + String expectedSignature1 = generateHmacSHA256(signedPayload1, secretKey); + + if (MessageDigest.isEqual( + receivedSignature.getBytes(StandardCharsets.UTF_8), + expectedSignature1.getBytes(StandardCharsets.UTF_8))) { + return true; + } + + // Format 2: payload only + String expectedSignature2 = generateHmacSHA256(payload, secretKey); + + if (MessageDigest.isEqual( + receivedSignature.getBytes(StandardCharsets.UTF_8), + expectedSignature2.getBytes(StandardCharsets.UTF_8))) { + return true; + } + + // Format 3: t=timestamp.payload (with t= prefix) + String signedPayload3 = signatureHeader.split(";")[0] + "." + payload; + String expectedSignature3 = generateHmacSHA256(signedPayload3, secretKey); + + if (MessageDigest.isEqual( + receivedSignature.getBytes(StandardCharsets.UTF_8), + expectedSignature3.getBytes(StandardCharsets.UTF_8))) { + return true; + } + + // No format matched + log.error("Signature verification failed - no matching format found"); + return false; + + } catch (Exception e) { + log.error("Error verifying signature: {}" + e.getMessage()); + return false; + } + } + + /** + * Generates HMAC SHA256 signature + */ + private String generateHmacSHA256(String data, String key) + throws NoSuchAlgorithmException, InvalidKeyException { + Mac mac = Mac.getInstance(HMAC_SHA256); + SecretKeySpec secretKeySpec = new SecretKeySpec( + key.getBytes(StandardCharsets.UTF_8), + HMAC_SHA256); + mac.init(secretKeySpec); + + byte[] hmacBytes = mac.doFinal(data.getBytes(StandardCharsets.UTF_8)); + return bytesToHex(hmacBytes); + } + + /** + * Converts byte array to hex string + */ + private String bytesToHex(byte[] bytes) { + StringBuilder hexString = new StringBuilder(); + for (byte b : bytes) { + String hex = Integer.toHexString(0xff & b); + if (hex.length() == 1) { + hexString.append('0'); + } + hexString.append(hex); + } + return hexString.toString(); + } +}