poolster-plugin-java 0.5.0-alpha.1

Native Java SDK generator for Poolster
Documentation
// Generated by Poolster. Do not edit.
package __PACKAGE__;
import java.util.*;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

/** Standard Webhooks HMAC v1. Replay deduplication belongs to the application. */
public final class StandardWebhooks {
    private StandardWebhooks() {}
    public static final class VerificationException extends IllegalArgumentException {
        public VerificationException(String reason) { super(reason); }
    }
    private static VerificationException fail(String reason) { return new VerificationException(reason); }
    private static byte[] base64(String encoded) {
        if (!encoded.matches("(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?")) throw fail("invalid webhook encoding");
        try { return Base64.getDecoder().decode(encoded); } catch (IllegalArgumentException error) { throw fail("invalid webhook encoding"); }
    }
    public static byte[] verify(byte[] body, Map<String,List<String>> headers, List<String> secrets) {
        return verify(body, headers, secrets, java.time.Instant.now().getEpochSecond(), 300);
    }
    public static byte[] verify(byte[] body, Map<String,List<String>> headers, List<String> secrets, double now, double tolerance) {
        if (body == null || !Double.isFinite(now) || !Double.isFinite(tolerance) || tolerance < 0) throw fail("invalid webhook verification options");
        var metadata = new HashMap<String,String>();
        headers.forEach((name, values) -> {
            var lower = name.toLowerCase(Locale.ROOT);
            if (!Set.of("webhook-id","webhook-timestamp","webhook-signature").contains(lower)) return;
            if (values == null || values.size() != 1 || values.get(0) == null || metadata.putIfAbsent(lower, values.get(0)) != null) throw fail("duplicate webhook metadata header");
        });
        var id = metadata.getOrDefault("webhook-id","");
        var stamp = metadata.getOrDefault("webhook-timestamp","");
        if (id.isEmpty() || id.contains(".") || !stamp.matches("[0-9]+")) throw fail("invalid webhook metadata");
        long timestamp;
        try { timestamp = Long.parseLong(stamp); } catch (NumberFormatException error) { throw fail("invalid webhook metadata"); }
        if (Math.abs(now - timestamp) > tolerance) throw fail("webhook timestamp outside tolerance");
        var keys = new ArrayList<byte[]>();
        for (var secret : secrets) {
            if (secret == null || !secret.startsWith("whsec_")) throw fail("invalid webhook signing secret");
            var key = base64(secret.substring(6));
            if (key.length < 24 || key.length > 64) throw fail("invalid webhook signing secret");
            keys.add(key);
        }
        if (keys.isEmpty()) throw fail("trusted webhook signing secret required");
        var prefix = (id+"."+stamp+".").getBytes(StandardCharsets.UTF_8);
        var signed = Arrays.copyOf(prefix, prefix.length + body.length);
        System.arraycopy(body, 0, signed, prefix.length, body.length);
        boolean valid = false;
        for (var entry : metadata.getOrDefault("webhook-signature","").split("\\s+")) {
            if (!entry.startsWith("v1,")) continue;
            byte[] signature;
            try { signature = base64(entry.substring(3)); } catch (VerificationException error) { continue; }
            if (signature.length != 32) continue;
            for (var key : keys) {
                try {
                    var mac = Mac.getInstance("HmacSHA256"); mac.init(new SecretKeySpec(key,"HmacSHA256"));
                    valid |= MessageDigest.isEqual(mac.doFinal(signed), signature);
                } catch (java.security.GeneralSecurityException error) { throw fail("webhook cryptography unavailable"); }
            }
        }
        if (!valid) throw fail("webhook signature does not match a trusted key");
        return body.clone();
    }
    public static <T> T verifyAndDecode(byte[] body, Map<String,List<String>> headers, List<String> secrets, double now, double tolerance, java.util.function.Function<byte[],T> decoder) {
        var verified = verify(body,headers,secrets,now,tolerance);
        try { return decoder.apply(verified); } catch (RuntimeException error) { throw fail("verified webhook payload does not match destination type"); }
    }
}