Skip to content

Commit 60063ec

Browse files
committed
Linux: encrypted file fallback for cred storage
- When no system keyring (GNOME Keyring / KDE Wallet) is available, credentials now fall back to an encrypted file at ~/.local/share/cmdr/credentials.enc - Uses cocoon crate (Chacha20-Poly1305) with /etc/machine-id as key, 0600 file permissions - All operations try Secret Service first, file backend second - Corrupted credential files recover gracefully (start fresh, log warning) - New is_using_credential_file_fallback Tauri command for frontend toast notification - Add "Credentials stored locally (no system keyring detected)" toast
1 parent e65d993 commit 60063ec

13 files changed

Lines changed: 810 additions & 146 deletions

File tree

Cargo.lock

Lines changed: 236 additions & 31 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/desktop/src-tauri/Cargo.lock

Lines changed: 236 additions & 31 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/desktop/src-tauri/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ tauri-plugin-window-state = "2"
8787
trash = "5.2"
8888
# Credential storage via Linux secret service (GNOME Keyring / KDE Wallet)
8989
keyring = "3"
90+
# Encrypted file-based credential storage fallback when no secret service is available
91+
cocoon = "0.4"
9092
# D-Bus client for XDG Desktop Portal (accent color, appearance settings)
9193
zbus = "5"
9294

apps/desktop/src-tauri/src/commands/network.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,14 @@ pub fn delete_smb_credentials(server: String, share: Option<String>) -> Result<(
202202
keychain::delete_credentials(&server, share.as_deref())
203203
}
204204

205+
/// Returns whether credential storage is using an encrypted file fallback
206+
/// instead of the system keyring. The frontend can use this to show a one-time
207+
/// info toast when the user first saves credentials without a system keyring.
208+
#[tauri::command]
209+
pub fn is_using_credential_file_fallback() -> bool {
210+
keychain::is_using_file_fallback()
211+
}
212+
205213
/// Lists shares on a host using stored or provided credentials.
206214
/// This is the main command for authenticated share listing.
207215
///

apps/desktop/src-tauri/src/lib.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ use trash as _;
3636
// keyring crate is used in network/keychain_linux.rs for credential storage (Linux only)
3737
#[cfg(target_os = "linux")]
3838
use keyring as _;
39+
//noinspection ALL
40+
// cocoon is used in network/keychain_linux.rs for encrypted file-based credential fallback
41+
#[cfg(target_os = "linux")]
42+
use cocoon as _;
3943

4044
//noinspection ALL
4145
// MCP Bridge is only used in debug builds, so silence the warning in release builds
@@ -672,6 +676,8 @@ pub fn run() {
672676
#[cfg(any(target_os = "macos", target_os = "linux"))]
673677
commands::network::delete_smb_credentials,
674678
#[cfg(any(target_os = "macos", target_os = "linux"))]
679+
commands::network::is_using_credential_file_fallback,
680+
#[cfg(any(target_os = "macos", target_os = "linux"))]
675681
commands::network::list_shares_with_credentials,
676682
#[cfg(any(target_os = "macos", target_os = "linux"))]
677683
commands::network::mount_network_share,
@@ -704,6 +710,8 @@ pub fn run() {
704710
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
705711
stubs::network::delete_smb_credentials,
706712
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
713+
stubs::network::is_using_credential_file_fallback,
714+
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
707715
stubs::network::list_shares_with_credentials,
708716
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
709717
stubs::network::mount_network_share,

apps/desktop/src-tauri/src/network/CLAUDE.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ Discover, browse, and mount SMB network shares. Works on macOS and Linux.
1717
- `mount_linux.rs` — Linux `gio mount` for GVFS-based user-space mounts
1818
- **Auth** (platform-specific via `#[path]` in `mod.rs`):
1919
- `keychain.rs` — macOS Keychain via `security-framework`
20-
- `keychain_linux.rs`Linux secret service via `keyring` crate (GNOME Keyring / KDE Wallet)
20+
- `keychain_linux.rs`Two-tier: Secret Service via `keyring` crate → encrypted file via `cocoon` crate
2121
- **State**: `known_shares.rs` — Connection history in `known-shares.json` (usernames, last auth mode, timestamps).
2222

2323
## Platform strategy
@@ -27,7 +27,7 @@ Discover, browse, and mount SMB network shares. Works on macOS and Linux.
2727
| mDNS discovery | `mdns-sd` (pure Rust) | `mdns-sd` (same) |
2828
| SMB share listing | `smb` + `smb-rpc` crates | `smb` + `smb-rpc` (same) |
2929
| smbutil fallback | `smbutil view -G` | Not available (returns error, smb-rs handles most cases) |
30-
| Credential storage | `security-framework` (macOS Keychain) | `keyring` crate (secret service D-Bus API) |
30+
| Credential storage | `security-framework` (macOS Keychain) | `keyring` (Secret Service) → `cocoon` encrypted file fallback |
3131
| Mounting | `NetFSMountURLSync``/Volumes/` | `gio mount``/run/user/<uid>/gvfs/` |
3232

3333
## Key decisions
@@ -55,6 +55,10 @@ smb-rs connections are lightweight and created on-demand. Caching is at the shar
5555

5656
After first credential fetch, credentials cached in `CREDENTIAL_CACHE` (LazyLock + RwLock). Prevents repeated Keychain/secret-service round-trips during session. Cache keyed by `"smb://{server}/{share}"`.
5757

58+
### Linux credential storage fallback
59+
60+
On Linux, `keychain_linux.rs` tries Secret Service (GNOME Keyring / KDE Wallet) first. If unavailable (no D-Bus service, headless server, minimal DE), it falls back to an encrypted file at `~/.local/share/cmdr/credentials.enc`. The file is encrypted with `cocoon` (Chacha20-Poly1305) using `/etc/machine-id` as the password, with 0600 file permissions. A static `USING_FILE_FALLBACK` flag tracks whether the fallback is active for the frontend to show a one-time info toast. Corrupted credential files are handled gracefully (start fresh, log warning).
61+
5862
### Linux mounting via GVFS
5963

6064
`gio mount` is used for user-space SMB mounting on Linux. It requires the `gvfs-smb` package. If `gio` is not available, a helpful error message is returned. Mounts appear under `/run/user/<uid>/gvfs/`.

apps/desktop/src-tauri/src/network/keychain.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,11 @@ pub fn has_credentials(server: &str, share: Option<&str>) -> bool {
206206
get_credentials(server, share).is_ok()
207207
}
208208

209+
/// Always returns false on macOS (macOS Keychain is always available).
210+
pub fn is_using_file_fallback() -> bool {
211+
false
212+
}
213+
209214
#[cfg(test)]
210215
mod tests {
211216
use super::*;

0 commit comments

Comments
 (0)