Rust CLI for tangled

Move to keyring based session storage to fix macOS auth issues #3

open opened by dunkirk.sh targeting main

This fixes session persistence issues on macOS where login credentials were not being saved to the keychain. Also standardizes the config directory location across all platforms.

Changes#

Keyring Platform Support

  • Fixed macOS keychain integration by using apple-native feature instead of Linux-only sync-secret-service
  • Added platform-specific keyring features:
    • macOS: apple-native (uses macOS Keychain)
    • Linux: sync-secret-service, vendored (uses GNOME Keyring/KWallet)
    • Windows: windows-native (uses Windows Credential Manager)

Config Path Standardization

  • Moved config directory to ~/.config/tangled on all platforms for consistency
  • Previously used platform-specific paths (e.g., ~/Library/Application Support/tangled on macOS)

Error Messages

  • Improved keychain error messages to be more descriptive
Labels

None yet.

assignee

None yet.

Participants 2
AT URI
at://did:plc:krxbvxvis5skq7jj6eot23ul/sh.tangled.repo.pull/3m6d3o7lwnb22
+50 -10
Diff #0
+30 -3
Cargo.lock
··· 387 "libc", 388 ] 389 390 [[package]] 391 name = "core-foundation-sys" 392 version = "0.8.7" ··· 1117 source = "registry+https://github.com/rust-lang/crates.io-index" 1118 checksum = "eebcc3aff044e5944a8fbaf69eb277d11986064cba30c468730e8b9909fb551c" 1119 dependencies = [ 1120 "dbus-secret-service", 1121 "log", 1122 "openssl", 1123 "zeroize", 1124 ] 1125 ··· 1316 "openssl-probe", 1317 "openssl-sys", 1318 "schannel", 1319 - "security-framework", 1320 "security-framework-sys", 1321 "tempfile", 1322 ] ··· 1831 checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" 1832 dependencies = [ 1833 "bitflags", 1834 - "core-foundation", 1835 "core-foundation-sys", 1836 "libc", 1837 "security-framework-sys", ··· 2055 checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" 2056 dependencies = [ 2057 "bitflags", 2058 - "core-foundation", 2059 "system-configuration-sys", 2060 ] 2061
··· 387 "libc", 388 ] 389 390 + [[package]] 391 + name = "core-foundation" 392 + version = "0.10.1" 393 + source = "registry+https://github.com/rust-lang/crates.io-index" 394 + checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" 395 + dependencies = [ 396 + "core-foundation-sys", 397 + "libc", 398 + ] 399 + 400 [[package]] 401 name = "core-foundation-sys" 402 version = "0.8.7" ··· 1127 source = "registry+https://github.com/rust-lang/crates.io-index" 1128 checksum = "eebcc3aff044e5944a8fbaf69eb277d11986064cba30c468730e8b9909fb551c" 1129 dependencies = [ 1130 + "byteorder", 1131 "dbus-secret-service", 1132 "log", 1133 "openssl", 1134 + "security-framework 2.11.1", 1135 + "security-framework 3.5.1", 1136 + "windows-sys 0.60.2", 1137 "zeroize", 1138 ] 1139 ··· 1330 "openssl-probe", 1331 "openssl-sys", 1332 "schannel", 1333 + "security-framework 2.11.1", 1334 "security-framework-sys", 1335 "tempfile", 1336 ] ··· 1845 checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" 1846 dependencies = [ 1847 "bitflags", 1848 + "core-foundation 0.9.4", 1849 + "core-foundation-sys", 1850 + "libc", 1851 + "security-framework-sys", 1852 + ] 1853 + 1854 + [[package]] 1855 + name = "security-framework" 1856 + version = "3.5.1" 1857 + source = "registry+https://github.com/rust-lang/crates.io-index" 1858 + checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" 1859 + dependencies = [ 1860 + "bitflags", 1861 + "core-foundation 0.10.1", 1862 "core-foundation-sys", 1863 "libc", 1864 "security-framework-sys", ··· 2082 checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" 2083 dependencies = [ 2084 "bitflags", 2085 + "core-foundation 0.9.4", 2086 "system-configuration-sys", 2087 ] 2088
+1 -1
Cargo.toml
··· 41 42 # Storage 43 dirs = "5.0" 44 - keyring = { version = "3.6", features = ["sync-secret-service", "vendored"] } 45 46 # Error Handling 47 anyhow = "1.0"
··· 41 42 # Storage 43 dirs = "5.0" 44 + keyring = "3.6" 45 46 # Error Handling 47 anyhow = "1.0"
+9 -1
crates/tangled-config/Cargo.toml
··· 8 [dependencies] 9 anyhow = { workspace = true } 10 dirs = { workspace = true } 11 - keyring = { workspace = true } 12 serde = { workspace = true, features = ["derive"] } 13 serde_json = { workspace = true } 14 toml = { workspace = true } 15 chrono = { workspace = true } 16
··· 8 [dependencies] 9 anyhow = { workspace = true } 10 dirs = { workspace = true } 11 serde = { workspace = true, features = ["derive"] } 12 serde_json = { workspace = true } 13 toml = { workspace = true } 14 chrono = { workspace = true } 15 16 + [target.'cfg(target_os = "macos")'.dependencies] 17 + keyring = { workspace = true, features = ["apple-native"] } 18 + 19 + [target.'cfg(target_os = "linux")'.dependencies] 20 + keyring = { workspace = true, features = ["sync-secret-service", "vendored"] } 21 + 22 + [target.'cfg(target_os = "windows")'.dependencies] 23 + keyring = { workspace = true, features = ["windows-native"] } 24 +
+8 -3
crates/tangled-config/src/config.rs
··· 2 use std::path::{Path, PathBuf}; 3 4 use anyhow::{Context, Result}; 5 - use dirs::config_dir; 6 use serde::{Deserialize, Serialize}; 7 8 #[derive(Debug, Clone, Serialize, Deserialize, Default)] ··· 55 } 56 57 pub fn default_config_path() -> Result<PathBuf> { 58 - let base = config_dir().context("Could not determine platform config directory")?; 59 - Ok(base.join("tangled").join("config.toml")) 60 } 61 62 pub fn load_config(path: Option<&Path>) -> Result<Option<RootConfig>> {
··· 2 use std::path::{Path, PathBuf}; 3 4 use anyhow::{Context, Result}; 5 use serde::{Deserialize, Serialize}; 6 7 #[derive(Debug, Clone, Serialize, Deserialize, Default)] ··· 54 } 55 56 pub fn default_config_path() -> Result<PathBuf> { 57 + // Use ~/.config/tangled on all platforms for consistency 58 + let home = std::env::var("HOME") 59 + .or_else(|_| std::env::var("USERPROFILE")) 60 + .context("Could not determine home directory")?; 61 + Ok(PathBuf::from(home) 62 + .join(".config") 63 + .join("tangled") 64 + .join("config.toml")) 65 } 66 67 pub fn load_config(path: Option<&Path>) -> Result<Option<RootConfig>> {
+2 -2
crates/tangled-config/src/keychain.rs
··· 21 pub fn set_password(&self, secret: &str) -> Result<()> { 22 self.entry()? 23 .set_password(secret) 24 - .map_err(|e| anyhow!("keyring error: {e}")) 25 } 26 27 pub fn get_password(&self) -> Result<String> { 28 self.entry()? 29 .get_password() 30 - .map_err(|e| anyhow!("keyring error: {e}")) 31 } 32 33 pub fn delete_password(&self) -> Result<()> {
··· 21 pub fn set_password(&self, secret: &str) -> Result<()> { 22 self.entry()? 23 .set_password(secret) 24 + .map_err(|e| anyhow!("Failed to save credentials to keychain: {e}")) 25 } 26 27 pub fn get_password(&self) -> Result<String> { 28 self.entry()? 29 .get_password() 30 + .map_err(|e| anyhow!("Failed to load credentials from keychain: {e}")) 31 } 32 33 pub fn delete_password(&self) -> Result<()> {

History

1 round 3 comments
sign up or login to add to the discussion
dunkirk.sh submitted #0
1 commit
expand
f2ef3568
fix(config): use platform-specific keyring features and standardize config path
no conflicts, ready to merge
expand 3 comments

I tested this (since my macOS auth was broken without it), and it now works! However, I get a keychain prompt up for every CLI invocation to type in my password. Would be nice if it cached that, but I'm not sure what's going on with the Keychain -> Passwords app migration on macOS in recent versions.

If you select the prompt to never ask for this app, it will work in the future! It does seem to reset whenever the binary changes, so updates will retrigger it, but that seems like an acceptable compromise. The only way to not have it do that would be to sign the app with a developer cert but that seems over the top

Aha it was indeed my binary changing all the time that reset it. Signing with a developer cert for the eventual stable release binary seems like a reasonable solution to this in the longer term.