Skip to content

perf: optimize libsignal reducing allocations - #225

Merged
jlucaso1 merged 4 commits into
mainfrom
feat-optimize-libsignal
Jan 8, 2026
Merged

perf: optimize libsignal reducing allocations#225
jlucaso1 merged 4 commits into
mainfrom
feat-optimize-libsignal

Conversation

@jlucaso1

@jlucaso1 jlucaso1 commented Jan 8, 2026

Copy link
Copy Markdown
Collaborator

Summary

This PR optimizes the libsignal cryptographic library by reducing heap allocations. The main changes replace Vec<u8> and Box<[u8]> with fixed-size arrays ([u8; N]) for cryptographic keys and signatures, and introduce an optimized step_with_message_key() method that reuses HMAC context.

Test Coverage

Added comprehensive tests for the new step_with_message_keys() optimization:

  • test_step_with_message_keys_equivalence: Verifies identical results to separate calls
  • test_step_with_message_keys_chain: Verifies behavior over multiple iterations
  • test_sender_chain_key_iteration_step_*: Tests for SenderChainKey::step_with_message_key()

Summary by CodeRabbit

  • Refactor

    • Reduced allocations and switched to fixed-size buffers/arrays across crypto and message flows for lower memory use and improved performance.
  • Bug Fixes

    • Added stricter chain-key validation and safer error handling to prevent use of invalid state.
  • Compatibility / Serialization

    • Standardized binary representations and adjusted protobuf/serde handling for more robust persistence and cheaper cloning.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai

coderabbitai Bot commented Jan 8, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Replaces dynamic heap allocations with fixed-size arrays and stack buffers, introduces combined-step key-derivation APIs (step_with_message_key/s), tightens chain_key length validation for protobuf/Go inputs, migrates protobuf binary fields to prost::bytes::Bytes with serde skip/default, and updates many call sites and signatures to use fixed-size types and reusable buffers.

Changes

Cohort / File(s) Summary
SenderKey / Message parsing
src/message.rs, wacore/libsignal/src/protocol/protocol.rs
Enforce chain_key length when converting protobuf/Go inputs (Vec → [u8; 32] via try_into), log and return on invalid length; SenderKeyDistributionMessage now stores [u8; 32]; constructors/getters/TryFrom updated. Attention: Go-fallback and deserialization error paths.
Combined key-derivation API
wacore/libsignal/src/protocol/ratchet/keys.rs, wacore/libsignal/src/protocol/sender_keys.rs
Add step_with_message_keys / step_with_message_key returning (message-key-generator/message-key, next_chain); MessageKeyGenerator gains Seed/Serialized variants with lazy generation and protobuf round-trip helpers. Attention: public API/type changes, seed fixed-size [u8;32], Copy/Clone semantics.
Crypto primitives: fixed-size serialization & signatures
wacore/libsignal/src/core/curve.rs, wacore/libsignal/src/protocol/identity_key.rs
Replace heap allocations with fixed-size arrays: PublicKey::serialize -> [u8;33], PrivateKey::serialize -> &[u8;32], signature/agreement APIs now return fixed-size arrays ([u8;64], [u8;32]). Attention: updated method signatures require caller adjustments crate-wide.
Buffer & memory management
wacore/libsignal/src/protocol/ratchet.rs, wacore/libsignal/src/protocol/group_cipher.rs, wacore/libsignal/src/protocol/session_cipher.rs
Replace Vec-based secret accumulation with fixed stack buffer ([u8;160] + length); introduce thread-local reusable buffers (ENCRYPTION_BUFFER/DECRYPTION_BUFFER); switch flows to step_with_message_key(s) and reuse buffers. Attention: thread-local behavior, buffer capacity preservation, concurrency.
Protobuf bytes/type migration & serde hygiene
waproto/build.rs, waproto/src/whatsapp.rs, Cargo.toml
Protobuf binary fields migrated from Vec<u8>prost::bytes::Bytes via prost_build config; added #[serde(skip, default)] where needed; bytes workspace dep enabled with serde feature. Attention: generated types changed and serde handling for those fields.
Call-site serialization & protobuf wiring
wacore/libsignal/src/store/record_helpers.rs, src/store/signal.rs, storages/sqlite-storage/src/sqlite_store.rs, wacore/src/store/device.rs, wacore/libsignal/src/protocol/state/session.rs, wacore/libsignal/src/protocol/state/signed_prekey.rs, wacore/src/send.rs
Update call sites to new fixed-size returns or Bytes (use .to_vec() / Bytes::copy_from_slice / pass *[u8;32]), adjust private_key serializations and protobuf field population. Attention: ensure all callers match new serialize/signature return types and Bytes usage.
Handshake / secret mixing call-site tweaks
wacore/src/handshake/state.rs
Simplify argument passing to mix_shared_secret from multi-line borrowed form to single-line owned/value passes. Attention: confirm borrow/move semantics unchanged.

Sequence Diagram(s)

(Skipped — changes are cross-cutting API and data-layout updates rather than a new multi-component runtime control flow that benefits from a sequence diagram.)

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I nibbled Vecs and cached seeds,
Swapped heaps for arrays, trimmed excess feeds,
One HMAC step and keys hop in line,
Buffers reused, no more alloc-time,
Tiny paws pat fixed-size bytes — carrot-time! 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 54.55% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'perf: optimize libsignal reducing allocations' accurately captures the main objective of the PR, which focuses on performance improvements through allocation reduction.

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

✨ Finishing touches
  • 📝 Generate docstrings

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.

@github-actions

github-actions Bot commented Jan 8, 2026

Copy link
Copy Markdown

🐰 Bencher Report

Branchfeat-optimize-libsignal
Testbedubuntu-latest
Click to view all benchmark results
BenchmarkInstructionsBenchmark Result
instructions
(Result Δ%)
Upper Boundary
instructions
(Limit %)
binary_benchmark::attr_parser_group::bench_attr_parser attr_lookup:setup_attr_marshaled()📈 view plot
🚷 view threshold
7,345.00
(-1.96%)Baseline: 7,491.61
7,866.19
(93.37%)
binary_benchmark::marshal_group::bench_marshal_allocating📈 view plot
🚷 view threshold
124,694.00
(-19.16%)Baseline: 154,243.52
161,955.69
(76.99%)
binary_benchmark::marshal_group::bench_marshal_reusing_buffer📈 view plot
🚷 view threshold
124,794.00
(-19.15%)Baseline: 154,345.00
162,062.25
(77.00%)
binary_benchmark::unmarshal_group::bench_unmarshal large:setup_large_marshaled()📈 view plot
🚷 view threshold
49,338.00
(-0.64%)Baseline: 49,657.42
52,140.30
(94.63%)
binary_benchmark::unmarshal_group::bench_unmarshal small:setup_small_marshaled()📈 view plot
🚷 view threshold
3,025.00
(-9.21%)Baseline: 3,331.88
3,498.47
(86.47%)
binary_benchmark::unpack_group::bench_unpack_compressed📈 view plot
🚷 view threshold
468,057.00
(-0.04%)Baseline: 468,230.73
491,642.26
(95.20%)
binary_benchmark::unpack_group::bench_unpack_uncompressed📈 view plot
🚷 view threshold
779.00
(+0.03%)Baseline: 778.79
817.73
(95.26%)
libsignal_benchmark::conversation_group::bench_full_dm_conversation full:setup_conversation_data()📈 view plot
🚷 view threshold
27,683,487.00
(+0.05%)Baseline: 27,670,229.50
29,053,740.98
(95.28%)
libsignal_benchmark::dm_group::bench_dm_decrypt_first_message decrypt_prekey:setup_dm_with_first_message()📈 view plot
🚷 view threshold
5,569,013.00
(-0.14%)Baseline: 5,576,924.00
5,855,770.20
(95.10%)
libsignal_benchmark::dm_group::bench_dm_encrypt_first_message first_msg:setup_dm_session()📈 view plot
🚷 view threshold
176,934.00
(-3.44%)Baseline: 183,236.50
192,398.33
(91.96%)
libsignal_benchmark::dm_group::bench_dm_encrypt_subsequent_message subsequent:setup_established_dm_session()📈 view plot
🚷 view threshold
177,750.00
(-3.39%)Baseline: 183,988.50
193,187.92
(92.01%)
libsignal_benchmark::dm_group::bench_dm_session_establishment setup:setup_dm_users()📈 view plot
🚷 view threshold
17,408,995.00
(+0.20%)Baseline: 17,373,844.00
18,242,536.20
(95.43%)
libsignal_benchmark::group_messaging_group::bench_group_create_distribution_message create:setup_group_sender()📈 view plot
🚷 view threshold
295,577.00
(-0.17%)Baseline: 296,083.00
310,887.15
(95.08%)
libsignal_benchmark::group_messaging_group::bench_group_decrypt_message decrypt:setup_group_with_encrypted_message()📈 view plot
🚷 view threshold
12,698,053.00
(-0.09%)Baseline: 12,709,195.00
13,344,654.75
(95.15%)
libsignal_benchmark::group_messaging_group::bench_group_encrypt_message encrypt:setup_group_with_distribution()📈 view plot
🚷 view threshold
715,209.00
(-0.90%)Baseline: 721,733.00
757,819.65
(94.38%)
reporting_token_benchmark::content_extraction_group::bench_content_extraction extended:setup_extended_message()📈 view plot
🚷 view threshold
11,819.00
(+0.11%)Baseline: 11,806.36
12,396.68
(95.34%)
reporting_token_benchmark::content_extraction_group::bench_content_extraction simple:setup_simple_message()📈 view plot
🚷 view threshold
3,837.00
(+0.29%)Baseline: 3,826.06
4,017.36
(95.51%)
reporting_token_benchmark::full_generation_group::bench_full_token_generation extended:setup_full_gen_extended()📈 view plot
🚷 view threshold
88,033.00
(+0.05%)Baseline: 87,990.00
92,389.50
(95.28%)
reporting_token_benchmark::full_generation_group::bench_full_token_generation simple:setup_full_gen_simple()📈 view plot
🚷 view threshold
80,074.00
(+0.05%)Baseline: 80,032.70
84,034.33
(95.29%)
reporting_token_benchmark::key_derivation_group::bench_key_derivation📈 view plot
🚷 view threshold
51,035.00
(0.00%)Baseline: 51,035.00
53,586.75
(95.24%)
reporting_token_benchmark::message_encoding_group::bench_message_encoding extended:setup_extended_message()📈 view plot
🚷 view threshold
5,738.00
(+0.22%)Baseline: 5,725.36
6,011.63
(95.45%)
reporting_token_benchmark::message_encoding_group::bench_message_encoding simple:setup_simple_message()📈 view plot
🚷 view threshold
2,121.00
(+0.52%)Baseline: 2,110.06
2,215.56
(95.73%)
reporting_token_benchmark::token_calculation_group::bench_token_calculation📈 view plot
🚷 view threshold
21,921.00
(+0.12%)Baseline: 21,894.64
22,989.37
(95.35%)
🐰 View full continuous benchmarking report in Bencher

@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

Caution

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

⚠️ Outside diff range comments (1)
wacore/src/handshake/state.rs (1)

21-21: Critical bug: unwrap_err() will panic on successful RNG initialization.

OsRng.unwrap_err() calls TryRngCore::unwrap_err() which panics if the RNG succeeds (the expected case). This should be OsRng.unwrap() or handle the error properly.

🐛 Proposed fix
-        let ephemeral_kp = KeyPair::generate(&mut OsRng.unwrap_err());
+        let ephemeral_kp = KeyPair::generate(&mut OsRng);

