diff --git a/apps/desktop/src-tauri/src/rustdesk.rs b/apps/desktop/src-tauri/src/rustdesk.rs index 59aaf34..00b0e40 100644 --- a/apps/desktop/src-tauri/src/rustdesk.rs +++ b/apps/desktop/src-tauri/src/rustdesk.rs @@ -34,6 +34,14 @@ 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 RUSTDESK_CONFIG_FILES: [&str; 6] = [ + "RustDesk.toml", + "RustDesk_local.toml", + "RustDesk2.toml", + "password", + "passwd", + "passwd.txt", +]; const CREATE_NO_WINDOW: u32 = 0x08000000; static PROVISION_MUTEX: Lazy> = Lazy::new(|| Mutex::new(())); @@ -93,6 +101,13 @@ pub fn ensure_rustdesk( )), } + match purge_existing_rustdesk_profiles() { + Ok(_) => log_event("Configurações antigas do RustDesk limpas antes da reaplicação"), + Err(error) => log_event(&format!( + "Aviso: não foi possível limpar completamente os perfis existentes do RustDesk ({error})" + )), + } + if let Some(value) = config_string.and_then(|raw| { let trimmed = raw.trim(); if trimmed.is_empty() { None } else { Some(trimmed) } @@ -137,7 +152,10 @@ pub fn ensure_rustdesk( } match ensure_password_files(&password) { - Ok(_) => log_event("Senha e flags de segurança gravadas em todos os perfis do RustDesk"), + Ok(_) => { + log_event("Senha e flags de segurança gravadas em todos os perfis do RustDesk"); + log_password_replication(&password); + } Err(error) => log_event(&format!("Falha ao persistir senha nos perfis: {error}")), } @@ -707,6 +725,57 @@ fn replicate_password_artifacts() -> io::Result<()> { Ok(()) } +fn purge_existing_rustdesk_profiles() -> Result<(), String> { + let mut errors = Vec::new(); + let mut cleaned_any = false; + + for dir in remote_id_directories() { + match purge_config_dir(&dir) { + Ok(true) => { + cleaned_any = true; + log_event(&format!( + "Perfis antigos removidos em {}", + dir.display() + )); + } + Ok(false) => {} + Err(error) => errors.push(format!("{} -> {error}", dir.display())), + } + } + + if cleaned_any { + Ok(()) + } else if errors.is_empty() { + Ok(()) + } else { + Err(errors.join(" | ")) + } +} + +fn purge_config_dir(dir: &Path) -> Result { + if !dir.exists() { + return Ok(false); + } + + let mut removed = false; + fs::create_dir_all(dir)?; + + for name in RUSTDESK_CONFIG_FILES { + let path = dir.join(name); + if path.is_dir() { + fs::remove_dir_all(&path)?; + removed = true; + continue; + } + if path.exists() { + fs::remove_file(&path)?; + removed = true; + } + } + + Ok(removed) +} + fn run_powershell_elevated(script: &str) -> Result<(), String> { let temp_dir = env::temp_dir(); let payload = temp_dir.join("raven_payload.ps1"); @@ -991,6 +1060,65 @@ fn run_with_args(exe_path: &Path, args: &[&str]) -> Result<(), RustdeskError> { Ok(()) } +fn log_password_replication(secret: &str) { + for dir in remote_id_directories() { + let local_path = dir.join("RustDesk_local.toml"); + match read_password_from_file(&local_path) { + Some(value) if value == secret => { + log_event(&format!( + "Senha confirmada em {} ({})", + local_path.display(), + mask_secret(&value) + )); + } + Some(value) => { + log_event(&format!( + "Aviso: senha divergente ({}) em {}", + mask_secret(&value), + local_path.display() + )); + } + None => { + log_event(&format!( + "Aviso: chave 'password' não encontrada em {}", + local_path.display() + )); + } + } + } +} + +fn read_password_from_file(path: &Path) -> Option { + let content = fs::read_to_string(path).ok()?; + for line in content.lines() { + if let Some(value) = parse_assignment(line, "password") { + return Some(value); + } + } + None +} + +fn mask_secret(secret: &str) -> String { + if secret.is_empty() { + return "".to_string(); + } + let chars: Vec = secret.chars().collect(); + if chars.len() <= 4 { + return "*".repeat(chars.len()); + } + let prefix: String = chars.iter().take(2).copied().collect(); + let suffix: String = chars + .iter() + .rev() + .take(2) + .copied() + .collect::>() + .into_iter() + .rev() + .collect(); + format!("{}***{}", prefix, suffix) +} + fn hidden_command(program: impl AsRef) -> Command { let mut cmd = Command::new(program); cmd.creation_flags(CREATE_NO_WINDOW); diff --git a/docs/RUSTDESK-PROVISIONING.md b/docs/RUSTDESK-PROVISIONING.md index 857ceda..4c9853e 100644 --- a/docs/RUSTDESK-PROVISIONING.md +++ b/docs/RUSTDESK-PROVISIONING.md @@ -54,7 +54,25 @@ Fluxo ideal: - 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`. + - **Replicação completa de perfis**: após aplicar `--password`, limpamos quaisquer arquivos antigos (`RustDesk.toml`, `RustDesk_local.toml`, `password`, `passwd*`) em `%APPDATA%`, `ProgramData`, `LocalService` e `LocalSystem` e, em seguida, reescrevemos tudo (via `write_toml_kv`). `verification-method = "use-permanent-password"` e `approve-mode = "password"` são aplicados em todos os perfis, junto com a cópia dos artefatos `password/passwd/passwd.txt`. + - **Validação por arquivo**: o Raven agora registra no `rustdesk.log` se o `password` gravado em cada `RustDesk_local.toml` coincide com o PIN configurado. Para inspecionar manualmente, rode no PowerShell (e valide que todas as linhas exibem o PIN esperado): + + ```powershell + $targets = @( + "$env:APPDATA\RustDesk\config\RustDesk_local.toml", + "$env:ProgramData\RustDesk\config\RustDesk_local.toml", + "C:\\Windows\\ServiceProfiles\\LocalService\\AppData\\Roaming\\RustDesk\\config\\RustDesk_local.toml", + "C:\\Windows\\System32\\config\\systemprofile\\AppData\\Roaming\\RustDesk\\config\\RustDesk_local.toml" + ) + foreach ($path in $targets) { + if (Test-Path $path) { + "`n$path" + Select-String -Path $path -Pattern '^password' + } else { + "`n$path (inexistente)" + } + } + ``` - **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