🔐 Webhook Security Guide
Mava webhook security guide
Overview
Mava webhooks use a two-layer encryption system to ensure maximum security:
1. RSA encryption for key exchange
2. AES encryption for payload data
This dual-layer approach provides both security and performance, allowing us to safely handle large payloads while maintaining end-to-end encryption.
Key Components
Signing Key: A private key provided to you in the UI (prefixed with 'mava_wh_')
Encryption Key: A public key used to encrypt the symmetric key
Symmetric Key: A unique AES key generated for each webhook event
IV: A random initialization vector used for AES encryption
Implementation Guide
Verifying Webhook Authenticity
Each webhook includes a signature that you should verify before processing the payload:
async function verifyEventSignature(
encryptedEvent: string,
signature: string,
encryptedSymmetricKey: string,
signingKey: string
) {
try {
// Split the encrypted key into IV and symmetric key components
const [iv, symmetricKey] = encryptedSymmetricKey.split(':');
// Extract the private key (removing mava_wh_ prefix)
const key = signingKey.split('_')[2];
const privateKeyBuffer = Buffer.from(key, 'base64');
// Decrypt the symmetric key using RSA with OAEP padding
const decryptedSymmetricKey = crypto.privateDecrypt(
{
key: privateKeyBuffer,
format: 'der',
type: 'pkcs8',
padding: crypto.constants.RSA_PKCS1_OAEP_PADDING
},
Buffer.from(symmetricKey, 'base64')
);
// Create HMAC using the decrypted symmetric key
const hmac = crypto.createHmac('sha256', decryptedSymmetricKey.toString('base64'));
hmac.update(encryptedEvent);
const regeneratedSignature = hmac.digest('hex');
// Compare signatures using a timing-safe comparison
return crypto.timingSafeEqual(
Buffer.from(regeneratedSignature, 'hex'),
Buffer.from(signature, 'hex')
);
} catch (err) {
throw new Error('Failed to verify event signature');
}
}
2. Decrypting the Payload
After verifying the signature, decrypt the payload using this process:
async function decryptPayload(
encryptedPayload: string,
encryptedSymmetricKey: string,
signingKey: string
) {
try {
// Split the encrypted key into IV and symmetric key components
const [iv, symmetricKey] = encryptedSymmetricKey.split(':');
// Extract the private key (removing mava_wh_ prefix)
const key = signingKey.split('_')[2];
const privateKeyBuffer = Buffer.from(key, 'base64');
// Decrypt the symmetric key using RSA with OAEP padding
const decryptedSymmetricKey = crypto.privateDecrypt(
{
key: privateKeyBuffer,
format: 'der',
type: 'pkcs8',
padding: crypto.constants.RSA_PKCS1_OAEP_PADDING
},
Buffer.from(symmetricKey, 'base64')
);
// Decrypt the payload using AES-256-CBC
const decipher = crypto.createDecipheriv(
'aes-256-cbc',
decryptedSymmetricKey,
Buffer.from(iv, 'base64')
);
let decrypted = decipher.update(Buffer.from(encryptedPayload, 'base64'));
decrypted = Buffer.concat([decrypted, decipher.final()]);
return decrypted.toString('utf8');
} catch (err) {
throw new Error('Failed to decrypt payload');
}
}
Processing a Webhook
When you receive a webhook, you'll get:
payload
: The encrypted event data
key
: The encrypted symmetric key with IV (format: iv:encryptedKey
)
signature
: The HMAC signature for verification
webhookId
: A unique identifier for the webhook
Example webhook processing:
app.post('/webhook', async (req, res) => {
const { payload, key, signature, webhookId } = req.body;
const signingKey = process.env.MAVA_SIGNING_KEY; // Your signing key from the UI
try {
// 1. Verify the signature
const isValid = await verifyEventSignature(payload, signature, key, signingKey);
if (!isValid) {
return res.status(401).send('Invalid signature');
}
// 2. Decrypt the payload
const decryptedPayload = await decryptPayload(payload, key, signingKey);
const eventData = JSON.parse(decryptedPayload);
// 3. Process the event
await processEvent(eventData);
res.status(200).send('OK');
} catch (err) {
res.status(400).send('Failed to process webhook');
}
});
Security Notes
Store your signing key securely and never expose it publicly.
Always verify the signature before processing the payload.
Use timing-safe comparison for signature verification.
The encryption uses RSA-OAEP for key exchange and AES-256-CBC for payload encryption.
Last updated