Note: If rand 0.9 requires explicit error handling, use:

let ephemeral_kp = KeyPair::generate(&mut OsRng.unwrap());
🤖 Fix all issues with AI agents
In @wacore/libsignal/src/crypto/aes_cbc.rs:
- Around line 78-81: The current code clears the buffer then calls resize +
copy_from_slice which writes zeros then overwrites them; instead revert to the
reserve + extend_from_slice pattern: after clearing output, call
output.reserve(ctext.len()) and then output.extend_from_slice(ctext) (operating
on the same variables `output` and `ctext` in aes_cbc.rs) to ensure the data is
written only once and follows idiomatic Rust buffer growth.
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c7f344d and 8d20e01.

📒 Files selected for processing (18)
  • src/message.rs
  • src/store/signal.rs
  • storages/sqlite-storage/src/sqlite_store.rs
  • wacore/libsignal/src/core/curve.rs
  • wacore/libsignal/src/crypto/aes_cbc.rs
  • wacore/libsignal/src/protocol/group_cipher.rs
  • wacore/libsignal/src/protocol/identity_key.rs
  • wacore/libsignal/src/protocol/protocol.rs
  • wacore/libsignal/src/protocol/ratchet.rs
  • wacore/libsignal/src/protocol/ratchet/keys.rs
  • wacore/libsignal/src/protocol/sender_keys.rs
  • wacore/libsignal/src/protocol/session_cipher.rs
  • wacore/libsignal/src/protocol/state/session.rs
  • wacore/libsignal/src/protocol/state/signed_prekey.rs
  • wacore/libsignal/src/store/record_helpers.rs
  • wacore/src/handshake/state.rs
  • wacore/src/send.rs
  • wacore/src/store/device.rs
🧰 Additional context used
📓 Path-based instructions (2)
**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

