Skip to content

fix(message)!: decrypt secret encrypted edits on receive - #665

Merged
jlucaso1 merged 9 commits into
mainfrom
fix/secret-encrypted-message-edits
May 30, 2026
Merged

fix(message)!: decrypt secret encrypted edits on receive#665
jlucaso1 merged 9 commits into
mainfrom
fix/secret-encrypted-message-edits

Conversation

@jlucaso1

@jlucaso1 jlucaso1 commented May 30, 2026

Copy link
Copy Markdown
Collaborator

What

WhatsApp now sends message edits as message.secret_encrypted_message with MESSAGE_EDIT, encrypted with the parent message's messageSecret. This wires that path into receive dispatch:

  • persist inbound message_context_info.message_secret for every non-forwarded message, while preserving the existing bot-DM LID alias for msmsg
  • decrypt supported secret_encrypted_message add-ons during message dispatch, including MESSAGE_EDIT rewrap into the legacy protocol_message.edited_message shape
  • retry decrypt/lookup across LID/PN alternates for group sender/editor skew
  • re-cache the decrypted edit's next messageSecret under the parent id so edit-of-edit decrypts work
  • seed the msg-secret store from history-sync blobs, including when no event handlers are registered, while skipping forwarded rows just like live receive
  • batch history-sync msg-secret writes through the backend, with SQLite using chunked bulk upserts inside one transaction/retry path

Why

The crypto and store primitives already existed, but the receive path still surfaced encrypted edit envelopes as raw Event::Message. The root cause was twofold: message-secret capture was gated to bot contexts, and dispatch never attempted the add-on decrypt for secret_encrypted_message.

That meant normal user messages often never had their parent secret stored, and edits that did arrive could not be converted into the edit shape downstream consumers expect.

Breaking Change

BREAKING CHANGE: incoming secret_encrypted_message edits that decrypt successfully are no longer dispatched as raw encrypted envelopes. They are dispatched as the decrypted legacy edit shape: protocol_message.type == MESSAGE_EDIT with protocol_message.edited_message populated.

To migrate, handle message edits through the existing ProtocolMessage::MESSAGE_EDIT path and treat raw secret_encrypted_message as a decrypt-failure fallback only. Consumers with custom msg-secret stores do not need code changes because put_msg_secrets has a default implementation, but storage backends should override it to get transactional/batched history-sync persistence.

Tests

  • cargo fmt --all
  • cargo clippy --all --tests
  • cargo test -p wacore -p whatsapp-rust -p whatsapp-rust-sqlite-storage
  • focused coverage for non-bot secret capture, MESSAGE_EDIT dispatch, LID/PN fallback, edit-of-edit secret refresh, forwarded history-sync skip, history-sync secret capture without handlers, and batch msg-secret upsert behavior

@coderabbitai

coderabbitai Bot commented May 30, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: bf8cd6f8-40b6-4ef1-961d-3b8d021d8c58

📥 Commits

Reviewing files that changed from the base of the PR and between a848ad5 and 1fe256d.

📒 Files selected for processing (1)
  • src/history_sync.rs

📝 Walkthrough

Summary by CodeRabbit

  • New Features
    • Decrypt secret-encrypted messages and edits before delivery; persist per-message secrets from history sync and inbound messages (including bot DM aliasing).
  • Tests
    • Added unit and integration tests covering secret-encrypted edit decryption, chained edits, group/alternate-identity secret lookup, and history-sync secret extraction.
  • Chores
    • Updated development dependencies.

Walkthrough

This PR extracts per-message message_secret from history-sync blobs and inbound non-forwarded messages, persists secrets to the backend, and decrypts secret-encrypted message edits (with primary and alternate LID↔PN lookups), persisting recovered secrets for chained edits before dispatch.

Changes

Message Secret Persistence and Decryption Flow

