Decrypts and verifies incoming Banking Circle webhook payloads.
Banking Circle secures webhook bodies with AES-256-GCM, using a
32-character pre-shared key set when you create the subscription (see
BankingCircle.Webhooks.create_subscription/2). Per the docs, a
received payload carries three companion values:
- a checksum (44 base64 characters → 32 raw bytes)
- an authentication tag (24 base64 characters → 16 raw bytes)
- a nonce (16 base64 characters → 12 raw bytes)
This module treats the GCM authentication tag as the cryptographic
integrity guarantee (verified automatically by AEAD decryption itself —
decryption fails closed if the tag doesn't match). The checksum field
is additionally verified as a SHA-256 digest of the ciphertext, which is
the most common convention for this kind of "checksum" field; if your
account's payload docs specify a different checksum construction (e.g.
HMAC-SHA256 over ciphertext+AAD), override it via the :checksum_fun
option rather than trusting this default blindly — verify this against
a real captured payload from your sandbox before relying on it in
production.
Usage in a Plug/Phoenix controller
def webhook(conn, _params) do
{:ok, raw_body, conn} = Plug.Conn.read_body(conn)
case BankingCircle.Webhook.Verifier.verify_and_decrypt(raw_body,
checksum: get_req_header(conn, "x-bc-checksum") |> List.first(),
tag: get_req_header(conn, "x-bc-auth-tag") |> List.first(),
nonce: get_req_header(conn, "x-bc-nonce") |> List.first(),
key: Application.fetch_env!(:my_app, :bc_webhook_key)
) do
{:ok, event} ->
MyApp.WebhookHandler.handle(event)
send_resp(conn, 200, "")
{:error, reason} ->
Logger.warning("Rejected Banking Circle webhook: #{inspect(reason)}")
send_resp(conn, 400, "")
end
endHeader names above (x-bc-checksum etc.) are illustrative — confirm the
exact header names Banking Circle uses for your subscription against the
"Setting up Webhooks" guide, since this client doesn't hardcode them.
Summary
Functions
Verifies the checksum, verifies+decrypts via AES-256-GCM, and JSON-decodes the plaintext.
Types
Functions
@spec verify_and_decrypt(binary(), verify_opts()) :: {:ok, term()} | {:error, atom()}
Verifies the checksum, verifies+decrypts via AES-256-GCM, and JSON-decodes the plaintext.
Returns {:ok, decoded_event} or {:error, reason} where reason is
one of :invalid_checksum, :decryption_failed (bad tag/key/nonce —
treat as tampered or wrong key), or :invalid_json.