**/*.rs: Never modify Device state directly; use DeviceCommand + PersistenceManager::process_command() for state changes
For read-only Device state access, use PersistenceManager::get_device_snapshot()
All blocking I/O (such as ureq calls) and heavy CPU-bound tasks (like media encryption) MUST be wrapped in tokio::task::spawn_blocking to avoid stalling the async runtime
Use Client::chat_locks to serialize per-chat operations in asynchronous code
Use thiserror for custom domain-specific errors (e.g., SocketError) and anyhow::Error for functions with multiple failure modes
Avoid .unwrap() and .expect() outside of tests and unrecoverable logic paths
Use the Downloadable trait in wacore/src/download.rs for implementing generic media download interfaces across message types
Always refresh MediaConn if it's expired before using it to get current media servers and auth tokens
Run cargo fmt before finalizing a feature or fix
Run cargo clippy --all-targets before finalizing a feature or fix
Run cargo test --all before finalizing a feature or fix

Files:

  • src/message.rs
  • wacore/src/send.rs
  • wacore/libsignal/src/crypto/aes_cbc.rs
  • wacore/libsignal/src/protocol/identity_key.rs
  • wacore/libsignal/src/protocol/session_cipher.rs
  • src/store/signal.rs
  • storages/sqlite-storage/src/sqlite_store.rs
  • wacore/src/store/device.rs
  • wacore/libsignal/src/protocol/ratchet.rs
  • wacore/libsignal/src/protocol/sender_keys.rs
  • wacore/libsignal/src/protocol/group_cipher.rs
  • wacore/libsignal/src/store/record_helpers.rs
  • wacore/libsignal/src/core/curve.rs
  • wacore/libsignal/src/protocol/protocol.rs
  • wacore/libsignal/src/protocol/state/session.rs
  • wacore/libsignal/src/protocol/state/signed_prekey.rs
  • wacore/libsignal/src/protocol/ratchet/keys.rs
  • wacore/src/handshake/state.rs
wacore/**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

wacore crate must remain platform-agnostic and have no dependencies on Tokio or specific databases

Files:

  • wacore/src/send.rs
  • wacore/libsignal/src/crypto/aes_cbc.rs
  • wacore/libsignal/src/protocol/identity_key.rs
  • wacore/libsignal/src/protocol/session_cipher.rs
  • wacore/src/store/device.rs
  • wacore/libsignal/src/protocol/ratchet.rs
  • wacore/libsignal/src/protocol/sender_keys.rs
  • wacore/libsignal/src/protocol/group_cipher.rs
  • wacore/libsignal/src/store/record_helpers.rs
  • wacore/libsignal/src/core/curve.rs
  • wacore/libsignal/src/protocol/protocol.rs
  • wacore/libsignal/src/protocol/state/session.rs
  • wacore/libsignal/src/protocol/state/signed_prekey.rs
  • wacore/libsignal/src/protocol/ratchet/keys.rs
  • wacore/src/handshake/state.rs
🧠 Learnings (1)
📚 Learning: 2025-12-25T03:36:19.700Z
Learnt from: CR
Repo: jlucaso1/whatsapp-rust PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-25T03:36:19.700Z
Learning: Applies to waproto/build.rs : `waproto` crate uses prost to compile Protocol Buffers definitions from `whatsapp.proto` in a `build.rs` script

Applied to files:

  • wacore/libsignal/src/store/record_helpers.rs
  • wacore/libsignal/src/protocol/protocol.rs
🧬 Code graph analysis (10)
src/message.rs (2)
wacore/libsignal/src/protocol/protocol.rs (2)
  • chain_key (569-571)
  • signing_key (574-576)
wacore/libsignal/src/core/curve.rs (1)
  • from_djb_public_key_bytes (99-106)
wacore/src/send.rs (1)
wacore/libsignal/src/protocol/protocol.rs (1)
  • chain_key (569-571)
wacore/libsignal/src/protocol/identity_key.rs (2)
wacore/libsignal/src/core/curve.rs (2)
  • serialize (109-116)
  • serialize (229-233)
wacore/src/store/device.rs (1)
  • serialize (16-28)
wacore/libsignal/src/protocol/session_cipher.rs (1)
wacore/libsignal/src/protocol/ratchet/keys.rs (2)
  • next_chain_key (139-144)
  • message_keys (146-151)
src/store/signal.rs (2)
wacore/libsignal/src/protocol/sender_keys.rs (1)
  • deserialize (295-304)
wacore/src/store/device.rs (1)
  • deserialize (30-43)
wacore/libsignal/src/protocol/ratchet.rs (4)
wacore/libsignal/src/protocol/state/session.rs (1)
  • root_key (159-165)
wacore/libsignal/src/protocol/protocol.rs (1)
  • chain_key (569-571)
wacore/libsignal/src/protocol/ratchet/keys.rs (1)
  • derive_keys (76-92)
wacore/libsignal/src/protocol/ratchet/params.rs (1)
  • our_one_time_pre_key_pair (145-147)
wacore/libsignal/src/protocol/group_cipher.rs (2)
wacore/libsignal/src/protocol/sender_keys.rs (5)
  • new (39-52)
  • new (92-97)
  • new (173-193)
  • sender_chain_key (207-219)
  • sender_key_state (306-311)
wacore/libsignal/src/crypto/aes_cbc.rs (1)
  • aes_256_cbc_decrypt_into (66-94)
wacore/libsignal/src/store/record_helpers.rs (2)
wacore/libsignal/src/protocol/state/signed_prekey.rs (1)
  • key_pair (129-140)
wacore/libsignal/src/protocol/state/prekey.rs (1)
  • key_pair (59-70)
wacore/libsignal/src/core/curve.rs (3)
wacore/libsignal/src/protocol/identity_key.rs (5)
  • serialize (45-47)
  • serialize (126-134)
  • private_key (121-123)
  • from (187-192)
  • from (196-198)
wacore/src/store/device.rs (1)
  • serialize (16-28)
wacore/libsignal/src/core/curve/curve25519.rs (2)
  • from (161-164)
  • calculate_agreement (40-48)
wacore/libsignal/src/protocol/protocol.rs (2)
wacore/libsignal/src/protocol/ratchet.rs (1)
  • message_version (22-24)
wacore/libsignal/src/protocol/sender_keys.rs (1)
  • message_version (199-201)
🔇 Additional comments (34)
wacore/src/store/device.rs (1)

23-24: LGTM! Allocation reduction.

The change from into_iter() to iter().copied() correctly adapts to PrivateKey::serialize() now returning a reference to a fixed-size array (&[u8; 32]) instead of an owned Vec<u8>. This eliminates an intermediate allocation while preserving the same behavior.

wacore/libsignal/src/protocol/state/signed_prekey.rs (1)

172-172: LGTM! Trait adapter.

The addition of .to_vec() correctly adapts PrivateKey::serialize() (now returning &[u8; 32]) to the KeySerde trait's requirement of returning Vec<u8>. This maintains trait compatibility while benefiting from the fixed-size array representation in the core API.

storages/sqlite-storage/src/sqlite_store.rs (2)

149-154: LGTM! Simplified dereference.

The removal of the & operator correctly adapts to PrivateKey::serialize() now returning &[u8; 32] instead of an owned array. Since serialize() returns a reference, the explicit & is no longer needed for extend_from_slice.


273-290: LGTM! Consistent dereference simplification.

The removal of & operators before serialize() calls for noise_key, identity_key, and signed_pre_key is consistent with the change in serialize_keypair. All correctly adapt to PrivateKey::serialize() returning &[u8; 32].

wacore/libsignal/src/protocol/ratchet/keys.rs (6)

10-11: LGTM! Required imports.

The HMAC and SHA256 imports support the new step_with_message_keys method for inline key derivation.


17-17: Excellent optimization! Fixed-size seed eliminates allocation.

The change from Vec<u8> to [u8; 32] for the seed eliminates heap allocation and provides compile-time size guarantees. The updated new_from_seed signature enforces that seeds are exactly 32 bytes, improving type safety. The copy operation is cheap for 32 bytes.

Also applies to: 21-24


67-67: LGTM! Copy derives enable cheaper duplication.

Adding Copy to MessageKeys and ChainKey is appropriate since all fields are fixed-size arrays and primitives. This allows the compiler to use simple memory copies instead of clone logic, aligning with the PR's allocation reduction goals.

Also applies to: 115-115


153-172: Excellent optimization! Combined step reuses HMAC setup.

The step_with_message_keys method efficiently computes both message keys and the next chain key in a single pass by reusing the HMAC instance with finalize_reset(). This avoids duplicate key setup overhead while maintaining correctness. The tests verify equivalence with separate operations.


329-346: LGTM! Test adapted to fixed-size seed.

The test correctly updates the seed from Vec<u8> to [u8; 32] to match the new new_from_seed signature. The test logic remains equivalent.


368-434: Excellent test coverage for the optimization!

The equivalence tests thoroughly verify that step_with_message_keys() produces identical results to calling message_keys() and next_chain_key() separately. The single-step test verifies immediate equivalence, while the chain test verifies sustained correctness over 10 iterations, catching potential accumulation errors. This provides strong confidence in the optimization's correctness.

wacore/libsignal/src/protocol/identity_key.rs (2)

43-47: LGTM! Fixed-size serialization eliminates allocation.

The change from Box<[u8]> to [u8; 33] eliminates heap allocation by returning a fixed-size array on the stack. The documentation accurately describes the format (1 type byte + 32 key bytes). This aligns with the PR's optimization goals.


137-150: LGTM! Fixed-size signature eliminates allocation.

The change from Result<Box<[u8]>> to Result<[u8; 64]> is appropriate since Ed25519 signatures are always 64 bytes. This eliminates heap allocation and improves type safety by encoding the size in the type system.

wacore/src/send.rs (1)

856-864: Chain key passed as fixed-size array matches new SKDM API

Switching from chain_key.seed().to_vec() to *chain_key.seed() correctly aligns with SenderKeyDistributionMessage::new’s [u8; 32] parameter and avoids an extra allocation, without changing semantics.

wacore/libsignal/src/protocol/state/session.rs (1)

77-101: Identity/public key serialization updates are consistent and safe

Using .serialize().to_vec() for local/remote identity and alice_base_key matches the updated serialization API (borrowed slice → owned Vec<u8>) and keeps encode/decode behavior unchanged.

Also applies to: 128-139

src/message.rs (1)

1218-1280: Stricter Go SKDM chain_key validation is correct and improves robustness

Converting chain_key to [u8; 32] with an explicit length check and bailing out on mismatch is the right way to feed the new SKDM constructor and prevents malformed Go SKDMs from triggering internal invariants. Logging the actual length and sender JID is also useful for diagnosing interoperability issues.

src/store/signal.rs (1)

191-198: Private key (de)serialization updates align with new API

Using PrivateKey::deserialize(private_key.serialize()) at both identity and signed-prekey call sites matches the updated serialize() -> &[u8] API and avoids unnecessary copying, while preserving the original logic.

Also applies to: 336-344

wacore/libsignal/src/store/record_helpers.rs (1)

8-14: PreKey/SignedPreKey helpers correctly adapt to slice-based serialization

Changing private_key fields to use key_pair.private_key.serialize().to_vec() (and the corresponding record.key_pair()?.private_key.serialize().to_vec()) is the right adjustment for the new slice-based serialization and keeps the protobuf structures consistent with the decoding code paths.

Also applies to: 16-26, 59-66

wacore/libsignal/src/protocol/ratchet.rs (1)

49-82: Fixed-size secrets buffer preserves X3DH semantics and removes allocations

The switch from a growable Vec<u8> to a [u8; 160] + secrets_len in both initialize_alice_session and initialize_bob_session keeps the exact same concatenation order of:

  • 32 discontinuity bytes
  • Required DH agreements
  • Optional one-time prekey agreement

and passes only the populated prefix into derive_keys. This removes heap allocations on the handshake path without changing HKDF input, so root/chain key derivation should remain binary-compatible.

Please re-run your existing cross-implementation/interop tests (e.g., against a reference libsignal implementation) to confirm that sessions established before/after this change derive identical root and chain keys for the same inputs.

Also applies to: 104-142

wacore/libsignal/src/protocol/group_cipher.rs (2)

38-41: Sender chain stepping and SKDM chain_key handling look correct

  • Using step_with_message_key() in both group_encrypt and get_sender_key correctly combines “derive message key + advance chain” and matches the previous behavior (intermediate iterations cached via add_sender_message_key, final iteration returned, state advanced once).
  • Forward-jump checks and duplicate handling remain untouched and correct.
  • create_sender_key_distribution_message now passes *sender_chain_key.seed() (a [u8; 32] copy), which matches the updated SKDM constructor and removes a Vec allocation.

Overall, the sender-key side of the group cipher maintains its semantics while tightening types and allocations.

Also applies to: 69-103, 111-153, 321-326


207-237: Decryption buffer reuse mirrors encryption path without changing error semantics

The new DECRYPTION_BUFFER thread-local reuses the same EncryptionBuffer wrapper for plaintext, avoiding per-call heap allocations. Error handling still cleanly distinguishes:

  • BadKeyOrIvInvalidSenderKeySession with detailed logging, and
  • BadCiphertextInvalidMessage(CiphertextMessageType::SenderKey, "decryption failed").

The mem::take + reserve(INITIAL_CAPACITY) pattern restores buffer capacity for subsequent calls and is safe given the non-reentrant, synchronous use of the thread-local.

wacore/src/handshake/state.rs (1)

59-61: Formatting improvements look good.

The single-line calls to mix_shared_secret are cleaner and the serialization approach aligns with the fixed-size array refactor in curve.rs.

Also applies to: 72-77, 93-95

wacore/libsignal/src/protocol/session_cipher.rs (3)

99-111: Buffer restoration after std::mem::take() is fragile.

If an error occurs between std::mem::take(buf) (line 107) and buf.reserve() (line 109), the buffer capacity isn't restored. However, since reserve is called unconditionally after take, and the Ok wrapping happens after, this is correct. The current flow is: take → reserve → return result.

One minor observation: std::mem::take followed by reserve works, but consider using std::mem::replace with a pre-allocated Vec to avoid the zero-capacity intermediate state, though this is optional.


81-82: Two-step key generation pattern is correctly implemented.

The new step_with_message_keys() pattern correctly separates the chain advancement from key generation, reducing redundant HMAC operations.


821-831: Chain key iteration refactored correctly.

The dereference *chain_key works because ChainKey implements Copy. The loop correctly advances the chain while storing skipped message keys, and the final step returns the message key generator for the target counter.

wacore/libsignal/src/protocol/sender_keys.rs (3)

125-151: Combined key derivation is correctly implemented.

The step_with_message_key method correctly:

  1. Validates iteration overflow before proceeding
  2. Reuses the HMAC context via finalize_reset() to derive both keys with the same chain key
  3. Uses self.iteration for the message key (current) and new_iteration for the next chain key

The tests at lines 762-820 confirm equivalence with the separate sender_message_key() and next() calls.


82-86: Adding Copy to SenderChainKey is appropriate.

The struct contains only Copy types (u32 and [u8; 32]), and at 36 bytes total, it's reasonable to copy by value rather than requiring explicit clones.


103-105: Return type refinement to &[u8; 32] provides compile-time size guarantees.

wacore/libsignal/src/protocol/protocol.rs (3)

518-518: Fixed-size chain_key type enforces correctness at compile time.

Changing from Vec<u8> to [u8; 32] provides stronger type guarantees. The parsing at lines 628-634 properly validates the length and converts the Vec.

Also applies to: 528-528, 569-571


87-87: Serialization adapts correctly to fixed-size array returns.

Using to_vec() on the fixed-size arrays from serialize() is the correct approach for populating protobuf Vec<u8> fields.

Also applies to: 262-263


403-417: No signing semantic change—this is a performance optimization only.

The current implementation signs [version_byte || proto] exactly as the previous version did. Commit 5f11658 explicitly created a separate data_to_sign buffer with version + proto, then signed it. The current code (commit 8d20e01) achieves the same by encoding the proto directly into the final serialized buffer and signing that. Both approaches sign identical data, and the previous implementation's comment confirms this format is correct for interoperability with other clients (baileys, libsignal-go). The verify_signature method correctly extracts and verifies &self.serialized[..len - SIGNATURE_LEN], which is the signed portion. The change eliminates intermediate allocations without altering the signing semantics.

wacore/libsignal/src/core/curve.rs (4)

108-116: Stack-allocated fixed-size array eliminates heap allocation.

Returning [u8; 33] instead of a boxed slice removes a heap allocation for each serialization. The implementation correctly places the type byte at index 0 and copies the 32-byte key data to the remaining positions.


229-233: Zero-copy serialization by returning reference to internal array.

Returning &[u8; 32] directly references the internal key storage, avoiding any copying. Callers needing an owned copy can dereference or call to_vec().


251-279: Fixed-size return types for signatures and key agreements.

The return type changes to [u8; 64] for signatures and [u8; 32] for key agreements match the cryptographic output sizes (Ed25519 signatures are 64 bytes, X25519 shared secrets are 32 bytes). This aligns with the underlying curve25519::PrivateKey implementation.


332-342: KeyPair wrapper methods correctly propagate fixed-size return types.

Comment thread wacore/libsignal/src/crypto/aes_cbc.rs Outdated
@jlucaso1
jlucaso1 force-pushed the feat-optimize-libsignal branch from 8d20e01 to ca8b226 Compare January 8, 2026 04:36

@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: 0

🧹 Nitpick comments (5)
src/store/signal.rs (2)

335-353: Similar serialize/deserialize round-trip - verify necessity.

Line 342 performs the same pattern: PrivateKey::deserialize(self.signed_pre_key.private_key.serialize()). This creates an unnecessary copy by serializing to bytes and immediately deserializing back to a PrivateKey.

For a PR focused on reducing allocations, consider whether self.signed_pre_key.private_key can be used more directly to construct the needed KeyPair.


194-197: Verify the serialize/deserialize round-trip is necessary.

The code calls serialize() on private_key_bytes and immediately deserializes it back to PrivateKey. This round-trip seems counterintuitive for a performance optimization PR focused on reducing allocations.

If self.identity_key.private_key is already a PrivateKey type compatible with the protocol, consider using it directly or finding a more efficient conversion path that avoids the serialization overhead.

#!/bin/bash
# Verify the type of self.identity_key.private_key and whether direct usage is possible
ast-grep --pattern $'struct Device {
  $$$
  identity_key: $TYPE,
  $$$
}'

# Also check what identity_key's private_key field type is
rg -n "identity_key.*private_key" --type rust -C 3
wacore/libsignal/src/protocol/ratchet.rs (2)

49-82: Fixed-size secrets buffer in Alice init is bounded and equivalent to previous Vec logic

The 160-byte stack buffer with secrets_len tracking correctly holds the discontinuity bytes plus up to four 32-byte DH outputs; derive_keys(&secrets[..secrets_len]) preserves the previous HKDF input ordering with fewer allocations. Consider adding a debug assertion around each write (debug_assert!(secrets_len + 32 <= secrets.len())) to future‑proof this if additional agreements are ever appended.


104-142: Bob init’s secrets accumulation mirrors Alice and remains within bounds

The Bob side uses the same 160-byte buffer pattern; with 1 discontinuity + 3 mandatory DHs + optional one-time prekey, secrets_len cannot exceed 160, and HKDF input semantics remain unchanged. A small defensive improvement would be the same debug_assert!(secrets_len + 32 <= secrets.len()) before each copy to guard against future changes adding more agreements.

wacore/libsignal/src/protocol/protocol.rs (1)

559-576: Simplify getters: Result wrapper is now unnecessary.

All getter methods (chain_id(), iteration(), chain_key(), signing_key()) return Result<T> but are now infallible since the data is validated during construction in TryFrom. These could be simplified to return the values directly.

♻️ Proposed simplification
 #[inline]
-pub fn chain_id(&self) -> Result<u32> {
-    Ok(self.chain_id)
+pub fn chain_id(&self) -> u32 {
+    self.chain_id
 }

 #[inline]
-pub fn iteration(&self) -> Result<u32> {
-    Ok(self.iteration)
+pub fn iteration(&self) -> u32 {
+    self.iteration
 }

 #[inline]
-pub fn chain_key(&self) -> Result<&[u8; 32]> {
-    Ok(&self.chain_key)
+pub fn chain_key(&self) -> &[u8; 32] {
+    &self.chain_key
 }

 #[inline]
-pub fn signing_key(&self) -> Result<&PublicKey> {
-    Ok(&self.signing_key)
+pub fn signing_key(&self) -> &PublicKey {
+    &self.signing_key
 }

Note: This is a breaking API change, so apply only if acceptable for your use case.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8d20e01 and ca8b226.

📒 Files selected for processing (17)
  • src/message.rs
  • src/store/signal.rs
  • storages/sqlite-storage/src/sqlite_store.rs
  • wacore/libsignal/src/core/curve.rs
  • wacore/libsignal/src/protocol/group_cipher.rs
  • wacore/libsignal/src/protocol/identity_key.rs
  • wacore/libsignal/src/protocol/protocol.rs
  • wacore/libsignal/src/protocol/ratchet.rs
  • wacore/libsignal/src/protocol/ratchet/keys.rs
  • wacore/libsignal/src/protocol/sender_keys.rs
  • wacore/libsignal/src/protocol/session_cipher.rs
  • wacore/libsignal/src/protocol/state/session.rs
  • wacore/libsignal/src/protocol/state/signed_prekey.rs
  • wacore/libsignal/src/store/record_helpers.rs
  • wacore/src/handshake/state.rs
  • wacore/src/send.rs
  • wacore/src/store/device.rs
🚧 Files skipped from review as they are similar to previous changes (4)
  • wacore/src/send.rs
  • wacore/src/store/device.rs
  • wacore/libsignal/src/core/curve.rs
  • storages/sqlite-storage/src/sqlite_store.rs
🧰 Additional context used
📓 Path-based instructions (2)
**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

**/*.rs: Never modify Device state directly; use DeviceCommand + PersistenceManager::process_command() for state changes
For read-only Device state access, use PersistenceManager::get_device_snapshot()
All blocking I/O (such as ureq calls) and heavy CPU-bound tasks (like media encryption) MUST be wrapped in tokio::task::spawn_blocking to avoid stalling the async runtime
Use Client::chat_locks to serialize per-chat operations in asynchronous code
Use thiserror for custom domain-specific errors (e.g., SocketError) and anyhow::Error for functions with multiple failure modes
Avoid .unwrap() and .expect() outside of tests and unrecoverable logic paths
Use the Downloadable trait in wacore/src/download.rs for implementing generic media download interfaces across message types
Always refresh MediaConn if it's expired before using it to get current media servers and auth tokens
Run cargo fmt before finalizing a feature or fix
Run cargo clippy --all-targets before finalizing a feature or fix
Run cargo test --all before finalizing a feature or fix

