diff --git a/go.mod b/go.mod index fe4ede743..09427deeb 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/charmbracelet/glamour v0.7.0 github.com/charmbracelet/lipgloss v0.12.1 github.com/containers/common v0.59.2 + github.com/danieljoos/wincred v1.2.1 github.com/deepmap/oapi-codegen/v2 v2.2.0 github.com/docker/cli v26.1.5+incompatible github.com/docker/docker v26.1.5+incompatible @@ -114,7 +115,6 @@ require ( github.com/curioswitch/go-reassign v0.2.0 // indirect github.com/cyphar/filepath-securejoin v0.2.5 // indirect github.com/daixiang0/gci v0.13.4 // indirect - github.com/danieljoos/wincred v1.2.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/denis-tingaikin/go-header v0.5.0 // indirect github.com/distribution/reference v0.6.0 // indirect diff --git a/internal/utils/credentials/keyring_darwin.go b/internal/utils/credentials/keyring_darwin.go new file mode 100644 index 000000000..37c370603 --- /dev/null +++ b/internal/utils/credentials/keyring_darwin.go @@ -0,0 +1,31 @@ +//go:build darwin + +package credentials + +import ( + "os/exec" + + "github.com/go-errors/errors" +) + +const execPathKeychain = "/usr/bin/security" + +func deleteAll(service string) error { + // Delete each secret in a while loop until there is no more left + for { + if err := exec.Command( + execPathKeychain, + "delete-generic-password", + "-s", service, + ).Run(); err == nil { + continue + } else if errors.Is(err, exec.ErrNotFound) { + return errors.New(ErrNotSupported) + } else if exitError, ok := err.(*exec.ExitError); ok && exitError.ExitCode() == 44 { + // Exit 44 means no item exists for this service name + return nil + } else { + return errors.Errorf("failed to delete all credentials: %w", err) + } + } +} diff --git a/internal/utils/credentials/keyring_linux.go b/internal/utils/credentials/keyring_linux.go new file mode 100644 index 000000000..08095cfb7 --- /dev/null +++ b/internal/utils/credentials/keyring_linux.go @@ -0,0 +1,33 @@ +//go:build linux + +package credentials + +import ( + "github.com/go-errors/errors" + ss "github.com/zalando/go-keyring/secret_service" +) + +func deleteAll(service string) error { + svc, err := ss.NewSecretService() + if err != nil { + return errors.Errorf("failed to create secret service: %w", err) + } + + collection := svc.GetLoginCollection() + if err := svc.Unlock(collection.Path()); err != nil { + return errors.Errorf("failed to unlock collection: %w", err) + } + + search := map[string]string{"service": service} + results, err := svc.SearchItems(collection, search) + if err != nil { + return errors.Errorf("failed to search items: %w", err) + } + + for _, item := range results { + if err := svc.Delete(item); err != nil { + return errors.Errorf("failed to delete all credentials: %w", err) + } + } + return nil +} diff --git a/internal/utils/credentials/keyring_test.go b/internal/utils/credentials/keyring_test.go new file mode 100644 index 000000000..0569276aa --- /dev/null +++ b/internal/utils/credentials/keyring_test.go @@ -0,0 +1,28 @@ +package credentials + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/zalando/go-keyring" +) + +func TestDeleteAll(t *testing.T) { + service := "test-cli" + // Nothing to delete + err := deleteAll(service) + assert.NoError(t, err) + // Setup 2 items + err = keyring.Set(service, "key1", "value") + assert.NoError(t, err) + err = keyring.Set(service, "key2", "value") + assert.NoError(t, err) + // Delete all items + err = deleteAll(service) + assert.NoError(t, err) + // Check items are gone + _, err = keyring.Get(service, "key1") + assert.ErrorIs(t, err, keyring.ErrNotFound) + _, err = keyring.Get(service, "key2") + assert.ErrorIs(t, err, keyring.ErrNotFound) +} diff --git a/internal/utils/credentials/keyring_windows.go b/internal/utils/credentials/keyring_windows.go new file mode 100644 index 000000000..0366a686d --- /dev/null +++ b/internal/utils/credentials/keyring_windows.go @@ -0,0 +1,25 @@ +//go:build windows + +package credentials + +import ( + "github.com/danieljoos/wincred" + "github.com/go-errors/errors" +) + +func deleteAll(service string) error { + if err := assertKeyringSupported(); err != nil { + return err + } + creds, err := wincred.FilteredList(service + ":") + if err != nil { + return errors.Errorf("failed to list credentials: %w", err) + } + for _, c := range creds { + gc := wincred.GenericCredential{Credential: *c} + if err := gc.Delete(); err != nil { + return errors.Errorf("failed to delete all credentials: %w", err) + } + } + return nil +} diff --git a/internal/utils/credentials/store.go b/internal/utils/credentials/store.go index 32a2abd24..ac7f01866 100644 --- a/internal/utils/credentials/store.go +++ b/internal/utils/credentials/store.go @@ -53,6 +53,11 @@ func Delete(project string) error { return nil } +// Deletes all stored credentials for the namespace +func DeleteAll() error { + return deleteAll(namespace) +} + func assertKeyringSupported() error { // Suggested check: https://github.com/microsoft/WSL/issues/423 if f, err := os.ReadFile("/proc/sys/kernel/osrelease"); err == nil {