From 6c221dc16f6892f5c70112d20d1dfe4db3c1e08c Mon Sep 17 00:00:00 2001 From: aomizu Date: Fri, 30 May 2025 08:29:30 +0900 Subject: [PATCH] added keychain support --- go.mod | 3 ++ go.sum | 6 ++++ pkg/service/service.go | 78 ++++++++++++++++++++++++++++++++++++++++-- pkg/utils/keychain.go | 71 ++++++++++++++++++++++++++++++++++++++ pkg/utils/prefs.go | 1 + 5 files changed, 157 insertions(+), 2 deletions(-) create mode 100644 pkg/utils/keychain.go diff --git a/go.mod b/go.mod index 7cb4bef..c606545 100644 --- a/go.mod +++ b/go.mod @@ -5,8 +5,10 @@ go 1.24.3 require fyne.io/fyne/v2 v2.6.1 require ( + al.essio.dev/pkg/shellescape v1.5.1 // indirect fyne.io/systray v1.11.0 // indirect github.com/BurntSushi/toml v1.4.0 // indirect + github.com/danieljoos/wincred v1.2.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/fredbi/uri v1.1.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect @@ -32,6 +34,7 @@ require ( github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect github.com/stretchr/testify v1.10.0 // indirect github.com/yuin/goldmark v1.7.8 // indirect + github.com/zalando/go-keyring v0.2.6 // indirect golang.org/x/image v0.24.0 // indirect golang.org/x/net v0.35.0 // indirect golang.org/x/sys v0.30.0 // indirect diff --git a/go.sum b/go.sum index 83677a5..06e7af5 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +al.essio.dev/pkg/shellescape v1.5.1 h1:86HrALUujYS/h+GtqoB26SBEdkWfmMI6FubjXlsXyho= +al.essio.dev/pkg/shellescape v1.5.1/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890= fyne.io/fyne/v2 v2.6.1 h1:kjPJD4/rBS9m2nHJp+npPSuaK79yj6ObMTuzR6VQ1Is= fyne.io/fyne/v2 v2.6.1/go.mod h1:YZt7SksjvrSNJCwbWFV32WON3mE1Sr7L41D29qMZ/lU= fyne.io/systray v1.11.0 h1:D9HISlxSkx+jHSniMBR6fCFOUjk1x/OOOJLa9lJYAKg= @@ -5,6 +7,8 @@ fyne.io/systray v1.11.0/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs= github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/danieljoos/wincred v1.2.2 h1:774zMFJrqaeYCK2W57BgAem/MLi6mtSE47MB6BOJ0i0= +github.com/danieljoos/wincred v1.2.2/go.mod h1:w7w4Utbrz8lqeMbDAK0lkNJUv5sAOkFi7nd/ogr0Uh8= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g= @@ -65,6 +69,8 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic= github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= +github.com/zalando/go-keyring v0.2.6 h1:r7Yc3+H+Ux0+M72zacZoItR3UDxeWfKTcabvkI8ua9s= +github.com/zalando/go-keyring v0.2.6/go.mod h1:2TCrxYrbUNYfNS/Kgy/LSrkSQzZ5UPVH85RwfczwvcI= golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ= golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8= golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= diff --git a/pkg/service/service.go b/pkg/service/service.go index a843ef5..97d5a05 100644 --- a/pkg/service/service.go +++ b/pkg/service/service.go @@ -100,18 +100,52 @@ func StartRosettaX87Service(myWindow fyne.Window, updateAllStatuses func()) { // Clean up any existing rosettax87 processes first CleanupExistingServices() + // Load user preferences + prefs, err := utils.LoadPrefs() + if err != nil { + log.Printf("Failed to load preferences: %v", err) + prefs = &utils.UserPrefs{} // Use default prefs + } + + // Try to get saved password if the user has enabled saving + var savedPassword string + if prefs.SaveSudoPassword { + savedPassword, _ = utils.GetSudoPassword() // Ignore errors, just use empty string + } + // Show password dialog passwordEntry := widget.NewPasswordEntry() passwordEntry.SetPlaceHolder("Enter your sudo password") + passwordEntry.SetText(savedPassword) // Prefill with saved password if available passwordEntry.Resize(fyne.NewSize(300, passwordEntry.MinSize().Height)) + // Create checkbox for saving password + savePasswordCheck := widget.NewCheck("Save password securely in keychain", nil) + savePasswordCheck.SetChecked(prefs.SaveSudoPassword) + + // Add status label if password is already saved + var statusLabel *widget.Label + if utils.HasSavedSudoPassword() { + statusLabel = widget.NewLabel("✓ Password already saved in keychain") + statusLabel.Importance = widget.LowImportance + } + // Create a container with proper sizing passwordForm := widget.NewForm(widget.NewFormItem("Password:", passwordEntry)) - passwordContainer := container.NewVBox( + + var containerItems []fyne.CanvasObject + containerItems = append(containerItems, widget.NewLabel("Enter your sudo password to start the RosettaX87 service:"), passwordForm, + savePasswordCheck, ) - passwordContainer.Resize(fyne.NewSize(400, 100)) + + if statusLabel != nil { + containerItems = append(containerItems, statusLabel) + } + + passwordContainer := container.NewVBox(containerItems...) + passwordContainer.Resize(fyne.NewSize(400, 140)) // Create the dialog variable so we can reference it in the callback var passwordDialog dialog.Dialog @@ -124,6 +158,25 @@ func StartRosettaX87Service(myWindow fyne.Window, updateAllStatuses func()) { return } + // Handle password saving/deleting based on checkbox state + shouldSavePassword := savePasswordCheck.Checked + if shouldSavePassword { + // Save password to keychain + if err := utils.SaveSudoPassword(password); err != nil { + log.Printf("Failed to save password to keychain: %v", err) + // Don't block the service start, just log the error + } + } else { + // Delete any existing saved password + utils.DeleteSudoPassword() // Ignore errors + } + + // Update preferences + prefs.SaveSudoPassword = shouldSavePassword + if err := utils.SavePrefs(prefs); err != nil { + log.Printf("Failed to save preferences: %v", err) + } + // Close the dialog passwordDialog.Hide() @@ -360,3 +413,24 @@ func CleanupService() { serviceCmd = nil servicePID = 0 } + +// ClearSavedPassword removes the saved password and shows a confirmation dialog +func ClearSavedPassword(myWindow fyne.Window) { + if !utils.HasSavedSudoPassword() { + dialog.ShowInformation("Password Status", "No password is currently saved.", myWindow) + return + } + + dialog.ShowConfirm("Clear Saved Password", + "Are you sure you want to remove the saved password from the keychain?", + func(confirmed bool) { + if confirmed { + err := utils.DeleteSudoPassword() + if err != nil { + dialog.ShowError(fmt.Errorf("failed to clear saved password: %v", err), myWindow) + } else { + dialog.ShowInformation("Password Cleared", "The saved password has been removed from the keychain.", myWindow) + } + } + }, myWindow) +} diff --git a/pkg/utils/keychain.go b/pkg/utils/keychain.go new file mode 100644 index 0000000..905e283 --- /dev/null +++ b/pkg/utils/keychain.go @@ -0,0 +1,71 @@ +package utils + +import ( + "fmt" + "log" + + "github.com/zalando/go-keyring" +) + +const ( + serviceName = "TurtleSilicon" + accountName = "sudo_password" +) + +// SaveSudoPassword securely stores the sudo password in the system keychain +func SaveSudoPassword(password string) error { + if password == "" { + return fmt.Errorf("password cannot be empty") + } + + err := keyring.Set(serviceName, accountName, password) + if err != nil { + return fmt.Errorf("failed to save password to keychain: %v", err) + } + + log.Println("Password saved securely to keychain") + return nil +} + +// GetSudoPassword retrieves the saved sudo password from the system keychain +func GetSudoPassword() (string, error) { + password, err := keyring.Get(serviceName, accountName) + if err != nil { + // If the password doesn't exist, return empty string instead of error + if err == keyring.ErrNotFound { + return "", nil + } + return "", fmt.Errorf("failed to retrieve password from keychain: %v", err) + } + + return password, nil +} + +// DeleteSudoPassword removes the saved sudo password from the system keychain +func DeleteSudoPassword() error { + err := keyring.Delete(serviceName, accountName) + if err != nil { + // If the password doesn't exist, that's fine + if err == keyring.ErrNotFound { + return nil + } + return fmt.Errorf("failed to delete password from keychain: %v", err) + } + + log.Println("Password removed from keychain") + return nil +} + +// HasSavedSudoPassword checks if a sudo password is saved in the keychain +func HasSavedSudoPassword() bool { + _, err := keyring.Get(serviceName, accountName) + return err == nil +} + +// GetPasswordStatusText returns a user-friendly status text for password saving +func GetPasswordStatusText() string { + if HasSavedSudoPassword() { + return "Password saved in keychain" + } + return "No password saved" +} diff --git a/pkg/utils/prefs.go b/pkg/utils/prefs.go index 64be7f4..b913e77 100644 --- a/pkg/utils/prefs.go +++ b/pkg/utils/prefs.go @@ -11,6 +11,7 @@ type UserPrefs struct { TurtleWoWPath string `json:"turtlewow_path"` CrossOverPath string `json:"crossover_path"` EnvironmentVariables string `json:"environment_variables"` + SaveSudoPassword bool `json:"save_sudo_password"` } func getPrefsPath() (string, error) {