Layer / File(s) Summary
Dependencies, imports, and core persistence helper
Cargo.toml, src/history_sync.rs, src/message.rs
Added flate2 dev dependency; updated history_sync imports; implemented persist_msg_secret_bytes with length validation and backend persistence.
Inbound message capture, decryption, and tests
src/message.rs
dispatch_parsed_message captures inbound secrets; maybe_capture_inbound_msg_secret persists embedded messageSecret for non-forwarded messages; added own_jid_for_secret_encrypted, maybe_decrypt_secret_encrypted_message, alternate secret lookup (alternate_msg_secret_jid), and unit tests for secret-encrypted edits, chained edits, and group fallback.
wacore conversation partial-decode and extraction
wacore/src/history_sync.rs
Introduced ConversationInternalFields partial decode, HistoryMsgSecretRecord type, changed tc-token extraction to operate on decoded view, and added tests verifying extraction of message secrets from history-sync conversations.
History-sync retention and persistence
src/history_sync.rs
process_history_sync returns msg_secret_records; process_history_sync_task passes retain_history_blob and calls store_history_sync_msg_secrets to derive candidate senders and persist secrets; dispatch of Event::HistorySync is conditional on handler presence; includes tests ensuring persistence without listeners and bot DM aliasing.
Trait and backend batch upserts
wacore/src/store/traits.rs, wacore/src/store/in_memory.rs, storages/sqlite-storage/src/sqlite_store.rs
Added MsgSecretEntry and MsgSecretStore::put_msg_secrets default impl; implemented put_msg_secrets in in-memory and sqlite backends and tests for batch upsert/overwrite behavior.

Sequence Diagram(s)

sequenceDiagram
  participant DispatchMsg as dispatch_parsed_message
  participant CaptureSecret as maybe_capture_inbound_msg_secret
  participant PersistSecret as persist_msg_secret_bytes
  participant DecryptSecret as maybe_decrypt_secret_encrypted_message
  participant Backend as backend.get_msg_secret / put_msg_secrets
  participant EventBus as Event::Message
  DispatchMsg->>CaptureSecret: parsed message
  CaptureSecret->>CaptureSecret: extract message_context_info.message_secret (if not forwarded)
  alt secret exists
    CaptureSecret->>PersistSecret: persist secret_bytes
    PersistSecret->>Backend: put_msg_secret(chat, sender, msg_id)
  end
  DispatchMsg->>DecryptSecret: secret_encrypted_message present
  DecryptSecret->>DecryptSecret: resolve own_jid (LID/PN)
  DecryptSecret->>Backend: get_msg_secret(primary lookup)
  alt secret found
    Backend-->>DecryptSecret: secret_bytes
  else not found
    DecryptSecret->>DecryptSecret: compute alternate identity
    DecryptSecret->>Backend: get_msg_secret(alternate lookup)
    Backend-->>DecryptSecret: secret_bytes
  end
  DecryptSecret->>PersistSecret: persist newly-obtained secret via put_msg_secrets
  DecryptSecret-->>DispatchMsg: decrypted message (or original)
  DispatchMsg->>EventBus: dispatch
Loading
sequenceDiagram
  participant HistoryTask as process_history_sync_task
  participant ProcessFunc as process_history_sync
  participant StoreSecrets as store_history_sync_msg_secrets
  participant SendersHelper as history_msg_secret_senders
  participant Backend as backend.put_msg_secrets
  HistoryTask->>ProcessFunc: call process_history_sync(retain_history_blob)
  ProcessFunc->>StoreSecrets: return msg_secret_records
  loop each message_secret record
    StoreSecrets->>SendersHelper: derive candidate senders
    SendersHelper-->>StoreSecrets: list of sender JIDs
    StoreSecrets->>Backend: put_msg_secrets(entries)
  end
  StoreSecrets-->>HistoryTask: count stored secrets
  HistoryTask->>HistoryTask: check has_listeners
  alt no listeners
    HistoryTask->>HistoryTask: return after secrets persisted
  else listeners present
    HistoryTask->>HistoryTask: dispatch Event::HistorySync
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Mark: verify persistence semantics, alternate-identity lookups, and chained-edit secret refresh paths; these must be correct and covered by tests.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the core change: decrypting secret encrypted message edits on receive, which is the primary feature addition across all modified files.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, covering what was changed, why it was needed, breaking changes, and test coverage for the secret encrypted message handling.
Docstring Coverage ✅ Passed Docstring coverage is 94.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/secret-encrypted-message-edits

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@jlucaso1
jlucaso1 marked this pull request as ready for review May 30, 2026 05:33

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e4333f2f7f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/history_sync.rs Outdated
@github-actions

