Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,15 @@ htmd = "0.5.4"
base64 = "0.22"
ts-rs = { version = "12.0.1", features = ["serde-compat", "serde-json-impl"] }

# Same crate (and version) that `keyring`'s sync secret-service backend uses
# internally, so this adds no new transitive code. We need it directly to
# create the Secret Service "default" collection when none exists (live
# sessions, autologin accounts, fresh users — anywhere PAM never created the
# login keyring). `keyring` 3.x cannot self-heal in that case: its create
# path for the special `default` target only re-reads the alias.
[target.'cfg(target_os = "linux")'.dependencies]
dbus-secret-service = "4.1"

[dev-dependencies]
# Temporary directories for testing
tempfile = "3"
69 changes: 67 additions & 2 deletions src-tauri/src/assistant/auth/secrets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,71 @@ fn entry_for(service_name: &str, secret_ref: &str) -> Result<Entry, keyring::Err
Entry::new(service_name, secret_ref)
}

/// Store a secret, transparently creating the Secret Service default
/// collection when it is missing (Linux only).
///
/// On live sessions, autologin setups, and fresh accounts, gnome-keyring may
/// be running with *no* keyring collection at all: PAM only creates and
/// unlocks the "login" collection during a password login. The Secret
/// Service then reports "no result found" for the `default` alias, and the
/// `keyring` crate cannot self-heal because its create path for the special
/// `default` target only re-reads the alias instead of creating a
/// collection. In that case we ask the Secret Service daemon to create a
/// collection with the `default` alias — the daemon shows its own password
/// dialog, so the user stays in control of the keyring password — and then
/// retry the write once.
fn set_password_creating_default_collection(
entry: &Entry,
secret: &str,
) -> Result<(), keyring::Error> {
match entry.set_password(secret) {
#[cfg(target_os = "linux")]
Err(error) if is_missing_default_collection(&error) => {
create_default_collection()?;
entry.set_password(secret)
}
result => result,
}
}

/// True when the keyring error means the Secret Service is reachable but has
/// no collection behind the `default` alias ("no result found").
#[cfg(target_os = "linux")]
fn is_missing_default_collection(error: &keyring::Error) -> bool {
matches!(error, keyring::Error::NoStorageAccess(_))
&& error
.to_string()
.to_ascii_lowercase()
.contains("no result found")
}

/// Create a Secret Service collection registered as the `default` alias.
///
/// The Secret Service daemon (gnome-keyring, KWallet, ...) owns the
/// creation: it prompts the user for the new keyring's password through its
/// own dialog, exactly as `secret-tool` or Seahorse would. Every libsecret
/// consumer resolves the same `default` alias afterwards, so the collection
/// created here is shared system-wide, not CLAI-specific.
#[cfg(target_os = "linux")]
fn create_default_collection() -> Result<(), keyring::Error> {
use dbus_secret_service::{EncryptionType, Error as SsError, SecretService};

let session = SecretService::connect(EncryptionType::Plain)
.map_err(|error| keyring::Error::PlatformFailure(Box::new(error)))?;
match session.create_collection("Default keyring", "default") {
Ok(_) => Ok(()),
// The user dismissed the daemon's "create keyring" password dialog.
Err(SsError::Prompt) => Err(keyring::Error::NoStorageAccess(
"no default keyring exists and the keyring creation dialog was dismissed. \
Retry and choose a password, or create a default keyring with Seahorse \
(\"Passwords and Keys\"). On autologin setups, enabling password login \
lets the system create and unlock the keyring automatically."
.into(),
)),
Err(error) => Err(keyring::Error::PlatformFailure(Box::new(error))),
}
}

pub struct ProviderSecretStorage;

impl ProviderSecretStorage {
Expand All @@ -16,7 +81,7 @@ impl ProviderSecretStorage {

pub fn set_secret(secret_ref: &str, secret: &str) -> Result<(), keyring::Error> {
let entry = Self::entry(secret_ref)?;
entry.set_password(secret)
set_password_creating_default_collection(&entry, secret)
}

#[allow(dead_code)]
Expand Down Expand Up @@ -47,7 +112,7 @@ impl McpSecretStorage {

pub fn set_secret(secret_ref: &str, secret: &str) -> Result<(), keyring::Error> {
let entry = Self::entry(secret_ref)?;
entry.set_password(secret)
set_password_creating_default_collection(&entry, secret)
}

pub fn get_secret(secret_ref: &str) -> Result<Option<String>, keyring::Error> {
Expand Down
Loading