HMAC signatures
How to confirm the request really came from DroidSender and not from someone else.
| Header | What it holds |
|---|---|
X-DroidSender-Event | the event name, the same one in the body |
X-DroidSender-Delivery | unique id for this delivery attempt; deduplicate by it |
X-DroidSender-Signature | the signature, described below |
User-Agent | DroidSender-Webhooks/2.0 |
Every request carries the X-DroidSender-Signature header in the form t=<unix seconds>,v1=<hex>, where v1 is the HMAC-SHA256 of "<t>.<raw body>" using your webhook's secret.
const crypto = require("crypto");
function signatureIsValid(rawBody, header, secret) {
const match = /^t=(\d+),v1=([0-9a-f]{64})$/.exec(header ?? "");
if (!match) return false;
const [, t, received] = match;
// the timestamp is inside what is signed: an intercepted delivery
// cannot be replayed tomorrow, and t cannot be rewritten without the secret
const age = Math.abs(Math.floor(Date.now() / 1000) - Number(t));
if (age > 300) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(`${t}.${rawBody}`)
.digest("hex");
// constant-time comparison: a plain === lets the secret be measured
return crypto.timingSafeEqual(
Buffer.from(expected, "hex"),
Buffer.from(received, "hex"),
);
}Reject anything older than five minutes
Without the age check a replayed request still validates. Five minutes is the recommended window.
Use the raw body
The signature is computed over the JSON exactly as it arrived. If you verify it after parsing it into an object and serialising it again, the whitespace changes and the signature never matches.