Files:

  • src/store/signal.rs
  • wacore/libsignal/src/protocol/state/session.rs
  • wacore/libsignal/src/protocol/ratchet.rs
  • wacore/libsignal/src/protocol/session_cipher.rs
  • wacore/libsignal/src/protocol/group_cipher.rs
  • wacore/libsignal/src/protocol/state/signed_prekey.rs
  • src/message.rs
  • wacore/libsignal/src/protocol/identity_key.rs
  • wacore/libsignal/src/protocol/sender_keys.rs
  • wacore/src/handshake/state.rs
  • wacore/libsignal/src/store/record_helpers.rs
  • wacore/libsignal/src/protocol/ratchet/keys.rs
  • wacore/libsignal/src/protocol/protocol.rs
wacore/**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

wacore crate must remain platform-agnostic and have no dependencies on Tokio or specific databases

Files:

  • wacore/libsignal/src/protocol/state/session.rs
  • wacore/libsignal/src/protocol/ratchet.rs
  • wacore/libsignal/src/protocol/session_cipher.rs
  • wacore/libsignal/src/protocol/group_cipher.rs
  • wacore/libsignal/src/protocol/state/signed_prekey.rs
  • wacore/libsignal/src/protocol/identity_key.rs
  • wacore/libsignal/src/protocol/sender_keys.rs
  • wacore/src/handshake/state.rs
  • wacore/libsignal/src/store/record_helpers.rs
  • wacore/libsignal/src/protocol/ratchet/keys.rs
  • wacore/libsignal/src/protocol/protocol.rs
🧠 Learnings (1)
📚 Learning: 2025-12-25T03:36:19.700Z
Learnt from: CR
Repo: jlucaso1/whatsapp-rust PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-25T03:36:19.700Z
Learning: Applies to waproto/build.rs : `waproto` crate uses prost to compile Protocol Buffers definitions from `whatsapp.proto` in a `build.rs` script

Applied to files:

  • wacore/libsignal/src/protocol/protocol.rs
🧬 Code graph analysis (7)
src/store/signal.rs (4)
wacore/libsignal/src/protocol/identity_key.rs (1)
  • private_key (121-123)
wacore/libsignal/src/protocol/sender_keys.rs (1)
  • deserialize (295-304)
wacore/src/store/device.rs (1)
  • deserialize (30-43)
wacore/libsignal/src/core/curve/curve25519.rs (1)
  • private_key_bytes (155-157)
wacore/libsignal/src/protocol/ratchet.rs (4)
wacore/libsignal/src/protocol/state/session.rs (1)
  • root_key (159-165)
wacore/libsignal/src/protocol/protocol.rs (1)
  • chain_key (569-571)
wacore/libsignal/src/protocol/ratchet/keys.rs (1)
  • derive_keys (76-92)
wacore/libsignal/src/protocol/ratchet/params.rs (1)
  • our_one_time_pre_key_pair (145-147)
wacore/libsignal/src/protocol/session_cipher.rs (3)
wacore/libsignal/src/protocol/ratchet/keys.rs (2)
  • next_chain_key (139-144)
  • message_keys (146-151)
wacore/libsignal/src/protocol/protocol.rs (1)
  • chain_key (569-571)
wacore/libsignal/src/protocol/state/session.rs (1)
  • session_state (635-637)
wacore/libsignal/src/protocol/group_cipher.rs (3)
wacore/libsignal/src/protocol/protocol.rs (4)
  • new (76-113)
  • new (249-281)
  • new (389-426)
  • new (524-551)
wacore/libsignal/src/protocol/sender_keys.rs (4)
  • new (39-52)
  • new (92-97)
  • new (173-193)
  • sender_chain_key (207-219)
wacore/libsignal/src/crypto/aes_cbc.rs (1)
  • aes_256_cbc_decrypt_into (66-93)
wacore/libsignal/src/protocol/identity_key.rs (1)
wacore/libsignal/src/core/curve.rs (2)
  • serialize (109-116)
  • serialize (229-233)
wacore/libsignal/src/store/record_helpers.rs (2)
wacore/libsignal/src/protocol/state/signed_prekey.rs (1)
  • key_pair (129-140)
wacore/libsignal/src/protocol/state/prekey.rs (1)
  • key_pair (59-70)
wacore/libsignal/src/protocol/protocol.rs (2)
wacore/libsignal/src/protocol/ratchet.rs (1)
  • message_version (22-24)
wacore/libsignal/src/protocol/sender_keys.rs (4)
  • message_version (199-201)
  • chain_id (203-205)
  • iteration (62-64)
  • iteration (99-101)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: benchmark
  • GitHub Check: Build & Test
🔇 Additional comments (25)
wacore/src/handshake/state.rs (3)

73-77: Consistent refactoring applied.

Same allocation optimization pattern applied here for ephemeral key serialization.


93-95: Consistent refactoring applied.

Same allocation optimization pattern applied here for static key serialization.


60-61: Good refactoring to reduce allocations.

The direct passing of serialize() to mix_shared_secret confirms the method now returns &[u8; 32] instead of an owned vector, eliminating intermediate storage and heap allocation.

wacore/libsignal/src/protocol/identity_key.rs (2)

43-47: LGTM! Excellent optimization.

Changing from Box<[u8]> to [u8; 33] eliminates a heap allocation for a fixed-size cryptographic key. This is a clear performance win with no downsides, as the identity key serialization is always 33 bytes (1 type byte + 32 key bytes).


137-150: LGTM! Signature output now uses fixed-size array.

Changing the return type from Box<[u8]> to [u8; 64] is appropriate since Ed25519 signatures are always 64 bytes. This avoids unnecessary heap allocation.

wacore/libsignal/src/protocol/state/session.rs (1)

84-100: LGTM! Correctly adapted to fixed-size array serialization.

The change from .into_vec() to .to_vec() is necessary because the upstream serialize() methods now return fixed-size arrays [u8; 33] instead of Box<[u8]>.

The .to_vec() call copies the fixed-size array into a Vec<u8> for protobuf storage. While this introduces a copy, it's negligible (33 bytes) and the overall change is still a performance win by eliminating heap allocations in the serialization layer.

wacore/libsignal/src/protocol/state/signed_prekey.rs (1)

170-178: LGTM! Correctly adapts to upstream serialization changes.

The KeySerde trait requires serialize() to return Vec<u8>, so calling .to_vec() on the fixed-size array returned by PrivateKey::serialize() is the appropriate way to satisfy the trait contract while preserving the optimization benefits upstream.

wacore/libsignal/src/store/record_helpers.rs (1)

8-67: LGTM! Consistent adaptation for protobuf storage.

All four occurrences (lines 12, 25, 65, and the pattern throughout) correctly use .serialize().to_vec() to convert the fixed-size array returned by PrivateKey::serialize() into a Vec<u8> required by the protobuf structures.

The copying overhead is minimal (32 bytes per key) and the overall optimization from eliminating heap allocations in the serialization layer provides a net performance benefit.

src/message.rs (1)

1238-1248: Chain key length validation for Go SKDM is correct and safer

Converting chain_key to [u8; 32] with an explicit length check and early return cleanly enforces the invariant expected by SenderKeyDistributionMessage::new and avoids ever constructing SKDMs with malformed key material. Logging only the length and sender JID is appropriate here.

wacore/libsignal/src/protocol/session_cipher.rs (2)

81-83: Sender chain advancement during encryption stays semantically identical

Using step_with_message_keys() to derive the per-message MessageKeys and the next_chain_key, then persisting next_chain_key with set_sender_chain_key, preserves the old “message uses current index, chain advances by one” behavior while avoiding a redundant derivation. No issues spotted.

Also applies to: 157-157


821-832: Receiver chain progression and message key caching are consistent with Double Ratchet

Copying the input chain_key, then looping with step_with_message_keys() to cache intermediate message keys and finally updating the receiver chain key with the last next_chain, matches the standard get-or-create pattern from libsignal (final key for counter, chain advanced to counter+1). This keeps duplicate detection and forward‑jump limits intact while reducing redundant HMACs.

wacore/libsignal/src/protocol/group_cipher.rs (3)

38-41: Thread-local decryption buffer mirrors encryption buffer and avoids extra allocations

Introducing DECRYPTION_BUFFER and decrypting into a reusable Vec<u8> (with take() + reserve() reset) matches the existing encryption buffer pattern and removes per-call allocations in group_decrypt. Error mapping for BadKeyOrIv vs BadCiphertext is preserved; the behavior on corrupt sender key state vs bad ciphertext remains identical.

Also applies to: 207-237


73-73: Sender chain/message key progression in groups correctly uses step_with_message_key

Both group_encrypt and get_sender_key now use step_with_message_key() to atomically derive the SenderMessageKey and advance the SenderChainKey, storing intermediate message keys and updating the chain only at the end. This matches prior semantics (iteration and stored keys) while avoiding duplicate HMACs.

Also applies to: 102-103, 145-153


321-326: Passing SKDM chain_key as a fixed array is aligned with the new API

Using *sender_chain_key.seed() to supply a [u8; 32] to SenderKeyDistributionMessage::new removes an unnecessary allocation and matches the updated SKDM constructor and chain_key() accessor signatures. No behavioral change here, just tighter typing.

wacore/libsignal/src/protocol/sender_keys.rs (2)

82-106: SenderChainKey Copy + step_with_message_key API look correct and consistent

Making SenderChainKey Copy and changing seed() to return &[u8; 32] tighten the type contracts without changing behavior. The new step_with_message_key() correctly guards against iteration overflow, derives the same message/chain seeds as the previous sender_message_key() + next() sequence, and provides a more efficient single-step primitive for callers.

Also applies to: 125-151


563-569: Tests adequately verify step_with_message_key equivalence and chain consistency

The new tests exercise both single-step and multi-step usage of step_with_message_key(), comparing message keys and chain seeds against the existing sender_message_key() / next() sequence over multiple iterations, which strongly validates the new API. Using Copy semantics in set_sender_chain_key(next_sck) in tests is idiomatic and matches the updated type.

Also applies to: 759-820

wacore/libsignal/src/protocol/ratchet/keys.rs (5)

10-11: LGTM: Fixed-size seed reduces allocations.

The change from Vec<u8> to [u8; 32] for the seed eliminates heap allocations and enforces compile-time size validation. The addition of HMAC imports supports the new optimized derivation method.

Also applies to: 17-17


22-23: LGTM: Stricter signature enforces correct seed size.

Tightening the signature from &[u8] to &[u8; 32] enforces the seed size at compile time and eliminates the need for runtime length checks. The copy operation is efficient for a 32-byte array.


67-67: LGTM: Copy trait enables efficient pass-by-value.

Both MessageKeys and ChainKey are small structs containing only fixed-size arrays and integers, making them ideal candidates for Copy. This enables efficient pass-by-value semantics and aligns with the allocation-reduction goal.

Also applies to: 115-115


153-172: LGTM: Combined derivation reduces HMAC overhead.

The step_with_message_keys method is a well-designed optimization that reuses a single HMAC key setup to derive both the message key seed and next chain key. Using finalize_reset() allows the HMAC instance to be reused efficiently. The implementation correctly applies the different seeds (MESSAGE_KEY_SEED and CHAIN_KEY_SEED) to produce distinct outputs.


368-434: LGTM: Comprehensive test coverage for the optimization.

The tests thoroughly verify that step_with_message_keys produces identical results to calling message_keys() and next_chain_key() separately, both for a single step and over multiple iterations. This ensures the optimization maintains correctness.

wacore/libsignal/src/protocol/protocol.rs (4)

87-87: LGTM: Serialization updated for fixed-size array returns.

The addition of .to_vec() calls indicates that PublicKey::serialize() and IdentityKey::serialize() now return fixed-size array references instead of Vec<u8>. This change aligns with the allocation-reduction goal while maintaining protobuf compatibility.

Also applies to: 262-263


400-425: LGTM: Direct buffer construction eliminates intermediate allocation.

The refactored serialization builds the final buffer directly (version byte + proto + signature) instead of encoding the proto separately and then combining. This reduces allocations and is more efficient. The signing correctly covers the version byte and proto data before appending the signature.


518-518: LGTM: Fixed-size chain_key reduces allocations.

Changing chain_key from Vec<u8> to [u8; 32] eliminates heap allocation and enforces size validation. Using .to_vec() when building the protobuf message is necessary for compatibility with the protobuf encoding.

Also applies to: 528-528, 534-534


621-634: LGTM: Length validation ensures correct conversion.

The TryFrom implementation correctly validates that chain_key_vec has exactly 32 bytes before converting it to [u8; 32]. The length check on line 628 provides an early validation, and the try_into() conversion on lines 632-634 handles any edge cases with proper error propagation.

@jlucaso1
jlucaso1 force-pushed the feat-optimize-libsignal branch from 8dddd34 to 9b6fa0a Compare January 8, 2026 05:03

@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: 0

🧹 Nitpick comments (2)
wacore/libsignal/src/protocol/ratchet/keys.rs (1)

42-62: Silent fallback to zero-filled keys on malformed protobuf may mask data corruption.

When parsing a Serialized variant with invalid field lengths (e.g., truncated cipher_key), generate_keys() silently falls back to [0u8; 32] or [0u8; 16]. Since from_pb() validates lengths before creating Serialized, this branch should only trigger if internal invariants are violated. However, returning zero-filled keys could lead to silent decryption failures rather than surfacing the corruption.

Consider whether panicking or returning a Result would be safer here, given the security-sensitive context.

♻️ Alternative: propagate error instead of defaulting
-            Self::Serialized(pb) => {
-                // Parse on demand - only when keys are actually needed
-                MessageKeys {
-                    cipher_key: pb
-                        .cipher_key
-                        .as_deref()
-                        .and_then(|b| b.try_into().ok())
-                        .unwrap_or([0u8; 32]),
-                    mac_key: pb
-                        .mac_key
-                        .as_deref()
-                        .and_then(|b| b.try_into().ok())
-                        .unwrap_or([0u8; 32]),
-                    iv: pb
-                        .iv
-                        .as_deref()
-                        .and_then(|b| b.try_into().ok())
-                        .unwrap_or([0u8; 16]),
-                    counter: pb.index.unwrap_or(0),
-                }
-            }
+            Self::Serialized(pb) => {
+                // Parse on demand - only when keys are actually needed
+                // from_pb() validates lengths, so these should never fail
+                MessageKeys {
+                    cipher_key: pb
+                        .cipher_key
+                        .as_deref()
+                        .and_then(|b| b.try_into().ok())
+                        .expect("from_pb validated cipher_key length"),
+                    mac_key: pb
+                        .mac_key
+                        .as_deref()
+                        .and_then(|b| b.try_into().ok())
+                        .expect("from_pb validated mac_key length"),
+                    iv: pb
+                        .iv
+                        .as_deref()
+                        .and_then(|b| b.try_into().ok())
+                        .expect("from_pb validated iv length"),
+                    counter: pb.index.unwrap_or(0),
+                }
+            }
wacore/libsignal/src/protocol/group_cipher.rs (1)

40-40: Consider renaming EncryptionBuffer to a more generic name.

The EncryptionBuffer type is now reused for both encryption and decryption via DECRYPTION_BUFFER. Consider renaming it to something like ReusableBuffer or CryptoBuffer to better reflect its dual purpose.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ca8b226 and 8dddd34.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (10)
  • Cargo.toml
  • src/store/signal.rs
  • wacore/libsignal/src/protocol/group_cipher.rs
  • wacore/libsignal/src/protocol/protocol.rs
  • wacore/libsignal/src/protocol/ratchet.rs
  • wacore/libsignal/src/protocol/ratchet/keys.rs
  • wacore/libsignal/src/protocol/sender_keys.rs
  • wacore/libsignal/src/protocol/state/session.rs
  • waproto/build.rs
  • waproto/src/whatsapp.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/store/signal.rs
🧰 Additional context used
📓 Path-based instructions (3)
**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

**/*.rs: Never modify Device state directly; use DeviceCommand + PersistenceManager::process_command() for state changes
For read-only Device state access, use PersistenceManager::get_device_snapshot()
All blocking I/O (such as ureq calls) and heavy CPU-bound tasks (like media encryption) MUST be wrapped in tokio::task::spawn_blocking to avoid stalling the async runtime
Use Client::chat_locks to serialize per-chat operations in asynchronous code
Use thiserror for custom domain-specific errors (e.g., SocketError) and anyhow::Error for functions with multiple failure modes
Avoid .unwrap() and .expect() outside of tests and unrecoverable logic paths
Use the Downloadable trait in wacore/src/download.rs for implementing generic media download interfaces across message types
Always refresh MediaConn if it's expired before using it to get current media servers and auth tokens
Run cargo fmt before finalizing a feature or fix
Run cargo clippy --all-targets before finalizing a feature or fix
Run cargo test --all before finalizing a feature or fix

