Skip to content

feat(edit): support message-secret encrypted edits (secret_encrypted_message) - #762

Merged
jlucaso1 merged 2 commits into
mainfrom
feat/secret-message-edit-send
Jun 8, 2026
Merged

jlucaso1 merged 2 commits into
mainfrom
feat/secret-message-edit-send

Conversation

@jlucaso1

@jlucaso1 jlucaso1 commented Jun 8, 2026

Copy link
Copy Markdown
Collaborator

Closes the features-30 gap.

Only the plaintext protocolMessage edit could be sent; the message-secret encrypted form (secret_encrypted_message, secret_enc_type = MESSAGE_EDIT) was decode-only. That encrypted form is what Community Announcement Group / channel edits require, and what WA Web sends when message_edit_to_message_secret_sender_enabled is on.

Adds Client::edit_message_encrypted(to, original_id, message_secret, new_content):

  • Builds the same protocolMessage(MESSAGE_EDIT) inner via the existing build_edit_message.
  • Encrypts it under the original message's secret with the existing wacore::message_edit::encrypt_message_edit (ModificationType::MessageEdit).
  • Wraps it as secret_encrypted_message { target_message_key, enc_payload, enc_iv, secret_enc_type = MESSAGE_EDIT } + messageContextInfo.messageSecret, matching WAWebGenerateSecretMessageEditProto.

You can only edit your own message, so the HKDF original-sender and editor are both self (resolved to the chat's namespace — own participant JID for groups, PN for 1:1). The send path already classifies this envelope as edit=1 (existing infer_stanza_metadata test).

This is opt-in: the plaintext edit_message stays the default for normal chats (WA Web gates the encrypted producer behind an AB prop, so it's not the universal default).

Tests: encrypt → decrypt roundtrip recovers the edited content; a wrong secret fails to decrypt.

Verified against docs/captured-js/ references for WAWebGenerateSecretMessageEditProto / WAWebCreateEncryptedMessageEditMsgData and the existing decode path in src/features/message_edit.rs.

…message)

Only the plaintext protocolMessage edit could be sent; the message-secret
encrypted form (secret_encrypted_message, secret_enc_type=MESSAGE_EDIT) was
decode-only. That form is what Community Announcement Group / channel edits
require, and what WA Web sends when message_edit_to_message_secret_sender_enabled
is on.

Add Client::edit_message_encrypted(to, original_id, message_secret, new_content):
it builds the same protocolMessage(MESSAGE_EDIT) inner via build_edit_message,
encrypts it under the original message's secret with the existing
wacore::message_edit::encrypt_message_edit (ModificationType::MessageEdit), and
wraps it as secret_encrypted_message + messageContextInfo.messageSecret, matching
WAWebGenerateSecretMessageEditProto. You can only edit your own message, so the
HKDF original-sender and editor are both self (resolved to the chat's namespace,
own participant JID for groups). The send path already classifies this envelope
as edit=1.

Opt-in: the plaintext edit_message stays the default for normal chats.
Tests: encrypt/decrypt roundtrip recovers the edited content; a wrong secret
fails to decrypt.
@coderabbitai

coderabbitai Bot commented Jun 8, 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: 57479089-a358-4fa8-8cee-a5c42a2409c4

📥 Commits

Reviewing files that changed from the base of the PR and between 3ec3573 and 3e0cbf2.

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

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Encrypted message editing: securely modify previously sent messages while preserving end-to-end encryption and message integrity; editing is blocked for newsletters/channels.
  • Tests

    • Added unit tests validating encrypted edit workflows, including successful encrypt/decrypt roundtrips and rejection when using an incorrect secret.

Walkthrough

Adds a public API Client::edit_message_encrypted that validates inputs, resolves the editor JID, builds a secret-encrypted MESSAGE_EDIT envelope via a new helper build_secret_message_edit (which uses wacore::message_edit::encrypt_message_edit and attaches the original 32-byte secret), sends it with send_message_impl, and returns the original message ID.

Changes

Encrypted Message Edit Sending

Layer / File(s) Summary
Public encrypted edit sending API
src/client/messaging.rs
edit_message_encrypted validates recipient type and message_secret length, chooses editor JID for group vs non-group targets, calls the internal builder to produce a secret_encrypted_message edit envelope, sends it via send_message_impl with EditAttribute::MessageEdit, and returns the original ID.
Secret message edit construction and validation
src/client/messaging.rs
build_secret_message_edit composes the plaintext edit stanza, wraps it in wacore::message_edit::MessageEditContext, encrypts with wacore::message_edit::encrypt_message_edit using the provided 32-byte secret, returns a wa::Message with secret_encrypted_message and message_context_info.message_secret, and includes tests for successful decrypt with the same secret and failure with a different secret.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Builder as build_secret_message_edit
  participant WACore as wacore::message_edit
  participant Sender as send_message_impl

  Client->>Builder: build_secret_message_edit(original_id, self_jid, new_content, message_secret)
  Builder->>WACore: encrypt_message_edit(MessageEditContext, plaintext, message_secret)
  WACore-->>Builder: secret_encrypted_message
  Builder-->>Client: wa::Message (secret_encrypted_message + message_context_info)
  Client->>Sender: send_message_impl(wa::Message, EditAttribute::MessageEdit)
  Sender-->>Client: send result / ok
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • oxidezap/whatsapp-rust#725: Introduces newsletter/channel recipient guards that align with the new sender-side rejection for newsletter JIDs.
  • oxidezap/whatsapp-rust#618: Adds receiver-side decryption and envelope extraction for secret_encrypted_message edits that this sender-side code constructs.
  • oxidezap/whatsapp-rust#723: Provides the build_edit_message helper used by the new build_secret_message_edit.

Mark: code needs to work reliably — focus review on the editor JID resolution, strict 32-byte secret handling, and the integration point with send_message_impl.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main feature addition: support for message-secret encrypted edits using secret_encrypted_message.
Description check ✅ Passed The description comprehensively explains what was added, why it was needed, implementation details, and how it was tested—all directly related to the changeset.
Docstring Coverage ✅ Passed Docstring coverage is 100.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 feat/secret-message-edit-send

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.

@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: 3ec35731c6

ℹ️ 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/client/messaging.rs
new_content,
)?;

