Files
rustdesk/src/trusted_devices/tests.rs
T
fufesou c5201d600b Allow trusted devices for direct IP connections (#16778)
* Allow trusted devices for direct IP connections

Signed-off-by: fufesou <linlong1266@gmail.com>

* test: preserve existing V2 trusted-device credentials

Signed-off-by: fufesou <linlong1266@gmail.com>

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2026-11-01 13:39:15 +08:00

292 lines
11 KiB
Rust

use super::{
authority::{self, Request, Response},
client::Credentials,
config, host, random_token, token_verifier, DeviceInfo, Grants, TOKEN_BYTES,
};
use base::config::keys;
use hbb_common::{
config::{Config, Config2, PeerConfig},
sodiumoxide::crypto::hash::sha256,
};
use std::fs;
mod configuration;
mod credentials;
mod recovery;
const PASSWORD: &str = "permanent-test-password";
pub(crate) fn initialize_config() {
static INITIALIZED: std::sync::Once = std::sync::Once::new();
INITIALIZED.call_once(|| {
*hbb_common::config::APP_NAME.write().unwrap() =
format!("RustDeskV2Test{}", uuid::Uuid::new_v4());
});
}
fn proof(password: &str, token: &[u8]) -> host::LoginProof {
let salt = Config::get_effective_permanent_password_salt();
let challenge = "trusted-device-test".to_owned();
let mut material = hbb_common::config::compute_permanent_password_h1(password, &salt).to_vec();
material.extend_from_slice(challenge.as_bytes());
host::LoginProof {
password: sha256::hash(&material).0.to_vec(),
salt,
challenge,
token: token.to_vec(),
supports_v2: true,
..Default::default()
}
}
fn configure() -> std::collections::HashMap<String, String> {
initialize_config();
let totp = crate::auth_2fa::TOTPInfo {
name: "test".into(),
secret: vec![1; 20],
digits: 6,
created_at: 1,
};
assert!(config::set_password(PASSWORD).unwrap());
let mut options = Config::get_options();
options.insert("2fa".into(), totp.into_string().unwrap());
options.insert(keys::OPTION_ENABLE_TRUSTED_DEVICES.into(), "Y".into());
config::set_options(options.clone()).unwrap();
options
}
async fn begin_enrollment() -> host::Attempt {
let (attempt, valid, _) = host::begin(proof(PASSWORD, &[])).await.unwrap().unwrap();
assert!(!valid);
let mut attempt = attempt.unwrap();
attempt.enroll = true;
attempt
}
async fn is_trusted(token: &[u8]) -> bool {
let (_, valid, _) = host::begin(proof(PASSWORD, token)).await.unwrap().unwrap();
valid
}
fn legacy_proof(hwid: &[u8], info: &DeviceInfo, supports_v2: bool) -> host::LoginProof {
host::LoginProof {
legacy: super::legacy::verifier(hwid, info).unwrap(),
supports_v2,
..proof(PASSWORD, &[])
}
}
#[hbb_common::tokio::test]
async fn password_and_configuration_revocation_gate_real_persisted_grants() {
let mut options = configure();
configuration::malformed_host_field_preserves_authentication();
configuration::legacy_settings_survive_saving().await;
configuration::expiration_changes_preserve_grants().await;
recovery::unknown_expiration_preserves_login_and_settings().await;
recovery::unusable_grant_scope_keeps_current_password_totp_available().await;
assert!(configuration::devices().await.is_empty());
assert!(host::begin(proof("wrong-password", &[])).await.is_err());
let enrollment = begin_enrollment().await;
let token = enrollment.finish(DeviceInfo::default()).await.unwrap();
let (trusted, valid, _) = host::begin(proof(PASSWORD, &token)).await.unwrap().unwrap();
let trusted = trusted.unwrap();
assert!(valid);
trusted.check().await.unwrap();
configuration::encrypted_store_preserves_grants(&token).await;
configuration::renewal_preserves_authorization(&token).await;
let pending = begin_enrollment().await;
options.insert(keys::OPTION_ENABLE_TRUSTED_DEVICES.into(), "N".into());
let disabled = options.clone();
config::update_auth(true, move || config::set_options(disabled))
.await
.unwrap();
assert!(trusted.check().await.is_err());
assert!(pending.finish(DeviceInfo::default()).await.is_err());
options.insert(keys::OPTION_ENABLE_TRUSTED_DEVICES.into(), "Y".into());
config::update_auth(true, move || config::set_options(options))
.await
.unwrap();
assert!(!is_trusted(&token).await);
service_operations_share_and_revoke_persisted_grants().await;
recovery::service_sync_preserves_grants_and_revocations().await;
unavailable_grants_keep_password_totp_available().await;
password_write_failure_is_reported().await;
}
async fn password_write_failure_is_reported() {
let result = config::update_auth(true, || {
let path = Config::file();
fs::rename(&path, path.with_extension("backup"))?;
fs::create_dir(&path)?;
config::set_password("changed-test-password")
})
.await;
let path = Config::file();
fs::remove_dir(&path).unwrap();
fs::rename(path.with_extension("backup"), &path).unwrap();
config::set_password(PASSWORD).unwrap();
assert!(
result.is_err(),
"Password persistence failure must be reported"
);
}
async fn unavailable_grants_keep_password_totp_available() {
Config2::set_trusted_devices_v2("invalid grant store".into()).unwrap();
assert!(host::begin(proof("wrong-password", &[])).await.is_err());
let login = host::begin(proof(PASSWORD, &[])).await;
let (attempt, valid, _) = login.unwrap().unwrap();
assert!(attempt.is_some());
assert!(!valid);
assert!(configuration::devices().await.is_empty());
let enrollment = begin_enrollment().await;
let token = enrollment.finish(DeviceInfo::default()).await.unwrap();
let path = Config2::file();
fs::write(&path, b"\xff").unwrap();
Config::set_option("trusted-device-test-option".into(), "N".into());
let after_save: Config2 =
hbb_common::toml::from_str(&fs::read_to_string(&path).unwrap()).unwrap();
assert_eq!(after_save.options["trusted-device-test-option"], "N");
assert!(Config2::get_trusted_devices_v2().unwrap().is_empty());
assert!(!is_trusted(&token).await);
}
async fn service_operations_share_and_revoke_persisted_grants() {
let enrollment = begin_enrollment().await;
let token = enrollment.finish(DeviceInfo::default()).await.unwrap();
let synced = serde_json::to_value(Config2::get()).unwrap();
assert!(
synced["trusted_devices_v2"].as_str()
== Some(Config2::get_trusted_devices_v2().unwrap().as_str()),
"The service configuration snapshot must include newly enrolled grants"
);
let (pending, valid, _) = host::begin(proof(PASSWORD, &token)).await.unwrap().unwrap();
let mut pending = pending.unwrap();
assert!(valid);
pending.enroll = true;
let devices = configuration::devices().await;
let record_id: [u8; TOKEN_BYTES] =
serde_json::from_value(devices[0]["record_id"].clone()).unwrap();
assert!(!is_trusted(&record_id).await);
let enrollment = begin_enrollment().await;
let other_token = enrollment.finish(DeviceInfo::default()).await.unwrap();
assert!(matches!(
authority::request(Request::Remove(vec![record_id]))
.await
.unwrap(),
Response::Done
));
assert!(pending.check().await.is_err());
assert!(!is_trusted(&token).await);
let (remaining, valid, _) = host::begin(proof(PASSWORD, &other_token))
.await
.unwrap()
.unwrap();
assert!(valid);
let remaining = remaining.unwrap();
remaining.check().await.unwrap();
let stale_config: Config2 = hbb_common::config::load_path(Config2::file());
assert_eq!(configuration::devices().await.len(), 1);
authority::request(Request::Clear).await.unwrap();
// Ordinary settings saves must not restore grants from a stale snapshot.
config::set_config2(stale_config).unwrap();
Config::set_option("trusted-device-test-option".into(), "Y".into());
assert!(remaining.check().await.is_err());
assert!(pending.finish(DeviceInfo::default()).await.is_err());
assert!(!is_trusted(&other_token).await);
assert!(configuration::devices().await.is_empty());
}
const NOW: i64 = 1_700_000_000_000;
const SCOPE: [u8; TOKEN_BYTES] = [7; TOKEN_BYTES];
#[test]
fn only_original_tokens_satisfy_trust_and_use_does_not_refresh_expiry() {
let mut grants = Grants::default();
grants.revoke(Some(SCOPE)).unwrap();
let ticket = grants.begin(&SCOPE).unwrap();
let token = random_token().unwrap();
let expires_at = super::policy::expires_at(NOW, 90);
grants
.enroll_digest(
&ticket,
token_verifier(&token).unwrap(),
DeviceInfo {
time: NOW,
expires_at: Some(expires_at),
..Default::default()
},
)
.unwrap();
for candidate in [
&token[..],
&sha256::hash(&token).0[..],
&token[..TOKEN_BYTES - 1],
&[],
] {
let accepted = grants
.verify_digest(&ticket, token_verifier(candidate).as_ref(), NOW)
.unwrap();
assert_eq!(accepted, candidate == token);
}
let verifier = token_verifier(&token);
assert!(grants
.verify_digest(&ticket, verifier.as_ref(), expires_at - 1)
.unwrap());
assert!(!grants
.verify_digest(&ticket, verifier.as_ref(), expires_at)
.unwrap());
assert!(grants.begin(&[8; TOKEN_BYTES]).is_err());
}
#[test]
fn controller_credentials_are_bound_and_storage_errors_surface() {
initialize_config();
let peer_id = uuid::Uuid::new_v4().to_string();
let path = Config::path(format!("peers/{peer_id}.toml"));
let mut settings = PeerConfig::default();
settings.password = vec![5; TOKEN_BYTES];
settings.info.platform = "Windows".into();
settings
.options
.insert("alias".into(), "trusted-peer".into());
settings.store(&peer_id);
let credentials =
Credentials::new(peer_id.clone(), "server-a".into(), &[1; TOKEN_BYTES]).unwrap();
let token = random_token().unwrap();
credentials.save(&token, settings.info.clone()).unwrap();
assert_eq!(credentials.load().unwrap(), Some(token.to_vec()));
let loaded = PeerConfig::load(&peer_id);
assert_eq!(loaded.password, settings.password);
assert_eq!(loaded.options, settings.options);
assert!(serde_json::to_value(&loaded)
.unwrap()
.get("trusted_device_v2")
.is_none());
settings.store(&peer_id);
assert_eq!(credentials.load().unwrap(), Some(token.to_vec()));
for (namespace, key) in [
("server-b", [1; TOKEN_BYTES]),
("server-a", [2; TOKEN_BYTES]),
] {
assert!(Credentials::new(peer_id.clone(), namespace.into(), &key)
.unwrap()
.load()
.unwrap()
.is_none());
}
recovery::corrupt_controller_credentials_allow_reenrollment(&credentials, &peer_id);
let corrupt = format!("{}\ninvalid = [\n", fs::read_to_string(&path).unwrap());
for corrupt in [corrupt.as_bytes(), b"\xff"] {
fs::write(&path, corrupt).unwrap();
assert!(credentials.load().is_err());
settings.store(&peer_id);
assert_eq!(PeerConfig::load(&peer_id).options, settings.options);
assert!(credentials.load().unwrap().is_none());
}
let blocked_peer = uuid::Uuid::new_v4().to_string();
fs::create_dir_all(Config::path(format!("peers/{blocked_peer}.toml"))).unwrap();
let invalid = Credentials::new(blocked_peer, "server-a".into(), &[1; TOKEN_BYTES]).unwrap();
assert!(invalid.save(&token, settings.info).is_err());
}