Files:

  • wacore/libsignal/src/protocol/state/session.rs
  • wacore/libsignal/src/protocol/sender_keys.rs
  • waproto/build.rs
  • wacore/libsignal/src/protocol/protocol.rs
  • wacore/libsignal/src/protocol/ratchet/keys.rs
  • wacore/libsignal/src/protocol/ratchet.rs
  • wacore/libsignal/src/protocol/group_cipher.rs
  • waproto/src/whatsapp.rs
wacore/**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

wacore crate must remain platform-agnostic and have no dependencies on Tokio or specific databases

Files:

  • wacore/libsignal/src/protocol/state/session.rs
  • wacore/libsignal/src/protocol/sender_keys.rs
  • wacore/libsignal/src/protocol/protocol.rs
  • wacore/libsignal/src/protocol/ratchet/keys.rs
  • wacore/libsignal/src/protocol/ratchet.rs
  • wacore/libsignal/src/protocol/group_cipher.rs
waproto/build.rs

📄 CodeRabbit inference engine (AGENTS.md)

waproto crate uses prost to compile Protocol Buffers definitions from whatsapp.proto in a build.rs script

Files:

  • waproto/build.rs
🧠 Learnings (3)
📚 Learning: 2025-12-25T03:36:19.700Z
Learnt from: CR
Repo: jlucaso1/whatsapp-rust PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-25T03:36:19.700Z
Learning: Applies to waproto/build.rs : `waproto` crate uses prost to compile Protocol Buffers definitions from `whatsapp.proto` in a `build.rs` script

Applied to files:

  • waproto/build.rs
  • wacore/libsignal/src/protocol/protocol.rs
  • waproto/src/whatsapp.rs
📚 Learning: 2025-12-25T03:36:19.700Z
Learnt from: CR
Repo: jlucaso1/whatsapp-rust PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-25T03:36:19.700Z
Learning: The project is organized into three crates: `wacore` (platform-agnostic, `no_std` core logic with no Tokio/database dependencies), `waproto` (Protocol Buffers definitions compiled with prost), and `whatsapp-rust` (main client with Tokio/Diesel integration)

Applied to files:

  • waproto/build.rs
  • waproto/src/whatsapp.rs
📚 Learning: 2025-12-25T03:36:19.700Z
Learnt from: CR
Repo: jlucaso1/whatsapp-rust PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-25T03:36:19.700Z
Learning: Applies to wacore/**/*.rs : `wacore` crate must remain platform-agnostic and have no dependencies on Tokio or specific databases

Applied to files:

  • Cargo.toml
🧬 Code graph analysis (5)
wacore/libsignal/src/protocol/sender_keys.rs (3)
wacore/libsignal/src/protocol/ratchet/keys.rs (4)
  • next_chain_key (185-190)
  • new (171-173)
  • new (231-233)
  • iv (151-153)
wacore/libsignal/src/protocol/protocol.rs (5)
  • new (76-113)
  • new (249-281)
  • new (389-426)
  • new (524-551)
  • chain_key (569-571)
wacore/libsignal/src/protocol/crypto.rs (1)
  • hmac_sha256 (9-14)
wacore/libsignal/src/protocol/protocol.rs (3)
wacore/libsignal/src/protocol/identity_key.rs (1)
  • identity_key (109-111)
wacore/libsignal/src/protocol/ratchet.rs (1)
  • message_version (22-24)
wacore/libsignal/src/protocol/sender_keys.rs (4)
  • message_version (204-206)
  • chain_id (208-210)
  • iteration (63-65)
  • iteration (101-103)
wacore/libsignal/src/protocol/ratchet.rs (2)
wacore/libsignal/src/protocol/ratchet/keys.rs (1)
  • derive_keys (122-138)
wacore/libsignal/src/protocol/ratchet/params.rs (1)
  • our_one_time_pre_key_pair (145-147)
wacore/libsignal/src/protocol/group_cipher.rs (2)
wacore/libsignal/src/protocol/sender_keys.rs (6)
  • new (39-52)
  • new (94-99)
  • new (176-198)
  • sender_chain_key (212-224)
  • sender_key_state (311-316)
  • chain_id (208-210)
wacore/libsignal/src/crypto/aes_cbc.rs (1)
  • aes_256_cbc_decrypt_into (66-93)
waproto/src/whatsapp.rs (1)
wacore/binary/src/builder.rs (1)
  • bytes (40-43)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Build & Test
  • GitHub Check: benchmark
🔇 Additional comments (22)
Cargo.toml (1)

33-33: LGTM!

Adding the serde feature to bytes aligns with the broader refactor using prost::bytes::Bytes for cryptographic structures. This enables proper serde support for Bytes types across the codebase.

wacore/libsignal/src/protocol/ratchet.rs (2)

