Purge legacy RustDesk configs before provisioning

This commit is contained in:
Esdras Renan 2025-11-12 14:25:50 -03:00
parent ddcff6768d
commit daca17a93d
2 changed files with 148 additions and 2 deletions

View file

@ -34,6 +34,14 @@ const ACL_FLAG_FILENAME: &str = "rustdesk_acl_unlocked.flag";
const RUSTDESK_ACL_STORE_KEY: &str = "rustdeskAclUnlockedAt"; const RUSTDESK_ACL_STORE_KEY: &str = "rustdeskAclUnlockedAt";
const SECURITY_VERIFICATION_VALUE: &str = "use-permanent-password"; const SECURITY_VERIFICATION_VALUE: &str = "use-permanent-password";
const SECURITY_APPROVE_MODE_VALUE: &str = "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; const CREATE_NO_WINDOW: u32 = 0x08000000;
static PROVISION_MUTEX: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(())); static PROVISION_MUTEX: Lazy<Mutex<()>> = 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| { if let Some(value) = config_string.and_then(|raw| {
let trimmed = raw.trim(); let trimmed = raw.trim();
if trimmed.is_empty() { None } else { Some(trimmed) } if trimmed.is_empty() { None } else { Some(trimmed) }
@ -137,7 +152,10 @@ pub fn ensure_rustdesk(
} }
match ensure_password_files(&password) { 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}")), Err(error) => log_event(&format!("Falha ao persistir senha nos perfis: {error}")),
} }
@ -707,6 +725,57 @@ fn replicate_password_artifacts() -> io::Result<()> {
Ok(()) 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<bool, io::Error> {
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> { fn run_powershell_elevated(script: &str) -> Result<(), String> {
let temp_dir = env::temp_dir(); let temp_dir = env::temp_dir();
let payload = temp_dir.join("raven_payload.ps1"); let payload = temp_dir.join("raven_payload.ps1");
@ -991,6 +1060,65 @@ fn run_with_args(exe_path: &Path, args: &[&str]) -> Result<(), RustdeskError> {
Ok(()) 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<String> {
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 "<vazio>".to_string();
}
let chars: Vec<char> = 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::<Vec<char>>()
.into_iter()
.rev()
.collect();
format!("{}***{}", prefix, suffix)
}
fn hidden_command(program: impl AsRef<OsStr>) -> Command { fn hidden_command(program: impl AsRef<OsStr>) -> Command {
let mut cmd = Command::new(program); let mut cmd = Command::new(program);
cmd.creation_flags(CREATE_NO_WINDOW); cmd.creation_flags(CREATE_NO_WINDOW);

View file

@ -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. - 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. - **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. - **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`). - **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: - **Sincronização**: `syncRustdeskAccess(machineToken, info)` chama `/api/machines/remote-access`. Há retries automáticos:
```ts ```ts