github-actions Bot commented May 30, 2026

Copy link
Copy Markdown

Benchmark Results

1 regression(s) detected (>2% threshold):

Benchmark Current Baseline Change
libsignal_benchmark::group_messaging_group::bench_group_decrypt_message decrypt:setup_group_with_encrypted_message() 12,704,191 12,280,711 +3.4%
66 unchanged benchmark(s)
Benchmark Current Baseline Change
reporting_token_benchmark::content_extraction_group::bench_content_extraction simple:setup_simple_message() 3,933 3,933 +0.0%
reporting_token_benchmark::content_extraction_group::bench_content_extraction extended:setup_extended_message() 12,038 12,038 +0.0%
reporting_token_benchmark::key_derivation_group::bench_key_derivation 43,514 43,514 +0.0%
reporting_token_benchmark::token_calculation_group::bench_token_calculation 19,365 19,365 +0.0%
reporting_token_benchmark::full_generation_group::bench_full_token_generation simple:setup_full_gen_simple() 68,579 68,579 +0.0%
reporting_token_benchmark::full_generation_group::bench_full_token_generation extended:setup_full_gen_extended() 76,679 76,679 +0.0%
reporting_token_benchmark::message_encoding_group::bench_message_encoding simple:setup_simple_message() 2,230 2,230 +0.0%
reporting_token_benchmark::message_encoding_group::bench_message_encoding extended:setup_extended_message() 5,988 5,988 +0.0%
send_receive_benchmark::dm_send::bench_dm_send text:setup_dm_send() 181,153 180,977 +0.1%
send_receive_benchmark::dm_recv::bench_dm_recv text:setup_dm_recv() 2,205,397 2,205,397 +0.0%
send_receive_benchmark::group_send::bench_group_send group_10:setup_group_send_10() 888,721 888,753 -0.0%
send_receive_benchmark::group_send::bench_group_send group_50:setup_group_send_50() 1,028,008 1,027,995 +0.0%
send_receive_benchmark::group_send::bench_group_send group_256:setup_group_send_256() 1,761,001 1,760,910 +0.0%
send_receive_benchmark::group_send_skdm::bench_group_send_skdm skdm_10:setup_group_skdm_10() 1,131,699 1,131,681 +0.0%
send_receive_benchmark::group_send_skdm::bench_group_send_skdm skdm_50:setup_group_skdm_50() 2,133,752 2,139,547 -0.3%
send_receive_benchmark::group_send_skdm::bench_group_send_skdm skdm_256:setup_group_skdm_256() 7,321,230 7,309,218 +0.2%
send_receive_benchmark::group_recv::bench_group_recv text:setup_group_recv() 12,676,006 12,570,214 +0.8%
binary_benchmark::marshal_group::bench_marshal_allocating 71,326 71,326 +0.0%
binary_benchmark::marshal_group::bench_marshal_auto_allocating 71,379 71,379 +0.0%
binary_benchmark::marshal_group::bench_marshal_exact_allocating 98,446 98,446 +0.0%
binary_benchmark::marshal_group::bench_marshal_reusing_buffer 78,826 78,826 +0.0%
binary_benchmark::marshal_group::bench_marshal_reusing_buffer_vec_writer 71,426 71,426 +0.0%
binary_benchmark::marshal_group::bench_marshal_long_string 7,593 7,593 +0.0%
binary_benchmark::marshal_group::bench_marshal_auto_long_string 7,636 7,636 +0.0%
binary_benchmark::marshal_group::bench_marshal_exact_long_string 9,348 9,348 +0.0%
binary_benchmark::marshal_group::bench_marshal_huge_bytes_allocating 530,583 530,583 +0.0%
binary_benchmark::marshal_group::bench_marshal_auto_huge_bytes_allocating 530,151 530,151 +0.0%
binary_benchmark::marshal_group::bench_marshal_exact_huge_bytes_allocating 531,506 531,506 +0.0%
binary_benchmark::marshal_group::bench_marshal_many_children_allocating 8,506,239 8,506,239 +0.0%
binary_benchmark::marshal_group::bench_marshal_auto_many_children_allocating 8,450,491 8,450,491 +0.0%
binary_benchmark::marshal_group::bench_marshal_exact_many_children_allocating 19,678,026 19,678,026 +0.0%
binary_benchmark::unmarshal_group::bench_unmarshal small:setup_small_marshaled() 2,468 2,468 +0.0%
binary_benchmark::unmarshal_group::bench_unmarshal large:setup_large_marshaled() 33,558 33,558 +0.0%
binary_benchmark::unpack_group::bench_unpack_uncompressed 787 787 +0.0%
binary_benchmark::unpack_group::bench_unpack_compressed 526,732 526,732 +0.0%
binary_benchmark::attr_parser_group::bench_attr_parser attr_lookup:setup_attr_marshaled() 4,986 4,986 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip small:setup_small_marshaled() 5,315 5,315 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip large:setup_large_marshaled() 61,874 61,874 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip_auto small:setup_small_marshaled() 5,347 5,347 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip_auto large:setup_large_marshaled() 61,942 61,942 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip_exact small:setup_small_marshaled() 6,734 6,734 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip_exact large:setup_large_marshaled() 85,585 85,585 +0.0%
binary_benchmark::child_iteration_group::bench_get_children_by_tag 477,570 477,570 +0.0%
binary_benchmark::jid_optimization_group::bench_jid_to_owned_access jid_access:setup_jid_heavy_marshaled() 11,563 11,563 +0.0%
numeric_attr_benchmark::bench_group::bench_baseline_u32 396 396 +0.0%
numeric_attr_benchmark::bench_group::bench_proposed_u32 120 120 +0.0%
numeric_attr_benchmark::bench_group::bench_baseline_u64 439 439 +0.0%
numeric_attr_benchmark::bench_group::bench_proposed_u64 153 153 +0.0%
numeric_attr_benchmark::bench_group::bench_baseline_i64 499 499 +0.0%
numeric_attr_benchmark::bench_group::bench_proposed_i64 162 162 +0.0%
numeric_attr_benchmark::bench_group::bench_baseline_loop_100_u64 44,624 44,624 +0.0%
numeric_attr_benchmark::bench_group::bench_proposed_loop_100_u64 16,424 16,424 +0.0%
libsignal_benchmark::dm_group::bench_dm_session_establishment setup:setup_dm_users() 17,107,287 17,261,685 -0.9%
libsignal_benchmark::dm_group::bench_dm_encrypt_first_message first_msg:setup_dm_session() 157,179 157,179 +0.0%
libsignal_benchmark::dm_group::bench_dm_decrypt_first_message decrypt_prekey:setup_dm_with_first_message() 5,513,975 5,513,975 +0.0%
libsignal_benchmark::dm_group::bench_dm_encrypt_subsequent_message subsequent:setup_established_dm_session() 157,539 157,539 +0.0%
libsignal_benchmark::group_messaging_group::bench_group_create_distribution_message create:setup_group_sender() 296,767 296,767 +0.0%
libsignal_benchmark::group_messaging_group::bench_group_encrypt_message encrypt:setup_group_with_distribution() 706,282 706,282 +0.0%
libsignal_benchmark::conversation_group::bench_full_dm_conversation full:setup_conversation_data() 27,634,193 27,346,141 +1.1%
libsignal_benchmark::signature_group::bench_signature_creation sign:setup_keypair_with_message() 3,467,011 3,467,011 +0.0%
libsignal_benchmark::signature_group::bench_signature_verification verify:setup_keypair_with_message() 125,338,393 126,489,153 -0.9%
libsignal_benchmark::signature_group::bench_key_generation keygen 2,830,452 2,830,452 +0.0%
libsignal_benchmark::session_optimization_group::bench_decrypt_with_previous_session previous_session:setup_with_archived_sessions() 46,566 46,566 +0.0%
libsignal_benchmark::session_optimization_group::bench_out_of_order_decryption out_of_order:setup_out_of_order_messages() 5,197,012 5,197,012 +0.0%
libsignal_benchmark::session_optimization_group::bench_promote_matching_session promote:setup_promote_matching_session() 360,648 360,648 +0.0%
libsignal_benchmark::session_optimization_group::bench_message_key_eviction eviction:setup_message_key_eviction() 14,255,917 14,255,917 +0.0%

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/history_sync.rs`:
- Around line 252-301: store_history_sync_msg_secrets currently calls
wa::HistorySync::decode and iterates every conversation/message on the async
executor; move the CPU-bound decode + traversal into a
tokio::task::spawn_blocking closure that returns a Vec of lightweight records
(e.g., tuples of (chat: Jid string, sender: SenderType, msg_id: String, secret:
Vec<u8>)) by using wa::HistorySync::decode + the existing conversation/message
walking and history_msg_secret_senders logic, then await that spawn_blocking
result inside store_history_sync_msg_secrets and perform only the async
persistence calls (persist_msg_secret_bytes) over the returned records; keep
error handling (decode failure -> return 0) inside the spawn_blocking task or
propagate a clear empty Vec so the async path does only await and call
persist_msg_secret_bytes for each record.

In `@src/message.rs`:
- Around line 385-397: The current branch only persists refreshed edit secrets
under the (chat, original_sender, target_id) alias in persist_msg_secret_bytes,
which leaves the alternate identity alias stale and can cause subsequent chained
edits to fail; update the logic so that when message_context_info.message_secret
is present you persist the secret for both aliases (primary and alternate) —
either by invoking persist_msg_secret_bytes twice (once for the primary alias
and once for the alternate alias derived from the message/identity lookup) or by
extending persist_msg_secret_bytes to accept and write both PN/LID keys
internally; ensure you reference message_context_info, message_secret,
original_sender, target_id, persist_msg_secret_bytes and
alternate_msg_secret_lookup when locating where to add the second write.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: dfb7c6cf-4869-4e39-a907-d2ad65c3377e

📥 Commits

Reviewing files that changed from the base of the PR and between 4961b0e and e4333f2.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (3)
  • Cargo.toml
  • src/history_sync.rs
  • src/message.rs

Comment thread src/history_sync.rs Outdated
Comment thread src/message.rs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/history_sync.rs`:
- Around line 223-224: process_history_sync() currently decodes and walks the
decompressed history blob, but you then decode it again in
extract_history_sync_msg_secret_records(); instead, perform message-secret
extraction during the first parse/walk in process_history_sync() and pass the
resulting secret records to store_history_sync_msg_secrets() (which should be
reduced to an async persistence-only function). Update process_history_sync() to
return or yield the extracted secret records, remove the second decode in
extract_history_sync_msg_secret_records() (or eliminate that function and merge
its logic into the initial parse), and change the call site that currently does
self.store_history_sync_msg_secrets(decompressed.clone()).await to pass the
extracted records so the protobuf decode and conversation walk only happen once
(apply same refactor at the other duplicated locations mentioned).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 5b63b74e-dbc7-4676-b9a9-24c9145874b1

