Group Encryption (BGW)
Group chat is Packet's headline encryption mode. A room can hold one admin and up to millions of members, and every message is end-to-end encrypted to the current member set — enforced by the math, not by access control. This page explains the cryptographic design behind that: BGW broadcast encryption over the BLS12-381 pairing curve.
If you have read the 1:1 encryption model, the contrast is the fastest way in.
The problem it solves
A 1:1 thread encrypts each body to a small set of readers — usually sender and receiver. Each reader gets their own encrypted copy of the symmetric content key (per-reader X25519). That is perfect for two parties, and fine for a handful. It does not scale to a group:
- One copy per reader. Encrypting to 10,000 members would mean 10,000 wrapped keys attached to every message — linear in group size, paid on every send.
- Membership churn. People join and leave. A per-reader scheme has to re-wrap keys on every change, and has no built-in way to stop a new member from reading old ciphertext or a removed member from reading new ciphertext.
- State to carry. Ratchet/session schemes give strong properties but require each device to hold and migrate per-conversation state.
Packet's group mode is built around two goals that follow directly from these pain points:
- One ciphertext for everyone. A single compact header per epoch delivers one shared key to every current member — no per-recipient copies, so a message costs the same whether the room has 5 members or 5 million.
- Stateless recovery. A member recovers full group access from only their Solana wallet plus an RPC endpoint. There is no key server, no local session to back up, and no per-device state to migrate. Reinstall on a new machine and you read the whole room again.
How BGW broadcast encryption works
BGW (Boneh–Gentry–Waters) broadcast encryption uses a pairing: a bilinear map e(·, ·) over the BLS12-381 curve that lets the scheme "cancel" terms across two groups so that exactly the intended recipients — and no one else — can recompute a shared key. Three pieces of material combine:
| Material | Scope | What it is |
|---|---|---|
| Public parameters | Global, reusable | A large set of curve points (powers of a setup secret). Generated once per capacity and reused by every room. Contains no secrets. |
Room secret (gamma) | One per room | A scalar held — or re-derived — by the room admin. Produces the room's public key and every member's secret. |
Member secret (d_i) | One per slot | Binds the global parameter for a member's slot to the room secret. Delivered to the member encrypted to their identity key. |
The separation of the global setup secret (used once to build the parameters, then destroyed) from the per-room secret is what makes one multi-hundred-megabyte parameter artifact serviceable for every room ever created: a new room needs only a fresh room secret, never a regeneration of the global parameters. See BGW params for the artifact itself.
The epoch header
Each epoch of a room has exactly one header that encapsulates a single fresh epoch key to the current recipient set. The header is tiny and constant-size regardless of how many members it serves:
C0 = g2^t // an ephemeral public value
C1 = (room_key · Π recipients)^t // binds the epoch key to the recipient set
epoch_key = H( e(parameters)^t ) // a shared secret only recipients can recompute
The published header carries C0, C1, a commitment to the epoch key, and a verifiable description of the recipient set — but not the epoch key itself. Any current member combines (C0, C1) with their own member secret and the public parameters indexed by the other recipients, and the pairing cancels everything down to the same epoch_key. A non-recipient runs the same arithmetic and is left with an uncancelled term they cannot strip — so they recover nothing.
That is the whole trick: one small header replaces one-copy-per-recipient, because every recipient reconstructs the shared key from public material plus their own secret, rather than being handed a personal copy.
Membership lifecycle
A room advances through numbered epochs. Every add, every remove, and every admin key-rotation publishes a new header. The recipient set of each epoch is recorded compactly so a reader can reconstruct exactly who could read any given epoch — with bounded work, not a full replay of room history.
Each header describes its recipient set in one of four ways:
| Header kind | Recipient set |
|---|---|
reuse | Unchanged from the previous epoch |
delta | One add or remove relative to the previous epoch |
inlineCheckpoint | The full recipient set, encoded inline |
externalCheckpoint | The full recipient set, staged across pages when too large to inline |
Deltas keep individual membership changes cheap. To keep reader work bounded, a checkpoint is forced after at most 64 deltas, so reconstructing any epoch's recipient set means walking back to the nearest checkpoint and replaying a short chain of deltas forward — never the entire history. A hash chain links every step, and a tampered, skipped, or reordered header fails closed (see Recipient-set integrity).
History and forward protection: the epoch-key chain
Two guarantees define group membership cryptographically:
- New members cannot read history sent before they joined.
- Removed members cannot read anything sent after they were removed.
Both come from how epoch keys are chained. Within a stretch of epochs (a segment) the epoch keys form a backward hash chain: the key for an earlier epoch is a one-way hash of the key for a later one.
segment: x1 ← x2 ← x3 ← ... ← tip
(each step = one-way hash of the next)
- Newer key → all older keys (cheap). A member who recovers the latest epoch key can hash forward to derive every earlier key in the same segment, so they read the segment's history with no extra decapsulation.
- Older key → nothing newer. The hash is one-way, so knowing an old key never reveals a newer one. A removed member who could decrypt the last header they were part of cannot step forward to keys published after their removal. This is forward secrecy on removal.
When a member is added, the chain breaks: a brand-new segment starts at that epoch with an independent tip, sharing no material with prior segments. The new member receives keys only from the new segment, so everything sent before they joined stays permanently unreadable to them. Re-adding a previously removed member also breaks the chain, so messages sent during their absence stay locked. This is history protection on join (break-on-add).
Removals do not break the chain — the one-way direction already prevents stepping forward — they simply publish a new epoch whose header the removed slot can no longer open. Every message records the epoch it was encrypted under, so reading it means recovering that epoch's key, either by opening its header directly or by chain-deriving from a newer key the reader already holds.
Recipient-set integrity
The recipient set fed into the pairing must be exactly the one the room committed to — otherwise an admin could claim a header serves a set it does not. Packet enforces this without trusting anyone's word for it:
- Each header commits to its recipient set with a state root, and successive roots chain together.
- The on-chain program records epoch progression, header ordering, and the supplied root — but it computes the recipient root itself rather than trusting a root handed to it, so the recipient set cannot be forged by supplying a fabricated root.
- Every reader recomputes every root from the actual descriptor data and the chain, walking back to the nearest checkpoint and replaying deltas, and verifies that the recomputed root matches at each step. Any mismatch — a missing checkpoint, an altered delta, a wrong page, or a forged root — fails closed: the reader never decapsulates against an unverified recipient set.
The result is that the set of slots that can read an epoch is cryptographically pinned, end to end.
Message protection
The epoch key is a key-encapsulation output, not the key that encrypts your bytes. Packet derives down from it with domain separation at each step:
epoch key → per-message key = H(epoch key, roomId, messageId) → AES-256-GCM
- Per-message keys. Each message derives its own key bound to the room id and a client-chosen message id, then is sealed with AES-256-GCM. The message id is mixed into the AEAD's associated data, so a ciphertext cannot be replayed under a different id or room — the tag check fails.
- Off-chain bodies. Large bodies stored off-chain (Irys/Arweave/IPFS/HTTP) are sealed in a single room envelope under the same epoch key — one copy for the whole group, preserving the one-ciphertext property even for big payloads. The envelope is self-describing: it carries its epoch so any current reader recovers the matching epoch key with no per-member material. See Reading & sending and Envelope and message content.
- Member secrets. Each member's BGW secret is itself sealed to that member's registered Packet encryption key, reusing the same 1:1 encryption suite — only the member's identity key can open it.
Every sealed object gets AEAD integrity, and the associated data binds each ciphertext to its room, epoch, and message so it cannot be reused out of context.
Stateless recovery
What a member needs and what they reconstruct:
| A member supplies | The SDK reconstructs from on-chain state |
|---|---|
| Their Solana wallet (identity key) | Their membership and slot |
| A Photon-compatible RPC | Their decrypted BGW member secret |
| The room id | Every epoch key they are entitled to, by decapsulating headers and chain-deriving within segments |
| — | The full readable message history, decrypted |
Nothing is kept on the device. An admin's recovery is just as clean: the room secret is derived deterministically from a domain-separated wallet signature, so an admin reconstructs the room secret — and thus every member secret and all room state — on a new machine from the wallet alone.
Security properties
A confident, accurate summary. The full construction and security analysis is in the cryptography deep dive below.
| Property | Guarantee |
|---|---|
| Confidentiality vs non-recipients | A coalition outside the recipient set cannot distinguish an epoch key from random. This reduces to the Bilinear Diffie–Hellman Exponent assumption on BLS12-381. |
| Forward secrecy on removal | Removal publishes a fresh, independent epoch; the removed slot cannot open it. The send path enforces that new messages use a header covering current membership. |
| History protection on join | Break-on-add starts a new chain segment with no link to prior segments, so a new member cannot derive any earlier epoch key. |
| Recipient-set integrity | Readers recompute and verify every recipient root; forged or inconsistent roots fail closed. The recipient set cannot be forged. |
| AEAD integrity & binding | AES-256-GCM seals every object; associated data binds each ciphertext to its room, epoch, and message id. |
| Domain separation | Every distinct hash/derivation uses an explicit, versioned domain string, preventing cross-context key reuse. |
Cryptography deep dive
This section adapts the formal construction for readers who want the math behind the overview. It mirrors the implementation: the pairing engine, the on-chain recipient root chain, and the SDK key schedule.
Relationship to BGW'05
The broadcast-encryption core is System 1 of Boneh–Gentry–Waters, Collusion Resistant Broadcast Encryption with Short Ciphertexts and Private Keys (CRYPTO 2005, eprint 2005/018) — the variant with constant-size ciphertexts and private keys. The public parameters (including the deliberately omitted power n+1), the per-member secret, the epoch header, the session key, and the decapsulation identity below come directly from that scheme; its confidentiality reduces to the (decision) Bilinear Diffie-Hellman Exponent assumption.
Two things are Packet-specific. The original paper uses a symmetric (Type-1) pairing; Packet instantiates System 1 on a modern asymmetric (Type-3) pairing over BLS12-381 (secret-side in G1, header-side in G2) — a standard porting, not a new scheme. And BGW supplies only the broadcast KEM; the epoch/membership model, the recipient checkpoint–delta chain, the break-on-add key schedule, and stateless on-chain recovery are Packet's protocol layer on top, not part of the paper's results.
Setting and the two secrets
The scheme is BGW broadcast encryption over the asymmetric (Type-3) BLS12-381 pairing e : G1 × G2 → GT, with generators g1, g2. Secret-side material lives in G1, header-side material in G2; there is no efficient isomorphism between them. Let n be the room capacity (maximum slot count); slots are 1-based, 1 ≤ i ≤ n.
Two scalars are kept strictly separate:
alpha— global setup secret. Used only to generate the public parameters, then destroyed. Shared by the whole protocol.gamma— per-room secret. Independent per room; held or re-derivable by the room admin.
Keeping these distinct is both a correctness and a security boundary: member secrets and the room key depend on gamma, not alpha, so the multi-megabyte parameter artifact is reused by every room while a fresh room only needs a fresh gamma. Compromise of one room's gamma exposes that room alone; alpha — from which the whole system's keys ultimately derive — exists only transiently during setup and is then destroyed.
Public parameters and the omitted power
The parameters for capacity n are powers of alpha in both groups:
P1[i] = g1^(alpha^i) for i = 1 .. 2n, with i = n+1 OMITTED
P2[i] = g2^(alpha^i) for i = 1 .. n
These are global and content-addressed. The per-room public value is v = g2^gamma (the room public key), and a parameter root commits to the capacity, v, and the powers under a domain-separated hash.
P1[n+1] = g1^(alpha^(n+1)) is deliberately never serialized. The epoch key is K = e(g1, g2)^(alpha^(n+1) · t), so anyone holding P1[n+1] could compute K for every epoch of every room from the public header value alone. Omitting it is a hard security requirement, not an optimization: the engine leaves that power empty during generation and rejects a non-empty n+1 on decode. Honest decapsulation never needs it — the index arithmetic only lands on n+1 for the recipient's own term, which is excluded from the product.
Member secret, encapsulation, decapsulation
The member secret for slot i binds the global power to the room secret:
d_i = P1[i]^gamma = g1^(gamma · alpha^i)
It is epoch-independent and delivered to the member encrypted to their identity key. To encapsulate an epoch key for recipient set S, the publisher draws a fresh scalar t (derived deterministically inside the engine from caller-supplied randomness, bound to the epoch, recipient root, and associated data) and computes:
C0 = g2^t
C1 = (v · Π_{j ∈ S} P2[n+1-j])^t
K = e(g1, g2)^(alpha^(n+1) · t)
epoch_key = H("xpkt-bgw-epoch-key-v1", K, aad)
A recipient i ∈ S recovers K from (C0, C1), its secret d_i, and the powers indexed by the other recipients:
numerator = e(P1[i], C1)
denominator = e( d_i · Π_{j ∈ S, j ≠ i} P1[n+1-j+i] , C0 )
K = numerator / denominator
By bilinearity, numerator and denominator differ in exactly the j = i term, namely alpha^i · alpha^(n+1-i) = alpha^(n+1), so the quotient is K. Crucially, decapsulation needs the actual recipient set S (or an equivalent aggregate) to form the product — a mere hash of S is not enough — which is why headers carry verifiable recipient descriptors, not just a root. The decapsulator then recomputes the epoch-key commitment and rejects the header if it does not match, so a tampered header yields a rejection rather than a wrong key.
Security arguments
- Confidentiality against non-recipients. A coalition whose slots are not in
S(and who lackalpha/P1[n+1]) cannot distinguishKfrom random. This is the standard BGW guarantee, reducing to the Bilinear Diffie–Hellman Exponent (BDHE) family over the powersg^(alpha^i). Excluded colluders contribute their own secretsd_jforj ∉ S, but the decapsulation identity only cancels toKwhen the contributing index is inS; forj ∉ Sa non-cancelling power remains that cannot be stripped withoutalpha. The single load-bearing extra assumption is thatP1[n+1]is never published. - Forward secrecy on removal. Removal publishes a new epoch with
Sexcluding the removed slot and a fresh, independentt. The send guard requires messages to be sent under a header covering current membership, so a membership change is reflected before new messages are accepted. (A removed member can still read traffic from epochs they were a recipient of — removal protects the future, not the past they could already read.) - History protection on join. The epoch key is not used directly; per segment the SDK derives a backward hash chain whose tip is bound to
gamma, the room id, and the segment's start epoch. A reader who opens epochederives older keys by hashing forward but cannot reache+1. An add forces a chain break — a new segment with an independent tip — so a new member derives nothing from a previous segment. - Recipient-set integrity. Two root constructions (a checkpoint root over a full descriptor and a delta root chaining one mutation onto the prior root), plus a page chain hash for staged checkpoints. The reader recomputes every root from descriptor data and the chain and requires the header's binding root to equal the hash of the recomputed state root, failing closed on any mismatch.
- AEAD. AES-256-GCM with domain-separated keys and associated-data binding seals the wrapped chain key (bound to room, epoch, and recipient binding root), each message (bound to room and message id), and each member secret (bound to a member-secret domain). GCM provides integrity; the associated data prevents cross-context reuse.
Trust model
There are two roles: an admin (adds/removes members, publishes headers, may also be a member) and a member (reads, and sends while active). The admin is a publisher and key-distributor, trusted for availability and correct publication — but the trust is bounded:
- A malformed header (bad
C1, or a descriptor that does not match the BGW math) makes affected readers fail closed rather than accept a wrong key — a liveness issue, not a confidentiality break. - The admin cannot read an epoch from which they are cryptographically excluded, cannot grant decryption to a slot that is not a recipient, and cannot forge recipient-set history undetectably: every honest reader recomputes the recipient root and rejects a binding root that does not match the descriptor chain.
The strongest operational assumption is the generation of the public parameters: alpha must come from good randomness, P1[n+1] must never be exposed, and alpha must be destroyed afterward. Generating the parameters through a verifiable multi-party computation — where alpha is the combination of independent contributions and each party discards their share — ensures the full alpha is never assembled in one place, so confidentiality holds as long as one contributor discards honestly. The per-room gamma, by contrast, is a routine application secret derived from a wallet signature.
Domain separation
All hashing is SHA-256 with an explicit, versioned domain prefix, so changing a hashed layout requires bumping the version. A representative selection:
| Domain string | Layer | Purpose |
|---|---|---|
xpkt-bgw-bls12-381-params-root-v1 | engine | Public-parameter root |
xpkt-bgw-epoch-key-v1 | engine | Epoch-key derivation from K |
xpkt-bgw-epoch-key-commitment-v1 | engine | Epoch-key commitment |
xpkt-room-chain-tip-v1 / xpkt-room-chain-step-v1 | SDK | Per-segment chain tip / backward step |
xpkt-room-message-key-v1 / xpkt-room-msg-v1 | SDK | Per-message key / AEAD associated data |
xpkt-room-member-secret-v1 | SDK | Member-secret envelope |
xpkt-recipient-checkpoint-v1 / xpkt-recipient-delta-v1 / xpkt-recipient-page-v1 | program | Recipient checkpoint / delta / page chain roots |
One deliberate convention split: the on-chain recipient root chain encodes integers little-endian, while the BGW key schedule uses big-endian (matching the engine internals). This is intentional and kept in lock-step by cross-language test vectors.
Where to go next
- Rooms & Group Messaging — the room mechanics this builds on (slots, epochs, buckets).
- 1:1 Encryption — the per-reader X25519 model and the shared primitives.
- SDK room quickstart — create a room, add members, send and read.
- BGW params — the public parameter artifact and how to host or generate it.
- Reading & sending — epoch-key recovery, buckets, and message decryption in code.
Reference
- Dan Boneh, Craig Gentry, Brent Waters. Collusion Resistant Broadcast Encryption with Short Ciphertexts and Private Keys. CRYPTO 2005. eprint 2005/018 — the broadcast-encryption scheme (System 1) Packet's group mode is built on.