Amarula.Protocol.Signal.CryptoHelpers (amarula v0.1.0)

View Source

Crypto primitives for the Signal v3 session cipher, ported byte-for-byte from node_modules/libsignal/src/crypto.js (the implementation Baileys runs).

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_cbc_decrypt(key, data, iv)

@spec aes_cbc_decrypt(binary(), binary(), binary()) :: binary()

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_cbc_encrypt(key, data, iv)

@spec aes_cbc_encrypt(binary(), binary(), binary()) :: binary()

AES-256-CBC encrypt with PKCS7 padding (inverse of aes_cbc_decrypt/3).

calculate_mac(key, data)

@spec calculate_mac(binary(), binary()) :: binary()

HMAC-SHA256 of data keyed by key.

derive_secrets(input, salt, info, chunks \\ 3)

@spec derive_secrets(binary(), binary(), binary(), 1..3) :: [binary()]

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.

verify_mac(data, key, mac, length)

@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.