mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-09-15 00:41:01 +03:00
swtich_code for hbbs (#15615)
* swtich_code for hbbs to bypass ACL * improve register_switch_grant: skip public server, log at error level Also document why registration is fire-and-forget with no retry: the peer connects within seconds, so a late retry would land after its punch request was already rejected; a failed switch is recovered by the user triggering it again, which registers a fresh grant. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * add timestamp Signed-off-by: 21pages <sunboeasy@gmail.com> * fix(switch-sides): handle grant registration clock skew - retry registration once with the server-provided timestamp - require an explicit accepted response from hbbs - report malformed or incomplete responses Signed-off-by: 21pages <sunboeasy@gmail.com> * fix(switch-sides): register grants with code verifiers - send a derived verifier instead of the raw switch code - use detached signatures for grant registration - add verifier and signed-message tests Signed-off-by: 21pages <sunboeasy@gmail.com> --------- Signed-off-by: 21pages <sunboeasy@gmail.com> Co-authored-by: 21pages <sunboeasy@gmail.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -426,8 +426,8 @@ impl Client {
|
|||||||
NatType::from_i32(my_nat_type).unwrap_or(NatType::UNKNOWN_NAT)
|
NatType::from_i32(my_nat_type).unwrap_or(NatType::UNKNOWN_NAT)
|
||||||
};
|
};
|
||||||
|
|
||||||
if !key.is_empty() && !token.is_empty() {
|
let switch_code = interface.get_switch_code();
|
||||||
// mainly for the security of token
|
if !key.is_empty() && (!token.is_empty() || !switch_code.is_empty()) {
|
||||||
secure_tcp(&mut socket, &key)
|
secure_tcp(&mut socket, &key)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| anyhow!("Failed to secure tcp: {}", e))?;
|
.map_err(|e| anyhow!("Failed to secure tcp: {}", e))?;
|
||||||
@@ -469,6 +469,7 @@ impl Client {
|
|||||||
udp_port: udp_nat_port as _,
|
udp_port: udp_nat_port as _,
|
||||||
force_relay: interface.is_force_relay(),
|
force_relay: interface.is_force_relay(),
|
||||||
socket_addr_v6: ipv6.1.unwrap_or_default(),
|
socket_addr_v6: ipv6.1.unwrap_or_default(),
|
||||||
|
switch_code,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
});
|
});
|
||||||
for i in 1..=3 {
|
for i in 1..=3 {
|
||||||
@@ -716,6 +717,7 @@ impl Client {
|
|||||||
let mut direct = !conn.is_err();
|
let mut direct = !conn.is_err();
|
||||||
if interface.is_force_relay() || conn.is_err() {
|
if interface.is_force_relay() || conn.is_err() {
|
||||||
if !relay_server.is_empty() {
|
if !relay_server.is_empty() {
|
||||||
|
let switch_code = interface.get_switch_code();
|
||||||
conn = Self::request_relay(
|
conn = Self::request_relay(
|
||||||
peer_id,
|
peer_id,
|
||||||
relay_server.to_owned(),
|
relay_server.to_owned(),
|
||||||
@@ -724,6 +726,7 @@ impl Client {
|
|||||||
key,
|
key,
|
||||||
token,
|
token,
|
||||||
conn_type,
|
conn_type,
|
||||||
|
&switch_code,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
if let Err(e) = conn {
|
if let Err(e) = conn {
|
||||||
@@ -844,6 +847,7 @@ impl Client {
|
|||||||
key: &str,
|
key: &str,
|
||||||
token: &str,
|
token: &str,
|
||||||
conn_type: ConnType,
|
conn_type: ConnType,
|
||||||
|
switch_code: &str,
|
||||||
) -> ResultType<Stream> {
|
) -> ResultType<Stream> {
|
||||||
let mut succeed = false;
|
let mut succeed = false;
|
||||||
let mut uuid = "".to_owned();
|
let mut uuid = "".to_owned();
|
||||||
@@ -855,8 +859,7 @@ impl Client {
|
|||||||
.await
|
.await
|
||||||
.with_context(|| "Failed to connect to rendezvous server")?;
|
.with_context(|| "Failed to connect to rendezvous server")?;
|
||||||
|
|
||||||
if !key.is_empty() && !token.is_empty() {
|
if !key.is_empty() && (!token.is_empty() || !switch_code.is_empty()) {
|
||||||
// mainly for the security of token
|
|
||||||
secure_tcp(&mut socket, key).await?;
|
secure_tcp(&mut socket, key).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -877,6 +880,7 @@ impl Client {
|
|||||||
uuid: uuid.clone(),
|
uuid: uuid.clone(),
|
||||||
relay_server: relay_server.clone(),
|
relay_server: relay_server.clone(),
|
||||||
secure,
|
secure,
|
||||||
|
switch_code: switch_code.to_owned(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
});
|
});
|
||||||
socket.send(&msg_out).await?;
|
socket.send(&msg_out).await?;
|
||||||
@@ -3754,6 +3758,16 @@ pub trait Interface: Send + Clone + 'static + Sized {
|
|||||||
self.get_lch().read().unwrap().force_relay
|
self.get_lch().read().unwrap().force_relay
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_switch_code(&self) -> String {
|
||||||
|
match self.get_lch().read().unwrap().switch_uuid.clone() {
|
||||||
|
Some(u) if !u.is_empty() => {
|
||||||
|
use hbb_common::sodiumoxide::crypto::hash::sha256;
|
||||||
|
crate::encode64(sha256::hash(u.as_bytes()).0)
|
||||||
|
}
|
||||||
|
_ => String::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn swap_modifier_mouse(&self, _msg: &mut hbb_common::protos::message::MouseEvent) {}
|
fn swap_modifier_mouse(&self, _msg: &mut hbb_common::protos::message::MouseEvent) {}
|
||||||
|
|
||||||
fn update_direct(&self, direct: Option<bool>) {
|
fn update_direct(&self, direct: Option<bool>) {
|
||||||
|
|||||||
@@ -308,3 +308,135 @@ fn handle_config_options(config_options: HashMap<String, String>) {
|
|||||||
pub fn is_pro() -> bool {
|
pub fn is_pro() -> bool {
|
||||||
PRO.lock().unwrap().clone()
|
PRO.lock().unwrap().clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fire-and-forget by design: the switch flow must not block on this POST.
|
||||||
|
// If the device clock is outside the server's accepted window, the server
|
||||||
|
// returns its current Unix time and this task re-signs and retries once.
|
||||||
|
#[cfg(feature = "flutter")]
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
|
pub fn register_switch_grant(switch_uuid: String) {
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let api_server = crate::ui_interface::get_api_server();
|
||||||
|
if api_server.is_empty() || crate::is_public(&api_server) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
use hbb_common::sodiumoxide::crypto::{hash::sha256, sign};
|
||||||
|
let switch_code = crate::encode64(sha256::hash(switch_uuid.as_bytes()).0);
|
||||||
|
let switch_code_verifier = switch_code_verifier(&switch_code);
|
||||||
|
let timestamp = (hbb_common::get_time() / 1000).to_string();
|
||||||
|
let id = Config::get_id();
|
||||||
|
let kp = Config::get_key_pair();
|
||||||
|
let Some(sk) = sign::SecretKey::from_slice(&kp.0) else {
|
||||||
|
log::error!("Failed to register switch grant: no device key");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let url = format!("{}/api/switch-grant", api_server);
|
||||||
|
let mut timestamp = timestamp;
|
||||||
|
for attempt in 0..2 {
|
||||||
|
let signature = sign::sign_detached(
|
||||||
|
&switch_grant_signed_msg(&id, &switch_code_verifier, ×tamp),
|
||||||
|
&sk,
|
||||||
|
);
|
||||||
|
let body = json!({
|
||||||
|
"id": &id,
|
||||||
|
"switch_code_verifier": &switch_code_verifier,
|
||||||
|
"timestamp": ×tamp,
|
||||||
|
"signature": crate::encode64(signature.to_bytes()),
|
||||||
|
})
|
||||||
|
.to_string();
|
||||||
|
let response = match crate::post_request(url.clone(), body, "").await {
|
||||||
|
Ok(response) => response,
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Failed to register switch grant: {}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let response = match serde_json::from_str::<Value>(&response) {
|
||||||
|
Ok(response) => response,
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Failed to register switch grant: invalid response: {}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
match response.get("accepted").and_then(Value::as_bool) {
|
||||||
|
Some(true) => return,
|
||||||
|
Some(false) => {}
|
||||||
|
None => {
|
||||||
|
log::error!("Failed to register switch grant: missing accepted response");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let Some(server_time) = response["server_time"].as_i64() else {
|
||||||
|
log::error!("Failed to register switch grant: rejected by server");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if attempt == 0 {
|
||||||
|
log::warn!("Switch grant timestamp rejected, retrying with server time");
|
||||||
|
timestamp = server_time.to_string();
|
||||||
|
} else {
|
||||||
|
log::error!("Failed to register switch grant after retrying with server time");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "flutter")]
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
|
fn switch_code_verifier(switch_code: &str) -> String {
|
||||||
|
use hbb_common::sodiumoxide::crypto::hash::sha256;
|
||||||
|
|
||||||
|
let prefix = b"switch-grant-verifier\0";
|
||||||
|
let mut msg = Vec::with_capacity(prefix.len() + switch_code.len());
|
||||||
|
msg.extend_from_slice(prefix);
|
||||||
|
msg.extend_from_slice(switch_code.as_bytes());
|
||||||
|
crate::encode64(sha256::hash(&msg).0)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "flutter")]
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
|
fn switch_grant_signed_msg(id: &str, switch_code_verifier: &str, timestamp: &str) -> Vec<u8> {
|
||||||
|
let mut msg =
|
||||||
|
Vec::with_capacity(13 + id.len() + 1 + switch_code_verifier.len() + 1 + timestamp.len());
|
||||||
|
msg.extend_from_slice(b"switch-grant\0");
|
||||||
|
msg.extend_from_slice(id.as_bytes());
|
||||||
|
msg.push(0);
|
||||||
|
msg.extend_from_slice(switch_code_verifier.as_bytes());
|
||||||
|
msg.push(0);
|
||||||
|
msg.extend_from_slice(timestamp.as_bytes());
|
||||||
|
msg
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(
|
||||||
|
test,
|
||||||
|
feature = "flutter",
|
||||||
|
not(any(target_os = "android", target_os = "ios"))
|
||||||
|
))]
|
||||||
|
mod tests {
|
||||||
|
use super::{switch_code_verifier, switch_grant_signed_msg};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_switch_code_verifier_is_not_raw_switch_code() {
|
||||||
|
let switch_code = "code-abc";
|
||||||
|
let verifier = switch_code_verifier(switch_code);
|
||||||
|
assert_ne!(verifier, switch_code);
|
||||||
|
assert_eq!(verifier, switch_code_verifier(switch_code));
|
||||||
|
assert_eq!(
|
||||||
|
verifier,
|
||||||
|
"dMIn3uiPe77XodFB5IKi7PrKJ7l7+zVquNn0ObSaHQc="
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_switch_grant_signed_msg_layout() {
|
||||||
|
let expected: Vec<u8> = [
|
||||||
|
&b"switch-grant\0"[..],
|
||||||
|
b"id1",
|
||||||
|
b"\0",
|
||||||
|
b"c1",
|
||||||
|
b"\0",
|
||||||
|
b"1700000000",
|
||||||
|
]
|
||||||
|
.concat();
|
||||||
|
assert_eq!(switch_grant_signed_msg("id1", "c1", "1700000000"), expected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -978,6 +978,7 @@ async fn handle(data: Data, stream: &mut Connection) {
|
|||||||
Data::SwitchSidesRequest(id) => {
|
Data::SwitchSidesRequest(id) => {
|
||||||
let uuid = uuid::Uuid::new_v4();
|
let uuid = uuid::Uuid::new_v4();
|
||||||
crate::server::insert_switch_sides_uuid(id, uuid.clone());
|
crate::server::insert_switch_sides_uuid(id, uuid.clone());
|
||||||
|
crate::hbbs_http::sync::register_switch_grant(uuid.to_string());
|
||||||
allow_err!(
|
allow_err!(
|
||||||
stream
|
stream
|
||||||
.send(&Data::SwitchSidesRequest(uuid.to_string()))
|
.send(&Data::SwitchSidesRequest(uuid.to_string()))
|
||||||
|
|||||||
Reference in New Issue
Block a user