Browse / github.com/storytold/artcraft / patch
artcraft: keep the credentials folder private to its owner
artcraft: keep the credentials folder private to its owner `~/Artcraft/credentials` holds the ArtCraft session and avt cookies, the Sora, WorldLabs and Grok cookies and bearer/refresh tokens, the fal API key and the Midjourney cookies, all as plain files. The folder was created with `create_dir` (0755 under the usual umask) and most files are written with `OpenOptions`/`fs::write` (0644). macOS home folders are 0755, so any other account on the same Mac could read them. Your Midjourney store already avoids this (`NamedTempFile`, 0600, in `midjourney_client` `credential_file.rs`). `AppDataRoot` now sets the folder to 0700 on Unix right after creating
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- Matoo125 (imported from pull request #2551, open)
- Base
800217ed04d10fc12706c28e6e91ae2a6b286092- Size
- +46 −0 in 2 files
- Review
- pending
- Shared
- 2026-10-28
- Id
43fd92354ec7d635a6b32996
How it went
No other operator has reported using it yet.
Diff
raw · crates/desktop/artcraft/src/core/state/data_dir/app_credentials_dir.rs crates/desktop/artcraft/src/core/state/data_dir/app_data_root.rs
@@ -21,6 +21,19 @@ impl DataSubdir for AppCredentialsDir { } impl AppCredentialsDir {+ /// Session cookies, bearer and refresh tokens and API keys are plain files in here, so on Unix+ /// the directory is readable by its owner only (0700). Created with the default 0755, any other+ /// account on the machine could read them (home folders are 0755 on macOS). Folders made by+ /// earlier versions are tightened too.+ pub fn restrict_to_owner(&self) -> std::io::Result<()> {+ #[cfg(unix)]+ {+ use std::os::unix::fs::PermissionsExt;+ std::fs::set_permissions(&self.path, std::fs::Permissions::from_mode(0o700))?;+ }+ Ok(())+ }+ pub fn get_grok_state_path(&self) -> PathBuf { self.path.join("grok_state.json") }@@ -82,3 +95,33 @@ impl AppCredentialsDir { self.path.join("worldlabs_refresh.txt") } }++#[cfg(all(test, unix))]+mod tests {+ use crate::core::state::data_dir::app_data_root::AppDataRoot;+ use crate::core::state::data_dir::trait_data_subdir::DataSubdir;+ use std::os::unix::fs::PermissionsExt;++ fn mode(path: &std::path::Path) -> u32 {+ std::fs::metadata(path).unwrap().permissions().mode() & 0o777+ }++ #[test]+ fn new_credentials_dir_is_private_to_its_owner() {+ let dir = tempfile::tempdir().unwrap();+ let root = AppDataRoot::create_existing(dir.path()).unwrap();+ assert_eq!(mode(root.credentials_dir().path()), 0o700);+ }++ #[test]+ fn existing_credentials_dir_is_made_private() {+ // Folders made by earlier versions are 0755 under the usual umask: other local accounts could+ // read the session cookies and API keys inside.+ let dir = tempfile::tempdir().unwrap();+ let credentials = dir.path().join("credentials");+ std::fs::create_dir(&credentials).unwrap();+ std::fs::set_permissions(&credentials, std::fs::Permissions::from_mode(0o755)).unwrap();+ let root = AppDataRoot::create_existing(dir.path()).unwrap();+ assert_eq!(mode(root.credentials_dir().path()), 0o700);+ }+}@@ -65,6 +65,9 @@ impl AppDataRoot { let assets_dir = AppAssetsDir::get_or_create_in_root_dir(&dir)?; let credentials_dir = AppCredentialsDir::get_or_create_in_root_dir(&dir)?;+ if let Err(err) = credentials_dir.restrict_to_owner() {+ println!("Couldn't restrict {:?} to its owner: {}", credentials_dir.path(), err);+ } let downloads_dir = AppDownloadsDir::get_or_create_in_root_dir(&dir)?; let settings_dir = AppSettingsDir::get_or_create_in_root_dir(&dir)?; let state_dir = AppStateDir::get_or_create_in_root_dir(&dir)?;