Skip to content

Commit efdcaaf

Browse files
fix: Use constant-time comparison for HMAC signatures (box/box-codegen#739) (#630)
1 parent a89b07f commit efdcaaf

File tree

6 files changed

+132
-91
lines changed

6 files changed

+132
-91
lines changed

.codegen.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{ "engineHash": "4e4fdf6", "specHash": "630fc85", "version": "1.15.1" }
1+
{ "engineHash": "d18eeee", "specHash": "630fc85", "version": "1.15.1" }

src/internal/utils.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { v4 as uuidv4 } from 'uuid';
22
import {
33
calculateMD5Hash,
4+
compareSignatures,
45
computeWebhookSignature,
56
createAgent,
67
createJwtAssertion,
@@ -45,6 +46,7 @@ export {
4546
readTextFromFile,
4647
createAgent,
4748
jsonStringifyWithEscapedUnicode,
49+
compareSignatures,
4850
computeWebhookSignature,
4951
calculateMD5Hash,
5052
getEnvVar,

src/internal/utilsBrowser.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,7 @@ export function jsonStringifyWithEscapedUnicode(body: string) {
282282
* @param {string} body - The request body of the webhook message
283283
* @param {Object} headers - The request headers of the webhook message
284284
* @param {string} signatureKey - The signature to verify the message with
285+
* @param {string} escapeBody - Indicates if payload should be escaped or left as is
285286
* @returns {?string} - The message signature (or null, if it can't be computed)
286287
* @private
287288
*/
@@ -293,14 +294,14 @@ export async function computeWebhookSignature(
293294
signatureKey: string,
294295
escapeBody: boolean = false,
295296
): Promise<string | null> {
296-
const escapedBody = escapeBody ? jsonStringifyWithEscapedUnicode(body) : body;
297297
if (headers['box-signature-version'] !== '1') {
298298
return null;
299299
}
300300
if (headers['box-signature-algorithm'] !== 'HmacSHA256') {
301301
return null;
302302
}
303303
let signature: string | null = null;
304+
const escapedBody = escapeBody ? jsonStringifyWithEscapedUnicode(body) : body;
304305
const hashFunc = createSHA256();
305306
const hmac = await createHMAC(hashFunc, signatureKey);
306307
hmac.init();
@@ -312,6 +313,23 @@ export async function computeWebhookSignature(
312313
return signature;
313314
}
314315

316+
export async function compareSignatures(
317+
expectedSignature: string | null,
318+
receivedSignature: string | null,
319+
): Promise<boolean> {
320+
if (!expectedSignature || !receivedSignature) {
321+
return false;
322+
}
323+
if (expectedSignature.length !== receivedSignature.length) return false;
324+
325+
let result = 0;
326+
for (let i = 0; i < expectedSignature.length; i++) {
327+
result |= expectedSignature.charCodeAt(i) ^ receivedSignature.charCodeAt(i);
328+
}
329+
330+
return result === 0;
331+
}
332+
315333
export async function calculateMD5Hash(data: string | Buffer): Promise<string> {
316334
return await sha1(data);
317335
}

src/internal/utilsNode.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,6 @@ export async function computeWebhookSignature(
249249
signatureKey: string,
250250
escapeBody: boolean = false,
251251
): Promise<string | null> {
252-
const escapedBody = escapeBody ? jsonStringifyWithEscapedUnicode(body) : body;
253252
if (headers['box-signature-version'] !== '1') {
254253
return null;
255254
}
@@ -258,13 +257,32 @@ export async function computeWebhookSignature(
258257
}
259258
let signature: string | null = null;
260259

260+
const escapedBody = escapeBody ? jsonStringifyWithEscapedUnicode(body) : body;
261261
let hmac = crypto.createHmac('sha256', signatureKey);
262262
hmac.update(escapedBody);
263263
hmac.update(headers['box-delivery-timestamp']);
264264
signature = hmac.digest('base64');
265265
return signature;
266266
}
267267

268+
export async function compareSignatures(
269+
expectedSignature: string | null,
270+
receivedSignature: string | null,
271+
): Promise<boolean> {
272+
if (!expectedSignature || !receivedSignature) {
273+
return false;
274+
}
275+
276+
const expectedBuffer = Buffer.from(expectedSignature, 'base64');
277+
const receivedBuffer = Buffer.from(receivedSignature, 'base64');
278+
279+
if (expectedBuffer.length !== receivedBuffer.length) {
280+
return false;
281+
}
282+
283+
return crypto.timingSafeEqual(expectedBuffer, receivedBuffer);
284+
}
285+
268286
export function random(min: number, max: number): number {
269287
return Math.random() * (max - min) + min;
270288
}

src/managers/webhooks.generated.ts

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { CancellationToken } from '../internal/utils.js';
2323
import { sdToJson } from '../serialization/json.js';
2424
import { SerializedData } from '../serialization/json.js';
2525
import { computeWebhookSignature } from '../internal/utils.js';
26+
import { compareSignatures } from '../internal/utils.js';
2627
import { dateTimeFromString } from '../internal/utils.js';
2728
import { getEpochTimeInSeconds } from '../internal/utils.js';
2829
import { dateTimeToEpochSeconds } from '../internal/utils.js';
@@ -680,29 +681,37 @@ export class WebhooksManager {
680681
}
681682
if (
682683
primaryKey &&
683-
(await computeWebhookSignature(body, headers, primaryKey, false)) ==
684-
headers['box-signature-primary']
684+
(await compareSignatures(
685+
await computeWebhookSignature(body, headers, primaryKey, false),
686+
headers['box-signature-primary'],
687+
))
685688
) {
686689
return true;
687690
}
688691
if (
689692
primaryKey &&
690-
(await computeWebhookSignature(body, headers, primaryKey, true)) ==
691-
headers['box-signature-primary']
693+
(await compareSignatures(
694+
await computeWebhookSignature(body, headers, primaryKey, true),
695+
headers['box-signature-primary'],
696+
))
692697
) {
693698
return true;
694699
}
695700
if (
696701
secondaryKey &&
697-
(await computeWebhookSignature(body, headers, secondaryKey, false)) ==
698-
headers['box-signature-secondary']
702+
(await compareSignatures(
703+
await computeWebhookSignature(body, headers, secondaryKey, false),
704+
headers['box-signature-secondary'],
705+
))
699706
) {
700707
return true;
701708
}
702709
if (
703710
secondaryKey &&
704-
(await computeWebhookSignature(body, headers, secondaryKey, true)) ==
705-
headers['box-signature-secondary']
711+
(await compareSignatures(
712+
await computeWebhookSignature(body, headers, secondaryKey, true),
713+
headers['box-signature-secondary'],
714+
))
706715
) {
707716
return true;
708717
}

0 commit comments

Comments
 (0)