Dart Requires Chilkat v11.5.0+
Dart
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
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.
}