feat(alerting): HmacSigner for webhook signature
HmacSHA256 signer returning sha256=<lowercase-hex>. 5 unit tests covering known vector, prefix, hex casing, and different secrets/bodies. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
package com.cameleer.server.app.alerting.notify;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HexFormat;
|
||||
|
||||
/**
|
||||
* Computes HMAC-SHA256 webhook signatures.
|
||||
* <p>
|
||||
* Output format: {@code sha256=<lowercase hex>}
|
||||
*/
|
||||
@Component
|
||||
public class HmacSigner {
|
||||
|
||||
/**
|
||||
* Signs {@code body} with {@code secret} using HmacSHA256.
|
||||
*
|
||||
* @param secret plain-text secret (UTF-8 encoded)
|
||||
* @param body request body bytes to sign
|
||||
* @return {@code "sha256=" + hex(hmac)}
|
||||
*/
|
||||
public String sign(String secret, byte[] body) {
|
||||
try {
|
||||
Mac mac = Mac.getInstance("HmacSHA256");
|
||||
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
|
||||
byte[] digest = mac.doFinal(body);
|
||||
return "sha256=" + HexFormat.of().formatHex(digest);
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("HMAC signing failed", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package com.cameleer.server.app.alerting.notify;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class HmacSignerTest {
|
||||
|
||||
private final HmacSigner signer = new HmacSigner();
|
||||
|
||||
/**
|
||||
* Pre-computed:
|
||||
* secret = "test-secret-key"
|
||||
* body = "hello world"
|
||||
* result = sha256=b3df71b4790eb32b24c2f0bbb20f215d82b0da5e921caa880c74acfc97cf7e5b
|
||||
*
|
||||
* Verified with: python3 -c "import hmac,hashlib; print('sha256='+hmac.new(b'test-secret-key',b'hello world',hashlib.sha256).hexdigest())"
|
||||
*/
|
||||
@Test
|
||||
void knownVector() {
|
||||
String result = signer.sign("test-secret-key", "hello world".getBytes(StandardCharsets.UTF_8));
|
||||
assertThat(result).isEqualTo("sha256=b3df71b4790eb32b24c2f0bbb20f215d82b0da5e921caa880c74acfc97cf7e5b");
|
||||
}
|
||||
|
||||
@Test
|
||||
void outputStartsWithSha256Prefix() {
|
||||
String result = signer.sign("any-secret", "body".getBytes(StandardCharsets.UTF_8));
|
||||
assertThat(result).startsWith("sha256=");
|
||||
}
|
||||
|
||||
@Test
|
||||
void outputIsLowercaseHex() {
|
||||
String result = signer.sign("key", "data".getBytes(StandardCharsets.UTF_8));
|
||||
// After "sha256=" every char must be a lowercase hex digit
|
||||
String hex = result.substring("sha256=".length());
|
||||
assertThat(hex).matches("[0-9a-f]{64}");
|
||||
}
|
||||
|
||||
@Test
|
||||
void differentSecretsProduceDifferentSignatures() {
|
||||
byte[] body = "payload".getBytes(StandardCharsets.UTF_8);
|
||||
String sig1 = signer.sign("secret-a", body);
|
||||
String sig2 = signer.sign("secret-b", body);
|
||||
assertThat(sig1).isNotEqualTo(sig2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void differentBodiesProduceDifferentSignatures() {
|
||||
String sig1 = signer.sign("secret", "body1".getBytes(StandardCharsets.UTF_8));
|
||||
String sig2 = signer.sign("secret", "body2".getBytes(StandardCharsets.UTF_8));
|
||||
assertThat(sig1).isNotEqualTo(sig2);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user