From 7864066a0e4d7554190f0f023b5e5b7f5785fd45 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Fri, 4 Sep 2026 18:18:31 +0800 Subject: [PATCH] port forward: a UI login answers the challenge its own connection was given `handle_login_from_ui` hashed the typed password against `lc.hash`, the window's shared handler field, and the window's password prompt is broadcast to every listener. With two mappings both waiting on that prompt, the `Hash` that arrived last had overwritten the other's, so one of the two answered the wrong challenge and failed to log in. Master shares the same state and broadcasts the same way. The `Hash` is now a parameter of the login; `Session` keeps it beside the connection it belongs to, and the per-accept clone that `with_port_forward` makes gets a slot of its own. `lc.hash` stays for `handle_peer_info`, which only needs the salt, and that is per peer. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab --- src/client.rs | 57 +++++++++++++++++++++++++++++++++++-- src/ui_session_interface.rs | 8 ++++++ 2 files changed, 63 insertions(+), 2 deletions(-) diff --git a/src/client.rs b/src/client.rs index b8ab0f1c2..874d20d23 100644 --- a/src/client.rs +++ b/src/client.rs @@ -3681,6 +3681,7 @@ async fn send_login( /// * `password` - Password. /// * `remember` - Whether to remember password. /// * `port_forward` - Target of a port-forward login; ignored by other types. +/// * `hash` - The challenge this connection was given. /// * `peer` - [`Stream`] for communicating with peer. pub async fn handle_login_from_ui( lc: Arc>, @@ -3689,6 +3690,7 @@ pub async fn handle_login_from_ui( password: String, remember: bool, port_forward: PortForward, + hash: Hash, peer: &mut Stream, ) { let mut hash_password = if password.is_empty() { @@ -3704,7 +3706,7 @@ pub async fn handle_login_from_ui( lc.write().unwrap().password_source = Default::default(); let mut hasher = Sha256::new(); hasher.update(password); - hasher.update(&lc.read().unwrap().hash.salt); + hasher.update(&hash.salt); let res = hasher.finalize(); lc.write().unwrap().remember = remember; res[..].into() @@ -3712,7 +3714,7 @@ pub async fn handle_login_from_ui( lc.write().unwrap().password = hash_password.clone(); let mut hasher2 = Sha256::new(); hasher2.update(&hash_password[..]); - hasher2.update(&lc.read().unwrap().hash.challenge); + hasher2.update(&hash.challenge); hash_password = hasher2.finalize()[..].to_vec(); send_login(lc.clone(), os_username, os_password, hash_password, port_forward, peer).await; @@ -4091,6 +4093,57 @@ mod login_scope_tests { assert_eq!((pf(&b).host.as_str(), pf(&b).port), ("b", 2)); assert!(pf(&a).multiplex); } + + /// Each connection answers its own challenge: the `Hash` is a parameter + /// of the login, not a field two accepts could overwrite in the handler. + #[test] + fn a_ui_login_answers_the_challenge_it_was_given() { + use hbb_common::{protobuf::Message as _, tcp::FramedStream, tokio, Stream}; + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); + rt.block_on(async { + let l = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = l.local_addr().unwrap(); + let client = tokio::net::TcpStream::connect(addr).await.unwrap(); + let (server, _) = l.accept().await.unwrap(); + let mut ours = Stream::Tcp(FramedStream::from(client, addr)); + let mut peer = Stream::Tcp(FramedStream::from(server, addr)); + let lc = Arc::new(RwLock::new(LoginConfigHandler::default())); + let hash = |challenge: &str| Hash { + salt: "salt".to_owned(), + challenge: challenge.to_owned(), + ..Default::default() + }; + let expected = |challenge: &str| { + let mut h = Sha256::new(); + h.update("pw"); + h.update("salt"); + let salted = h.finalize(); + let mut h2 = Sha256::new(); + h2.update(&salted[..]); + h2.update(challenge); + h2.finalize()[..].to_vec() + }; + for challenge in ["a", "b"] { + handle_login_from_ui( + lc.clone(), + String::new(), + String::new(), + "pw".to_owned(), + false, + Default::default(), + hash(challenge), + &mut ours, + ) + .await; + let bytes = peer.next().await.unwrap().unwrap(); + let msg = Message::parse_from_bytes(&bytes).unwrap(); + assert_eq!(msg.login_request().password, expected(challenge)); + } + }); + } } pub async fn hc_connection( diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index b170b0f05..d2b27f00e 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -60,6 +60,10 @@ pub struct Session { /// Per clone, set by `Interface::with_port_forward`: the target a /// port-forward login asks for. pub port_forward: PortForward, + /// The `Hash` this connection was challenged with. A session's clones + /// share it, as they share the connection; a port-forward accept's clone + /// gets its own, since every accept is a connection of its own. + pub login_hash: Arc>, pub lc: Arc>, pub sender: Arc>>>, pub thread: Arc>>>, @@ -1874,10 +1878,12 @@ impl Interface for Session { fn with_port_forward(&self, port_forward: PortForward) -> Self { let mut scoped = self.clone(); scoped.port_forward = port_forward; + scoped.login_hash = Default::default(); scoped } async fn handle_hash(&self, pass: &str, hash: Hash, peer: &mut Stream) -> bool { + *self.login_hash.write().unwrap() = hash.clone(); handle_hash(self.lc.clone(), pass, hash, self.port_forward.clone(), self, peer).await } @@ -1889,6 +1895,7 @@ impl Interface for Session { remember: bool, peer: &mut Stream, ) { + let hash = self.login_hash.read().unwrap().clone(); handle_login_from_ui( self.lc.clone(), os_username, @@ -1896,6 +1903,7 @@ impl Interface for Session { password, remember, self.port_forward.clone(), + hash, peer, ) .await;