# How Baileys Handles Poll Message Encryption and Decryption

> Learn how Baileys encrypts and decrypts poll messages using HMAC-SHA256 and AES-256-GCM for secure vote payloads. Discover the Baileys encryption mechanism.

- Repository: [WhiskeySockets/Baileys](https://github.com/WhiskeySockets/Baileys)
- Tags: internals
- Published: 2026-08-01

---

**Baileys encrypts and decrypts poll messages using a per-poll 32-byte message secret that generates symmetric keys via HMAC-SHA256 derivation, with vote payloads secured using AES-256-GCM authenticated encryption.**

Poll messages in WhatsApp require special cryptographic handling because vote contents must remain encrypted to all parties except the original poll creator. The Baileys library implements this through a multi-stage process involving secret generation, key derivation, and authenticated decryption. This article examines the exact implementation based on the WhiskeySockets/Baileys source code.

---

## Poll Creation: Generating the Message Secret

When a user creates a poll, Baileys attaches a **message secret** to the poll-creation message. This 32-byte random value serves as the root key for all subsequent vote encryption and decryption.

In [`src/Utils/messages.ts`](https://github.com/WhiskeySockets/Baileys/blob/main/src/Utils/messages.ts) (lines 66-69), the secret is generated or accepted from user input:

```typescript
m.messageContextInfo = {
    // encKey
    messageSecret: message.poll.messageSecret || randomBytes(32)   // ← secret generation
}

```

**Key characteristics of this design:**

- The secret is **never transmitted to WhatsApp servers** — it lives only in the message context sent to conversation participants
- If the user pre-supplies a `messageSecret`, that value is used; otherwise, `randomBytes(32)` generates cryptographically secure randomness
- The poll creator retains this secret locally to decrypt future votes

---

## Receiving Encrypted Votes: The Poll Update Flow

When someone votes in a poll, WhatsApp encrypts their vote using the poll's secret and delivers it as a `pollUpdateMessage`. The payload contains two critical fields:

- **`encPayload`**: The encrypted vote ciphertext
- **`encIv`**: The initialization vector for AES-GCM

The Baileys client receives only these encrypted values — the secret must already be present from the original poll creation.

### Locating the Stored Secret

Before decryption can occur, Baileys fetches the original poll-creation message to retrieve the stored `pollEncKey`. This key lookup happens in [`src/Utils/process-message.ts`](https://github.com/WhiskeySockets/Baileys/blob/main/src/Utils/process-message.ts) during message processing.

---

## Key Derivation: The HMAC-SHA256 Chain

The actual decryption key is not the raw secret. Instead, Baileys derives a unique key per vote using a **deterministic HMAC-based key derivation function** defined in [`src/Utils/process-message.ts`](https://github.com/WhiskeySockets/Baileys/blob/main/src/Utils/process-message.ts) (lines 34-57).

The derivation process executes three steps:

1. **Base key generation** (`key0`)
2. **Signature construction** (`sign`)
3. **Final decryption key** (`decKey`)

```typescript
const key0 = hmacSign(pollEncKey, new Uint8Array(32), 'sha256')
const decKey = hmacSign(sign, key0, 'sha256')

```

The `sign` buffer concatenates multiple identifiers to ensure key uniqueness per voter and poll:

| Component | Purpose |
|-----------|---------|
| `pollMsgId` | Prevents cross-poll key collision |
| `pollCreatorJid` | Binds to specific poll creator |
| `voterJid` | Ensures per-voter key isolation |
| `'Poll Vote'` + `0x01` | Domain separation constant |

This construction follows cryptographic best practices: **unique keys per operation, context binding via authenticated data, and explicit protocol versioning** (the `0x01` suffix allows future protocol upgrades).

---

## AES-256-GCM Decryption

With the derived `decKey`, Baileys performs **AES-256-GCM authenticated decryption**. The implementation lives in [`src/Utils/crypto.ts`](https://github.com/WhiskeySockets/Baileys/blob/main/src/Utils/crypto.ts) (lines 63-72):

```typescript
export function aesDecryptGCM(ciphertext, key, iv, additionalData) {
    const decipher = createDecipheriv('aes-256-gcm', key, iv)
    const enc = ciphertext.slice(0, ciphertext.length - GCM_TAG_LENGTH)
    const tag = ciphertext.slice(ciphertext.length - GCM_TAG_LENGTH)
    decipher.setAAD(additionalData)
    decipher.setAuthTag(tag)
    return Buffer.concat([decipher.update(enc), decipher.final()])
}

```

**Critical parameters:**

- **Key**: 256-bit output from HMAC-SHA256 derivation
- **IV**: 12-byte value from `encIv` field
- **AAD** (Additional Authenticated Data): `pollMsgId` + null byte + `voterJid` — ensures ciphertext integrity binds to specific poll and voter

The authentication tag (16 bytes) is appended to the ciphertext and verified during decryption. Any tampering with the vote payload causes `final()` to throw, preventing acceptance of forged votes.

---

## Integration: From Encrypted Payload to Emitted Event

The complete decryption flow in [`src/Utils/process-message.ts`](https://github.com/WhiskeySockets/Baileys/blob/main/src/Utils/process-message.ts) (lines 747-778) orchestrates:

1. Detect `pollUpdateMessage` in incoming payload
2. Normalize and fetch the original poll creation message
3. Extract `pollEncKey` from stored message context
4. Execute `decryptPollVote()` with all contextual parameters
5. Emit decrypted `PollVoteMessage` via `messages.update` event

```typescript
const voteMsg = decryptPollVote(content.pollUpdateMessage.vote!, {
    pollEncKey,
    pollCreatorJid,
    pollMsgId: creationMsgKey.id!,
    voterJid,
})
ev.emit('messages.update', [{ 
    key: creationMsgKey, 
    update: { 
        pollUpdates: [{ 
            pollUpdateMessageKey: message.key, 
            vote: voteMsg,
            // ...
        }]
    }
}])

```

---

## Practical Implementation Examples

### Creating a Poll with Automatic Secret Generation

```typescript
import { makeWASocket } from '@adiwajshing/baileys'

const sock = makeWASocket({ /* auth config */ })

await sock.sendMessage('12345@s.whatsapp.net', {
    poll: {
        name: 'What is your favorite color?',
        values: ['Red', 'Blue', 'Green'],
        selectableCount: 1,
        // messageSecret is auto-generated if omitted
    }
})

```

### Handling Decrypted Vote Events

```typescript
sock.ev.on('messages.update', ({ messages }) => {
    for (const msg of messages) {
        const pollUpdates = msg.message.pollUpdates
        if (pollUpdates) {
            for (const upd of pollUpdates) {
                // upd.vote is a decoded PollVoteMessage
                console.log('Selected options:', upd.vote.selectedOptions)
            }
        }
    }
})

```

---

## Security Design Analysis

| Aspect | Implementation |
|--------|----------------|
| **Forward secrecy** | None — poll secret persists for poll lifetime |
| **Key uniqueness** | Per-voter derivation via HMAC-SHA256 |
| **Authentication** | AES-GCM with AAD binding poll+identity |
| **Malleability protection** | GCM authentication tag verification |
| **Rollback protection** | Implicit via message key ordering |

The design prioritizes **efficiency over forward secrecy**: the single poll secret simplifies multi-device synchronization but means compromising the secret exposes all historical votes. This tradeoff aligns with WhatsApp's threat model where server compromise is not considered, but participant device compromise is.

---

## Summary

- Baileys generates a **32-byte random `messageSecret`** during poll creation in [`src/Utils/messages.ts`](https://github.com/WhiskeySockets/Baileys/blob/main/src/Utils/messages.ts)
- Vote payloads arrive encrypted as `encPayload`/`encIv` in `pollUpdateMessage` stanzas
- Decryption keys derive via **double HMAC-SHA256** using poll metadata and voter identity
- **AES-256-GCM** with per-voter AAD provides authenticated confidentiality
- Decrypted votes emit through the standard `messages.update` event stream

---

## Frequently Asked Questions

### How is the poll secret protected during transmission?

The poll secret is embedded in `messageContextInfo` within the poll-creation message. This structure is encrypted using the **Signal Protocol** double-ratchet, identical to standard message encryption. WhatsApp servers cannot access the plaintext secret — only conversation participants with valid Signal sessions can decrypt it.

### Can I use a pre-existing secret for poll creation?

Yes. The `message.poll.messageSecret` field accepts a `Buffer` containing 32 bytes of your choosing. If provided, Baileys uses your value instead of calling `randomBytes(32)`. This enables deterministic testing or integration with external key management systems.

### What happens if the original poll message is unavailable?

Decryption fails. The `decryptPollVote` function requires `pollEncKey` from the stored creation message. If Baileys cannot locate this message in its store, the vote remains undecryptable and will not appear in `pollUpdates`. This design ensures votes are only readable by poll creators who retain conversation history.

### Why does Baileys use HMAC-SHA256 instead of HKDF?

The implementation uses a **two-stage HMAC construction** that functionally approximates HKDF-Extract-then-Expand. For the fixed output length (32 bytes) and constrained input domain (always 32-byte key + structured info), this construction provides equivalent security with less code complexity. The explicit `sign` buffer construction provides clear domain separation without HKDF's labeling overhead.