feat(mse): Message Stream Encryption (MSE) support - #633
Conversation
|
Thanks for the PR I will not be merging dead code, we need a change of strategy here - got burnt by this when someone tried doing btv2 recently and never finished- this left useless dead code around. Either send a stack of PRs that implement the feature fully, |
a441340 to
3218ea1
Compare
Thanks for the detailed feedback — this is exactly the guidance I needed, and I've restructured the PR accordingly. Top-down, implemented fully, no dead code. Instead of the bottom-up primitives-first approach, I've reorganized the work into a top-down series following the "wiring → handshake" order you described:
Every commit is a complete, compilable slice, and the final PR has no dead code — every function is referenced by a live call site or a protocol test. I also added a unit test locking To be explicit about the lessons from btv2: nothing here is a placeholder left for a later commit — all three commits together form the full MSE implementation (DH-768 + RC4 + SHA-1 key derivation, incoming/outgoing handshakes, plaintext sniffing and fallback), all wired and tested. The top-down ordering is only about commit structure so each step is reviewable independently. |
ikatson
left a comment
There was a problem hiding this comment.
It's not too big, so let's just shrink into one commit, don't bother with splitting into top-down
I haven't finished looking in detail, but this is a first high-level pass.
Main concerns are:
- lots of custom code for crypto. Need to try to use crates for it, esp with hardware acceleration if any (openssl etc)
- spaghetti code where mse becomes intrusive into the rest of the logic
Add full MSE (Azureus protocol encryption) support: DH-768 key exchange + RC4 stream cipher + SHA-1 key derivation, incoming/outgoing handshakes, plaintext sniffing/fallback, and a three-state MseMode config (Disabled/Enabled/Forced, default Disabled). The handshake logic is abstracted out of the session/peer-connection core into StreamConnector::connect_with_handshake (outgoing) and accept_with_handshake (incoming), so session.rs and peer_connection.rs only consume the unified OutgoingHandshake/IncomingHandshake results and carry no MSE details. Crypto: RC4 stays a self-contained implementation because the `rc4` crate cannot represent the state-clone/short-write semantics MSE's stream wrappers require (OpenSSL 1.1+ / mbedTLS 3 removed RC4, so libtorrent/transmission/ aria2 all implement it inline); DH-768 delegates modular exponentiation to `crypto-bigint` (the 768-bit group is MSE-specified, not an OpenSSL standard group, so the prime is hardcoded and Montgomery pow is computed by the crate); SHA-1 uses the existing sha1w wrapper. MSE applies to any connection kind (TCP/uTP/SOCKS) when enabled; the old TCP-only guard is gone. Plaintext peers are accepted unless Forced, and outgoing MSE failures redial plaintext on a fresh connection. Tests: rc4 vectors, dh768 external bigint vectors, stream wrappers, duplex handshake, plaintext sniff fallback, zero-length IA, fragmented prefix replay, fresh-redial, Disabled single-connection, Forced failure, plus a default-to-Disabled lock test.
3218ea1 to
f7c7107
Compare
Thanks again for the review — I've reworked the PR accordingly. It's now a single commit, and I've addressed both concerns plus the inline comments. Response below. Crypto crates. You were right about the wheel-invention. To be upfront: I originally started from the libtorrent / transmission implementations (which inline both RC4 and the DH-768 group), and I wasn't familiar enough with the Rust crypto ecosystem to question that assumption — so I mirrored the reference engines rather than looking for a better fit. I've now replaced both:
One caveat on hardware acceleration: RC4 and this fixed DH-768 group have no meaningful HWA available (OpenSSL 3 removed RC4; the group isn't standard), so those two are pure-Rust — only SHA-1 gets a hardware backend today. For a per-connection handshake I don't think that's a real loss. Leftover code. Apologies — the Abstraction. Implemented per your sketch: outgoing via Happy to adjust further if anything still looks off. |
Closes #617
Summary
Adds full MSE (Message Stream Encryption) support to rqbit: DH-768 key exchange + RC4 stream cipher + SHA-1 key derivation, the incoming (responder) and outgoing (initiator) handshakes, plaintext sniffing/fallback, and a three-state
MseModeconfig — all wired into peer connections (peer_connection.rsfor outgoing,session.rsfor incoming). No dead code: every piece is referenced by a live call site or protocol test.Commit history is organized top-down (wiring → incoming → outgoing) so each step is reviewable on its own.
Size: 13 files, +1706 / −24 (net ~+1682).
What's in it
MseModeconfig (mse/mod.rs, defaultDisabled) — three states:Disabled(plaintext only),Enabled(prefer MSE, plaintext redial on failure),Forced(require MSE, drop peers that don't complete the handshake). Threaded throughSessionOptions,PeerConnectionOptions, andManagedTorrentOptions.mse/rc4.rs(140 LoC) — RC4 stream cipher (new/apply_keystream/discard, incl. the MSE drop-1024 step). RC4 is cryptographically broken but MSE requires it for wire-level interoperability.mse/dh768.rs(249 LoC) — DH-768 key exchange: fixed Azureus-spec 768-bit group (incl. the 769-bit modular-reduction borrow case), degenerate-key rejection.mse/stream.rs(187 LoC) —Rc4Reader/Rc4Writerwrappers that encrypt/decrypt post-handshake traffic.mse/mod.rs)Ya + PadA, probe the first 20 bytes for a plaintext BT handshake (2s sniff window), then complete the exchange;OutgoingOutcome(Encrypted/PlaintextPeer).YA → YB+PadB → req1/SKEY → VC → provide → PadC → IA → PadD); resolves the obfuscated info hash via a SKEY-hash snapshot of the torrent db;IncomingOutcome(Encrypted/Plaintext).connect_with_mse_fallbackinpeer_connection.rs— MSE attempt, plaintext redial on a fresh connection on failure/plaintext detection;mse_appliedcontrols whether the handshake was already consumed as IA.check_incoming_connectioninsession.rs— RC4-wrapped streams continue encrypted; plaintext peers accepted unlessForced.#[cfg(test)]connection injector inStreamConnector.Default value
MseModedefaults toDisabled, so this merge is a zero-behavior change: every existing connection takes the plaintext path exactly as before. Users opt in viaSessionOptions::mse_mode. This follows the precedent of the uTP support PR (#322, "disabled by default"). A unit test locks the default toDisabled.Provenance
This MSE implementation was developed on my rqbit-9 fork (
lingdiansr/rqbit, branchmse-dev, based on v9.0.0) to fix a real-world problem: librqbit lacked MSE, so peers that require encryption (Xunlei, BitComet, etc.) dropped plaintext handshakes — swarms ran 85–97% dead peers and downloads stalled below 1 MB/s (FluxDown issue #341). The implementation follows the Azureus/libtorrent MSE spec (DH-768 + RC4 + SHA-1 key derivation); the fork is consumed by FluxDown PR 475 via a git dependency, where it measured 29 MB/s (Ubuntu) and 20–24.5 MB/s (Win11 pure-DHT magnet) vs <1 MB/s before, with encrypted peer connections now succeeding.Verification
cargo check -p librqbit --all-targets— cleancargo test -p librqbit --lib— 50 passed, 0 failed, 5 ignored (incl. 18 MSE tests + the default-Disabledlock)cargo clippy -p librqbit --all-targets— nodead_code/ unused warningsSessionOptions::mse_modetemporarily set toEnabled, all MSE handshake/fallback tests still passNotes on authorship
The implementation was developed with AI assistance (code generation and review), then verified by hand: I traced the 9.0 peer-I/O adaptation points (
into_vectored_compat, single-readReadBuflimitation), ran the full test suite, and validated the crypto primitives against external test vectors.