diff --git a/package-lock.json b/package-lock.json index cd9d8e6..9e73f71 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,8 +9,6 @@ "version": "3.22.0", "license": "MIT", "dependencies": { - "buffer-equal-constant-time": "1.0.1", - "crypto-js": "4.2.0", "got": "^11.8.5", "lodash": "^4.17.15", "qs": "^6.9.1", @@ -1654,11 +1652,6 @@ "node-int64": "^0.4.0" } }, - "node_modules/buffer-equal-constant-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" - }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -1979,11 +1972,6 @@ "which": "bin/which" } }, - "node_modules/crypto-js": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", - "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==" - }, "node_modules/cssom": { "version": "0.4.4", "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz", diff --git a/src/webhooks.test.ts b/src/webhooks.test.ts index 1c190de..e482487 100644 --- a/src/webhooks.test.ts +++ b/src/webhooks.test.ts @@ -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); @@ -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"; diff --git a/src/webhooks.ts b/src/webhooks.ts index 97790db..420633c 100644 --- a/src/webhooks.ts +++ b/src/webhooks.ts @@ -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 = @@ -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 };