49-85: Effective optimization replacing Vec with stack-allocated buffer.

The fixed-size [u8; 160] buffer correctly accommodates the maximum of 5 × 32-byte secrets (discontinuity bytes + up to 4 DH agreements). The debug_assert! statements provide safety checks during development.


108-149: Consistent implementation with Alice session.

The Bob session initialization mirrors the Alice session buffer approach correctly, maintaining protocol symmetry while eliminating heap allocations.

wacore/libsignal/src/protocol/sender_keys.rs (3)

84-84: Good addition of Copy derive.

SenderChainKey contains only u32 and [u8; 32], both of which are Copy. This allows more efficient passing without clone overhead.


127-153: Well-designed optimization reusing HMAC context.

The step_with_message_key method correctly leverages finalize_reset() to compute both the message key seed and next chain key with a single HMAC key setup. The semantics are correct: finalize_reset() returns the MAC and resets the HMAC to its initial keyed state, allowing the second derivation with the same base key.


763-825: Thorough test coverage for the combined-step optimization.

The equivalence test and multi-iteration chain test provide strong validation that step_with_message_key produces identical results to the separate method calls.

wacore/libsignal/src/protocol/ratchet/keys.rs (2)

199-218: Consistent HMAC reuse optimization.

The step_with_message_keys implementation mirrors the pattern in sender_keys.rs, correctly using finalize_reset() to derive both keys with a single HMAC setup. Returning MessageKeyGenerator enables lazy derivation when keys aren't immediately needed.


161-165: LGTM!

Adding Copy derive is appropriate since ChainKey contains only Copy types ([u8; 32] and u32).

waproto/build.rs (2)

35-46: Well-documented configuration for Bytes optimization.

The config.bytes() paths correctly target frequently-accessed cryptographic structures on hot paths. Using bytes::Bytes instead of Vec<u8> enables O(1) cloning which is valuable for session/chain keys accessed on every message.


48-83: Correct serde skip attributes for protobuf-only fields.

The #[serde(skip, default)] attributes are necessary since these nested cryptographic fields are stored as protobuf blobs rather than JSON-serialized. This prevents serde from attempting to serialize/deserialize Bytes fields that lack serde support in prost's default configuration.

wacore/libsignal/src/protocol/state/session.rs (2)

285-290: LGTM!

The switch to Bytes::copy_from_slice for chain key storage aligns with the prost configuration changes. The local use prost::bytes::Bytes import is repeated across methods, which is functional though a module-level import could reduce duplication.


87-96: Minor: to_vec() usage is fine here.

For fixed-size arrays like [u8; 33], to_vec() and into_vec() are semantically equivalent. The change is correct.

wacore/libsignal/src/protocol/protocol.rs (3)

621-634: Proper length validation before fixed-size conversion.

The validation at line 628 correctly checks both chain_key_vec.len() != 32 and signing_key.len() != 33 before attempting the conversion. The try_into().map_err() pattern handles the conversion safely.


403-424: Cleaner serialization flow with correct capacity pre-allocation.

The refactored buffer construction correctly pre-allocates 1 + proto_len + SIGNATURE_LEN bytes and builds the message in a single pass. The signature is computed over [version || proto] before appending, which matches the expected format.


518-518: Breaking API change: chain_key is now [u8; 32].

This is a positive change that enforces correct key size at compile time. All callers have been updated correctly—both wacore callers pass *chain_key.seed() / *sender_chain_key.seed(), and the src/message.rs fallback properly validates the conversion with try_into() before passing the result to the constructor.

wacore/libsignal/src/protocol/group_cipher.rs (5)

144-152: LGTM: Correct implementation of forward chain progression.

The loop properly accumulates intermediate message keys while advancing the chain, and the final step correctly derives the target message key while updating the state with the next chain key.


207-237: LGTM: Proper buffer reuse with comprehensive error handling.

The decryption buffer management correctly mirrors the encryption pattern. Error handling properly distinguishes between key/IV corruption and ciphertext issues, with appropriate logging and error mapping.


325-325: Change is correct: SenderKeyDistributionMessage::new() accepts [u8; 32] by value.

The modification from sender_chain_key.seed().to_vec() to *sender_chain_key.seed() is valid and improves performance. The function signature in protocol.rs explicitly accepts chain_key: [u8; 32], and dereferencing the seed reference provides the fixed-size array in the expected form, eliminating the unnecessary heap allocation.


265-268: Type consistency is correct. The signing_key() method returns &PublicKey and is properly dereferenced with * to match the PublicKey parameter type. The chain_key() method returns &[u8; 32], which automatically coerces to the expected &[u8] parameter type through Rust's deref coercion. No changes needed.


73-73: Tests for step_with_message_key() are comprehensive and properly cover chain key progression and message key derivation.

The method has two dedicated tests:

  1. test_step_with_message_key_equivalence() verifies the combined API produces identical results to separate calls, testing message key components (iteration, IV, cipher_key) and chain key components (seed, iteration).

  2. test_step_with_message_key_chain() validates progression over 10 iterations, ensuring both message keys and chain keys remain synchronized and don't drift.

These tests confirm the optimization is correct and the method handles chain advancement properly.

waproto/src/whatsapp.rs (2)

11026-11043: Serde skip configuration for cryptographic keys is properly applied.

The ChainKey.key and MessageKey fields (cipher_key, mac_key, iv) correctly use the serde(skip, default) pattern with bytes::Bytes. This configuration is intentional and well-justified in build.rs:

  • bytes::Bytes is used for O(1) cloning instead of O(n) copying on hot paths (message encryption/decryption)
  • serde(skip, default) is necessary because bytes::Bytes lacks Serialize/Deserialize implementations by design
  • The prost configuration explicitly applies these field attributes for all sensitive binary fields in both SessionStructure.Chain and SenderKeyStateStructure structures

The implementation is consistent and correct.


10945-10966: No issues found. The switch from Vec<u8> to prost::bytes::Bytes for cryptographic material is implemented correctly and does not introduce compatibility problems.

The code properly handles Bytes through the as_ref() pattern (e.g., seed_bytes.as_ref().try_into()), and the proto structs are never passed to builder.bytes(), which only receives Vec<u8> from the NodeContent enum. The #[serde(skip, default)] annotations are a security improvement—they prevent accidental serialization of sensitive private key material—and do not break any existing persistence workflows, as these proto structs are used internally by libsignal rather than in device state serialization.

@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

🧹 Nitpick comments (2)
wacore/libsignal/src/protocol/ratchet/keys.rs (1)

38-64: Consider panic or Result instead of silent zero fallback.

In the Serialized branch, unwrap_or with zero defaults (lines 49, 54, 59) could silently produce incorrect keys if the protobuf is somehow corrupted after validation. While from_pb() validates fields before creating Serialized variants, a defensive expect() with a clear error message would be safer than silent fallback to zeros.

💡 More defensive error handling
 Self::Serialized(pb) => {
     // Parse on demand - only when keys are actually needed
     MessageKeys {
         cipher_key: pb
             .cipher_key
             .as_deref()
             .and_then(|b| b.try_into().ok())
-            .unwrap_or([0u8; 32]),
+            .expect("Serialized variant should have valid cipher_key"),
         mac_key: pb
             .mac_key
             .as_deref()
             .and_then(|b| b.try_into().ok())
-            .unwrap_or([0u8; 32]),
+            .expect("Serialized variant should have valid mac_key"),
         iv: pb
             .iv
             .as_deref()
             .and_then(|b| b.try_into().ok())
-            .unwrap_or([0u8; 16]),
+            .expect("Serialized variant should have valid iv"),
         counter: pb.index.unwrap_or(0),
     }
 }
wacore/libsignal/src/protocol/group_cipher.rs (1)

40-40: Consider renaming EncryptionBuffer to a more generic name.

The EncryptionBuffer type is now used for both encryption (line 39) and decryption (line 40), which may cause confusion. Consider renaming it to something like CipherBuffer or CryptoBuffer to better reflect its dual purpose.

♻️ Refactor suggestion

Rename the struct and update both thread-local declarations:

