Amarula.Protocol.Crypto.Crypto (amarula v0.1.0)

View Source

Cryptographic utilities for WhatsApp Noise protocol implementation.

This module provides functions for Curve25519 key operations, AES-GCM encryption, SHA256 hashing, HKDF key derivation, and HMAC signing required by the Noise protocol.

Summary

Functions

Decrypt ciphertext using AES-256-CTR.

Decrypt ciphertext using AES-256-GCM.

Encrypt plaintext using AES-256-CTR.

Encrypt plaintext using AES-256-GCM.

Derive the link-code pairing key from the pairing code and salt.

Generate a random IV for AES-GCM encryption.

Generate a Curve25519 key pair using built-in crypto.

Generate a registration ID for Signal protocol.

Generates a Signal Protocol public key by prefixing with key bundle type if needed.

Generate a signed pre-key ID.

HMAC-based Key Derivation Function (HKDF).

HMAC-SHA256 signing.

HMAC-SHA512 (used by app-state value MACs).

Generate random bytes of specified length.

Generate SHA-256 hash of input data.

Calculate shared secret from private and public keys using Curve25519.

Sign data with a 32-byte X25519 private key using XEd25519 (libsignal-compatible, matches Baileys Curve.sign).

Verify an XEd25519 signature against a 32-byte Montgomery (X25519) public key (libsignal-compatible, matches Baileys Curve.verify).

Types

decryption_result()

@type decryption_result() :: {:ok, binary()} | {:error, term()}

encryption_result()

@type encryption_result() :: {:ok, binary()} | {:error, term()}

key_pair()

@type key_pair() :: %{private: binary(), public: binary()}

Functions

aes_decrypt_ctr(ciphertext, key, iv)

@spec aes_decrypt_ctr(binary(), binary(), binary()) :: binary()

Decrypt ciphertext using AES-256-CTR.

Mirrors Baileys aesDecryptCTR. Returns the plaintext as a binary.

aes_decrypt_gcm(ciphertext, key, iv, additional_data)

@spec aes_decrypt_gcm(binary(), binary(), binary(), binary()) :: decryption_result()

Decrypt ciphertext using AES-256-GCM.

Returns {:ok, plaintext} or {:error, reason}.

aes_encrypt_ctr(plaintext, key, iv)

@spec aes_encrypt_ctr(binary(), binary(), binary()) :: binary()

Encrypt plaintext using AES-256-CTR.

Used by the link-code (phone-number) pairing flow to wrap ephemeral public keys. Mirrors Baileys aesEncryptCTR. Returns the ciphertext as a binary.

aes_encrypt_gcm(plaintext, key, iv, additional_data)

@spec aes_encrypt_gcm(binary(), binary(), binary(), binary()) :: encryption_result()

Encrypt plaintext using AES-256-GCM.

Returns {:ok, ciphertext} or {:error, reason}.

derive_pairing_code_key(pairing_code, salt)

@spec derive_pairing_code_key(binary(), binary()) :: binary()

Derive the link-code pairing key from the pairing code and salt.

PBKDF2-HMAC-SHA256, 131_072 iterations (2 << 16), 32-byte output — matches Baileys derivePairingCodeKey.

generate_iv(counter)

@spec generate_iv(non_neg_integer()) :: binary()

Generate a random IV for AES-GCM encryption.

Creates a 12-byte IV with the counter in the last 4 bytes. According to WhatsApp implementation (Baileys/whatsmeow):

  • 8 leading zero bytes
  • 4 bytes big-endian counter (bytes 8-11) This matches: 0x0000000000000000 || be32(counter)

generate_key_pair()

@spec generate_key_pair() :: key_pair()

Generate a Curve25519 key pair using built-in crypto.

Returns a map with :private and :public keys as binaries.

generate_registration_id()

@spec generate_registration_id() :: non_neg_integer()

Generate a registration ID for Signal protocol.

Returns a random 14-bit integer (0-16383), matching Baileys implementation. WhatsApp requires registration IDs to be within this range.

generate_signal_pub_key(pub_key)

@spec generate_signal_pub_key(binary()) :: binary()

Generates a Signal Protocol public key by prefixing with key bundle type if needed.

Signal Protocol expects public keys to be 33 bytes (1 byte type + 32 bytes key). If the key is already 33 bytes, return as-is. Otherwise, prefix with KEY_BUNDLE_TYPE.

This matches Baileys: pubKey.length === 33 ? pubKey : Buffer.concat([KEY_BUNDLE_TYPE, pubKey])

generate_signed_pre_key_id()

@spec generate_signed_pre_key_id() :: non_neg_integer()

Generate a signed pre-key ID.

Returns a random 16-bit integer.

hkdf(input_key_material, output_length, salt \\ <<>>, info \\ <<>>)

@spec hkdf(binary(), non_neg_integer(), binary(), binary()) :: binary()

HMAC-based Key Derivation Function (HKDF).

Derives keys from input key material using HKDF with SHA-256. Returns the derived key as a binary.

hmac_sign(data, key)

@spec hmac_sign(binary(), binary()) :: binary()

HMAC-SHA256 signing.

Returns the HMAC signature as a binary.

hmac_sign_sha512(data, key)

@spec hmac_sign_sha512(binary(), binary()) :: binary()

HMAC-SHA512 (used by app-state value MACs).

random_bytes(length)

@spec random_bytes(non_neg_integer()) :: binary()

Generate random bytes of specified length.

Returns random binary data.

sha256(data)

@spec sha256(binary()) :: binary()

Generate SHA-256 hash of input data.

Returns the hash as a binary.

shared_key(private_key, public_key)

@spec shared_key(binary(), binary()) :: binary()

Calculate shared secret from private and public keys using Curve25519.

Returns the shared secret as a binary.

sign(data, private_key)

@spec sign(binary(), binary()) :: binary()

Sign data with a 32-byte X25519 private key using XEd25519 (libsignal-compatible, matches Baileys Curve.sign).

Returns the signature as a binary (64 bytes).

verify(data, signature, public_key)

@spec verify(binary(), binary(), binary()) :: boolean()

Verify an XEd25519 signature against a 32-byte Montgomery (X25519) public key (libsignal-compatible, matches Baileys Curve.verify).

Returns true if signature is valid, false otherwise.