📥 Commits

Reviewing files that changed from the base of the PR and between e4333f2 and cf0de6d.

📒 Files selected for processing (2)
  • src/history_sync.rs
  • src/message.rs

Comment thread src/history_sync.rs Outdated
@jlucaso1

Copy link
Copy Markdown
Collaborator Author

@claude review

@claude

claude Bot commented May 30, 2026

Copy link
Copy Markdown

Claude finished @jlucaso1's task in 4m 8s —— View job


Claude PR Review

  • Read changed files (Cargo.toml, src/history_sync.rs, src/message.rs, wacore/src/history_sync.rs, src/features/message_edit.rs)
  • Analyze diff against origin/main
  • Cross-check prior review feedback (codex, coderabbitai) to confirm what's been addressed
  • Produce review feedback

Overall

This is a solid fix. The previously-flagged concerns from Codex and CodeRabbit have been correctly addressed:

  • Single-pass extraction: wacore::history_sync::process_history_sync now extracts HistoryMsgSecretRecords during the existing streaming walk of the decompressed blob, and store_history_sync_msg_secrets is reduced to async persistence over a Vec<HistoryMsgSecretRecord>. No more double protobuf decode. (wacore/src/history_sync.rs:65-90, wacore/src/history_sync.rs:317-345, src/history_sync.rs:251-273)
  • LID/PN alias refresh on edit-of-edit: maybe_decrypt_secret_encrypted_message now writes the refreshed messageSecret under both original_sender and its alternate alias via the pre-computed fallback_original_sender. (src/message.rs:385-406)
  • Non-bot secret capture: maybe_capture_inbound_msg_secret persists messageContextInfo.message_secret for every non-forwarded inbound message and only keeps the bot-DM LID alias as a second entry. (src/message.rs:152-183)
  • History-sync secrets without handlers: dispatched even when no Event::HistorySync listeners are registered — the early return happens only at the dispatch site. (src/history_sync.rs:220-240)
  • Tests are comprehensive: top-level vs. MessageContextInfo secret extraction, LID/PN fallback decrypt in groups, edit-of-edit chained refresh, and history-sync no-handler persistence.