self.send_message_impl(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Route channel edits off the E2E send path

When to is a WhatsApp channel/newsletter JID, this new API still delegates to send_message_impl, but that path explicitly rejects newsletters before building any stanza (send.rs returns newsletter JIDs are not valid on the E2E send path). That means the documented channel use case for edit_message_encrypted always errors instead of sending the encrypted edit; handle newsletter/channel edits through the channel send path or reject them before constructing this envelope.

Useful? React with 👍 / 👎.

@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/client/messaging.rs`:
- Around line 121-127: The edit_message_encrypted API accepts message_secret:
&[u8] but does not validate its length; add an explicit check in
edit_message_encrypted that message_secret.len() == 32 and return a clear error
(e.g. using anyhow::anyhow! with a descriptive message) if it isn't, before
calling encrypt_message_edit or other crypto functions so callers get a fast,
meaningful validation error referencing edit_message_encrypted and avoiding
downstream cryptic failures.
- Around line 354-361: The helper build_secret_message_edit should validate its
inputs before using them: add a defensive check at the top of
build_secret_message_edit validating message_secret (and optionally
participant/self_jid_str format if relevant) and return an anyhow::Error when
the key is the wrong length or shape instead of deferring to the crypto layer;
locate build_secret_message_edit (params: original_id, message_secret,
participant, self_jid_str) and perform an explicit length/format check (compare
message_secret.len() against the expected secret key length constant or value)
and return a clear error via anyhow::Error if it fails.
🪄 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: bda3df6b-894d-430b-8c57-088e86c23385

📥 Commits

Reviewing files that changed from the base of the PR and between ca93294 and 3ec3573.

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

Comment thread src/client/messaging.rs
Comment thread src/client/messaging.rs
@github-actions

github-actions Bot commented Jun 8, 2026

Copy link
Copy Markdown

Benchmark Results

67 unchanged benchmark(s)
Benchmark Current Baseline Change
reporting_token_benchmark::content_extraction_group::bench_content_extraction simple:setup_simple_message() 2,925 2,925 +0.0%
reporting_token_benchmark::content_extraction_group::bench_content_extraction extended:setup_extended_message() 8,446 8,446 +0.0%
reporting_token_benchmark::key_derivation_group::bench_key_derivation 31,317 31,317 +0.0%
reporting_token_benchmark::token_calculation_group::bench_token_calculation 13,827 13,827 +0.0%
reporting_token_benchmark::full_generation_group::bench_full_token_generation simple:setup_full_gen_simple() 49,485 49,485 +0.0%
reporting_token_benchmark::full_generation_group::bench_full_token_generation extended:setup_full_gen_extended() 55,001 55,001 +0.0%
reporting_token_benchmark::message_encoding_group::bench_message_encoding simple:setup_simple_message() 1,679 1,679 +0.0%
reporting_token_benchmark::message_encoding_group::bench_message_encoding extended:setup_extended_message() 4,393 4,393 +0.0%
send_receive_benchmark::dm_send::bench_dm_send text:setup_dm_send() 113,211 112,962 +0.2%
send_receive_benchmark::dm_recv::bench_dm_recv text:setup_dm_recv() 1,656,621 1,656,625 -0.0%
send_receive_benchmark::group_send::bench_group_send group_10:setup_group_send_10() 651,855 651,867 -0.0%
send_receive_benchmark::group_send::bench_group_send group_50:setup_group_send_50() 875,896 875,796 +0.0%
send_receive_benchmark::group_send::bench_group_send group_256:setup_group_send_256() 2,083,718 2,083,364 +0.0%
send_receive_benchmark::group_send_skdm::bench_group_send_skdm skdm_10:setup_group_skdm_10() 749,075 749,072 +0.0%
send_receive_benchmark::group_send_skdm::bench_group_send_skdm skdm_50:setup_group_skdm_50() 1,329,794 1,329,633 +0.0%
send_receive_benchmark::group_send_skdm::bench_group_send_skdm skdm_256:setup_group_skdm_256() 4,371,125 4,371,180 -0.0%
send_receive_benchmark::group_recv::bench_group_recv text:setup_group_recv() 517,361 521,142 -0.7%
binary_benchmark::marshal_group::bench_marshal_allocating 45,381 45,381 +0.0%
binary_benchmark::marshal_group::bench_marshal_auto_allocating 45,431 45,431 +0.0%
binary_benchmark::marshal_group::bench_marshal_exact_allocating 66,334 66,334 +0.0%
binary_benchmark::marshal_group::bench_marshal_reusing_buffer 43,492 43,492 +0.0%
binary_benchmark::marshal_group::bench_marshal_reusing_buffer_vec_writer 45,487 45,487 +0.0%
binary_benchmark::marshal_group::bench_marshal_long_string 4,945 4,945 +0.0%
binary_benchmark::marshal_group::bench_marshal_auto_long_string 4,976 4,976 +0.0%
binary_benchmark::marshal_group::bench_marshal_exact_long_string 6,747 6,747 +0.0%
binary_benchmark::marshal_group::bench_marshal_huge_bytes_allocating 528,544 528,544 +0.0%
binary_benchmark::marshal_group::bench_marshal_auto_huge_bytes_allocating 528,165 528,165 +0.0%
binary_benchmark::marshal_group::bench_marshal_exact_huge_bytes_allocating 529,411 529,411 +0.0%
binary_benchmark::marshal_group::bench_marshal_many_children_allocating 5,417,732 5,417,732 +0.0%
binary_benchmark::marshal_group::bench_marshal_auto_many_children_allocating 5,362,047 5,362,047 +0.0%
binary_benchmark::marshal_group::bench_marshal_exact_many_children_allocating 13,276,365 13,276,365 +0.0%
binary_benchmark::unmarshal_group::bench_unmarshal small:setup_small_marshaled() 1,850 1,850 +0.0%
binary_benchmark::unmarshal_group::bench_unmarshal large:setup_large_marshaled() 29,217 29,217 +0.0%
binary_benchmark::unpack_group::bench_unpack_uncompressed 618 618 +0.0%
binary_benchmark::unpack_group::bench_unpack_compressed 672,890 672,890 +0.0%
binary_benchmark::attr_parser_group::bench_attr_parser attr_lookup:setup_attr_marshaled() 3,736 3,736 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip small:setup_small_marshaled() 3,840 3,840 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip large:setup_large_marshaled() 48,274 48,274 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip_auto small:setup_small_marshaled() 3,866 3,866 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip_auto large:setup_large_marshaled() 48,335 48,335 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip_exact small:setup_small_marshaled() 5,206 5,206 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip_exact large:setup_large_marshaled() 66,659 66,659 +0.0%
binary_benchmark::child_iteration_group::bench_get_children_by_tag 310,312 310,312 +0.0%
binary_benchmark::jid_optimization_group::bench_jid_to_owned_access jid_access:setup_jid_heavy_marshaled() 8,291 8,291 +0.0%
numeric_attr_benchmark::bench_group::bench_baseline_u32 254 254 +0.0%
numeric_attr_benchmark::bench_group::bench_proposed_u32 91 91 +0.0%
numeric_attr_benchmark::bench_group::bench_baseline_u64 292 292 +0.0%
numeric_attr_benchmark::bench_group::bench_proposed_u64 137 137 +0.0%
numeric_attr_benchmark::bench_group::bench_baseline_i64 317 317 +0.0%
numeric_attr_benchmark::bench_group::bench_proposed_i64 145 145 +0.0%
numeric_attr_benchmark::bench_group::bench_baseline_loop_100_u64 27,425 27,425 +0.0%
numeric_attr_benchmark::bench_group::bench_proposed_loop_100_u64 10,725 10,725 +0.0%
libsignal_benchmark::dm_group::bench_dm_session_establishment setup:setup_dm_users() 4,138,587 4,144,504 -0.1%
libsignal_benchmark::dm_group::bench_dm_encrypt_first_message first_msg:setup_dm_session() 100,133 100,133 +0.0%
libsignal_benchmark::dm_group::bench_dm_decrypt_first_message decrypt_prekey:setup_dm_with_first_message() 4,264,189 4,264,189 +0.0%
libsignal_benchmark::dm_group::bench_dm_encrypt_subsequent_message subsequent:setup_established_dm_session() 100,399 100,399 +0.0%
libsignal_benchmark::group_messaging_group::bench_group_create_distribution_message create:setup_group_sender() 210,249 210,262 -0.0%
libsignal_benchmark::group_messaging_group::bench_group_encrypt_message encrypt:setup_group_with_distribution() 496,921 496,921 +0.0%
libsignal_benchmark::group_messaging_group::bench_group_decrypt_message decrypt:setup_group_with_encrypted_message() 508,316 509,250 -0.2%
libsignal_benchmark::conversation_group::bench_full_dm_conversation full:setup_conversation_data() 11,977,042 11,980,707 -0.0%
libsignal_benchmark::signature_group::bench_signature_creation sign:setup_keypair_with_message() 2,466,138 2,466,138 +0.0%
libsignal_benchmark::signature_group::bench_signature_verification verify:setup_keypair_with_message() 4,911,412 4,876,062 +0.7%
libsignal_benchmark::signature_group::bench_key_generation keygen 2,043,397 2,043,397 +0.0%
libsignal_benchmark::session_optimization_group::bench_decrypt_with_previous_session previous_session:setup_with_archived_sessions() 37,404 37,404 +0.0%
libsignal_benchmark::session_optimization_group::bench_out_of_order_decryption out_of_order:setup_out_of_order_messages() 3,617,967 3,617,967 +0.0%
libsignal_benchmark::session_optimization_group::bench_promote_matching_session promote:setup_promote_matching_session() 230,648 230,648 +0.0%
libsignal_benchmark::session_optimization_group::bench_message_key_eviction eviction:setup_message_key_eviction() 9,980,959 9,980,959 +0.0%
No significant changes detected.

…ge_encrypted

Review follow-ups on the encrypted-edit PR:
- Codex: a newsletter/channel JID would always error deep in send_message_impl
  (which rejects newsletters from the E2E path). Encrypted edits don't apply to
  plaintext channels anyway, so reject them up front with a clear boundary error
  pointing at edit_message.
- CodeRabbit: validate message_secret is 32 bytes at the API boundary so a
  wrong-size secret fails fast instead of erroring deep inside encrypt_message_edit.
@jlucaso1
jlucaso1 merged commit 0dc762d into main Jun 8, 2026
11 checks passed
@jlucaso1
jlucaso1 deleted the feat/secret-message-edit-send branch June 8, 2026 12:57
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