app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

commit 7ff92e96faf93269a294fe0963318e2a43223c5b
parent c4d4c3571826963ac56a136ec7f0ced1bd553c29
Author: triesap <tyson@radroots.org>
Date:   Fri, 20 Mar 2026 18:52:27 +0000

desktop: harden app data directory permissions

Diffstat:
Mcrates/desktop/src/main.rs | 72++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
1 file changed, 64 insertions(+), 8 deletions(-)

diff --git a/crates/desktop/src/main.rs b/crates/desktop/src/main.rs @@ -11,6 +11,8 @@ use radroots_nostr_accounts::prelude::{ RadrootsNostrAccountsManager, RadrootsNostrFileAccountStore, RadrootsNostrSecretVaultOsKeyring, RadrootsNostrSelectedAccountStatus, }; +#[cfg(target_os = "macos")] +use std::path::{Path, PathBuf}; use std::sync::Arc; #[cfg(target_os = "macos")] @@ -29,22 +31,52 @@ struct DesktopBackend; impl DesktopBackend { #[cfg(target_os = "macos")] - fn app_data_root() -> Result<std::path::PathBuf, String> { + fn radroots_root() -> Result<PathBuf, String> { let base_dirs = BaseDirs::new().ok_or_else(|| "failed to resolve home directory".to_owned())?; - Ok(base_dirs - .home_dir() - .join(".radroots") - .join("app") - .join("desktop")) + Ok(base_dirs.home_dir().join(".radroots")) + } + + #[cfg(target_os = "macos")] + fn app_data_root() -> Result<PathBuf, String> { + Ok(Self::radroots_root()?.join("app").join("desktop")) + } + + #[cfg(target_os = "macos")] + fn private_directory_chain(root: &Path, leaf: &Path) -> Result<Vec<PathBuf>, String> { + let relative = leaf + .strip_prefix(root) + .map_err(|_| "private directory escaped radroots root".to_owned())?; + let mut current = root.to_path_buf(); + let mut chain = vec![current.clone()]; + for component in relative.components() { + current.push(component); + chain.push(current.clone()); + } + Ok(chain) + } + + #[cfg(target_os = "macos")] + fn ensure_private_directory_tree(leaf: &Path) -> Result<(), String> { + use std::os::unix::fs::PermissionsExt; + + std::fs::create_dir_all(leaf) + .map_err(|source| format!("failed to create accounts directory: {source}"))?; + + for path in Self::private_directory_chain(&Self::radroots_root()?, leaf)? { + std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o700)).map_err( + |source| format!("failed to set private directory permissions: {source}"), + )?; + } + + Ok(()) } #[cfg(target_os = "macos")] fn accounts_manager() -> Result<RadrootsNostrAccountsManager, String> { let accounts_path = Self::app_data_root()?.join("nostr").join("accounts.json"); if let Some(parent) = accounts_path.parent() { - std::fs::create_dir_all(parent) - .map_err(|source| format!("failed to create accounts directory: {source}"))?; + Self::ensure_private_directory_tree(parent)?; } let store = Arc::new(RadrootsNostrFileAccountStore::new(accounts_path)); @@ -143,3 +175,27 @@ fn main() -> eframe::Result<()> { Box::new(|_cc| Ok(Box::new(RadrootsApp::new(Box::new(DesktopBackend))))), ) } + +#[cfg(all(test, target_os = "macos"))] +mod tests { + use super::DesktopBackend; + use std::path::PathBuf; + + #[test] + fn private_directory_chain_covers_only_radroots_subtree() { + let root = PathBuf::from("/tmp/example/.radroots"); + let leaf = root.join("app").join("desktop").join("nostr"); + + let chain = DesktopBackend::private_directory_chain(&root, &leaf).unwrap(); + + assert_eq!( + chain, + vec![ + PathBuf::from("/tmp/example/.radroots"), + PathBuf::from("/tmp/example/.radroots/app"), + PathBuf::from("/tmp/example/.radroots/app/desktop"), + PathBuf::from("/tmp/example/.radroots/app/desktop/nostr"), + ] + ); + } +}