diff --git a/lib/utils.ts b/lib/utils.ts index d93723d..ed2e7da 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -1,4 +1,5 @@ import { IncomingMessage } from 'http'; +import { createHmac, timingSafeEqual } from 'crypto'; import { IdentifierType } from '../lib/types'; export const isEmpty = (value: unknown) => { @@ -51,3 +52,21 @@ export class MissingParamError extends Error { this.message = `${param} is required`; } } + +export const verifyWebhookSignature = ( + webhookSigningSecret: string, + timestamp: string, + signature: string, + payload: Buffer, +): boolean => { + if (isEmpty(webhookSigningSecret) || isEmpty(timestamp) || isEmpty(signature) || isEmpty(payload)) { + throw new MissingParamError('webhookSigningSecret, timestamp, signature, payload'); + } + + const hmac = createHmac('sha256', webhookSigningSecret); + hmac.update(`v0:${timestamp}:`); + hmac.update(payload); + + const hash = hmac.digest(); + return timingSafeEqual(hash, Buffer.from(signature, 'hex')); +}; diff --git a/test/util.ts b/test/util.ts index c2f07ee..a05000b 100644 --- a/test/util.ts +++ b/test/util.ts @@ -1,5 +1,5 @@ import avaTest, { TestFn } from 'ava'; -import { cleanEmail } from '../lib/utils'; +import { cleanEmail, verifyWebhookSignature } from '../lib/utils'; type TestContext = {}; @@ -18,3 +18,29 @@ test('#cleanEmail correctly formats email (default)', (t) => { result = cleanEmail(email); t.is(result, ''); }); + +test('#verifyWebhookSignature: should return false if an argument is empty', (t) => { + const timestamp = ''; + const payload = '{"hello": "world"}'; + const signature = '2380722c30fe151144a151cbbf6b5a207291314d78582594cb18bcdb66098d5c'; + const webhookSigningSecret = 'abcd'; + t.throws(() => verifyWebhookSignature(webhookSigningSecret, timestamp, signature, Buffer.from(payload)), { + message: 'webhookSigningSecret, timestamp, signature, payload is required', + }); +}); + +test('#verifyWebhookSignature: should return false if signature is wrong', (t) => { + const timestamp = '1536353830'; + const payload = '{"hello": "world"}'; + const signature = '2e4c14c338df161be248bca5cf0e7836a96077c6f773a97e6b8f3e0cc9a14a1a'; + const webhookSigningSecret = 'abcd'; + t.false(verifyWebhookSignature(webhookSigningSecret, timestamp, signature, Buffer.from(payload))); +}); + +test('#verifyWebhookSignature: should return true if signature is matching', (t) => { + const timestamp = '1536353830'; + const payload = '{"hello": "world"}'; + const signature = '2380722c30fe151144a151cbbf6b5a207291314d78582594cb18bcdb66098d5c'; + const webhookSigningSecret = 'abcd'; + t.true(verifyWebhookSignature(webhookSigningSecret, timestamp, signature, Buffer.from(payload))); +});