Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions apps/api/src/webhooks/webhook.types.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { createHmac } from 'crypto';
import { verifyWebhookSignature } from './webhook.types';

describe('verifyWebhookSignature', () => {
const secret = 'whsec_test';
const body = JSON.stringify({ event: 'swap' });
const valid = createHmac('sha256', secret).update(body).digest('hex');

it('accepts a correct HMAC-SHA256 hex digest', () => {
expect(verifyWebhookSignature(body, valid, secret)).toBe(true);
});

it('rejects a well-formed but wrong 64-character signature', () => {
expect(verifyWebhookSignature(body, 'a'.repeat(64), secret)).toBe(false);
});

it('rejects a valid digest followed by extra characters', () => {
expect(verifyWebhookSignature(body, `${valid}zz`, secret)).toBe(false);
expect(verifyWebhookSignature(body, `${valid}ab`, secret)).toBe(false);
});

it('rejects a valid prefix padded with non-hex characters to 64 characters', () => {
const padded = `${valid.slice(0, 62)}zz`;
expect(padded).toHaveLength(64);
expect(verifyWebhookSignature(body, padded, secret)).toBe(false);
});

it('rejects odd-length and non-hex signatures', () => {
expect(verifyWebhookSignature(body, valid.slice(0, 63), secret)).toBe(false);
expect(verifyWebhookSignature(body, 'not-a-signature', secret)).toBe(false);
});

it('rejects an empty signature or secret', () => {
expect(verifyWebhookSignature(body, '', secret)).toBe(false);
expect(verifyWebhookSignature(body, valid, '')).toBe(false);
});
});
5 changes: 5 additions & 0 deletions apps/api/src/webhooks/webhook.types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { createHmac, timingSafeEqual } from 'crypto';

// Buffer.from(x, 'hex') silently stops at the first invalid character, so the
// format has to be checked before decoding.
const SHA256_HEX_PATTERN = /^[0-9a-fA-F]{64}$/;

/**
* Verify that a received webhook payload matches the HMAC-SHA256 signature
* sent in the `X-Swyft-Signature` header. Returns false for any invalid input.
Expand All @@ -10,6 +14,7 @@ export function verifyWebhookSignature(
secret: string,
): boolean {
if (!signature || !secret) return false;
if (!SHA256_HEX_PATTERN.test(signature)) return false;
try {
const expected = createHmac('sha256', secret).update(body).digest('hex');
const sigBuf = Buffer.from(signature, 'hex');
Expand Down