// Package keyring provides secure password storage for sudo authentication package keyring import ( "errors" "fmt" "runtime" "github.com/99designs/keyring" ) // ErrNoBackend indicates no keyring backend is available var ErrNoBackend = errors.New("no keyring backend available") // ErrDaemonNotRunning indicates the keyring daemon is not running var ErrDaemonNotRunning = errors.New("keyring daemon is not running") // CheckDependencies checks if keyring dependencies are installed and functional // Returns an error with installation instructions if dependencies are missing func CheckDependencies() error { // Non-Linux platforms have native keyring support if runtime.GOOS != "linux" { return nil } status := DetectStatus() // Check if any backend is available if len(status.AvailableBackends) == 0 || status.ActiveBackend == BackendNone { return fmt.Errorf("%w\n\nInstall with: %s", ErrNoBackend, status.InstallHint) } // Check if daemon is running (not needed for file/pass backends) if status.ActiveBackend != BackendFile && status.ActiveBackend != BackendPass { if !status.DaemonRunning { return fmt.Errorf("%w\n\nThe %s daemon is not running. Please start it and try again.", ErrDaemonNotRunning, status.ActiveBackend) } } return nil } // NeedsInstallPrompt returns true if the user needs to install keyring dependencies // This is used to show an installation prompt in the UI func NeedsInstallPrompt() bool { // Non-Linux platforms have native keyring support if runtime.GOOS != "linux" { return false } status := DetectStatus() // Need to prompt if no backends at all if len(status.AvailableBackends) == 0 || status.ActiveBackend == BackendNone { return true } // If file backend is available, we can always work (no prompt needed) for _, backend := range status.AvailableBackends { if backend == BackendFile { return false } } // If active backend doesn't need a daemon, no prompt needed if status.ActiveBackend == BackendFile || status.ActiveBackend == BackendPass { return false } // For SecretService/KWallet, if daemon is running, no prompt needed if status.DaemonRunning { return false } // Daemon-based backend but daemon not running - show prompt return true } // CanUseKeyring returns true if keyring can be used for password storage // This is a convenience function that combines dependency checking func CanUseKeyring() bool { return CheckDependencies() == nil } // GetSetupInstructions returns human-readable setup instructions // Returns an empty string if no setup is needed func GetSetupInstructions() string { // Non-Linux platforms have native keyring support if runtime.GOOS != "linux" { return "" } status := DetectStatus() // No setup needed if backend is available and working if len(status.AvailableBackends) > 0 && status.ActiveBackend != BackendNone { if status.ActiveBackend == BackendFile || status.ActiveBackend == BackendPass { return "" } if status.DaemonRunning { return "" } } // Generate setup instructions based on detected environment var instructions string switch status.ActiveBackend { case BackendSecretService: instructions = fmt.Sprintf( "GNOME Keyring is installed but not running.\n\n"+ "To start it automatically:\n"+ "1. Ensure you're logged in with a display manager that supports PAM\n"+ "2. Or run: eval $(gnome-keyring-daemon --start)\n"+ "3. Then set: export SSH_AUTH_SOCK\n\n"+ "Alternatively, install and configure: %s", status.InstallHint, ) case BackendKWallet: instructions = fmt.Sprintf( "KWallet is installed but not running.\n\n"+ "To start it:\n"+ "1. Open KDE System Settings\n"+ "2. Go to Account Details → KDE Wallet\n"+ "3. Enable the wallet system\n\n"+ "Alternatively, install: %s", status.InstallHint, ) case BackendNone: instructions = fmt.Sprintf( "No keyring backend detected.\n\n"+ "To enable secure password storage for sudo authentication:\n\n"+ "%s\n\n"+ "After installation, restart mpv-manager.", status.InstallHint, ) default: instructions = fmt.Sprintf( "Keyring setup may be required.\n\n"+ "Install with: %s", status.InstallHint, ) } return instructions } // ValidateSetup checks the keyring setup and returns detailed status // This is useful for diagnostics and troubleshooting func ValidateSetup() (bool, string) { // Non-Linux platforms have native keyring support if runtime.GOOS != "linux" { return true, "Native keyring available on " + runtime.GOOS } status := DetectStatus() // Check backend availability if len(status.AvailableBackends) == 0 { return false, "No keyring backends detected. " + status.InstallHint } // Check daemon status for backends that require it if status.ActiveBackend != BackendFile && status.ActiveBackend != BackendPass { if !status.DaemonRunning { return false, fmt.Sprintf( "%s backend detected but daemon is not running.", status.ActiveBackend, ) } } // Try to open the keyring to verify it works kr, err := Open() if err != nil { return false, "Failed to open keyring: " + err.Error() } // Try to set and get a test value testKey := "test-connection" testValue := "test-value-" + status.Distro err = kr.ring.Set(keyring.Item{ Key: testKey, Data: []byte(testValue), }) if err != nil { return false, "Failed to write to keyring: " + err.Error() } // Clean up test value _ = kr.ring.Remove(testKey) return true, fmt.Sprintf( "Keyring is working. Active backend: %s, Desktop: %s, Distro: %s", status.ActiveBackend, status.DesktopEnv, status.Distro, ) }