Skip to content

Commit

Permalink
Merge pull request #175 from SteveOfficerSeccl/master
Browse files Browse the repository at this point in the history
Reintroduce replacement of crypto-js
  • Loading branch information
bekundayo authored May 1, 2024
2 parents 6d833ff + d3b153a commit e3a18a4
Show file tree
Hide file tree
Showing 3 changed files with 30 additions and 32 deletions.
12 changes: 0 additions & 12 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 12 additions & 1 deletion src/webhooks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ const requestBody = JSON.stringify(
JSON.parse(fs.readFileSync("src/fixtures/webhook_body.json", "utf8"))
);

const requestBodyBuffer = Buffer.from(requestBody);

const webhookSecret = "ED7D658C-D8EB-4941-948B-3973214F2D49"
const signatureHeader = "2693754819d3e32d7e8fcb13c729631f316c6de8dc1cf634d6527f1c07276e7e";


describe(".parse", () => {
test("parses a webhook response body with valid signature", () => {
test("parses a string body with valid signature", () => {
const result = webhook.parse(requestBody, webhookSecret, signatureHeader);

expect(result.length).toBe(2);
Expand All @@ -19,6 +21,15 @@ describe(".parse", () => {
expect(firstEvent.id).toBe("EV00BD05S5VM2T");
});

test("parses a buffer body with valid signature", () => {
const result = webhook.parse(requestBodyBuffer, webhookSecret, signatureHeader);

expect(result.length).toBe(2);

const firstEvent = result[0];
expect(firstEvent.id).toBe("EV00BD05S5VM2T");
});

test("parses a webhook response body with an invalid signature", () => {
const badSignatureHeader = "NOTVERYCONVINCING";

Expand Down
37 changes: 18 additions & 19 deletions src/webhooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
* JSON object into an `GoCardless.Event` class.
*/

import cryptoJS from 'crypto-js';
import safeCompare from 'buffer-equal-constant-time';
import crypto from 'crypto';
import type { Event } from './types/Types';

function InvalidSignatureError() {
this.message =
Expand All @@ -23,42 +23,41 @@ function InvalidSignatureError() {
* Validates that a webhook was genuinely sent by GoCardless, then parses each `event`
* object into an array of `GoCardless.Event` classes.
*
* @body [string]: The raw webhook body.
* @webhookSecret [string]: The webhook endpoint secret for your webhook endpoint, as
* @body The raw webhook body.
* @webhookSecret The webhook endpoint secret for your webhook endpoint, as
* configured in your GoCardless Dashboard.
* @signatureHeader [string]: The signature included in the webhook request, as specified
* @signatureHeader The signature included in the webhook request, as specified
* by the `Webhook-Signature` header.
*/
function parse(body: string, webhookSecret: string, signatureHeader: string) {
function parse(body: crypto.BinaryLike, webhookSecret: string, signatureHeader: string): Event[] {
verifySignature(body, webhookSecret, signatureHeader);

const eventsData = JSON.parse(body)['events'];
return eventsData.map(eventJson => eventJson);
const bodyString = typeof body === 'string' ? body : body.toString();
const eventsData = JSON.parse(bodyString) as { events: Event[] };
return eventsData.events;
}

/**
* Validate the signature header. Note, we're using the `buffer-equal-constant-time`
* Validate the signature header. Note, we're using the `crypto.timingSafeEqual`
* library for the hash comparison, to protect against timing attacks.
*
* @body [string]: The raw webhook body.
* @webhookSecret [string]: The webhook endpoint secret for your webhook endpoint, as
* @body The raw webhook body.
* @webhookSecret The webhook endpoint secret for your webhook endpoint, as
* configured in your GoCardless Dashboard.
* @signatureHeader [string]: The signature included in the webhook request, as specified
* @signatureHeader The signature included in the webhook request, as specified
* by the `Webhook-Signature` header.
*/
function verifySignature(
body: string,
body: crypto.BinaryLike,
webhookSecret: string,
signatureHeader: string
) {
const rawDigest = cryptoJS.HmacSHA256(body, webhookSecret);
const bufferDigest = crypto.createHmac('sha256', webhookSecret).update(body).digest();
const bufferSignatureHeader = Buffer.from(signatureHeader, 'hex');

const bufferDigest = Buffer.from(rawDigest.toString(cryptoJS.enc.Hex));
const bufferSignatureHeader = Buffer.from(signatureHeader);

if (!safeCompare(bufferDigest, bufferSignatureHeader)) {
if ((bufferDigest.length !== bufferSignatureHeader.length) || !crypto.timingSafeEqual(bufferDigest, bufferSignatureHeader)) {
throw new InvalidSignatureError();
}
}

export { parse, InvalidSignatureError };
export { parse, verifySignature, InvalidSignatureError };

0 comments on commit e3a18a4

Please sign in to comment.