Findings

🟡 P2 — retain_history_blob is hard-coded to true, regressing the no-listener path

src/history_sync.rs:148-178

let has_listeners = self.core.event_bus.has_handlers();
let retain_history_blob = true;process_history_sync(compressed_data, own_user.as_deref(), retain_history_blob,)

Before this PR, has_listeners was passed through, so process_history_sync could drop the decompressed Bytes allocation right after the streaming walk when nobody was going to consume it. Now it's always retained, kept alive on sync_result.decompressed_bytes until the early return at line 224 drops the result.

The msg-secret extraction does not depend on retention — it runs unconditionally on &buf[pos..end] slices during process_history_sync (wacore/src/history_sync.rs:75-95). So the rename should just be:

let retain_history_blob = has_listeners;

…which restores the pre-PR memory profile for offline-only clients while keeping the secret extraction intact. Fix this →

🟡 P2 — alternate_msg_secret_jid errors are silently downgraded to "no mapping"

src/message.rs:261-265 and src/message.rs:299-305

let fallback_original_sender = self
    .alternate_msg_secret_jid(&backend, &original_sender)
    .await
    .unwrap_or_default();

alternate_msg_secret_jid returns Result<Option<Jid>, StoreError> and already maps "no mapping known" to Ok(None). The only Err case is a real backend failure (e.g. transient SQLite lock). unwrap_or_default() then collapses both Err and Ok(None) to None, so a store hiccup silently disables the LID/PN fallback decrypt and the user only sees the trailing secret_encrypted_message MessageEdit decrypt failed warn — without any hint that the alternate path was unreachable. Worth at least a log::warn! on the Err arm (mirroring the existing backend error reading secret_encrypted_message secret style at line 290) so a flaky store doesn't masquerade as a missing PN/LID mapping.

