Decrypt poll votes, ported from Baileys decryptPollVote
(src/Utils/process-message.ts).
A vote's encPayload/encIv are AES-256-GCM encrypted under a key derived from
the poll's message_secret and the ids of the poll, its creator, and the voter:
sign = poll_msg_id ++ poll_creator_jid ++ voter_jid ++ "Poll Vote" ++ <<1>>
key0 = HMAC-SHA256(key=<<0::256>>, data=message_secret) # key is 32 zero bytes
dec_key= HMAC-SHA256(key=key0, data=sign)
aad = "<poll_msg_id>\0<voter_jid>"
plain = AES-256-GCM-decrypt(encPayload, key=dec_key, iv=encIv, aad)
→ Proto.Message.PollVoteMessage (selectedOptions = SHA-256 option hashes)GCM tag is the trailing 16 bytes of encPayload.
Summary
Functions
Decrypt a poll vote (enc = %Proto.Message.PollEncValue{} or a map with
:encPayload/:encIv). Returns {:ok, %PollVoteMessage{}} (its
selectedOptions are SHA-256 hashes of the chosen option names — match against
Poll.tally/3), or {:error, reason}.
Encrypt a poll vote — the inverse of decrypt_vote/2. selected_option_hashes
is the list of sha256(option_name) for the options being voted for. Returns
%Proto.Message.PollEncValue{encPayload: ct<>tag, encIv: iv} ready to wrap in a
pollUpdateMessage. Uses the same key derivation + AAD as the receive side —
including the account-level normalization described on decrypt_vote/2 — so a
vote we send is reproducible by any recipient, not just by us.
Types
Functions
Decrypt a poll vote (enc = %Proto.Message.PollEncValue{} or a map with
:encPayload/:encIv). Returns {:ok, %PollVoteMessage{}} (its
selectedOptions are SHA-256 hashes of the chosen option names — match against
Poll.tally/3), or {:error, reason}.
Jid forms must match the sender's
The key is derived from poll_creator_jid and voter_jid, so they must match
the identities the voter keyed on. Device and agent segments are stripped
for you (see below), but the PN/LID choice is not — it can't be: they are
different identities, not different spellings of one. In a LID group the
voter keys on their LID (the participant on the vote message), not their
PN; passing the PN (or vice-versa) derives the wrong key and decryption fails
({:error, :decrypt_failed}) — the root of Baileys #2158. Use the author jid as
it appears on the vote's key.participant (resolve LID↔PN with
Amarula.Contacts.pn_for_lid/2 only if you need to compare identities — not
to build this context).
Identities are normalized to account level
poll_creator_jid and voter_jid are reduced to their account-level form
(device and agent stripped) before deriving the key, on both this and
encrypt_vote/2 — matching Baileys' jidNormalizedUser, which it applies to
every identity feeding decryptPollVote. A device-bearing jid would otherwise
derive a key nobody else can reproduce: Amarula.own_address/1 carries this
companion's device, so votes encrypted with it silently never counted (#48).
Encrypt a poll vote — the inverse of decrypt_vote/2. selected_option_hashes
is the list of sha256(option_name) for the options being voted for. Returns
%Proto.Message.PollEncValue{encPayload: ct<>tag, encIv: iv} ready to wrap in a
pollUpdateMessage. Uses the same key derivation + AAD as the receive side —
including the account-level normalization described on decrypt_vote/2 — so a
vote we send is reproducible by any recipient, not just by us.