webhooks.md
· 3.2 KiB · Markdown
Raw
# Parsing webhooks
To parse webhooks, here is the algorithm you should/must follow:
**Examples are provided in JS as of right now**
- Check the protocol version:
- The current protocol version is `splashtail`
- Check the `X-Webhook-Protocol` header and ensure that it is equal to the current protocol version
```js
if (req.headers["x-webhook-protocol"] != supportedProtocol) {
reply.status(403).send({
message: "Invalid protocol version!",
error: true,
});
return;
}
```
- A nonce is used to randomize the signature for retries. Ensure a nonce exists by checking the header's existence:
```js
if (!req.headers["x-webhook-nonce"]) {
reply.status(403).send({
message: "No nonce provided?",
error: true,
});
return;
}
```
- Next calculate the expected signature
- To do so, you must first get the body of the request
- Then use HMAC-SHA512 with the webhook secret as key and the body as the request body to get the ``signedBody``. Note that the format/digest should be ``hex``
- Then use HMAC-SHA512 with the nonce as the key and the signed body as the message to get the expected signature. Note that the format/digest should be ``hex``
```js
let body: string = req.body;
if (!body) {
reply.status(400).send({
message: "No request body provided?",
error: true,
});
return;
}
// Create hmac 512 hash
let signedBody = crypto
.createHmac("sha512", webhookSecret)
.update(body)
.digest("hex");
// Create the actual signature using x-webhook-nonce by performing a second hmac
let nonce = req.headers["x-webhook-nonce"].toString();
let expectedTok = crypto
.createHmac("sha512", nonce)
.update(signedBody)
.digest("hex");
```
- Compare this value with the ``X-Webhook-Signature`` header
- If they are equal, the request is valid and you can continue processing it
- If they are not equal, the request is invalid and you should return a 403 status code
```js
if (req.headers["x-webhook-signature"] != expectedTok) {
console.log(
`Expected: ${expectedTok} Got: ${req.headers["x-webhook-signature"]}`
);
reply.status(403).send({
message: "Invalid signature",
});
return;
}
```
- Next decrypt the request body. This is an additional security to prevent sensitive information from being leaked
- First hash the concatenation of the webhook secret and the nonce using SHA256
- Then read the body as a hex string and decrypt it using AES-256-GCM with the hashed secret as the key
```js
// sha256 on key
let hashedKey = crypto
.createHash("sha256")
.update(webhookSecret + nonce)
.digest();
let enc = Buffer.from(body, "hex");
const tag = enc.subarray(enc.length - tagLength, enc.length);
const iv = enc.subarray(0, ivLength);
const toDecrypt = enc.subarray(ivLength, enc.length - tag.length);
const decipher = crypto.createDecipheriv("aes-256-gcm", hashedKey, iv);
decipher.setAuthTag(tag);
const res = Buffer.concat([decipher.update(toDecrypt), decipher.final()]);
// Parse the decrypted body
let data = JSON.parse(res.toString("utf-8"));
if (data.created_at == undefined) {
reply.status(400).send({
message: "Invalid body",
error: true,
});
return;
```
Parsing webhooks
To parse webhooks, here is the algorithm you should/must follow:
Examples are provided in JS as of right now
- Check the protocol version:
- The current protocol version is
splashtail
- Check the
X-Webhook-Protocol
header and ensure that it is equal to the current protocol version
- The current protocol version is
if (req.headers["x-webhook-protocol"] != supportedProtocol) {
reply.status(403).send({
message: "Invalid protocol version!",
error: true,
});
return;
}
- A nonce is used to randomize the signature for retries. Ensure a nonce exists by checking the header's existence:
if (!req.headers["x-webhook-nonce"]) {
reply.status(403).send({
message: "No nonce provided?",
error: true,
});
return;
}
- Next calculate the expected signature
- To do so, you must first get the body of the request
- Then use HMAC-SHA512 with the webhook secret as key and the body as the request body to get the
signedBody
. Note that the format/digest should behex
- Then use HMAC-SHA512 with the nonce as the key and the signed body as the message to get the expected signature. Note that the format/digest should be
hex
let body: string = req.body;
if (!body) {
reply.status(400).send({
message: "No request body provided?",
error: true,
});
return;
}
// Create hmac 512 hash
let signedBody = crypto
.createHmac("sha512", webhookSecret)
.update(body)
.digest("hex");
// Create the actual signature using x-webhook-nonce by performing a second hmac
let nonce = req.headers["x-webhook-nonce"].toString();
let expectedTok = crypto
.createHmac("sha512", nonce)
.update(signedBody)
.digest("hex");
- Compare this value with the
X-Webhook-Signature
header- If they are equal, the request is valid and you can continue processing it
- If they are not equal, the request is invalid and you should return a 403 status code
if (req.headers["x-webhook-signature"] != expectedTok) {
console.log(
`Expected: ${expectedTok} Got: ${req.headers["x-webhook-signature"]}`
);
reply.status(403).send({
message: "Invalid signature",
});
return;
}
- Next decrypt the request body. This is an additional security to prevent sensitive information from being leaked
- First hash the concatenation of the webhook secret and the nonce using SHA256
- Then read the body as a hex string and decrypt it using AES-256-GCM with the hashed secret as the key
// sha256 on key
let hashedKey = crypto
.createHash("sha256")
.update(webhookSecret + nonce)
.digest();
let enc = Buffer.from(body, "hex");
const tag = enc.subarray(enc.length - tagLength, enc.length);
const iv = enc.subarray(0, ivLength);
const toDecrypt = enc.subarray(ivLength, enc.length - tag.length);
const decipher = crypto.createDecipheriv("aes-256-gcm", hashedKey, iv);
decipher.setAuthTag(tag);
const res = Buffer.concat([decipher.update(toDecrypt), decipher.final()]);
// Parse the decrypted body
let data = JSON.parse(res.toString("utf-8"));
if (data.created_at == undefined) {
reply.status(400).send({
message: "Invalid body",
error: true,
});
return;