🟢 Minor — extract_msg_secret_records doesn't gate on is_forwarded

wacore/src/history_sync.rs:344-371

The live receive path (maybe_capture_inbound_msg_secret at src/message.rs:163-165) explicitly skips msg.is_forwarded() payloads, mirroring WA Web's processRenderableMessages which "skips caching messageSecret for forwarded payloads" (per the doc comment in wacore/src/proto_helpers.rs:162-166). The history-sync extraction has no equivalent gate, so backfilled forwarded messages will index a messageSecret under (chat, sender, msg_id) that the live path would have ignored. Probably benign in practice — the secret is only ever consulted for add-on decrypts that key off the same (chat, sender, msg_id) tuple — but worth a deliberate decision rather than an accidental asymmetry. A short comment justifying the divergence (e.g. "history backfill always trusts the upstream message_secret because we have no live is_forwarded provenance to disambiguate") would close the loop.

🟢 Praise — fallback decrypt combinations

src/message.rs:307-382's 4-way combinatorial fallback (primary/alt × editor/alt-editor) is the right level of paranoia for the LID/PN mid-migration window, and last_err is preserved across attempts so the eventual warn carries a meaningful failure. Nice.

Build / Tests

I did not run cargo test locally in this review (sandbox restrictions), but the test additions (process_history_sync_task_stores_message_secrets_without_handlers, secret_encrypted_message_edit_dispatches_legacy_edit, secret_encrypted_message_edit_uses_lid_pn_fallback_in_group, decrypted_message_edit_refreshes_alternate_secret_alias, decrypted_message_edit_recaptures_secret_for_next_edit, and the wacore-side test_message_secrets_extracted_from_history_sync) directly cover the five behaviours called out in the PR body.
• Branch: fix/secret-encrypted-message-edits

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5e6496cf06

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/history_sync.rs Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/history_sync.rs (1)

