Ether Framework
Unified API docs for Ether modules
Loading...
Searching...
No Matches
HmacWebhookSignerVerifier.java
Go to the documentation of this file.
1package dev.rafex.ether.webhook.crypto;
2
3/*-
4 * #%L
5 * ether-webhook
6 * %%
7 * Copyright (C) 2025 - 2026 Raúl Eduardo González Argote
8 * %%
9 * Permission is hereby granted, free of charge, to any person obtaining a copy
10 * of this software and associated documentation files (the "Software"), to deal
11 * in the Software without restriction, including without limitation the rights
12 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13 * copies of the Software, and to permit persons to whom the Software is
14 * furnished to do so, subject to the following conditions:
15 *
16 * The above copyright notice and this permission notice shall be included in
17 * all copies or substantial portions of the Software.
18 *
19 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25 * THE SOFTWARE.
26 * #L%
27 */
28
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;
35
36import javax.crypto.Mac;
37import javax.crypto.spec.SecretKeySpec;
38
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;
44
45/**
46 * Implementación de HMAC para firmar y verificar webhooks.
47 * Utiliza HMAC-SHA256 por defecto para garantizar integridad y autenticidad.
48 */
50
51 private static final String DEFAULT_ALGORITHM = "HmacSHA256";
52
53 private final byte[] secret;
54 private final String algorithm;
55 private final Clock clock;
56
57 /**
58 * Crea un signer/verifier con secreto y algoritmo por defecto.
59 *
60 * @param secret el secreto para HMAC
61 */
62 public HmacWebhookSignerVerifier(final byte[] secret) {
63 this(secret, DEFAULT_ALGORITHM, Clock.systemUTC());
64 }
65
66 /**
67 * Crea un signer/verifier con configuración completa.
68 *
69 * @param secret el secreto para HMAC
70 * @param algorithm el algoritmo HMAC (ej: HmacSHA256)
71 * @param clock el reloj para marcas de tiempo
72 */
73 public HmacWebhookSignerVerifier(final byte[] secret, final String algorithm, final Clock clock) {
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");
79 }
80 }
81
82 /**
83 * Firma un payload de webhook con HMAC.
84 *
85 * @param payload el payload a firmar
86 * @return la firma generada
87 */
88 @Override
89 public WebhookSignature sign(final WebhookPayload payload) {
90 final long timestamp = Instant.now(clock).toEpochMilli();
91 return new WebhookSignature(algorithm, computeSignature(payload, timestamp), timestamp);
92 }
93
94 /**
95 * Verifica la firma de un payload de webhook.
96 *
97 * @param payload el payload a verificar
98 * @param signature la firma a validar
99 * @return el resultado de la verificación
100 */
101 @Override
102 public WebhookVerificationResult verify(final WebhookPayload payload, final WebhookSignature signature) {
103 if (signature == null) {
104 return WebhookVerificationResult.failed("missing_signature", null);
105 }
106 if (!algorithm.equals(signature.algorithm())) {
107 return WebhookVerificationResult.failed("unsupported_algorithm", signature);
108 }
109
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);
114 }
115 return WebhookVerificationResult.ok(signature);
116 }
117
118 /**
119 * Calcula la firma HMAC para un payload.
120 *
121 * @param payload el payload a firmar
122 * @param timestamp la marca de tiempo
123 * @return la firma en base64
124 */
125 private String computeSignature(final WebhookPayload payload, final long timestamp) {
126 try {
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);
133 }
134 }
135
136 /**
137 * Crea el payload canónico para la firma.
138 *
139 * @param payload el payload original
140 * @param timestamp la marca de tiempo
141 * @return el payload canónico como string
142 */
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()));
147 }
148
149 /**
150 * Convierte un valor nulo a string vacío.
151 *
152 * @param value el valor a convertir
153 * @return el valor o string vacío si es nulo
154 */
155 private static String nullToEmpty(final String value) {
156 return value == null ? "" : value;
157 }
158}
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.