package org.jenkinsci.plugins.github.webhook;
import hudson.util.Secret;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import static com.google.common.base.Preconditions.checkNotNull;
import static java.nio.charset.StandardCharsets.UTF_8;
/**
* Utility class for dealing with signatures of incoming requests.
*
* @see <a href=https://developer.github.com/webhooks/#payloads>API documentation</a>
* @since 1.21.0
*/
public class GHWebhookSignature {
private static final Logger LOGGER = LoggerFactory.getLogger(GHWebhookSignature.class);
private static final String HMAC_SHA1_ALGORITHM = "HmacSHA1";
public static final String INVALID_SIGNATURE = "COMPUTED_INVALID_SIGNATURE";
private final String payload;
private final Secret secret;
private GHWebhookSignature(String payload, Secret secret) {
this.payload = payload;
this.secret = secret;
}
/**
* @param payload Clear-text to create signature of.
* @param secret Key to sign with.
*/
public static GHWebhookSignature webhookSignature(String payload, Secret secret) {
checkNotNull(payload, "Payload can't be null");
checkNotNull(secret, "Secret should be defined to compute sign");
return new GHWebhookSignature(payload, secret);
}
/**
* Computes a RFC 2104-compliant HMAC digest using SHA1 of a payloadFrom with a given key (secret).
*
* @return HMAC digest of payloadFrom using secret as key. Will return COMPUTED_INVALID_SIGNATURE
* on any exception during computation.
*/
public String sha1() {
try {
final SecretKeySpec keySpec = new SecretKeySpec(secret.getPlainText().getBytes(UTF_8), HMAC_SHA1_ALGORITHM);
final Mac mac = Mac.getInstance(HMAC_SHA1_ALGORITHM);
mac.init(keySpec);
final byte[] rawHMACBytes = mac.doFinal(payload.getBytes(UTF_8));
return Hex.encodeHexString(rawHMACBytes);
} catch (Exception e) {
LOGGER.error("", e);
return INVALID_SIGNATURE;
}
}
/**
* @param digest computed signature from external place (GitHub)
*
* @return true if computed and provided signatures identical
*/
public boolean matches(String digest) {
String computed = sha1();
LOGGER.trace("Signature: calculated={} provided={}", computed, digest);
return StringUtils.equals(computed, digest);
}
}