Amarula.Protocol.Signal.CryptoHelpers (amarula v0.1.0)
View SourceCrypto primitives for the Signal v3 session cipher, ported byte-for-byte from
node_modules/libsignal/src/crypto.js (the implementation Baileys runs).
derive_secrets/4— RFC 5869 HKDF-SHA256 returning the first N 32-byte chunkscalculate_mac/2— HMAC-SHA256verify_mac/4— constant-length truncated MAC check (raises on mismatch)aes_cbc_decrypt/3— AES-256-CBC (WhatsApp message payload cipher; NOT GCM)
These deliberately mirror libsignal, not the Noise/transport crypto in
Amarula.Protocol.Crypto.
Summary
Functions
AES-256-CBC decrypt. :crypto does no padding, so we strip PKCS#7 ourselves to
match node's createDecipheriv (which auto-removes it), as libsignal relies on.
AES-256-CBC encrypt with PKCS7 padding (inverse of aes_cbc_decrypt/3).
HMAC-SHA256 of data keyed by key.
HKDF-SHA256 (RFC 5869) returning the first chunks (1..3) 32-byte output blocks.
Verify that the first length bytes of HMAC-SHA256(key, data) equal mac.
Raises "Bad MAC" / "Bad MAC length" on failure, matching libsignal verifyMAC.
Functions
AES-256-CBC decrypt. :crypto does no padding, so we strip PKCS#7 ourselves to
match node's createDecipheriv (which auto-removes it), as libsignal relies on.
AES-256-CBC encrypt with PKCS7 padding (inverse of aes_cbc_decrypt/3).
HMAC-SHA256 of data keyed by key.
HKDF-SHA256 (RFC 5869) returning the first chunks (1..3) 32-byte output blocks.
Matches libsignal deriveSecrets: PRK = HMAC(salt, input); then T(i) = HMAC(PRK, T(i-1) || info || i), with T(0) empty. Salt must be 32 bytes.
@spec verify_mac(binary(), binary(), binary(), non_neg_integer()) :: :ok
Verify that the first length bytes of HMAC-SHA256(key, data) equal mac.
Raises "Bad MAC" / "Bad MAC length" on failure, matching libsignal verifyMAC.