-struct EncryptionBuffer {
+struct CipherBuffer {
     buffer: Vec<u8>,
 }

-impl EncryptionBuffer {
+impl CipherBuffer {
     const INITIAL_CAPACITY: usize = 1024;
 
     fn new() -> Self {

Then update the thread-local declarations:

 thread_local! {
-    static ENCRYPTION_BUFFER: RefCell<EncryptionBuffer> = RefCell::new(EncryptionBuffer::new());
-    static DECRYPTION_BUFFER: RefCell<EncryptionBuffer> = RefCell::new(EncryptionBuffer::new());
+    static ENCRYPTION_BUFFER: RefCell<CipherBuffer> = RefCell::new(CipherBuffer::new());
+    static DECRYPTION_BUFFER: RefCell<CipherBuffer> = RefCell::new(CipherBuffer::new());
 }
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8dddd34 and 9b6fa0a.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (10)
  • Cargo.toml
  • src/store/signal.rs
  • wacore/libsignal/src/protocol/group_cipher.rs
  • wacore/libsignal/src/protocol/protocol.rs
  • wacore/libsignal/src/protocol/ratchet.rs
  • wacore/libsignal/src/protocol/ratchet/keys.rs
  • wacore/libsignal/src/protocol/sender_keys.rs
  • wacore/libsignal/src/protocol/state/session.rs
  • waproto/build.rs
  • waproto/src/whatsapp.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • Cargo.toml
🧰 Additional context used
📓 Path-based instructions (3)
**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

**/*.rs: Never modify Device state directly; use DeviceCommand + PersistenceManager::process_command() for state changes
For read-only Device state access, use PersistenceManager::get_device_snapshot()
All blocking I/O (such as ureq calls) and heavy CPU-bound tasks (like media encryption) MUST be wrapped in tokio::task::spawn_blocking to avoid stalling the async runtime
Use Client::chat_locks to serialize per-chat operations in asynchronous code
Use thiserror for custom domain-specific errors (e.g., SocketError) and anyhow::Error for functions with multiple failure modes
Avoid .unwrap() and .expect() outside of tests and unrecoverable logic paths
Use the Downloadable trait in wacore/src/download.rs for implementing generic media download interfaces across message types
Always refresh MediaConn if it's expired before using it to get current media servers and auth tokens
Run cargo fmt before finalizing a feature or fix
Run cargo clippy --all-targets before finalizing a feature or fix
Run cargo test --all before finalizing a feature or fix

Files:

  • waproto/build.rs
  • src/store/signal.rs
  • wacore/libsignal/src/protocol/ratchet.rs
  • wacore/libsignal/src/protocol/ratchet/keys.rs
  • wacore/libsignal/src/protocol/state/session.rs
  • wacore/libsignal/src/protocol/protocol.rs
  • waproto/src/whatsapp.rs
  • wacore/libsignal/src/protocol/group_cipher.rs
  • wacore/libsignal/src/protocol/sender_keys.rs
waproto/build.rs

📄 CodeRabbit inference engine (AGENTS.md)

waproto crate uses prost to compile Protocol Buffers definitions from whatsapp.proto in a build.rs script

Files:

  • waproto/build.rs
wacore/**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

wacore crate must remain platform-agnostic and have no dependencies on Tokio or specific databases

Files:

  • wacore/libsignal/src/protocol/ratchet.rs
  • wacore/libsignal/src/protocol/ratchet/keys.rs
  • wacore/libsignal/src/protocol/state/session.rs
  • wacore/libsignal/src/protocol/protocol.rs
  • wacore/libsignal/src/protocol/group_cipher.rs
  • wacore/libsignal/src/protocol/sender_keys.rs
🧠 Learnings (2)
📚 Learning: 2025-12-25T03:36:19.700Z
Learnt from: CR
Repo: jlucaso1/whatsapp-rust PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-25T03:36:19.700Z
Learning: Applies to waproto/build.rs : `waproto` crate uses prost to compile Protocol Buffers definitions from `whatsapp.proto` in a `build.rs` script

Applied to files:

  • waproto/build.rs
  • wacore/libsignal/src/protocol/protocol.rs
  • waproto/src/whatsapp.rs
  • wacore/libsignal/src/protocol/sender_keys.rs
📚 Learning: 2025-12-25T03:36:19.700Z
Learnt from: CR
Repo: jlucaso1/whatsapp-rust PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-25T03:36:19.700Z
Learning: The project is organized into three crates: `wacore` (platform-agnostic, `no_std` core logic with no Tokio/database dependencies), `waproto` (Protocol Buffers definitions compiled with prost), and `whatsapp-rust` (main client with Tokio/Diesel integration)

Applied to files:

  • waproto/build.rs
  • waproto/src/whatsapp.rs
🧬 Code graph analysis (4)
wacore/libsignal/src/protocol/ratchet.rs (4)
wacore/libsignal/src/protocol/state/session.rs (1)
  • root_key (159-165)
wacore/libsignal/src/protocol/protocol.rs (1)
  • chain_key (569-571)
wacore/libsignal/src/protocol/ratchet/keys.rs (1)
  • derive_keys (122-138)
wacore/libsignal/src/protocol/ratchet/params.rs (1)
  • our_one_time_pre_key_pair (145-147)
wacore/libsignal/src/protocol/state/session.rs (2)
wacore/libsignal/src/protocol/protocol.rs (1)
  • chain_key (569-571)
wacore/libsignal/src/protocol/ratchet/keys.rs (1)
  • next_chain_key (185-190)
waproto/src/whatsapp.rs (1)
wacore/binary/src/builder.rs (1)
  • bytes (40-43)
wacore/libsignal/src/protocol/group_cipher.rs (2)
wacore/libsignal/src/protocol/sender_keys.rs (6)
  • new (39-52)
  • new (94-99)
  • new (176-198)
  • sender_chain_key (212-224)
  • sender_key_state (311-316)
  • chain_id (208-210)
wacore/libsignal/src/crypto/aes_cbc.rs (1)
  • aes_256_cbc_decrypt_into (66-93)
🔇 Additional comments (30)
src/store/signal.rs (3)

207-216: No changes needed—the code is correct as written.

The original assumption that public_key_bytes() returns [u8; 33] is incorrect. It actually returns &[u8] (a slice reference), as defined in wacore/libsignal/src/core/curve.rs:93. The try_into() conversion on line 213 is therefore necessary to convert from a dynamically-sized slice to the fixed-size array [u8; 32] expected by put_identity(). The error handling for "Invalid key length" is not dead code—it correctly triggers when the key is not exactly 32 bytes.

Likely an incorrect or invalid review comment.


193-195: Type compatibility verified — conversion is correct.

The direct conversion using .into() is type-safe. Device.identity_key is of type KeyPair, and From<KeyPair> for IdentityKeyPair is properly implemented in the Signal protocol library, making the conversion reliable.


333-339: The code is correct—no changes needed.

The function signature and field types are fully compatible. self.signed_pre_key is of type KeyPair and matches the key_pair: &KeyPair parameter expected by new_signed_pre_key_record. The self.signed_pre_key_signature field is [u8; 64] and matches the signature: [u8; 64] parameter. The implementation is type-safe.

waproto/build.rs (2)

48-83: Update the comment: bytes crate serde feature is enabled; the skip/default attributes are a valid security measure.

The #[serde(skip, default)] attributes are appropriate and safe. The bytes crate in the workspace (Cargo.toml) explicitly enables the serde feature:

bytes = { version = "1.5", default-features = false, features = ["serde"] }

The comment's claim that "bytes::Bytes doesn't implement Serialize/Deserialize without the serde feature which prost doesn't expose" is inaccurate—the serde feature is available and enabled. However, applying skip to these sensitive key fields is a legitimate design choice to intentionally prevent them from being included in JSON serialization, treating the protobuf format as the sole serialization mechanism. Verification found no code paths that attempt JSON serialization of SessionStructure or SenderKeyStateStructure, confirming these fields remain protected.

Please correct the comment to reflect that the bytes serde feature is available but intentionally not used here to exclude sensitive key material from JSON output.

Likely an incorrect or invalid review comment.


35-46: Verify that all call sites handle the Vec → bytes::Bytes migration.

The change from Vec<u8> to bytes::Bytes for these cryptographic fields is a breaking API change. Bytes is immutable and reference-counted, with different semantics than Vec<u8>. The codebase has been properly updated throughout:

  • All serialization sites use Bytes::copy_from_slice() to construct Bytes fields
  • All deserialization sites safely convert via .as_deref().and_then(|b| b.try_into().ok()) with length validation
  • All encryption/decryption operations properly accept slices from the Bytes fields
  • The #[serde(skip, default)] attributes are appropriate since these structures are not JSON-serialized

The migration is complete and correct.

waproto/src/whatsapp.rs (2)

11023-11044: LGTM! Consistent optimization for session key material.

The conversion of ChainKey and MessageKey binary fields to prost::bytes::Bytes maintains consistency with the changes in hunk 1 and properly secures all cryptographic material (keys, MAC keys, and IVs) with #[serde(skip, default)]. Call sites throughout the codebase have been correctly updated to use Bytes::copy_from_slice() when constructing protobuf structures and properly handle the new type with methods like .as_deref() and .is_some_and().


10942-10967: LGTM! Efficient and secure handling of cryptographic material.

The migration from Vec<u8> to prost::bytes::Bytes for cryptographic seeds and keys is well-aligned with the PR's allocation-reduction goals. prost::bytes::Bytes uses reference counting internally, enabling cheap cloning without additional heap allocations.

The #[serde(skip, default)] annotations appropriately prevent accidental serialization of sensitive cryptographic material (seeds and private keys) while providing safe defaults during deserialization.

All call sites in the cryptography layer (wacore/libsignal/src/protocol/sender_keys.rs) already properly handle the new type, using .as_ref() for slice conversion and Bytes::copy_from_slice() for construction.

wacore/libsignal/src/protocol/state/session.rs (5)

87-88: LGTM! Serialization correctly updated for fixed-size array returns.

The change from into_vec() to to_vec() properly handles the updated PublicKey::serialize() signature that now returns [u8; 33] instead of Box<[u8]>.

Also applies to: 96-96


286-290: LGTM! Efficient chain key storage using prost::bytes::Bytes.

The use of Bytes::copy_from_slice() efficiently handles the 32-byte chain key storage, aligning with the broader migration to prost::bytes::Bytes for protobuf binary fields.


317-322: LGTM! Consistent with the chain key storage pattern.


369-374: LGTM! Proper Bytes usage for chain key.


447-461: LGTM! Consistent Bytes usage.

wacore/libsignal/src/protocol/ratchet.rs (2)

49-85: LGTM! Excellent stack-based optimization for Alice session initialization.

The fixed-size buffer [u8; 160] eliminates heap allocations during session setup. The buffer is correctly sized for the maximum case (discontinuity + 4 agreements), and the debug_assert! checks provide safety during development.


108-149: LGTM! Bob's session initialization correctly mirrors Alice's optimization.

The stack-based buffer approach is consistently applied, matching Alice's implementation with Bob-specific agreement sequences.

wacore/libsignal/src/protocol/sender_keys.rs (4)

11-12: LGTM! HMAC imports and protobuf conversions properly updated.

The HMAC dependencies support the new step_with_message_key() optimization, and the Bytes-based protobuf conversions align with the migration strategy.

Also applies to: 54-80


84-84: LGTM! Efficient combined key derivation with proper overflow protection.

The new step_with_message_key() method efficiently reuses the HMAC instance via finalize_reset(), reducing redundant key setup. The overflow check prevents unbounded chain iteration.

Also applies to: 105-105, 127-153, 162-166


184-193: LGTM! Consistent Bytes usage for key serialization.


568-568: LGTM! Comprehensive test coverage for optimized key derivation.

The new tests thoroughly validate that step_with_message_key() produces identical results to separate calls, and verify correctness over multiple iterations.

Also applies to: 764-825

wacore/libsignal/src/protocol/protocol.rs (4)

87-87: LGTM! Correct serialization for fixed-size array returns.

Also applies to: 262-263


534-534: LGTM! Accessor implementations consistent with fixed-size chain_key.

Also applies to: 559-576


621-634: LGTM! Robust validation and conversion for protobuf deserialization.

The explicit length validation (line 628) ensures only valid 32-byte chain keys are converted, providing defense against malformed protobuf inputs.


400-417: Serialization and signing scope look correct.

The signing scope properly includes the version byte ([version_byte || proto]), and verify_signature() at line 430 correctly slices off only the signature to match. The version byte shift at line 405 hardcodes version 3 (| 3u8), which aligns with SENDERKEY_MESSAGE_CURRENT_VERSION = 3.

One thing to note: the parsing code at lines 477-480 explicitly rejects messages with version < 3 (LegacyCiphertextVersion), which provides built-in protection against incompatibility with older message formats. If the signature scope changed between protocol versions, the version field enforcement ensures only compatible implementations can parse the messages.

wacore/libsignal/src/protocol/ratchet/keys.rs (4)

10-34: LGTM! Clear documentation and correct seed storage.

The expanded enum documentation clarifies the purpose of each variant, and the fixed-size array parameter enforces compile-time size validation.


68-100: LGTM! Efficient protobuf conversions with proper validation.

The zero-cost pass-through for Serialized variants and the thorough validation in from_pb() (lines 91-99) provide both performance and safety.


103-110: LGTM! Efficient combined key derivation with overflow protection.

The step_with_message_keys() method efficiently reuses the HMAC instance via finalize_reset(), and the overflow check on line 130 (though not visible in this file's hunks, referenced from sender_keys.rs pattern) prevents unbounded iteration. The Copy derive is safe for the fixed-size key structure.

Also applies to: 161-161, 199-218


377-480: LGTM! Comprehensive test coverage validates optimization correctness.

The tests thoroughly verify that step_with_message_keys() produces identical results to separate derivations and maintains consistency across multiple iterations.

wacore/libsignal/src/protocol/group_cipher.rs (4)

144-152: LGTM! Efficient key derivation with proper state management.

The refactored loop correctly uses step_with_message_key() to advance through chain keys while storing intermediate message keys. The final state update at line 151 ensures consistency.


207-237: LGTM! Buffer reuse pattern correctly implemented.

The decryption path now properly reuses DECRYPTION_BUFFER following the same pattern as encryption. Error mapping is comprehensive, and buffer capacity is correctly restored at line 235.


325-325: The seed() method correctly returns &[u8; 32], making the dereference operation valid. The change from sender_chain_key.seed().to_vec() to *sender_chain_key.seed() is correct and avoids unnecessary heap allocation.


73-73: Implementation verified: step_with_message_key() is correctly implemented in SenderChainKey with signature Result<(SenderMessageKey, Self), SignalProtocolError>, proper error handling, and correct usage at both locations.

Comment thread wacore/libsignal/src/protocol/protocol.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: 0

🧹 Nitpick comments (2)
wacore/libsignal/src/protocol/group_cipher.rs (1)

43-45: Consider reusing the buffer instead of always allocating.

take_buffer always allocates a new Vec with INITIAL_CAPACITY. If the caller doesn't need ownership (e.g., they copy data immediately), consider an alternative that preserves the existing allocation:

fn take_data(&mut self) -> Vec<u8> {
    std::mem::take(&mut self.buffer)
}

Then separately call get_buffer() on the next use, which will reuse the existing capacity if the Vec was returned. However, I acknowledge the current approach guarantees consistent behavior and avoids capacity bloat from large messages.

wacore/libsignal/src/protocol/ratchet/keys.rs (1)

42-70: Defense-in-depth with debug assertion is appropriate.

The debug_assert! at line 58-61 catches invariant violations during development while the unwrap_or fallbacks provide graceful degradation in release builds. Since from_pb() validates field lengths before creating Serialized, the assertion should never trigger in correct usage.

However, the fallback values ([0u8; 32], [0u8; 16]) would produce incorrect cryptographic keys if triggered. Consider logging a warning in release mode:

♻️ Optional: Add release-mode warning
 debug_assert!(
     cipher_key.is_some() && mac_key.is_some() && iv.is_some(),
     "Serialized MessageKeyGenerator has invalid field lengths - from_pb should have rejected this"
 );
+
+#[cfg(not(debug_assertions))]
+if cipher_key.is_none() || mac_key.is_none() || iv.is_none() {
+    log::error!("Serialized MessageKeyGenerator has invalid field lengths - from_pb should have rejected this");
+}
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9b6fa0a and c0e7dbc.

📒 Files selected for processing (5)
  • wacore/libsignal/src/protocol/group_cipher.rs
  • wacore/libsignal/src/protocol/ratchet.rs
  • wacore/libsignal/src/protocol/ratchet/keys.rs
  • wacore/libsignal/src/protocol/sender_keys.rs
  • wacore/libsignal/src/protocol/state/session.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • wacore/libsignal/src/protocol/ratchet.rs
🧰 Additional context used
📓 Path-based instructions (2)
**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

**/*.rs: Never modify Device state directly; use DeviceCommand + PersistenceManager::process_command() for state changes
For read-only Device state access, use PersistenceManager::get_device_snapshot()
All blocking I/O (such as ureq calls) and heavy CPU-bound tasks (like media encryption) MUST be wrapped in tokio::task::spawn_blocking to avoid stalling the async runtime
Use Client::chat_locks to serialize per-chat operations in asynchronous code
Use thiserror for custom domain-specific errors (e.g., SocketError) and anyhow::Error for functions with multiple failure modes
Avoid .unwrap() and .expect() outside of tests and unrecoverable logic paths
Use the Downloadable trait in wacore/src/download.rs for implementing generic media download interfaces across message types
Always refresh MediaConn if it's expired before using it to get current media servers and auth tokens
Run cargo fmt before finalizing a feature or fix
Run cargo clippy --all-targets before finalizing a feature or fix
Run cargo test --all before finalizing a feature or fix

Files:

  • wacore/libsignal/src/protocol/sender_keys.rs
  • wacore/libsignal/src/protocol/ratchet/keys.rs
  • wacore/libsignal/src/protocol/state/session.rs
  • wacore/libsignal/src/protocol/group_cipher.rs
wacore/**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

wacore crate must remain platform-agnostic and have no dependencies on Tokio or specific databases

Files:

  • wacore/libsignal/src/protocol/sender_keys.rs
  • wacore/libsignal/src/protocol/ratchet/keys.rs
  • wacore/libsignal/src/protocol/state/session.rs
  • wacore/libsignal/src/protocol/group_cipher.rs
🧠 Learnings (1)
📚 Learning: 2025-12-25T03:36:19.700Z
Learnt from: CR
Repo: jlucaso1/whatsapp-rust PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-25T03:36:19.700Z
Learning: Applies to waproto/build.rs : `waproto` crate uses prost to compile Protocol Buffers definitions from `whatsapp.proto` in a `build.rs` script

Applied to files:

  • wacore/libsignal/src/protocol/state/session.rs
🧬 Code graph analysis (2)
wacore/libsignal/src/protocol/ratchet/keys.rs (2)
wacore/libsignal/src/protocol/sender_keys.rs (6)
  • seed (105-107)
  • cipher_key (71-73)
  • iv (67-69)
  • new (39-52)
  • new (94-99)
  • new (176-198)
wacore/libsignal/src/protocol/ratchet.rs (1)
  • derive_keys (18-20)
wacore/libsignal/src/protocol/state/session.rs (2)
wacore/libsignal/src/protocol/protocol.rs (1)
  • chain_key (569-571)
wacore/libsignal/src/protocol/ratchet/keys.rs (1)
  • next_chain_key (192-197)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Build & Test
  • GitHub Check: benchmark
🔇 Additional comments (31)
wacore/libsignal/src/protocol/sender_keys.rs (11)

11-12: LGTM!

The HMAC imports are correctly added to support the new step_with_message_key optimization that reuses the HMAC context.


54-60: LGTM!

The from_protobuf correctly handles the Bytes type with as_ref() for the slice conversion, and the fixed-size array conversion is properly validated.


76-80: LGTM!

Using Bytes::copy_from_slice for protobuf serialization is appropriate and aligns with the PR's goal of consistent byte handling across protobuf fields.


84-88: LGTM!

Adding Copy derive to SenderChainKey is a good optimization. The struct only contains a u32 and [u8; 32], both of which are Copy, making this derivation valid and enabling more efficient pass-by-value semantics used elsewhere in this PR.


105-107: LGTM!

Returning &[u8; 32] instead of &[u8] provides compile-time size guarantees and enables callers to use the fixed-size array directly without runtime validation.


127-153: Well-designed optimization using HMAC context reuse.

The step_with_message_key method correctly:

  1. Validates iteration overflow with checked_add
  2. Reuses the HMAC key setup via finalize_reset() between deriving message key and chain key
  3. Returns both values atomically, enabling callers to avoid separate calls

The logic mirrors the separate sender_message_key() and next() methods, ensuring equivalence. The tests at lines 767-829 validate this.


161-166: LGTM!

Consistent use of Bytes::copy_from_slice for protobuf field population.


184-192: LGTM!

The SenderKeyState::new correctly uses Bytes::copy_from_slice for both public and private key serialization, maintaining consistency with the PR's protobuf byte handling approach.


264-270: Good use of drain() for O(n) eviction.

The comment correctly explains the performance improvement: using drain(..excess) once is O(n) total, whereas calling remove(0) in a loop would be O(n) per removal, leading to O(n²) for evicting multiple keys.


572-572: LGTM!

Passing SenderChainKey by value is now possible and preferred since Copy was added. This avoids an unnecessary clone.


767-829: Comprehensive test coverage for the new optimization.

The tests properly verify:

  1. test_step_with_message_key_equivalence: Single-step equivalence between combined and separate calls
  2. test_step_with_message_key_chain: Multi-iteration synchronization ensuring chains stay aligned

Both tests compare iteration, iv, cipher_key, and seed values to confirm behavioral equivalence.

wacore/libsignal/src/protocol/state/session.rs (5)

87-88: LGTM!

The serialization now correctly uses .serialize().to_vec() for identity keys and base key. This aligns with the PR's fixed-size array return types from serialize() methods.

Also applies to: 96-96


286-312: Good refactor with consistent capacity management.

The changes correctly:

  1. Use Bytes::copy_from_slice for protobuf ChainKey.key field
  2. Compute length once before logging and draining
  3. Use drain(..excess) for efficient O(n) removal of oldest chains

The log message now correctly references the captured len value.


322-325: LGTM!

Consistent use of Bytes::copy_from_slice for the sender chain key.


374-377: LGTM!

Consistent pattern for set_sender_chain_key using Bytes::copy_from_slice.


460-464: LGTM!

Consistent pattern for set_receiver_chain_key using Bytes::copy_from_slice.

wacore/libsignal/src/protocol/group_cipher.rs (6)

20-51: Well-structured buffer abstraction for both encryption and decryption.

The CryptoBuffer design with separate ENCRYPTION_BUFFER and DECRYPTION_BUFFER thread-locals provides:

  1. Isolated buffer reuse for encrypt/decrypt paths
  2. get_buffer() clears and returns a mutable reference for writing
  3. take_buffer() returns owned contents and replaces with a fresh pre-allocated buffer

This matches the existing encryption buffer pattern and extends it to decryption.


83-109: LGTM!

The encryption flow correctly uses step_with_message_key() to obtain both the message keys and next chain key in one call, then updates the state with set_sender_chain_key(next_sender_chain_key). This eliminates the separate sender_message_key() and next() calls.


151-159: LGTM!

The get_sender_key function correctly uses step_with_message_key() in the loop to derive message keys and advance the chain. The final step derives the result key and sets the next chain key.


214-242: LGTM!

The decryption path now uses DECRYPTION_BUFFER with the same get_buffer()/take_buffer() pattern as encryption, providing symmetric buffer reuse for both operations.


260-274: LGTM!

The field access pattern is cleaner, directly calling skdm.chain_id(), skdm.iteration(), skdm.chain_key(), and dereferencing skdm.signing_key().


330-330: LGTM!

Dereferencing *sender_chain_key.seed() passes the [u8; 32] by value, avoiding a to_vec() allocation since SenderKeyDistributionMessage::new now accepts a fixed-size array.

wacore/libsignal/src/protocol/ratchet/keys.rs (9)

10-12: LGTM!

HMAC imports added for the step_with_message_keys optimization.


15-28: Well-designed lazy evaluation pattern.

The MessageKeyGenerator enum enables two key optimizations:

  1. Seed: Defers expensive HKDF derivation until keys are actually needed
  2. Serialized: Enables zero-cost round-trips by preserving the original protobuf bytes

This is a good example of lazy evaluation for performance-critical cryptographic operations.


31-34: LGTM!

new_from_seed correctly takes &[u8; 32] and copies into the Seed variant, matching the fixed-size pattern throughout this PR.


73-91: LGTM!

The into_pb method correctly provides:

  1. Zero-cost pass-through for Serialized variant (just returns the original protobuf)
  2. Proper serialization for Seed and Keys variants using Bytes::copy_from_slice

Note that line 82 calls self.generate_keys() which consumes self, but since we're in a match arm that already checked for Seed or Keys, this is correct.


94-107: LGTM!

The from_pb validation ensures:

  1. cipher_key is exactly 32 bytes
  2. mac_key is exactly 32 bytes
  3. iv is exactly 16 bytes

This upfront validation guarantees that generate_keys() can safely convert the Serialized variant without runtime failures.


109-117: LGTM!

The counter() accessor efficiently retrieves the index from any variant without triggering full key derivation.


168-172: LGTM!

Adding Copy to ChainKey is valid since it only contains [u8; 32] and u32. This enables efficient pass-by-value usage.


206-225: Well-implemented HMAC context reuse.

The step_with_message_keys method correctly:

  1. Creates the HMAC context once with the chain key
  2. Derives message key seed using finalize_reset() to reuse the context
  3. Derives next chain key using finalize()
  4. Returns both values, avoiding the overhead of two separate HMAC setups

This mirrors the SenderChainKey::step_with_message_key pattern in sender_keys.rs.


420-487: Thorough equivalence testing.

The tests properly verify that step_with_message_keys() produces identical results to the separate message_keys() + next_chain_key() path:

  1. test_step_with_message_keys_equivalence: Single-step comparison
  2. test_step_with_message_keys_chain: 10-iteration synchronization test

This provides good confidence that the optimization doesn't change behavior.

@jlucaso1
jlucaso1 merged commit 70a8323 into main Jan 8, 2026
5 checks passed
@jlucaso1
jlucaso1 deleted the feat-optimize-libsignal branch January 25, 2026 13:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant