Hardening RustDesk provisioning flow

This commit is contained in:
Esdras Renan 2025-11-12 14:10:56 -03:00
parent f3d622eedd
commit ddcff6768d
2 changed files with 184 additions and 46 deletions

View file

@ -6,6 +6,7 @@ use once_cell::sync::Lazy;
use parking_lot::Mutex;
use reqwest::blocking::Client;
use serde::Deserialize;
use serde_json::{Map as JsonMap, Value as JsonValue};
use sha2::{Digest, Sha256};
use std::env;
use std::ffi::OsStr;
@ -27,6 +28,10 @@ const SERVICE_NAME: &str = "RustDesk";
const CACHE_DIR_NAME: &str = "Rever\\RustDeskCache";
const LOCAL_SERVICE_CONFIG: &str = r"C:\\Windows\\ServiceProfiles\\LocalService\\AppData\\Roaming\\RustDesk\\config";
const LOCAL_SYSTEM_CONFIG: &str = r"C:\\Windows\\System32\\config\\systemprofile\\AppData\\Roaming\\RustDesk\\config";
const APP_IDENTIFIER: &str = "br.com.esdrasrenan.sistemadechamados";
const MACHINE_STORE_FILENAME: &str = "machine-agent.json";
const ACL_FLAG_FILENAME: &str = "rustdesk_acl_unlocked.flag";
const RUSTDESK_ACL_STORE_KEY: &str = "rustdeskAclUnlockedAt";
const SECURITY_VERIFICATION_VALUE: &str = "use-permanent-password";
const SECURITY_APPROVE_MODE_VALUE: &str = "password";
const CREATE_NO_WINDOW: u32 = 0x08000000;
@ -81,6 +86,13 @@ pub fn ensure_rustdesk(
"RustDesk já instalado, usando binário existente"
});
match stop_rustdesk_processes() {
Ok(_) => log_event("Instâncias existentes do RustDesk encerradas"),
Err(error) => log_event(&format!(
"Aviso: não foi possível parar completamente o RustDesk antes da reprovisionamento ({error})"
)),
}
if let Some(value) = config_string.and_then(|raw| {
let trimmed = raw.trim();
if trimmed.is_empty() { None } else { Some(trimmed) }
@ -120,19 +132,15 @@ pub fn ensure_rustdesk(
log_event(&format!("Falha ao ajustar approve-mode via CLI: {error}"));
}
match propagate_password_profile() {
Ok(true) => log_event("Perfil de senha propagado para ProgramData/LocalService"),
Ok(false) => match ensure_password_files(&password) {
Ok(_) => log_event("Senha persistida via fallback nos perfis do RustDesk"),
Err(error) => log_event(&format!("Falha ao persistir senha nos perfis: {error}")),
},
Err(error) => {
log_event(&format!("Falha ao copiar perfil de senha: {error}"));
match ensure_password_files(&password) {
Ok(_) => log_event("Senha persistida via fallback nos perfis do RustDesk"),
Err(inner) => log_event(&format!("Falha ao persistir senha nos perfis: {inner}")),
}
}
Ok(_) => log_event("Perfil base propagado para ProgramData e perfis de serviço"),
Err(error) => log_event(&format!("Falha ao copiar perfil de senha: {error}")),
}
match ensure_password_files(&password) {
Ok(_) => log_event("Senha e flags de segurança gravadas em todos os perfis do RustDesk"),
Err(error) => log_event(&format!("Falha ao persistir senha nos perfis: {error}")),
}
match replicate_password_artifacts() {
Ok(_) => log_event("Artefatos de senha replicados para o serviço do RustDesk"),
Err(error) => log_event(&format!("Falha ao replicar artefatos de senha: {error}")),
@ -272,12 +280,15 @@ fn write_config_files() -> Result<PathBuf, RustdeskError> {
main_path.display()
));
let service_profile = PathBuf::from(LOCAL_SERVICE_CONFIG).join("RustDesk2.toml");
let _ = ensure_service_profiles_writable_preflight();
if let Err(error) = write_file(&service_profile, &config_contents) {
log_event(&format!(
"Falha ao gravar config no perfil do serviço: {error}"
));
for service_dir in service_profile_dirs() {
let service_profile = service_dir.join("RustDesk2.toml");
if let Err(error) = write_file(&service_profile, &config_contents) {
log_event(&format!(
"Falha ao gravar config no perfil do serviço ({}): {error}",
service_profile.display()
));
}
}
if let Some(appdata_path) = user_appdata_config_path("RustDesk2.toml") {
@ -402,6 +413,30 @@ fn ensure_service_running(exe_path: &Path) -> Result<(), RustdeskError> {
}
}
fn stop_rustdesk_processes() -> Result<(), RustdeskError> {
if let Err(error) = run_sc(&["stop", SERVICE_NAME]) {
match error {
RustdeskError::CommandFailed { status: Some(code), .. } if code == 1062 || code == 1060 => {}
_ => log_event(&format!("Não foi possível parar o serviço RustDesk antes da sincronização: {error}")),
}
}
let status = hidden_command("taskkill")
.args(["/F", "/T", "/IM", "rustdesk.exe"])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()?;
if status.success() || matches!(status.code(), Some(128)) {
Ok(())
} else {
Err(RustdeskError::CommandFailed {
command: "taskkill /F /T /IM rustdesk.exe".into(),
status: status.code(),
})
}
}
fn run_sc(args: &[&str]) -> Result<(), RustdeskError> {
let status = hidden_command("sc")
.args(args)
@ -512,6 +547,13 @@ fn service_profile_dirs() -> Vec<PathBuf> {
]
}
fn propagation_destinations() -> Vec<PathBuf> {
let mut dirs = Vec::new();
dirs.push(program_data_config_dir());
dirs.extend(service_profile_dirs());
dirs
}
fn ensure_password_files(secret: &str) -> Result<(), String> {
let mut errors = Vec::new();
@ -598,29 +640,36 @@ fn propagate_password_profile() -> io::Result<bool> {
log_event("AppData do usuário não disponível para copiar RustDesk.toml (propagação ignorada)");
return Ok(false);
};
let src_path = src_dir.join("RustDesk.toml");
if !src_path.exists() {
let files = ["RustDesk.toml", "RustDesk_local.toml"];
let mut propagated = false;
for filename in files {
let src_path = src_dir.join(filename);
if !src_path.exists() {
continue;
}
log_event(&format!(
"Arquivo {} não encontrado; usando fallback de escrita",
"Copiando {} para ProgramData/serviços",
src_path.display()
));
return Ok(false);
}
log_event(&format!(
"Copiando {} para ProgramData/LocalService",
src_path.display()
));
let mut propagated = false;
for dest_root in [program_data_config_dir(), PathBuf::from(LOCAL_SERVICE_CONFIG)] {
let target_path = dest_root.join("RustDesk.toml");
copy_overwrite(&src_path, &target_path)?;
log_event(&format!(
"RustDesk.toml propagado para {}",
target_path.display()
));
propagated = true;
for dest_root in propagation_destinations() {
let target_path = dest_root.join(filename);
copy_overwrite(&src_path, &target_path)?;
log_event(&format!(
"{} propagado para {}",
filename,
target_path.display()
));
propagated = true;
}
}
if !propagated {
log_event("Nenhum arquivo de perfil encontrado para propagação; aplicando fallback");
}
Ok(propagated)
}
@ -628,7 +677,7 @@ fn replicate_password_artifacts() -> io::Result<()> {
let Some(src) = user_appdata_config_dir() else {
return Ok(());
};
let destinations = [program_data_config_dir(), PathBuf::from(LOCAL_SERVICE_CONFIG)];
let destinations = propagation_destinations();
let candidates = ["password", "passwd", "passwd.txt"];
for dest in destinations {
@ -750,28 +799,38 @@ try {{
}
fn ensure_service_profiles_writable_preflight() -> Result<(), String> {
let mut success = false;
let mut last_error: Option<String> = None;
let mut blocked_dirs = Vec::new();
for dir in service_profile_dirs() {
if can_write_dir(&dir) {
success = true;
continue;
if !can_write_dir(&dir) {
blocked_dirs.push(dir);
}
}
if blocked_dirs.is_empty() {
return Ok(());
}
if has_acl_unlock_flag() {
log_event("Perfis do serviço voltaram a bloquear escrita; reaplicando correção de ACL");
} else {
log_event("Executando ajuste inicial de ACL dos perfis do serviço (requer UAC)");
}
let mut last_error: Option<String> = None;
for dir in blocked_dirs.iter() {
log_event(&format!(
"Tentando corrigir ACL via UAC (preflight) em {}...",
dir.display()
));
if let Err(error) = fix_profile_acl(&dir) {
if let Err(error) = fix_profile_acl(dir) {
last_error = Some(error);
continue;
}
if can_write_dir(&dir) {
if can_write_dir(dir) {
log_event(&format!(
"ACL ajustada com sucesso em {}",
dir.display()
));
success = true;
} else {
last_error = Some(format!(
"continua sem permissão para {} mesmo após preflight",
@ -780,7 +839,8 @@ fn ensure_service_profiles_writable_preflight() -> Result<(), String> {
}
}
if success {
if blocked_dirs.iter().all(|dir| can_write_dir(dir)) {
mark_acl_unlock_flag();
Ok(())
} else {
Err(last_error.unwrap_or_else(|| "nenhum perfil de serviço acessível".into()))
@ -978,3 +1038,77 @@ fn append_log(dir: PathBuf, message: &str) -> io::Result<()> {
writeln!(file, "[{timestamp}] {message}")?;
Ok(())
}
fn raven_appdata_root() -> Option<PathBuf> {
env::var("LOCALAPPDATA")
.ok()
.map(|value| Path::new(&value).join(APP_IDENTIFIER))
}
fn machine_store_path() -> Option<PathBuf> {
raven_appdata_root().map(|dir| dir.join(MACHINE_STORE_FILENAME))
}
fn read_machine_store_object() -> Option<JsonMap<String, JsonValue>> {
let path = machine_store_path()?;
let contents = fs::read_to_string(path).ok()?;
let value: JsonValue = serde_json::from_str(&contents).ok()?;
value.as_object().cloned()
}
fn write_machine_store_object(map: JsonMap<String, JsonValue>) -> Result<(), String> {
let path = machine_store_path().ok_or_else(|| "LOCALAPPDATA não disponível".to_string())?;
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).map_err(|error| format!("mkdir AppData: {error}"))?;
}
let serialized = serde_json::to_vec_pretty(&JsonValue::Object(map))
.map_err(|error| format!("serialize machine-agent: {error}"))?;
fs::write(&path, serialized).map_err(|error| format!("write machine-agent: {error}"))?;
Ok(())
}
fn upsert_machine_store_value(key: &str, value: JsonValue) -> Result<(), String> {
let mut map = read_machine_store_object().unwrap_or_else(JsonMap::new);
map.insert(key.to_string(), value);
write_machine_store_object(map)
}
fn machine_store_key_exists(key: &str) -> bool {
read_machine_store_object()
.map(|map| map.contains_key(key))
.unwrap_or(false)
}
fn acl_flag_file_path() -> Option<PathBuf> {
raven_appdata_root().map(|dir| dir.join(ACL_FLAG_FILENAME))
}
fn has_acl_unlock_flag() -> bool {
if let Some(flag) = acl_flag_file_path() {
if flag.exists() {
return true;
}
}
machine_store_key_exists(RUSTDESK_ACL_STORE_KEY)
}
fn mark_acl_unlock_flag() {
let timestamp = Utc::now().timestamp_millis();
if let Some(flag_path) = acl_flag_file_path() {
if let Some(parent) = flag_path.parent() {
let _ = fs::create_dir_all(parent);
}
if let Err(error) = fs::write(&flag_path, timestamp.to_string()) {
log_event(&format!(
"Falha ao gravar flag de ACL em {}: {error}",
flag_path.display()
));
}
}
if let Err(error) = upsert_machine_store_value(RUSTDESK_ACL_STORE_KEY, JsonValue::from(timestamp)) {
log_event(&format!(
"Falha ao registrar flag de ACL no machine-agent: {error}"
));
}
}