diff --git a/lib/util.js b/lib/util.js index 949bafa..a9406e3 100644 --- a/lib/util.js +++ b/lib/util.js @@ -1,5 +1,3 @@ -const crypto = require("node:crypto"); - const ApiError = require("./error"); /** @@ -72,12 +70,10 @@ async function validateWebhook(requestData, secret) { const signedContent = `${id}.${timestamp}.${body}`; - const secretBytes = Buffer.from(signingSecret.split("_")[1], "base64"); - - const computedSignature = crypto - .createHmac("sha256", secretBytes) - .update(signedContent) - .digest("base64"); + const computedSignature = await createHMACSHA256( + signingSecret.split("_").pop(), + signedContent + ); const expectedSignatures = signature .split(" ") @@ -88,6 +84,62 @@ async function validateWebhook(requestData, secret) { ); } +/** + * @param {string} secret - base64 encoded string + * @param {string} data - text body of request + */ +async function createHMACSHA256(secret, data) { + const encoder = new TextEncoder(); + let crypto = globalThis.crypto; + + // In Node 18 the `crypto` global is behind a --no-experimental-global-webcrypto flag + if (typeof crypto === "undefined" && typeof require === "function") { + crypto = require("node:crypto").webcrypto; + } + + const key = await crypto.subtle.importKey( + "raw", + base64ToBytes(secret), + { name: "HMAC", hash: "SHA-256" }, + false, + ["sign"] + ); + + const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(data)); + return bytesToBase64(signature); +} + +/** + * Convert a base64 encoded string into bytes. + * + * @param {string} the base64 encoded string + * @return {Uint8Array} + * + * Two functions for encoding/decoding base64 strings using web standards. Not + * intended to be used to encode/decode arbitrary string data. + * See: https://developer.mozilla.org/en-US/docs/Glossary/Base64#javascript_support + * See: https://stackoverflow.com/a/31621532 + * + * Performance might take a hit because of the conversion to string and then to binary, + * if this is the case we might want to look at an alternative solution. + * See: https://jsben.ch/wnaZC + */ +function base64ToBytes(base64) { + return Uint8Array.from(atob(base64), (m) => m.codePointAt(0)); +} + +/** + * Convert a base64 encoded string into bytes. + * + * See {@link base64ToBytes} for caveats. + * + * @param {Uint8Array | ArrayBuffer} the base64 encoded string + * @return {string} + */ +function bytesToBase64(bytes) { + return btoa(String.fromCharCode.apply(null, new Uint8Array(bytes))); +} + /** * Automatically retry a request if it fails with an appropriate status code. *