Amarula.Protocol.Messages.Media (amarula v0.1.0)

View Source

WhatsApp media encryption + upload, ported from Baileys src/Utils/messages-media.ts.

Flow for an outgoing media message:

  1. media_key = 32 random bytes.
  2. HKDF-SHA256 expand media_key to 112 bytes with info "WhatsApp <Type> Keys" → iv(16) ++ cipher_key(32) ++ mac_key(32) ++ ref_key(rest).
  3. ciphertext = AES-256-CBC(cipher_key, iv, plaintext), mac = HMAC-SHA256(mac_key, iv ++ ciphertext) truncated to 10 bytes. The uploaded blob is ciphertext ++ mac.
  4. file_sha256 = sha256(plaintext), file_enc_sha256 = sha256(ciphertext ++ mac).
  5. Fetch a media connection (<iq xmlns="w:m"><media_conn/>), PUT the blob to https://<host>/mms/<type>/<encSha256B64>?auth=..&token=.., get direct_path/url.
  6. Build the per-type message (e.g. %Proto.Message{imageMessage: ...}).

decrypt/2 reverses 2–3 for a downloaded blob.

Summary

Functions

Decrypt a downloaded media blob (ciphertext ++ mac) with its media_key. Verifies the MAC before decrypting. Returns the plaintext.

Download an encrypted media blob from a message's :directPath (or :url) and decrypt it for type. ref is a map/struct with direct_path/directPath or url, plus media_key/mediaKey. Returns {:ok, plaintext} (still possibly compressed — history blobs are zlib-deflated; the caller inflates).

Encrypt plaintext for media type. Returns the uploadable blob plus the hashes/key the message stanza needs.

Upload an encrypted blob to the media servers. conn is the Connection; returns {:ok, %{direct_path: .., url: ..}}.

Types

media_type()

@type media_type() :: :image | :video | :audio | :document | :sticker | :history

Functions

decrypt(enc, media_key, type)

@spec decrypt(binary(), binary(), media_type()) :: {:ok, binary()} | {:error, term()}

Decrypt a downloaded media blob (ciphertext ++ mac) with its media_key. Verifies the MAC before decrypting. Returns the plaintext.

download(ref, type)

@spec download(map(), media_type()) :: {:ok, binary()} | {:error, term()}

Download an encrypted media blob from a message's :directPath (or :url) and decrypt it for type. ref is a map/struct with direct_path/directPath or url, plus media_key/mediaKey. Returns {:ok, plaintext} (still possibly compressed — history blobs are zlib-deflated; the caller inflates).

encrypt(plaintext, type)

@spec encrypt(binary(), media_type()) ::
  {:ok,
   %{
     enc: binary(),
     media_key: binary(),
     file_sha256: binary(),
     file_enc_sha256: binary(),
     file_length: non_neg_integer()
   }}

Encrypt plaintext for media type. Returns the uploadable blob plus the hashes/key the message stanza needs.

upload(conn, enc, file_enc_sha256, type)

@spec upload(GenServer.server(), binary(), binary(), media_type()) ::
  {:ok, %{direct_path: String.t() | nil, url: String.t()}} | {:error, term()}

Upload an encrypted blob to the media servers. conn is the Connection; returns {:ok, %{direct_path: .., url: ..}}.