Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions advanced/signal-protocol.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -657,6 +657,10 @@ Rather than distributing the sender key to all group devices on every message, t
On the **first** group send (or any send where the cached map is empty), the filter still runs unconditionally — every resolved participant device is treated as `has_key=false` and receives the SKDM. This matches WhatsApp Web, which iterates an empty `senderKey` Map as `false` per participant. There is no early-exit for an empty cache; otherwise the very first message after a fresh start would skip distribution entirely.
</Note>

<Note>
**Own devices are never marked `has_key=true` ([#999](https://github.com/oxidezap/whatsapp-rust/pull/999)).** The post-ACK warm mark excludes the account's own companion devices, matching WhatsApp Web's `!isMeDevice` guard on `markHasSenderKey`. They therefore never leave the "not yet tracked" bucket above and are re-included as SKDM targets on every send — see the follow-up note under ["Parallelized group encrypt fan-out"](#parallelized-group-encrypt-fan-out) for why. External group members are unaffected: a successful distribution still marks them warm.
</Note>

Location: `src/send.rs`, `src/client/sender_keys.rs`, `wacore/src/send.rs`

### Parallelized group encrypt fan-out
Expand Down Expand Up @@ -687,6 +691,10 @@ Prior to [#807](https://github.com/oxidezap/whatsapp-rust/pull/807), a single ch
**Session-setup failures are isolated per device (v0.6).** `ensure_sessions_for_devices` used to abort with `Err` the moment `process_prekey_bundle` failed for *any* one target device. Since `prepare_group_stanza` gates the entire SKDM fan-out on `session_plan.is_some()`, one device's X3DH failure nulled the plan and **every** device in the cohort — not just the failing one — got no SKDM, even though the `skmsg` still shipped and the phash covered the full set. An external member recovers via a retry receipt, but an own companion's retry hits `mark_forget_sender_key` with `exclude_own_devices=true`, which filters own-user JIDs and returns early — so that companion stayed `has_key=true` forever and couldn't decrypt the group from that device until an unrelated full rotation (participant removal or PN↔LID migration).

As of [#996](https://github.com/oxidezap/whatsapp-rust/pull/996), a device whose session setup fails is logged and skipped rather than aborting the plan — matching WhatsApp Web's `GroupKeyDistributionMsg`, which wraps each device's `ensureE2ESessions` in its own try/catch and drops only the failing one. The sessionless device is then naturally excluded by the encrypt fan-out (which already skips devices without a session), so every other device still receives its pairwise SKDM.

[#996](https://github.com/oxidezap/whatsapp-rust/pull/996) closed the primary harm — an *unrelated* device's setup failure no longer suppresses the whole cohort's SKDM. A narrower window remained: the **warm mark** (`update_sender_key_devices`, called after the server ACK) recorded the *full* distribution target as `has_key=true`, including our own companion devices, regardless of whether each one's pairwise SKDM encryption actually succeeded. Since the forget path (`mark_forget_sender_key`) excludes own devices for the reason above, an own companion whose one SKDM encryption failed — or that was warm-marked without ever receiving a node — was marked warm and could **never** be un-marked: a permanent orphan until an unrelated full rotation. External devices didn't have this problem; they recover through the retry-receipt forget path.

[#999](https://github.com/oxidezap/whatsapp-rust/pull/999) closes this residual by excluding own devices from the warm mark too (`exclude_own_devices=true`), mirroring WhatsApp Web's `ParticipantStore` helper, which guards *both* `markHasSenderKey` and `markForgetSenderKey` with the same `!isMeDevice` check. Own companions are therefore never memoized as `has_key=true` — `filter_skdm_targets` (["Per-device sender key tracking → Incremental targeting"](#per-device-sender-key-tracking) above) always re-includes them, so they get a fresh SKDM on every group send. This is a deliberate trade-off (a few extra pairwise SKDM nodes per send when the account has companions) in exchange for making the orphan impossible. External devices are unaffected: a successful distribution still marks them warm, and the retry-receipt path still repairs any that go stale.
</Note>

### In-memory sender key device cache
Expand Down