1package dev.rafex.ether.webhook.crypto;
29import java.nio.charset.StandardCharsets;
30import java.security.MessageDigest;
31import java.time.Clock;
32import java.time.Instant;
33import java.util.Base64;
34import java.util.Objects;
36import javax.crypto.Mac;
37import javax.crypto.spec.SecretKeySpec;
39import dev.rafex.ether.webhook.api.WebhookSigner;
40import dev.rafex.ether.webhook.api.WebhookVerifier;
41import dev.rafex.ether.webhook.model.WebhookPayload;
42import dev.rafex.ether.webhook.model.WebhookSignature;
43import dev.rafex.ether.webhook.model.WebhookVerificationResult;
51 private static final String DEFAULT_ALGORITHM =
"HmacSHA256";
53 private final byte[] secret;
54 private final String algorithm;
55 private final Clock clock;
63 this(secret, DEFAULT_ALGORITHM, Clock.systemUTC());
74 this.secret = secret ==
null ?
new byte[0] : secret.clone();
75 this.algorithm = Objects.requireNonNullElse(algorithm, DEFAULT_ALGORITHM);
76 this.clock = clock ==
null ? Clock.systemUTC() : clock;
77 if (this.secret.length == 0) {
78 throw new IllegalArgumentException(
"secret is required");
89 public WebhookSignature
sign(
final WebhookPayload payload) {
90 final long timestamp = Instant.now(clock).toEpochMilli();
91 return new WebhookSignature(algorithm, computeSignature(payload, timestamp), timestamp);
102 public WebhookVerificationResult
verify(
final WebhookPayload payload,
final WebhookSignature signature) {
103 if (signature ==
null) {
104 return WebhookVerificationResult.failed(
"missing_signature",
null);
106 if (!algorithm.equals(signature.algorithm())) {
107 return WebhookVerificationResult.failed(
"unsupported_algorithm", signature);
110 final String expected = computeSignature(payload, signature.timestampEpochMilli());
111 if (!MessageDigest.isEqual(expected.getBytes(StandardCharsets.UTF_8),
112 signature.value().getBytes(StandardCharsets.UTF_8))) {
113 return WebhookVerificationResult.failed(
"bad_signature", signature);
115 return WebhookVerificationResult.ok(signature);
125 private String computeSignature(
final WebhookPayload payload,
final long timestamp) {
127 final var mac = Mac.getInstance(algorithm);
128 mac.init(
new SecretKeySpec(secret, algorithm));
129 mac.update(canonicalPayload(payload, timestamp).getBytes(StandardCharsets.UTF_8));
130 return Base64.getUrlEncoder().withoutPadding().encodeToString(mac.doFinal());
131 }
catch (
final Exception e) {
132 throw new IllegalStateException(
"unable to sign webhook payload", e);
143 private static String canonicalPayload(
final WebhookPayload payload,
final long timestamp) {
144 return String.join(
"\n", nullToEmpty(payload.deliveryId()), nullToEmpty(payload.eventType()),
145 nullToEmpty(payload.contentType()), Long.toString(timestamp),
146 Base64.getUrlEncoder().withoutPadding().encodeToString(payload.body()));
155 private static String nullToEmpty(
final String value) {
156 return value ==
null ?
"" : value;
WebhookSignature sign(final WebhookPayload payload)
Firma un payload de webhook con HMAC.
WebhookVerificationResult verify(final WebhookPayload payload, final WebhookSignature signature)
Verifica la firma de un payload de webhook.
HmacWebhookSignerVerifier(final byte[] secret, final String algorithm, final Clock clock)
Crea un signer/verifier con configuración completa.
HmacWebhookSignerVerifier(final byte[] secret)
Crea un signer/verifier con secreto y algoritmo por defecto.
Interfaz para firmar payloads de webhook.
Interfaz para verificar firmas de payloads de webhook.