Skip to content

Commit cbfcc62

Browse files
authored
Remove dependency on node:crypto for verifying webhooks (#216)
1 parent 91a35e5 commit cbfcc62

File tree

1 file changed

+60
-8
lines changed

1 file changed

+60
-8
lines changed

lib/util.js

+60-8
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
const crypto = require("node:crypto");
2-
31
const ApiError = require("./error");
42

53
/**
@@ -72,12 +70,10 @@ async function validateWebhook(requestData, secret) {
7270

7371
const signedContent = `${id}.${timestamp}.${body}`;
7472

75-
const secretBytes = Buffer.from(signingSecret.split("_")[1], "base64");
76-
77-
const computedSignature = crypto
78-
.createHmac("sha256", secretBytes)
79-
.update(signedContent)
80-
.digest("base64");
73+
const computedSignature = await createHMACSHA256(
74+
signingSecret.split("_").pop(),
75+
signedContent
76+
);
8177

8278
const expectedSignatures = signature
8379
.split(" ")
@@ -88,6 +84,62 @@ async function validateWebhook(requestData, secret) {
8884
);
8985
}
9086

87+
/**
88+
* @param {string} secret - base64 encoded string
89+
* @param {string} data - text body of request
90+
*/
91+
async function createHMACSHA256(secret, data) {
92+
const encoder = new TextEncoder();
93+
let crypto = globalThis.crypto;
94+
95+
// In Node 18 the `crypto` global is behind a --no-experimental-global-webcrypto flag
96+
if (typeof crypto === "undefined" && typeof require === "function") {
97+
crypto = require("node:crypto").webcrypto;
98+
}
99+
100+
const key = await crypto.subtle.importKey(
101+
"raw",
102+
base64ToBytes(secret),
103+
{ name: "HMAC", hash: "SHA-256" },
104+
false,
105+
["sign"]
106+
);
107+
108+
const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(data));
109+
return bytesToBase64(signature);
110+
}
111+
112+
/**
113+
* Convert a base64 encoded string into bytes.
114+
*
115+
* @param {string} the base64 encoded string
116+
* @return {Uint8Array}
117+
*
118+
* Two functions for encoding/decoding base64 strings using web standards. Not
119+
* intended to be used to encode/decode arbitrary string data.
120+
* See: https://developer.mozilla.org/en-US/docs/Glossary/Base64#javascript_support
121+
* See: https://stackoverflow.com/a/31621532
122+
*
123+
* Performance might take a hit because of the conversion to string and then to binary,
124+
* if this is the case we might want to look at an alternative solution.
125+
* See: https://jsben.ch/wnaZC
126+
*/
127+
function base64ToBytes(base64) {
128+
return Uint8Array.from(atob(base64), (m) => m.codePointAt(0));
129+
}
130+
131+
/**
132+
* Convert a base64 encoded string into bytes.
133+
*
134+
* See {@link base64ToBytes} for caveats.
135+
*
136+
* @param {Uint8Array | ArrayBuffer} the base64 encoded string
137+
* @return {string}
138+
*/
139+
function bytesToBase64(bytes) {
140+
return btoa(String.fromCharCode.apply(null, new Uint8Array(bytes)));
141+
}
142+
91143
/**
92144
* Automatically retry a request if it fails with an appropriate status code.
93145
*

0 commit comments

Comments
 (0)