Sample code for 30+ languages & platforms
Dart 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 Dart Downloads

Dart
import 'package:chilkat/chilkat.dart';

void main() {
  // Requires Chilkat 11.5.0 or later and a previously unlocked Chilkat bundle.
  // See Global Unlock Sample.
  // Guide: Box API Guide.

  // Save the exact HTTP body bytes to this file, without JSON reserialization.
  final rawBody = CkBinData();
  try {
    rawBody.loadFile('qa_data/box_webhook_body.bin');
  } on ChilkatException catch (e) {
    print(e.lastErrorText);
    return;
  }

  // Read these fields case-insensitively from the incoming request headers.
  // For this offline example, enter the captured header values.
  final timestamp = 'BOX_DELIVERY_TIMESTAMP';
  final primarySignature = 'BOX_SIGNATURE_PRIMARY';
  final secondarySignature = 'BOX_SIGNATURE_SECONDARY';
  final signatureVersion = '1';
  final signatureAlgorithm = 'HmacSHA256';
  // Retrieve configured keys from protected application configuration.
  final primaryKey = 'BOX_PRIMARY_SIGNATURE_KEY';
  final secondaryKey = 'BOX_SECONDARY_SIGNATURE_KEY';
  if ((signatureVersion != '1') || (signatureAlgorithm != 'HmacSHA256')) {
    print('Unsupported signature version or algorithm.');
    return;
  }

  final delivered = CkDateTime();
  try {
    delivered.setFromTimestamp(timestamp);
  } on ChilkatException catch (e) {
    print(e.lastErrorText);
    return;
  }

  final now = CkDateTime();
  try {
    now.setFromCurrentSystemTime();
  } on ChilkatException catch (e) {
    print(e.lastErrorText);
    return;
  }

  final age = now.getAsUnixTime(false) - 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)) {
    print('Delivery timestamp is outside the accepted window.');
    return;
  }

  final signedBytes = CkBinData();
  try {
    signedBytes.appendBd(rawBody);
  } on ChilkatException catch (e) {
    print(e.lastErrorText);
    return;
  }

  try {
    signedBytes.appendString(timestamp, 'utf-8');
  } on ChilkatException catch (e) {
    print(e.lastErrorText);
    return;
  }

  final mac = CkCrypt2();
  mac.macAlgorithm = 'hmac';
  mac.hashAlgorithm = 'sha256';
  mac.encodingMode = 'base64';
  try {
    mac.setMacKeyString(primaryKey);
  } on ChilkatException catch (e) {
    print(e.lastErrorText);
    return;
  }

  final String expectedPrimary;
  try {
    expectedPrimary = mac.macBdENC(signedBytes);
  } on ChilkatException catch (e) {
    print(e.lastErrorText);
    return;
  }

  try {
    mac.setMacKeyString(secondaryKey);
  } on ChilkatException catch (e) {
    print(e.lastErrorText);
    return;
  }

  final String expectedSecondary;
  try {
    expectedSecondary = mac.macBdENC(signedBytes);
  } on ChilkatException catch (e) {
    print(e.lastErrorText);
    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.
  final random = CkPrng();
  final String comparisonKey;
  try {
    comparisonKey = random.genRandom(32, 'base64');
  } on ChilkatException catch (e) {
    print(e.lastErrorText);
    return;
  }

  try {
    mac.setMacKeyEncoded(comparisonKey, 'base64');
  } on ChilkatException catch (e) {
    print(e.lastErrorText);
    return;
  }

  final String pExpected;
  try {
    pExpected = mac.macStringENC(expectedPrimary);
  } on ChilkatException catch (e) {
    print(e.lastErrorText);
    return;
  }

  final String pReceived;
  try {
    pReceived = mac.macStringENC(primarySignature);
  } on ChilkatException catch (e) {
    print(e.lastErrorText);
    return;
  }

  final String sExpected;
  try {
    sExpected = mac.macStringENC(expectedSecondary);
  } on ChilkatException catch (e) {
    print(e.lastErrorText);
    return;
  }

  final String sReceived;
  try {
    sReceived = mac.macStringENC(secondarySignature);
  } on ChilkatException catch (e) {
    print(e.lastErrorText);
    return;
  }

  final validPrimary = (primaryKey != '') && ((primarySignature != '') && (pExpected == pReceived));
  final validSecondary = (secondaryKey != '') && ((secondarySignature != '') && (sExpected == sReceived));
  if ((!validPrimary) && (!validSecondary)) {
    print('Signature verification failed.');
    return;
  }

  // Parse JSON only after signature verification.
  final webhookEvent = CkJsonObject();
  try {
    webhookEvent.load(rawBody.getString('utf-8'));
  } on ChilkatException catch (e) {
    print(e.lastErrorText);
    return;
  }

  print('Verified event ID: ${webhookEvent.stringOf('id')}');
  print('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.
}