Java Requires Chilkat v11.5.0+
Chilkat2 Java
Box: Verify a Captured V2 Webhook Signature
See more Box Examples
Verify raw payload bytes and captured headers using the configured primary and secondary signing keys. The example rejects stale/future timestamps and compares randomized HMACs of the signatures to avoid exposing a signature-prefix timing comparison. It does not host an HTTP listener. The receiver must persist/deduplicate the verified event and acknowledge promptly.
See the Box API guide for setup and the complete workflow.
Chilkat Java Downloads
import com.chilkatsoft.chilkat2.BinData;
import com.chilkatsoft.chilkat2.ChilkatException;
import com.chilkatsoft.chilkat2.Crypt2;
import com.chilkatsoft.chilkat2.DateTime;
import com.chilkatsoft.chilkat2.JsonObject;
import com.chilkatsoft.chilkat2.Prng;
public class ChilkatExample {
public static void main(String[] args) {
boolean success = false;
// Requires Chilkat 11.5.0 or later and a previously unlocked Chilkat bundle.
// See Global Unlock Sample.
// Guide: Box API Guide.
success = false;
// Save the exact HTTP body bytes to this file, without JSON reserialization.
BinData rawBody = new BinData();
try {
rawBody.loadFile("qa_data/box_webhook_body.bin");
} catch (ChilkatException e) {
System.out.println(e.getLastErrorText());
return;
}
// Read these fields case-insensitively from the incoming request headers.
// For this offline example, enter the captured header values.
String timestamp = "BOX_DELIVERY_TIMESTAMP";
String primarySignature = "BOX_SIGNATURE_PRIMARY";
String secondarySignature = "BOX_SIGNATURE_SECONDARY";
String signatureVersion = "1";
String signatureAlgorithm = "HmacSHA256";
// Retrieve configured keys from protected application configuration.
String primaryKey = "BOX_PRIMARY_SIGNATURE_KEY";
String secondaryKey = "BOX_SECONDARY_SIGNATURE_KEY";
if ((!"1".equals(signatureVersion)) || (!"HmacSHA256".equals(signatureAlgorithm))) {
System.out.println("Unsupported signature version or algorithm.");
return;
}
DateTime delivered = new DateTime();
try {
delivered.setFromTimestamp(timestamp);
} catch (ChilkatException e) {
System.out.println(e.getLastErrorText());
return;
}
DateTime now = new DateTime();
try {
now.setFromCurrentSystemTime();
} catch (ChilkatException e) {
System.out.println(e.getLastErrorText());
return;
}
int age = (int) now.getAsUnixTime(false) - (int) delivered.getAsUnixTime(false);
// Box recommends rejecting deliveries older than ten minutes.
// Allow at most 60 seconds of future clock skew; keep the server clock synchronized.
if ((age > 600) || (age < -60)) {
System.out.println("Delivery timestamp is outside the accepted window.");
return;
}
BinData signedBytes = new BinData();
try {
signedBytes.appendBd(rawBody);
signedBytes.appendString(timestamp, "utf-8");
} catch (ChilkatException e) {
System.out.println(e.getLastErrorText());
return;
}
Crypt2 mac = new Crypt2();
mac.setMacAlgorithm("hmac");
mac.setHashAlgorithm("sha256");
mac.setEncodingMode("base64");
String expectedPrimary;
String expectedSecondary;
try {
mac.setMacKeyString(primaryKey);
expectedPrimary = mac.macBdENC(signedBytes);
mac.setMacKeyString(secondaryKey);
expectedSecondary = mac.macBdENC(signedBytes);
} catch (ChilkatException e) {
System.out.println(e.getLastErrorText());
return;
}
// Blind both sides with a fresh secret random key before equality comparison.
// Never compare the original HMAC strings using a prefix-short-circuit comparison.
// A host-language receiver can instead use its standard constant-time byte comparison.
Prng random = new Prng();
String comparisonKey;
String pExpected;
String pReceived;
String sExpected;
String sReceived;
try {
comparisonKey = random.genRandom(32, "base64");
mac.setMacKeyEncoded(comparisonKey, "base64");
pExpected = mac.macStringENC(expectedPrimary);
pReceived = mac.macStringENC(primarySignature);
sExpected = mac.macStringENC(expectedSecondary);
sReceived = mac.macStringENC(secondarySignature);
} catch (ChilkatException e) {
System.out.println(e.getLastErrorText());
return;
}
boolean validPrimary = (!"".equals(primaryKey)) && ((!"".equals(primarySignature)) && (pExpected.equals(pReceived)));
boolean validSecondary = (!"".equals(secondaryKey)) && ((!"".equals(secondarySignature)) && (sExpected.equals(sReceived)));
if ((!validPrimary) && (!validSecondary)) {
System.out.println("Signature verification failed.");
return;
}
// Parse JSON only after signature verification.
JsonObject webhookEvent = new JsonObject();
try {
webhookEvent.load(rawBody.getString("utf-8"));
} catch (ChilkatException e) {
System.out.println(e.getLastErrorText());
return;
}
System.out.println("Verified event ID: " + webhookEvent.stringOf("id"));
System.out.println("Trigger: " + webhookEvent.stringOf("trigger"));
// Deduplicate using event.id, not BOX-DELIVERY-ID, which changes on retries.
// Verify the target belongs to your configured webhook before dispatching work.
}
}