Webhooks
Encryption
Clip data is encrypted with a secret held only by you and us. The cipher is authenticated, so a payload that did not originate with us, or that was altered in transit, fails to decrypt rather than yielding wrong data.
Parameters
| Parameter | Value | Notes |
|---|---|---|
| Algorithm | AES-256-GCM | An authenticated cipher. AES-CBC is not a substitute, because it provides no integrity and must not be used. |
| Key | 256-bit | The shared secret, delivered as 64 hex characters |
| Nonce | 96-bit | Sent as the nonce field, base64-encoded |
| Auth tag | 128-bit | Appended to the ciphertext, so payload decodes to ciphertext followed by tag |
| Additional data | none | Pass an empty value or null |
| Plaintext | UTF-8 JSON | The object specified above |
Reference implementation
# pip install cryptography
import base64, json
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
def decrypt_delivery(secret_hex: str, nonce_b64: str, payload_b64: str) -> dict:
key = bytes.fromhex(secret_hex) # 32 bytes
nonce = base64.b64decode(nonce_b64) # 12 bytes
blob = base64.b64decode(payload_b64) # ciphertext || 16-byte tag
plaintext = AESGCM(key).decrypt(nonce, blob, None)
return json.loads(plaintext)// no dependencies
const crypto = require('crypto');
function decryptDelivery(secretHex, nonceB64, payloadB64) {
const key = Buffer.from(secretHex, 'hex'); // 32 bytes
const nonce = Buffer.from(nonceB64, 'base64'); // 12 bytes
const blob = Buffer.from(payloadB64, 'base64');
const tag = blob.subarray(blob.length - 16);
const ciphertext = blob.subarray(0, blob.length - 16);
const decipher = crypto.createDecipheriv('aes-256-gcm', key, nonce);
decipher.setAuthTag(tag);
const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
return JSON.parse(plaintext.toString('utf8'));
}