diff --git a/apps/figcraft/src/services.rs b/apps/figcraft/src/services.rs index 8d0c1f5..23269fd 100644 --- a/apps/figcraft/src/services.rs +++ b/apps/figcraft/src/services.rs @@ -141,17 +141,31 @@ pub fn save_state(app: &FigApp) { } /// Write `prefs` to `dir` atomically (a temporary file renamed over the old one: a crash leaves -/// either version whole). -fn write_prefs(dir: &Path, prefs: &Prefs) { +/// either version whole). They hold online owner tokens, so on Unix only the owner can read them. +pub(crate) fn write_prefs(dir: &Path, prefs: &Prefs) { let tmp = dir.join("state.json.tmp"); let written = std::fs::create_dir_all(dir) - .and_then(|()| std::fs::write(&tmp, json!({"prefs": prefs}).to_string())) + .and_then(|()| write_private(&tmp, json!({"prefs": prefs}).to_string().as_bytes())) .and_then(|()| std::fs::rename(&tmp, dir.join(STATE))); if let Err(e) = written { log::warn!("preferences: couldn't save them in {}: {e}", dir.display()); } } +/// Write `bytes` to `path`, owner read/write only on Unix from before the first byte (a leftover +/// file from a crash is narrowed too: the mode given at open only applies to new files). +fn write_private(path: &Path, bytes: &[u8]) -> std::io::Result<()> { + use std::io::Write; + let mut o = std::fs::OpenOptions::new(); + o.write(true).create(true).truncate(true); + #[cfg(unix)] + std::os::unix::fs::OpenOptionsExt::mode(&mut o, 0o600); + let mut f = o.open(path)?; + #[cfg(unix)] + f.set_permissions(std::os::unix::fs::PermissionsExt::from_mode(0o600))?; + f.write_all(bytes) +} + impl Services for Desktop { fn pick_open(&mut self, title: &str, exts: &[&str]) -> Option { rfd::FileDialog::new().set_title(title).add_filter("Supported files", exts).pick_file() diff --git a/apps/figcraft/src/services_tests.rs b/apps/figcraft/src/services_tests.rs index 2043299..2390b92 100644 --- a/apps/figcraft/src/services_tests.rs +++ b/apps/figcraft/src/services_tests.rs @@ -66,3 +66,19 @@ fn plugins_are_kept_when_the_app_is_killed() { assert!(!dir.join("state.json.tmp").exists(), "no temporary file is left behind"); let _ = std::fs::remove_dir_all(&dir); } + +#[cfg(unix)] +#[test] +fn preferences_are_readable_by_their_owner_only() { + use std::os::unix::fs::PermissionsExt; + let dir = root("private"); + let mode = |p: &std::path::Path| std::fs::metadata(p).unwrap().permissions().mode() & 0o777; + // An older world-readable file and a leftover temporary one from a crash. + for name in ["state.json", "state.json.tmp"] { + std::fs::write(dir.join(name), "{}").unwrap(); + std::fs::set_permissions(dir.join(name), std::fs::Permissions::from_mode(0o644)).unwrap(); + } + crate::services::write_prefs(&dir, &figcraft_engine::Prefs::default()); + assert_eq!(mode(&dir.join("state.json")), 0o600); + let _ = std::fs::remove_dir_all(&dir); +}