247-270: 🧹 Nitpick | 🔵 Trivial

On a big InitialBootstrap this writes secrets one row at a time — that won't scale.

Look, store_history_sync_msg_secrets awaits persist_msg_secret_bytes sequentially per sender per record. A real bootstrap can carry thousands of messages, and every secret here is a separate put_msg_secret round-trip (each its own spawn_blocking + retry). That serializes the entire sync worker on DB latency. Move it forward with a single batched/transactional write so the backfill stays fast.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/history_sync.rs` around lines 247 - 270, store_history_sync_msg_secrets
currently calls persist_msg_secret_bytes sequentially for every sender and
record, causing N separate DB round-trips; instead collect all secret write
entries (include chat Jid, sender Jid, msg_id, secret and any metadata derived
from device_snapshot/own_pn/own_lid) and call a single batched/transactional
persistence API (e.g., add a new PersistenceManager method like
put_msg_secrets_batch or persist_msg_secrets_tx) to perform one bulk
write/transaction; ensure you build the entries using
history_msg_secret_senders(...) as now, handle parse/skip of invalid chat Jids,
perform the batch write once, translate the batch result into the stored count
to return, and keep error handling/retries inside the new batched persistence
implementation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/history_sync.rs`:
- Around line 247-270: store_history_sync_msg_secrets currently calls
persist_msg_secret_bytes sequentially for every sender and record, causing N
separate DB round-trips; instead collect all secret write entries (include chat
Jid, sender Jid, msg_id, secret and any metadata derived from
device_snapshot/own_pn/own_lid) and call a single batched/transactional
persistence API (e.g., add a new PersistenceManager method like
put_msg_secrets_batch or persist_msg_secrets_tx) to perform one bulk
write/transaction; ensure you build the entries using
history_msg_secret_senders(...) as now, handle parse/skip of invalid chat Jids,
perform the batch write once, translate the batch result into the stored count
to return, and keep error handling/retries inside the new batched persistence
implementation.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 54a5b163-7430-48fe-9955-b6e945283951

📥 Commits

Reviewing files that changed from the base of the PR and between 5e6496c and 991f1f6.

📒 Files selected for processing (1)
  • src/history_sync.rs

@jlucaso1 jlucaso1 changed the title fix(message): decrypt secret encrypted edits on receive fix(message)!: decrypt secret encrypted edits on receive May 30, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@storages/sqlite-storage/src/sqlite_store.rs`:
- Around line 2555-2596: The current put_msg_secrets implementation inserts
entries one-by-one inside a transaction; refactor it to follow the chunked
bulk-insert pattern used by put_app_state_mutation_macs_for_device: collect
value tuples from the Arc<[MsgSecretEntry]> (include chat, sender, msg_id,
secret, device_id, created_at), split into chunks (e.g., 100), and for each
chunk call
diesel::insert_into(msg_secrets::table).values(&chunk).on_conflict((msg_secrets::chat,
msg_secrets::sender, msg_secrets::msg_id,
msg_secrets::device_id)).do_update().set((msg_secrets::secret.eq(...),
msg_secrets::created_at.eq(...))).execute(conn) inside the existing
immediate_transaction closure, summing and returning the total stored; preserve
use of device_id, now, and the with_retry("put_msg_secrets") wrapper.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 26d2b108-79b0-4c27-80fc-b8c2e321ba1d

📥 Commits

Reviewing files that changed from the base of the PR and between 991f1f6 and 64e1117.

📒 Files selected for processing (4)
  • src/history_sync.rs
  • storages/sqlite-storage/src/sqlite_store.rs
  • wacore/src/store/in_memory.rs
  • wacore/src/store/traits.rs

Comment thread storages/sqlite-storage/src/sqlite_store.rs

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 64e1117495

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread wacore/src/history_sync.rs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@wacore/src/history_sync.rs`:
- Around line 427-512: base_message() only peels each wrapper once in a fixed
order, so nested wrappers like view_once -> ephemeral -> extended_text stop
early; change base_message() to iteratively unwrap wrappers until no wrapper
fields point to an inner message (i.e., loop and replace current with the inner
message whenever any of device_sent_message, ephemeral_message,
view_once_message, view_once_message_v2, document_with_caption_message,
edited_message contain a nested .message), then return the final innermost
&Self; keep is_forwarded() as-is to call the fixed base_message() result. Ensure
you preserve lifetimes/types and use the same field names (device_sent_message,
ephemeral_message, view_once_message, view_once_message_v2,
document_with_caption_message, edited_message) when implementing the loop.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 422349a9-1287-4a6d-96b9-f3abcd379f1d

