From 15a7647e74c5988a59cd0e9612f41776946479c7 Mon Sep 17 00:00:00 2001 From: zhom <2717306+zhom@users.noreply.github.com> Date: Tue, 7 Jul 2026 22:44:31 +0400 Subject: [PATCH] security: restrict secret files to owner-only (0600) --- src-tauri/src/app_dirs.rs | 19 +++++++++++++++++++ src-tauri/src/cloud_auth.rs | 4 +++- src-tauri/src/settings_manager.rs | 9 ++++++--- src-tauri/src/sync/encryption.rs | 1 + src-tauri/src/vpn/storage.rs | 20 ++++++++++---------- 5 files changed, 39 insertions(+), 14 deletions(-) diff --git a/src-tauri/src/app_dirs.rs b/src-tauri/src/app_dirs.rs index a48873d..91e0621 100644 --- a/src-tauri/src/app_dirs.rs +++ b/src-tauri/src/app_dirs.rs @@ -190,6 +190,25 @@ pub fn set_test_cache_dir(dir: PathBuf) -> TestDirGuard { } } +/// Restrict a just-written file to owner-only read/write (`0600`) on Unix so +/// other local users/processes can't read secret material (tokens, E2E +/// password, encrypted vault files). Best-effort: the write already succeeded, +/// so a permission failure is logged, not propagated. On Windows the per-user +/// profile ACL already restricts access, so this is a no-op there. +pub fn restrict_to_owner(path: &std::path::Path) { + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + if let Err(e) = std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600)) { + log::warn!("Failed to restrict permissions on {}: {e}", path.display()); + } + } + #[cfg(not(unix))] + { + let _ = path; + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src-tauri/src/cloud_auth.rs b/src-tauri/src/cloud_auth.rs index eb9f4b0..7fe9560 100644 --- a/src-tauri/src/cloud_auth.rs +++ b/src-tauri/src/cloud_auth.rs @@ -289,6 +289,7 @@ impl CloudAuthManager { file_data.extend_from_slice(&ciphertext); fs::write(file_path, file_data).map_err(|e| format!("Failed to write file: {e}"))?; + crate::app_dirs::restrict_to_owner(file_path); Ok(()) } @@ -409,7 +410,8 @@ impl CloudAuthManager { } let json = serde_json::to_string_pretty(state).map_err(|e| format!("Failed to serialize: {e}"))?; - fs::write(path, json).map_err(|e| format!("Failed to write auth state: {e}"))?; + fs::write(&path, json).map_err(|e| format!("Failed to write auth state: {e}"))?; + crate::app_dirs::restrict_to_owner(&path); Ok(()) } diff --git a/src-tauri/src/settings_manager.rs b/src-tauri/src/settings_manager.rs index 4f3374c..2b1f459 100644 --- a/src-tauri/src/settings_manager.rs +++ b/src-tauri/src/settings_manager.rs @@ -266,7 +266,8 @@ impl SettingsManager { file_data.extend_from_slice(&(ciphertext.len() as u32).to_le_bytes()); file_data.extend_from_slice(&ciphertext); - std::fs::write(token_file, file_data)?; + std::fs::write(&token_file, file_data)?; + crate::app_dirs::restrict_to_owner(std::path::Path::new(&token_file)); Ok(()) } @@ -434,7 +435,8 @@ impl SettingsManager { file_data.extend_from_slice(&(ciphertext.len() as u32).to_le_bytes()); file_data.extend_from_slice(&ciphertext); - std::fs::write(token_file, file_data)?; + std::fs::write(&token_file, file_data)?; + crate::app_dirs::restrict_to_owner(std::path::Path::new(&token_file)); Ok(()) } @@ -573,7 +575,8 @@ impl SettingsManager { file_data.extend_from_slice(&(ciphertext.len() as u32).to_le_bytes()); file_data.extend_from_slice(&ciphertext); - std::fs::write(token_file, file_data)?; + std::fs::write(&token_file, file_data)?; + crate::app_dirs::restrict_to_owner(std::path::Path::new(&token_file)); Ok(()) } diff --git a/src-tauri/src/sync/encryption.rs b/src-tauri/src/sync/encryption.rs index f27916d..3c4340e 100644 --- a/src-tauri/src/sync/encryption.rs +++ b/src-tauri/src/sync/encryption.rs @@ -87,6 +87,7 @@ pub fn store_e2e_password(password: &str) -> Result<(), String> { std::fs::write(&file_path, file_data) .map_err(|e| format!("Failed to write e2e password file: {e}"))?; + crate::app_dirs::restrict_to_owner(std::path::Path::new(&file_path)); Ok(()) } diff --git a/src-tauri/src/vpn/storage.rs b/src-tauri/src/vpn/storage.rs index c2e6c50..07daf32 100644 --- a/src-tauri/src/vpn/storage.rs +++ b/src-tauri/src/vpn/storage.rs @@ -69,6 +69,12 @@ impl VpnStorage { let storage_path = dir.join("vpn_configs.json"); let key_path = dir.join(".vpn_key"); + // Persist a freshly generated key with owner-only (0600) permissions so the + // WireGuard-key-encrypting key isn't left world-readable. + let write_key = |key: &[u8; 32]| { + let _ = fs::write(&key_path, key); + crate::app_dirs::restrict_to_owner(&key_path); + }; let encryption_key = if key_path.exists() { if let Ok(key_data) = fs::read(&key_path) { if key_data.len() == 32 { @@ -77,17 +83,17 @@ impl VpnStorage { key } else { let key: [u8; 32] = rand::rng().random(); - let _ = fs::write(&key_path, key); + write_key(&key); key } } else { let key: [u8; 32] = rand::rng().random(); - let _ = fs::write(&key_path, key); + write_key(&key); key } } else { let key: [u8; 32] = rand::rng().random(); - let _ = fs::write(&key_path, key); + write_key(&key); key }; @@ -124,13 +130,7 @@ impl VpnStorage { // Generate a new key let key: [u8; 32] = rand::rng().random(); let _ = fs::write(&key_path, key); - - // Set restrictive permissions on Unix - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - let _ = fs::set_permissions(&key_path, fs::Permissions::from_mode(0o600)); - } + crate::app_dirs::restrict_to_owner(&key_path); key }