1
- const crypto = require ( "node:crypto" ) ;
2
-
3
1
const ApiError = require ( "./error" ) ;
4
2
5
3
/**
@@ -72,12 +70,10 @@ async function validateWebhook(requestData, secret) {
72
70
73
71
const signedContent = `${ id } .${ timestamp } .${ body } ` ;
74
72
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
+ ) ;
81
77
82
78
const expectedSignatures = signature
83
79
. split ( " " )
@@ -88,6 +84,62 @@ async function validateWebhook(requestData, secret) {
88
84
) ;
89
85
}
90
86
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
+
91
143
/**
92
144
* Automatically retry a request if it fails with an appropriate status code.
93
145
*
0 commit comments