📥 Commits

Reviewing files that changed from the base of the PR and between 991f1f6 and d1de841.

📒 Files selected for processing (5)
  • src/history_sync.rs
  • storages/sqlite-storage/src/sqlite_store.rs
  • wacore/src/history_sync.rs
  • wacore/src/store/in_memory.rs
  • wacore/src/store/traits.rs

Comment thread wacore/src/history_sync.rs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/history_sync.rs`:
- Around line 247-303: The history sync path stores secrets only under the
senders returned by history_msg_secret_senders(), but must also persist the
bot-DM alternate sender alias (as live receive does) so later lookups using that
alias succeed; in store_history_sync_msg_secrets, after computing senders
(history_msg_secret_senders) and before pushing MsgSecretEntry items, detect
when a bot-DM alternate identity exists (i.e., the DM alias differs from the
primary sender/non-ad form) and push a duplicate MsgSecretEntry for that
alternate sender with the same chat, msg_id and secret (use the same
chat_id/msg_id/secret cloning/taking logic used for other senders); ensure the
duplicate uses sender.to_non_ad_string() for the alias and include it in entries
so put_msg_secrets persists both keys.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 4269d92b-8e83-4828-8c9b-a7c910444b1a

📥 Commits

Reviewing files that changed from the base of the PR and between 991f1f6 and a848ad5.

📒 Files selected for processing (5)
  • src/history_sync.rs
  • storages/sqlite-storage/src/sqlite_store.rs
  • wacore/src/history_sync.rs
  • wacore/src/store/in_memory.rs
  • wacore/src/store/traits.rs

Comment thread src/history_sync.rs
@jlucaso1
jlucaso1 merged commit 2176010 into main May 30, 2026
10 checks passed
@jlucaso1
jlucaso1 deleted the fix/secret-encrypted-message-edits branch May 30, 2026 16:54
zdanysfa added a commit to zdanysfa/whatsapp-rust that referenced this pull request May 30, 2026
Edits to a message authored by another user never decrypted: they stayed
as `secret_encrypted_message` and were never rewrapped to the legacy
`protocol_message.edited_message`, so `Event::Message` consumers that key
off edits never observed them. Edits to our own messages worked.

Root cause: `maybe_decrypt_secret_encrypted_message` resolved the parent
author with `resolve_target_sender(target_message_key, my_jid)`. For an
incoming peer edit the `target_message_key` is written in the editor's
frame -- `from_me = true`, and no `participant` even in groups -- so it
resolved to `my_jid`. The parent `messageSecret` is stored under the real
author, so the lookup missed, decrypt returned `None`, and the still
-encrypted envelope was dispatched unchanged. Self-synced edits worked
only because there the editor is us.

Fix: add `SecretEncrypted::original_sender_for_dispatch`. A message can
only be edited by its author, so for MESSAGE_EDIT resolve the original
sender from the envelope frame (my_jid when from-me, else the envelope
sender) instead of trusting `target_message_key.from_me`. Poll/event
kinds can be modified by a non-author, so they keep target-key
resolution. The existing `original_sender_jid` is retained for the manual
`extract_envelope` API and the poll path.

Adds unit tests for the peer-edit, self-synced, and poll cases.

Follow-up to oxidezap#665.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant