From ddcff6768dd171181d7a29366066fefd7bdcb87c Mon Sep 17 00:00:00 2001 From: Esdras Renan Date: Wed, 12 Nov 2025 14:10:56 -0300 Subject: [PATCH] Hardening RustDesk provisioning flow --- apps/desktop/src-tauri/src/rustdesk.rs | 226 ++++++++++++++++++++----- docs/RUSTDESK-PROVISIONING.md | 4 + 2 files changed, 184 insertions(+), 46 deletions(-) diff --git a/apps/desktop/src-tauri/src/rustdesk.rs b/apps/desktop/src-tauri/src/rustdesk.rs index 55f5308..59aaf34 100644 --- a/apps/desktop/src-tauri/src/rustdesk.rs +++ b/apps/desktop/src-tauri/src/rustdesk.rs @@ -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 { 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 { ] } +fn propagation_destinations() -> Vec { + 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 { 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 = 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 = 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 { + env::var("LOCALAPPDATA") + .ok() + .map(|value| Path::new(&value).join(APP_IDENTIFIER)) +} + +fn machine_store_path() -> Option { + raven_appdata_root().map(|dir| dir.join(MACHINE_STORE_FILENAME)) +} + +fn read_machine_store_object() -> Option> { + 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) -> 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 { + 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}" + )); + } +} diff --git a/docs/RUSTDESK-PROVISIONING.md b/docs/RUSTDESK-PROVISIONING.md index 17da77b..857ceda 100644 --- a/docs/RUSTDESK-PROVISIONING.md +++ b/docs/RUSTDESK-PROVISIONING.md @@ -52,6 +52,10 @@ Fluxo ideal: - Instala/atualiza binário, configura `RustDesk2.toml` com `relay-server`, `api-server`, etc. - Define ID determinístico (hash do `machine_id`, mas depois compara com `--get-id` e usa o “reportado” caso o serviço tenha um ID próprio). - Reinicia serviço `RustDesk` (`sc start RustDesk` — pode falhar com `status Some(5)` se não há privilégio admin). Mesmo com falha, a CLI continua e grava o ID nos arquivos. + - **Autoelevação única**: na primeira execução do botão “Preparar”, o Raven dispara um PowerShell elevado (`takeown + icacls`) para liberar ACL dos perfis `LocalService` e `LocalSystem`. O sucesso grava `rustdeskAclUnlockedAt` dentro de `%LOCALAPPDATA%\br.com.esdrasrenan.sistemadechamados\machine-agent.json` e cria o flag `rustdesk_acl_unlocked.flag`, evitando novos prompts de UAC nas execuções seguintes. + - **Kill/restart seguro**: antes de tocar nos TOML, o Raven roda `sc stop RustDesk` + `taskkill /F /T /IM rustdesk.exe`. Isso garante que nenhum cliente sobrescreva o `RustDesk_local.toml` enquanto aplicamos a senha. + - **Replicação completa de perfis**: após aplicar `--password`, copiamos `RustDesk.toml` e `RustDesk_local.toml` do `%APPDATA%` para `C:\ProgramData\RustDesk\config`, `LocalService` e `LocalSystem`. Em seguida, escrevemos (via `write_toml_kv`) `password`, `verification-method = "use-permanent-password"` e `approve-mode = "password"` em **todos** os perfis, além de replicar `password/passwd/passwd.txt`. + - **Reforço contínuo**: toda nova execução do botão “Preparar” repete o ciclo (kill → copiar → gravar flags → `sc start`). Se alguém voltar manualmente para “Use both”, o Raven mata o processo, reescreve os TOML e reinicia o serviço — o RustDesk volta travado na senha permanente com o PIN registrado nos logs (`rustdesk.log`). - **Sincronização**: `syncRustdeskAccess(machineToken, info)` chama `/api/machines/remote-access`. Há retries automáticos: ```ts if (response.status === 401/500 contendo "token revogado") {