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:
RustDesk
2026-08-04 11:08:59 +08:00
committed by GitHub
parent 2f8822ec7a
commit d752823b8c
3 changed files with 151 additions and 4 deletions

View File

@@ -308,3 +308,135 @@ fn handle_config_options(config_options: HashMap<String, String>) {
pub fn is_pro() -> bool {
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, &timestamp),
&sk,
);
let body = json!({
"id": &id,
"switch_code_verifier": &switch_code_verifier,
"timestamp": &timestamp,
"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);
}
}