Sample code for 30+ languages & platforms
Java Requires Chilkat v11.5.0+

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

Chilkat2 Java
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.
    }
}