From 04554d91293bf701c2ddcea8903b494c17e1f20a Mon Sep 17 00:00:00 2001 From: 21pages Date: Sat, 2 May 2026 17:20:54 +0800 Subject: [PATCH] easy access: move grant exchange from rendezvous to API server Signed-off-by: 21pages --- flutter/lib/common/widgets/easy_access.dart | 2 +- src/client.rs | 153 ++++++++---- src/common.rs | 2 +- src/ipc.rs | 4 +- src/rendezvous_mediator.rs | 44 ++-- src/server.rs | 20 +- src/server/connection.rs | 260 +++++++++++--------- 7 files changed, 281 insertions(+), 204 deletions(-) diff --git a/flutter/lib/common/widgets/easy_access.dart b/flutter/lib/common/widgets/easy_access.dart index 19c4b0b53..df2cdab42 100644 --- a/flutter/lib/common/widgets/easy_access.dart +++ b/flutter/lib/common/widgets/easy_access.dart @@ -61,7 +61,7 @@ Future>> fetchEasyAccessManagers() async { } final response = await http.post( - Uri.parse('$url/api/devices/easy-access-managers'), + Uri.parse('$url/api/easy_access/managers'), headers: {'Content-Type': 'application/json'}, body: authBody, ); diff --git a/src/client.rs b/src/client.rs index 04cdfebde..c18312605 100644 --- a/src/client.rs +++ b/src/client.rs @@ -207,7 +207,6 @@ impl Client { debug_assert!(peer == interface.get_id()); interface.update_direct(None); interface.update_received(false); - interface.clear_easy_access_challenge(); match Self::_start(peer, key, token, conn_type, interface.clone()).await { Err(err) => { let err_str = err.to_string(); @@ -344,9 +343,7 @@ impl Client { contained, ); if udp.0.is_none() { - let conn = fut.await?; - interface.get_lch().write().unwrap().controller_config = conn.3; - return Ok((conn.0, conn.1, conn.2)); + return fut.await; } let mut connect_futures = Vec::new(); connect_futures.push(fut.boxed()); @@ -355,7 +352,7 @@ impl Client { key.to_owned(), token.to_owned(), conn_type, - interface.clone(), + interface, (None, None), None, rendezvous_server, @@ -364,10 +361,7 @@ impl Client { ); connect_futures.push(fut.boxed()); match select_ok(connect_futures).await { - Ok(conn) => { - interface.get_lch().write().unwrap().controller_config = conn.0 .3; - Ok((conn.0 .0, conn.0 .1, conn.0 .2)) - } + Ok(conn) => Ok((conn.0 .0, conn.0 .1, conn.0 .2)), Err(e) => Err(e), } } @@ -393,7 +387,6 @@ impl Client { ), (i32, String), bool, - Option, )> { let mut start = Instant::now(); let mut socket = connect_tcp(&*rendezvous_server, CONNECT_TIMEOUT).await; @@ -464,7 +457,6 @@ impl Client { }; let udp_nat_port = udp.1.map(|x| *x.lock().unwrap()).unwrap_or(0); let punch_type = if udp_nat_port > 0 { "UDP" } else { "TCP" }; - let mut controller_config = None; msg_out.set_punch_hole_request(PunchHoleRequest { id: peer.to_owned(), token: token.to_owned(), @@ -518,7 +510,6 @@ impl Client { relay_server = ph.relay_server; peer_addr = AddrMangle::decode(&ph.socket_addr); feedback = ph.feedback; - controller_config = ph.controller_config.into_option(); let s = udp.0.take(); if ph.is_udp && s.is_some() { if let Some(s) = s { @@ -558,7 +549,6 @@ impl Client { } } signed_id_pk = rr.pk().into(); - controller_config = rr.controller_config.into_option(); let fut = Self::create_relay( &peer, rr.uuid, @@ -589,7 +579,6 @@ impl Client { (conn, typ == "IPv6", pk, kcp, typ), (feedback, rendezvous_server), false, - controller_config, )); } _ => { @@ -633,12 +622,10 @@ impl Client { udp.0, ipv6.0, punch_type, - &mut controller_config, ) .await?, (feedback, rendezvous_server), true, - controller_config, )) } @@ -661,7 +648,6 @@ impl Client { udp_socket_nat: Option>, udp_socket_v6: Option>, punch_type: &str, - controller_config: &mut Option, ) -> ResultType<( Stream, bool, @@ -736,7 +722,6 @@ impl Client { key, token, conn_type, - controller_config, ) .await; if let Err(e) = conn { @@ -857,7 +842,6 @@ impl Client { key: &str, token: &str, conn_type: ConnType, - controller_config: &mut Option, ) -> ResultType { let mut succeed = false; let mut uuid = "".to_owned(); @@ -902,7 +886,6 @@ impl Client { if !rs.refuse_reason.is_empty() { bail!(rs.refuse_reason); } - *controller_config = rs.controller_config.into_option(); succeed = true; break; } @@ -1778,7 +1761,7 @@ pub struct LoginConfigHandler { pub enable_trusted_devices: bool, pub record_state: bool, pub record_permission: bool, - pub controller_config: Option, + easy_access_grant_id: Option>, } impl Deref for LoginConfigHandler { @@ -1893,6 +1876,7 @@ impl LoginConfigHandler { self.shared_password = shared_password; self.record_state = false; self.record_permission = true; + self.easy_access_grant_id = None; // `std::env::remove_var("IS_TERMINAL_ADMIN");` is called in `session_add_sync()` - `flutter_ffi.rs`. let is_terminal_admin = conn_type == ConnType::TERMINAL @@ -2673,16 +2657,15 @@ impl LoginConfigHandler { }; let mut avatar = get_builtin_option(keys::OPTION_AVATAR); if avatar.is_empty() { - avatar = serde_json::from_str::(&LocalConfig::get_option( - "user_info", - )) - .ok() - .and_then(|x| { - x.get("avatar") - .and_then(|x| x.as_str()) - .map(|x| x.trim().to_owned()) - }) - .unwrap_or_default(); + avatar = + serde_json::from_str::(&LocalConfig::get_option("user_info")) + .ok() + .and_then(|x| { + x.get("avatar") + .and_then(|x| x.as_str()) + .map(|x| x.trim().to_owned()) + }) + .unwrap_or_default(); } avatar = resolve_avatar_url(avatar); let mut display_name = get_builtin_option(keys::OPTION_DISPLAY_NAME); @@ -2728,11 +2711,7 @@ impl LoginConfigHandler { } else { Bytes::new() }; - let easy_access_challenge: Bytes = self - .controller_config - .as_ref() - .map(|config| config.easy_access_challenge.clone()) - .unwrap_or_default(); + let easy_access_grant_id = self.easy_access_grant_id.clone().unwrap_or_default().into(); let mut lr = LoginRequest { username: pure_id, password: password.into(), @@ -2750,7 +2729,7 @@ impl LoginConfigHandler { .into(), hwid, avatar, - easy_access_challenge, + easy_access_grant_id, ..Default::default() }; match self.conn_type { @@ -2778,6 +2757,99 @@ impl LoginConfigHandler { msg_out } + async fn request_easy_access_grant( + lc: Arc>, + hash: &Hash, + secure: bool, + ) { + let (peer, challenge, other_server) = { + let mut lc = lc.write().unwrap(); + lc.easy_access_grant_id = None; + ( + lc.id.clone(), + hash.easy_access_challenge.to_vec(), + lc.other_server.is_some(), + ) + }; + let grant_id = + Self::fetch_easy_access_grant_id(&peer, challenge, other_server, secure).await; + lc.write().unwrap().easy_access_grant_id = grant_id; + } + + async fn fetch_easy_access_grant_id( + peer: &str, + challenge: Vec, + other_server: bool, + secure: bool, + ) -> Option> { + if other_server || !secure || challenge.is_empty() { + return None; + } + if hbb_common::is_ip_str(peer) || hbb_common::is_domain_port_str(peer) { + return None; + } + let access_token = LocalConfig::get_option("access_token"); + if access_token.is_empty() { + return None; + } + let api_server = crate::get_api_server( + Config::get_option("api-server"), + Config::get_option("custom-rendezvous-server"), + ); + if api_server.is_empty() || crate::is_public(&api_server) { + return None; + } + + #[derive(Serialize)] + struct EasyAccessGrantRequest<'a> { + id: &'a str, + challenge: &'a str, + } + + #[derive(Deserialize)] + struct EasyAccessGrantResponse { + #[serde(default)] + grant_id: String, + } + + let challenge_base64 = crate::encode64(&challenge); + let body = match serde_json::to_string(&EasyAccessGrantRequest { + id: peer, + challenge: &challenge_base64, + }) { + Ok(body) => body, + Err(err) => { + log::warn!("Easy access grant request serialize failed: {}", err); + return None; + } + }; + let url = format!("{}/api/easy_access/grant", api_server); + let header = format!("Authorization: Bearer {}", access_token); + let response = match crate::post_request(url, body, &header).await { + Ok(response) => response, + Err(err) => { + log::debug!("Easy access grant request failed: {}", err); + return None; + } + }; + let response: EasyAccessGrantResponse = match serde_json::from_str(&response) { + Ok(response) => response, + Err(err) => { + log::warn!("Easy access grant response parse failed: {}", err); + return None; + } + }; + let grant_id = match crate::decode64(&response.grant_id) { + Ok(grant_id) if !grant_id.is_empty() => grant_id, + Ok(_) => return None, + Err(err) => { + log::warn!("Easy access grant id invalid: {}", err); + return None; + } + }; + Some(grant_id) + } + pub fn update_supported_decodings(&self) -> Message { let decoding = scrap::codec::Decoder::supported_decodings( Some(&self.id), @@ -3473,6 +3545,7 @@ pub async fn handle_hash( peer: &mut Stream, ) { lc.write().unwrap().hash = hash.clone(); + LoginConfigHandler::request_easy_access_grant(lc.clone(), &hash, peer.is_secured()).await; // Take care of password application order // switch_uuid @@ -3739,12 +3812,6 @@ pub trait Interface: Send + Clone + 'static + Sized { self.get_lch().write().unwrap().received = received; } - fn clear_easy_access_challenge(&self) { - if let Some(config) = self.get_lch().write().unwrap().controller_config.as_mut() { - config.easy_access_challenge = Default::default(); - } - } - fn on_establish_connection_error(&self, err: String) { let title = "Connection Error"; let text = err.to_string(); diff --git a/src/common.rs b/src/common.rs index fe901b0a5..69e3ec304 100644 --- a/src/common.rs +++ b/src/common.rs @@ -2603,7 +2603,7 @@ pub fn str2color(s: &str, alpha: u8) -> u32 { /// Returns: Some(true) = enabled, Some(false) = disabled, None = not set or invalid pub fn get_control_permission( permissions: u64, - permission: hbb_common::rendezvous_proto::controlled_config::Permission, + permission: hbb_common::rendezvous_proto::control_permissions::Permission, ) -> Option { use hbb_common::protobuf::Enum; let index = permission.value(); diff --git a/src/ipc.rs b/src/ipc.rs index 329da84ca..0cd30634a 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -1084,7 +1084,7 @@ async fn handle(data: Data, stream: &mut Connection) { } }, Data::ControlPermissionsRemoteModify(_) => { - use hbb_common::rendezvous_proto::controlled_config::Permission; + use hbb_common::rendezvous_proto::control_permissions::Permission; let state = crate::server::get_control_permission_state(Permission::remote_modify, true); allow_err!( @@ -1095,7 +1095,7 @@ async fn handle(data: Data, stream: &mut Connection) { } #[cfg(target_os = "windows")] Data::FileTransferEnabledState(_) => { - use hbb_common::rendezvous_proto::controlled_config::Permission; + use hbb_common::rendezvous_proto::control_permissions::Permission; let state = crate::server::get_control_permission_state(Permission::file, false); let enabled = state.unwrap_or_else(|| { crate::server::Connection::is_permission_enabled_locally( diff --git a/src/rendezvous_mediator.rs b/src/rendezvous_mediator.rs index 0e2b03b39..89d7fa01e 100644 --- a/src/rendezvous_mediator.rs +++ b/src/rendezvous_mediator.rs @@ -472,8 +472,7 @@ impl RendezvousMediator { rr.secure, false, Default::default(), - rr.controlled_config.clone().into_option(), - rr.request_id, + rr.control_permissions.clone().into_option(), ) .await } @@ -487,8 +486,7 @@ impl RendezvousMediator { secure: bool, initiate: bool, socket_addr_v6: bytes::Bytes, - controlled_config: Option, - request_id: bytes::Bytes, + control_permissions: Option, ) -> ResultType<()> { let peer_addr = AddrMangle::decode(&socket_addr); log::info!( @@ -506,7 +504,6 @@ impl RendezvousMediator { socket_addr: socket_addr.into(), version: crate::VERSION.to_owned(), socket_addr_v6, - request_id, ..Default::default() }; if initiate { @@ -523,7 +520,7 @@ impl RendezvousMediator { peer_addr, secure, is_ipv4(&self.addr), - controlled_config, + control_permissions, ) .await; Ok(()) @@ -546,7 +543,7 @@ impl RendezvousMediator { peer_addr_v6, addr, server.clone(), - fla.controlled_config.clone().into_option(), + fla.control_permissions.clone().into_option(), ) .await; } @@ -574,8 +571,7 @@ impl RendezvousMediator { true, true, socket_addr_v6, - fla.controlled_config.into_option(), - fla.request_id, + fla.control_permissions.into_option(), ) .await } @@ -602,7 +598,6 @@ impl RendezvousMediator { relay_server, version: crate::VERSION.to_owned(), socket_addr_v6, - request_id: fla.request_id, ..Default::default() }); let bytes = msg_out.write_to_bytes()?; @@ -612,7 +607,7 @@ impl RendezvousMediator { socket, peer_addr, true, - fla.controlled_config.into_option(), + fla.control_permissions.into_option(), ) .await; Ok(()) @@ -629,13 +624,13 @@ impl RendezvousMediator { let peer_addr_v6 = hbb_common::AddrMangle::decode(&ph.socket_addr_v6); let relay = use_ws() || Config::is_proxy() || ph.force_relay; let mut socket_addr_v6 = Default::default(); - let controlled_config = ph.controlled_config.into_option(); + let control_permissions = ph.control_permissions.into_option(); if peer_addr_v6.port() > 0 && !relay { socket_addr_v6 = start_ipv6( peer_addr_v6, peer_addr, server.clone(), - controlled_config.clone(), + control_permissions.clone(), ) .await; } @@ -656,8 +651,7 @@ impl RendezvousMediator { true, true, socket_addr_v6.clone(), - controlled_config, - ph.request_id, + control_permissions, ) .await; } @@ -670,12 +664,11 @@ impl RendezvousMediator { nat_type: nat_type.into(), version: crate::VERSION.to_owned(), socket_addr_v6, - request_id: ph.request_id, ..Default::default() }; if ph.udp_port > 0 { peer_addr.set_port(ph.udp_port as u16); - self.punch_udp_hole(peer_addr, server, msg_punch, controlled_config) + self.punch_udp_hole(peer_addr, server, msg_punch, control_permissions) .await?; return Ok(()); } @@ -692,7 +685,8 @@ impl RendezvousMediator { msg_out.set_punch_hole_sent(msg_punch); let bytes = msg_out.write_to_bytes()?; socket.send_raw(bytes).await?; - crate::accept_connection(server.clone(), socket, peer_addr, true, controlled_config).await; + crate::accept_connection(server.clone(), socket, peer_addr, true, control_permissions) + .await; Ok(()) } @@ -701,7 +695,7 @@ impl RendezvousMediator { peer_addr: SocketAddr, server: ServerPtr, msg_punch: PunchHoleSent, - controlled_config: Option, + control_permissions: Option, ) -> ResultType<()> { let mut msg_out = Message::new(); msg_out.set_punch_hole_sent(msg_punch); @@ -721,7 +715,7 @@ impl RendezvousMediator { peer_addr, peer_addr, server, - controlled_config, + control_permissions, ) .await?; Ok(()) @@ -880,7 +874,7 @@ async fn direct_server(server: ServerPtr) { hbb_common::Stream::from(stream, local_addr), addr, false, - None, // Direct connections don't have controlled_config + None, // Direct connections don't have control_permissions ) .await ); @@ -912,7 +906,7 @@ async fn start_ipv6( peer_addr_v6: SocketAddr, peer_addr_v4: SocketAddr, server: ServerPtr, - controlled_config: Option, + control_permissions: Option, ) -> bytes::Bytes { crate::test_ipv6().await; if let Some((socket, local_addr_v6)) = crate::get_ipv6_socket().await { @@ -924,7 +918,7 @@ async fn start_ipv6( peer_addr_v6, peer_addr_v4, server, - controlled_config, + control_permissions ) .await ); @@ -939,7 +933,7 @@ async fn udp_nat_listen( peer_addr: SocketAddr, peer_addr_v4: SocketAddr, server: ServerPtr, - controlled_config: Option, + control_permissions: Option, ) -> ResultType<()> { let tm = Instant::now(); let socket_cloned = socket.clone(); @@ -957,7 +951,7 @@ async fn udp_nat_listen( stream.1, peer_addr_v4, true, - controlled_config, + control_permissions, ) .await?; Ok(()) diff --git a/src/server.rs b/src/server.rs index 063fe59c1..86f7b5396 100644 --- a/src/server.rs +++ b/src/server.rs @@ -163,7 +163,7 @@ async fn accept_connection_( server: ServerPtr, socket: Stream, secure: bool, - controlled_config: Option, + control_permissions: Option, ) -> ResultType<()> { let local_addr = socket.local_addr(); drop(socket); @@ -180,7 +180,7 @@ async fn accept_connection_( Stream::from(stream, stream_addr), addr, secure, - controlled_config, + control_permissions, ) .await?; } @@ -192,7 +192,7 @@ pub async fn create_tcp_connection( stream: Stream, addr: SocketAddr, secure: bool, - controlled_config: Option, + control_permissions: Option, ) -> ResultType<()> { let mut stream = stream; let id = server.write().unwrap().get_new_id(); @@ -265,7 +265,7 @@ pub async fn create_tcp_connection( stream, id, Arc::downgrade(&server), - controlled_config, + control_permissions, ) .await; Ok(()) @@ -276,9 +276,9 @@ pub async fn accept_connection( socket: Stream, peer_addr: SocketAddr, secure: bool, - controlled_config: Option, + control_permissions: Option, ) { - if let Err(err) = accept_connection_(server, socket, secure, controlled_config).await { + if let Err(err) = accept_connection_(server, socket, secure, control_permissions).await { log::warn!("Failed to accept connection from {}: {}", peer_addr, err); } } @@ -290,7 +290,7 @@ pub async fn create_relay_connection( peer_addr: SocketAddr, secure: bool, ipv4: bool, - controlled_config: Option, + control_permissions: Option, ) { if let Err(err) = create_relay_connection_( server, @@ -299,7 +299,7 @@ pub async fn create_relay_connection( peer_addr, secure, ipv4, - controlled_config, + control_permissions, ) .await { @@ -319,7 +319,7 @@ async fn create_relay_connection_( peer_addr: SocketAddr, secure: bool, ipv4: bool, - controlled_config: Option, + control_permissions: Option, ) -> ResultType<()> { let mut stream = socket_client::connect_tcp( socket_client::ipv4_to_ipv6(crate::check_port(relay_server, RELAY_PORT), ipv4), @@ -334,7 +334,7 @@ async fn create_relay_connection_( ..Default::default() }); stream.send(&msg_out).await?; - create_tcp_connection(server, stream, peer_addr, secure, controlled_config).await?; + create_tcp_connection(server, stream, peer_addr, secure, control_permissions).await?; Ok(()) } diff --git a/src/server/connection.rs b/src/server/connection.rs index a409e513a..b03647077 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -35,10 +35,10 @@ use hbb_common::{ fs::{self, can_enable_overwrite_detection, JobType}, futures::{SinkExt, StreamExt}, get_time, get_version_number, + message_proto::EasyAccessManagerApproval, message_proto::{option_message::BoolOption, permission_info::Permission}, password_security::{self as password, ApproveMode}, rand, - rendezvous_proto::EasyAccessManagerApproval, sha2::{Digest, Sha256}, sleep, sodiumoxide::crypto::{ @@ -82,7 +82,7 @@ lazy_static::lazy_static! { static ref SESSIONS: Arc::>> = Default::default(); static ref ALIVE_CONNS: Arc::>> = Default::default(); pub static ref AUTHED_CONNS: Arc::>> = Default::default(); - pub static ref CONTROLLED_CONFIG_ARRAY: Arc::>> = Default::default(); + pub static ref CONTROL_PERMISSIONS_ARRAY: Arc::>> = Default::default(); static ref WAKELOCK_SENDER: Arc::>> = Arc::new(Mutex::new(start_wakelock_thread())); static ref WAKELOCK_KEEP_AWAKE_OPTION: Arc::>> = Default::default(); } @@ -287,7 +287,7 @@ pub struct Connection { recording: bool, block_input: bool, privacy_mode: bool, - controlled_config: Option, + control_permissions: Option, last_test_delay: Option, network_delay: u32, lock_after_session_end: bool, @@ -404,6 +404,7 @@ const MILLI1: Duration = Duration::from_millis(1); const SEND_TIMEOUT_VIDEO: u64 = 12_000; const SEND_TIMEOUT_OTHER: u64 = SEND_TIMEOUT_VIDEO * 10; const SESSION_TIMEOUT: Duration = Duration::from_secs(30); +const EASY_ACCESS_CHALLENGE_LEN: usize = 32; impl Connection { pub async fn start( @@ -411,19 +412,25 @@ impl Connection { stream: super::Stream, id: i32, server: super::ServerPtrWeak, - controlled_config: Option, + control_permissions: Option, ) { - // Android does not support control_permissions yet. + // Android is not supported yet, so we always set control_permissions to None. #[cfg(target_os = "android")] - let controlled_config = controlled_config.map(|mut c| { - c.control_permissions = 0; - c - }); + let control_permissions = None; let _raii_id = raii::ConnectionID::new(id); - let _raii_controlled_config_id = raii::ControlledConfigID::new(id, &controlled_config); + let _raii_control_permissions_id = + raii::ControlPermissionsID::new(id, &control_permissions); + let easy_access_challenge = if hbb_common::config::is_allow_easy_access() { + rand::random::<[u8; EASY_ACCESS_CHALLENGE_LEN]>() + .to_vec() + .into() + } else { + Default::default() + }; let hash = Hash { salt: Config::get_salt(), challenge: Config::get_auto_password(6), + easy_access_challenge, ..Default::default() }; let (tx_from_cm_holder, mut rx_from_cm) = mpsc::unbounded_channel::(); @@ -471,16 +478,16 @@ impl Connection { port_forward_address: "".to_owned(), tx_to_cm, authorized: false, - keyboard: Self::permission(keys::OPTION_ENABLE_KEYBOARD, &controlled_config), - clipboard: Self::permission(keys::OPTION_ENABLE_CLIPBOARD, &controlled_config), - audio: Self::permission(keys::OPTION_ENABLE_AUDIO, &controlled_config), + keyboard: Self::permission(keys::OPTION_ENABLE_KEYBOARD, &control_permissions), + clipboard: Self::permission(keys::OPTION_ENABLE_CLIPBOARD, &control_permissions), + audio: Self::permission(keys::OPTION_ENABLE_AUDIO, &control_permissions), // to-do: make sure is the option correct here - file: Self::permission(keys::OPTION_ENABLE_FILE_TRANSFER, &controlled_config), - restart: Self::permission(keys::OPTION_ENABLE_REMOTE_RESTART, &controlled_config), - recording: Self::permission(keys::OPTION_ENABLE_RECORD_SESSION, &controlled_config), - block_input: Self::permission(keys::OPTION_ENABLE_BLOCK_INPUT, &controlled_config), - privacy_mode: Self::permission(keys::OPTION_ENABLE_PRIVACY_MODE, &controlled_config), - controlled_config, + file: Self::permission(keys::OPTION_ENABLE_FILE_TRANSFER, &control_permissions), + restart: Self::permission(keys::OPTION_ENABLE_REMOTE_RESTART, &control_permissions), + recording: Self::permission(keys::OPTION_ENABLE_RECORD_SESSION, &control_permissions), + block_input: Self::permission(keys::OPTION_ENABLE_BLOCK_INPUT, &control_permissions), + privacy_mode: Self::permission(keys::OPTION_ENABLE_PRIVACY_MODE, &control_permissions), + control_permissions, last_test_delay: None, network_delay: 0, lock_after_session_end: false, @@ -1004,7 +1011,7 @@ impl Connection { match data { #[cfg(all(target_os = "windows", feature = "flutter"))] ipc::Data::PrinterData(data) => { - if Self::permission(keys::OPTION_ENABLE_REMOTE_PRINTER, &conn.controlled_config) { + if Self::permission(keys::OPTION_ENABLE_REMOTE_PRINTER, &conn.control_permissions) { conn.send_printer_request(data).await; } else { conn.send_remote_printing_disallowed().await; @@ -1547,9 +1554,6 @@ impl Connection { return false; } self.authorized = true; - if let Some(c) = self.controlled_config.as_mut() { - c.easy_access_grant = Default::default(); - } let (conn_type, auth_conn_type) = if self.file_transfer.is_some() { (1, AuthConnType::FileTransfer) } else if self.port_forward_socket.is_some() { @@ -2260,10 +2264,10 @@ impl Connection { fn permission( enable_prefix_option: &str, - controlled_config: &Option, + control_permissions: &Option, ) -> bool { - use hbb_common::rendezvous_proto::controlled_config::Permission; - if let Some(controlled_config) = controlled_config { + use hbb_common::rendezvous_proto::control_permissions::Permission; + if let Some(control_permissions) = control_permissions { let permission = match enable_prefix_option { keys::OPTION_ENABLE_KEYBOARD => Some(Permission::keyboard), keys::OPTION_ENABLE_REMOTE_PRINTER => Some(Permission::remote_printer), @@ -2281,7 +2285,7 @@ impl Connection { }; if let Some(permission) = permission { if let Some(enabled) = - crate::get_control_permission(controlled_config.control_permissions, permission) + crate::get_control_permission(control_permissions.permissions, permission) { return enabled; } @@ -2324,8 +2328,7 @@ impl Connection { device_nonce: &[u8], approved: bool, ) -> Vec { - const EASY_ACCESS_CONSUME_DECISION_DOMAIN: &[u8] = - b"easy-access-consume-decision/v1"; + const EASY_ACCESS_CONSUME_DECISION_DOMAIN: &[u8] = b"easy-access-consume-decision/v1"; let mut bytes = Vec::new(); bytes.extend_from_slice(EASY_ACCESS_CONSUME_DECISION_DOMAIN); @@ -2356,6 +2359,7 @@ impl Connection { device_nonce: String, approved: bool, signature: String, + grant: String, } async fn consume_easy_access_grant( @@ -2363,26 +2367,26 @@ impl Connection { target_uuid: &[u8], target_sk: &[u8], server_pk: &sign::PublicKey, - ) -> bool { + ) -> Option { let api_server = crate::get_api_server( Config::get_option("api-server"), Config::get_option("custom-rendezvous-server"), ); - if api_server.is_empty() { - log::warn!("Easy access consume skipped: api server missing"); - return false; + if api_server.is_empty() || crate::is_public(&api_server) { + log::warn!("Easy access consume skipped: api server missing or public"); + return None; } let Some(device_sign_sk) = sign::SecretKey::from_slice(target_sk) else { log::warn!("Easy access consume failed: target private key invalid"); - return false; + return None; }; let Ok(device_box_sk) = ed25519::to_curve25519_sk(&device_sign_sk) else { log::warn!("Easy access consume failed: target box private key invalid"); - return false; + return None; }; let Ok(server_box_pk) = ed25519::to_curve25519_pk(server_pk) else { log::warn!("Easy access consume failed: server box public key invalid"); - return false; + return None; }; let device_nonce: [u8; 32] = rand::random(); let plaintext = match serde_json::to_vec(&EasyAccessGrantConsumeDeviceAuthPayload { @@ -2393,7 +2397,7 @@ impl Connection { Ok(bytes) => bytes, Err(err) => { log::warn!("Easy access consume payload serialize failed: {}", err); - return false; + return None; } }; let nonce = box_::gen_nonce(); @@ -2408,45 +2412,45 @@ impl Connection { Ok(body) => body, Err(err) => { log::warn!("Easy access consume request serialize failed: {}", err); - return false; + return None; } }; - let url = format!("{}/api/devices/consume-easy-access-grant", api_server); + let url = format!("{}/api/easy_access/consume", api_server); let response = match crate::post_request(url, body, "").await { Ok(response) => response, Err(err) => { log::warn!("Easy access consume request failed: {}", err); - return false; + return None; } }; let response: EasyAccessGrantConsumeResponse = match serde_json::from_str(&response) { Ok(response) => response, Err(err) => { log::warn!("Easy access consume response parse failed: {}", err); - return false; + return None; } }; let response_grant_id = match crate::decode64(&response.grant_id) { Ok(grant_id) => grant_id, Err(err) => { log::warn!("Easy access consume response grant_id invalid: {}", err); - return false; + return None; } }; let response_device_nonce = match crate::decode64(&response.device_nonce) { Ok(device_nonce) => device_nonce, Err(err) => { log::warn!("Easy access consume response device_nonce invalid: {}", err); - return false; + return None; } }; if response_grant_id.as_slice() != grant_id { log::warn!("Easy access consume response grant_id mismatch"); - return false; + return None; } if response_device_nonce.as_slice() != device_nonce.as_slice() { log::warn!("Easy access consume response device_nonce mismatch"); - return false; + return None; } let signature = match crate::decode64(&response.signature) .ok() @@ -2455,7 +2459,7 @@ impl Connection { Some(signature) => signature, None => { log::warn!("Easy access consume response signature invalid"); - return false; + return None; } }; if !sign::verify_detached( @@ -2468,39 +2472,43 @@ impl Connection { server_pk, ) { log::warn!("Easy access consume response signature verify failed"); - return false; + return None; } if !response.approved { log::warn!("Easy access consume denied by server"); - return false; + return None; + } + match crate::decode64(&response.grant) { + Ok(grant) if !grant.is_empty() => match EasyAccessGrant::parse_from_bytes(&grant) { + Ok(grant) => Some(grant), + Err(err) => { + log::warn!("Easy access grant parse failed: {}", err); + None + } + }, + Ok(_) => None, + Err(err) => { + log::warn!("Easy access grant payload invalid: {}", err); + None + } } - true } if !hbb_common::config::is_allow_easy_access() { return false; } - let Some(ticket) = self - .controlled_config - .as_ref() - .and_then(|c| c.easy_access_grant.as_ref().cloned()) - else { + if !self.stream.is_secured() { + log::warn!("Easy access requires a secured peer connection"); + return false; + } + + let grant_id = self.lr.easy_access_grant_id.clone(); + if grant_id.is_empty() { return false; }; - if ticket.version != EASY_ACCESS_GRANT_VERSION { - log::warn!("Easy access grant version invalid: {}", ticket.version); - return false; - } - if ticket.server_approval_signature.is_empty() { - log::warn!("Easy access server approval signature missing"); - return false; - } - if ticket.device_bound_proof.is_empty() { - log::warn!("Easy access device-bound proof missing"); - return false; - } - let lr_challenge_bytes = self.lr.easy_access_challenge.clone(); - if lr_challenge_bytes.is_empty() { + let target_challenge = self.hash.easy_access_challenge.clone(); + if target_challenge.is_empty() { + log::warn!("Easy access target challenge missing"); return false; } let target_uuid = hbb_common::get_uuid(); @@ -2525,6 +2533,24 @@ impl Connection { return false; } }; + let Some(ticket) = + consume_easy_access_grant(&grant_id, target_uuid.as_slice(), &target_sk, &server_pk) + .await + else { + return false; + }; + if ticket.version != EASY_ACCESS_GRANT_VERSION { + log::warn!("Easy access grant version invalid: {}", ticket.version); + return false; + } + if ticket.server_approval_signature.is_empty() { + log::warn!("Easy access server approval signature missing"); + return false; + } + if ticket.device_bound_proof.is_empty() { + log::warn!("Easy access device-bound proof missing"); + return false; + } let manager_approval_bytes = match open_easy_access_device_bound_proof( &ticket.device_bound_proof, &ticket.server_approval_signature, @@ -2565,15 +2591,19 @@ impl Connection { log::warn!("Easy access target binding missing"); return false; }; - if manager_approval.manager_signing_pk.is_empty() { - log::warn!("Easy access manager signing public key missing"); + if manager_approval.manager_pk.is_empty() { + log::warn!("Easy access manager public key missing"); return false; } if manager_approval.manager_approval_signature.is_empty() { log::warn!("Easy access manager approval signature missing"); return false; } - if target_binding.challenge.as_ref() != lr_challenge_bytes.as_ref() { + if manager_approval.manager_id.is_empty() { + log::warn!("Easy access manager id missing"); + return false; + } + if target_binding.challenge.as_ref() != target_challenge.as_ref() { log::warn!("Easy access challenge mismatch"); return false; } @@ -2589,14 +2619,13 @@ impl Connection { log::warn!("Easy access grant id missing"); return false; } - let manager_signing_pk = - match sign::PublicKey::from_slice(&manager_approval.manager_signing_pk) { - Some(pk) => pk, - None => { - log::warn!("Easy access manager signing public key invalid"); - return false; - } - }; + let manager_pk = match sign::PublicKey::from_slice(&manager_approval.manager_pk) { + Some(pk) => pk, + None => { + log::warn!("Easy access manager public key invalid"); + return false; + } + }; let manager_approval_signature = match sign::Signature::from_bytes(&manager_approval.manager_approval_signature) { Ok(sig) => sig, @@ -2615,19 +2644,13 @@ impl Connection { if !sign::verify_detached( &manager_approval_signature, &target_binding_bytes, - &manager_signing_pk, + &manager_pk, ) { log::warn!("Easy access manager approval signature verify failed"); return false; } - if !consume_easy_access_grant( - target_binding.grant_id.as_ref(), - target_uuid.as_slice(), - &target_sk, - &server_pk, - ) - .await - { + if target_binding.grant_id.as_ref() != grant_id.as_ref() { + log::warn!("Easy access grant id mismatch"); return false; } log::info!("Easy access grant verified"); @@ -2729,8 +2752,10 @@ impl Connection { } match lr.union { Some(login_request::Union::FileTransfer(ft)) => { - if !Self::permission(keys::OPTION_ENABLE_FILE_TRANSFER, &self.controlled_config) - { + if !Self::permission( + keys::OPTION_ENABLE_FILE_TRANSFER, + &self.control_permissions, + ) { self.send_login_error("No permission of file transfer") .await; sleep(1.).await; @@ -2739,7 +2764,7 @@ impl Connection { self.file_transfer = Some((ft.dir, ft.show_hidden)); } Some(login_request::Union::ViewCamera(_vc)) => { - if !Self::permission(keys::OPTION_ENABLE_CAMERA, &self.controlled_config) { + if !Self::permission(keys::OPTION_ENABLE_CAMERA, &self.control_permissions) { self.send_login_error("No permission of viewing camera") .await; sleep(1.).await; @@ -2748,7 +2773,7 @@ impl Connection { self.view_camera = true; } Some(login_request::Union::Terminal(terminal)) => { - if !Self::permission(keys::OPTION_ENABLE_TERMINAL, &self.controlled_config) { + if !Self::permission(keys::OPTION_ENABLE_TERMINAL, &self.control_permissions) { self.send_login_error("No permission of terminal").await; sleep(1.).await; return false; @@ -2769,7 +2794,7 @@ impl Connection { self.terminal_service_id = terminal.service_id; } Some(login_request::Union::PortForward(mut pf)) => { - if !Self::permission(keys::OPTION_ENABLE_TUNNEL, &self.controlled_config) { + if !Self::permission(keys::OPTION_ENABLE_TUNNEL, &self.control_permissions) { self.send_login_error("No permission of IP tunneling").await; sleep(1.).await; return false; @@ -2873,22 +2898,13 @@ impl Connection { && is_logon(); if self.verify_easy_access().await { - // Consume the token so it cannot be reused - if let Some(c) = self.controlled_config.as_mut() { - c.easy_access_signature = Default::default(); - c.manager_id = Default::default(); - } // Easy access: token verified, skip password validation and click accept - if err_msg.is_empty() { - #[cfg(target_os = "linux")] - self.linux_headless_handle.wait_desktop_cm_ready().await; - if !self.send_logon_response_and_keep_alive().await { - return false; - } - self.try_start_cm(lr.my_id.clone(), lr.my_name.clone(), self.authorized); - } else { - self.send_login_error(err_msg).await; + #[cfg(target_os = "linux")] + self.linux_headless_handle.wait_desktop_cm_ready().await; + if !self.send_logon_response_and_keep_alive().await { + return false; } + self.try_start_cm(lr.my_id.clone(), lr.my_name.clone(), self.authorized); } else if (password::approve_mode() == ApproveMode::Click && !allow_logon_screen_password) || password::approve_mode() == ApproveMode::Both && !password::has_valid_password() @@ -6197,17 +6213,17 @@ impl Retina { } } -/// Get control permission state from CONNECTION_CONFIG_ARRAY. +/// Get control permission state from CONTROL_PERMISSIONS_ARRAY. /// Returns: Some(false) if any disable, Some(true) if any enable (and no disable), None if not set. pub fn get_control_permission_state( - permission: hbb_common::rendezvous_proto::controlled_config::Permission, + permission: hbb_common::rendezvous_proto::control_permissions::Permission, disable_if_has_disabled: bool, ) -> Option { - let controlled_configs = CONTROLLED_CONFIG_ARRAY.lock().unwrap(); + let control_permissions = CONTROL_PERMISSIONS_ARRAY.lock().unwrap(); let mut has_enable = false; let mut has_disable = false; - for (_, cc) in controlled_configs.iter() { - match crate::get_control_permission(cc.control_permissions, permission) { + for (_, cp) in control_permissions.iter() { + match crate::get_control_permission(cp.permissions, permission) { Some(false) => has_disable = true, Some(true) => has_enable = true, None => {} @@ -6447,30 +6463,30 @@ mod raii { } } - pub struct ControlledConfigID { + pub struct ControlPermissionsID { id: i32, - controlled_config: Option, + control_permissions: Option, } - impl Drop for ControlledConfigID { + impl Drop for ControlPermissionsID { fn drop(&mut self) { - if self.controlled_config.is_some() { - let mut lock = CONTROLLED_CONFIG_ARRAY.lock().unwrap(); + if self.control_permissions.is_some() { + let mut lock = CONTROL_PERMISSIONS_ARRAY.lock().unwrap(); lock.retain(|(conn_id, _)| *conn_id != self.id); } } } - impl ControlledConfigID { - pub fn new(id: i32, controlled_config: &Option) -> Self { - if let Some(s) = controlled_config { - CONTROLLED_CONFIG_ARRAY + impl ControlPermissionsID { + pub fn new(id: i32, control_permissions: &Option) -> Self { + if let Some(s) = control_permissions { + CONTROL_PERMISSIONS_ARRAY .lock() .unwrap() .push((id, s.clone())); } Self { id, - controlled_config: controlled_config.clone(), + control_permissions: control_permissions.clone(), } } }