diff --git a/keyring.go b/keyring.go index 75ab6d2..921b515 100644 --- a/keyring.go +++ b/keyring.go @@ -25,6 +25,8 @@ type Keyring interface { Get(service, user string) (string, error) // Delete secret from keyring. Delete(service, user string) error + // DeleteAll deletes all secrets for a given service + DeleteAll(service string) error } // Set password in keyring for user. @@ -41,3 +43,8 @@ func Get(service, user string) (string, error) { func Delete(service, user string) error { return provider.Delete(service, user) } + +// DeleteAll deletes all secrets for a given service +func DeleteAll(service string) error { + return provider.DeleteAll(service) +} diff --git a/keyring_darwin.go b/keyring_darwin.go index 26e8aa1..8e40243 100644 --- a/keyring_darwin.go +++ b/keyring_darwin.go @@ -113,6 +113,24 @@ func (k macOSXKeychain) Delete(service, username string) error { return err } +// DeleteAll deletes all secrets for a given service +func (k macOSXKeychain) DeleteAll(service string) error { + // Delete each secret in a while loop until there is no more left + // under the service + for { + out, err := exec.Command( + execPathKeychain, + "delete-generic-password", + "-s", service).CombinedOutput() + if strings.Contains(string(out), "could not be found") { + return nil + } else if err != nil { + return err + } + } + +} + func init() { provider = macOSXKeychain{} } diff --git a/keyring_fallback.go b/keyring_fallback.go index a2de1a2..75c61ff 100644 --- a/keyring_fallback.go +++ b/keyring_fallback.go @@ -21,3 +21,7 @@ func (fallbackServiceProvider) Get(service, user string) (string, error) { func (fallbackServiceProvider) Delete(service, user string) error { return ErrUnsupportedPlatform } + +func (fallbackServiceProvider) DeleteAll(service string) error { + return ErrUnsupportedPlatform +} diff --git a/keyring_mock.go b/keyring_mock.go index 0e514ea..e46b2e5 100644 --- a/keyring_mock.go +++ b/keyring_mock.go @@ -50,6 +50,15 @@ func (m *mockProvider) Delete(service, user string) error { return ErrNotFound } +// DeleteAll deletes all secrets for a given service +func (m *mockProvider) DeleteAll(service string) error { + if m.mockError != nil { + return m.mockError + } + delete(m.mockStore, service) + return nil +} + // MockInit sets the provider to a mocked memory store func MockInit() { provider = &mockProvider{} diff --git a/keyring_mock_test.go b/keyring_mock_test.go index 11931b2..4688679 100644 --- a/keyring_mock_test.go +++ b/keyring_mock_test.go @@ -76,6 +76,41 @@ func TestMockWithError(t *testing.T) { assertError(t, err, mp.mockError) } +// TestMockDeleteAll tests deleting all secrets for a given service. +func TestMockDeleteAll(t *testing.T) { + mp := mockProvider{} + + // Set up multiple secrets for the same service + err := mp.Set(service, user, password) + if err != nil { + t.Errorf("Should not fail, got: %s", err) + } + + err = mp.Set(service, user+"2", password+"2") + if err != nil { + t.Errorf("Should not fail, got: %s", err) + } + + // Delete all secrets for the service + err = mp.DeleteAll(service) + if err != nil { + t.Errorf("Should not fail, got: %s", err) + } + + // Verify that all secrets for the service are deleted + _, err = mp.Get(service, user) + assertError(t, err, ErrNotFound) + + _, err = mp.Get(service, user+"2") + assertError(t, err, ErrNotFound) + + // Verify that DeleteAll on an empty service doesn't cause an error + err = mp.DeleteAll(service) + if err != nil { + t.Errorf("Should not fail on empty service, got: %s", err) + } +} + func assertError(t *testing.T, err error, expected error) { if err != expected { t.Errorf("Expected error %s, got %s", expected, err) diff --git a/keyring_test.go b/keyring_test.go index ca288a8..f1a8af3 100644 --- a/keyring_test.go +++ b/keyring_test.go @@ -130,3 +130,40 @@ func TestDeleteNonExisting(t *testing.T) { t.Errorf("Expected error ErrNotFound, got %s", err) } } + +// TestDeleteAll tests deleting all secrets for a given service. +func TestDeleteAll(t *testing.T) { + // Set up multiple secrets for the same service + err := Set(service, user, password) + if err != nil { + t.Errorf("Should not fail, got: %s", err) + } + + err = Set(service, user+"2", password+"2") + if err != nil { + t.Errorf("Should not fail, got: %s", err) + } + + // Delete all secrets for the service + err = DeleteAll(service) + if err != nil { + t.Errorf("Should not fail, got: %s", err) + } + + // Verify that all secrets for the service are deleted + _, err = Get(service, user) + if err != ErrNotFound { + t.Errorf("Expected error ErrNotFound, got %s", err) + } + + _, err = Get(service, user+"2") + if err != ErrNotFound { + t.Errorf("Expected error ErrNotFound, got %s", err) + } + + // Verify that DeleteAll on an empty service doesn't cause an error + err = DeleteAll(service) + if err != nil { + t.Errorf("Should not fail on empty service, got: %s", err) + } +} diff --git a/keyring_unix.go b/keyring_unix.go index 523f6a3..1ab9a0e 100644 --- a/keyring_unix.go +++ b/keyring_unix.go @@ -124,6 +124,27 @@ func (s secretServiceProvider) Delete(service, user string) error { return svc.Delete(item) } +// DeleteAll deletes all secrets for a given service +func (s secretServiceProvider) DeleteAll(service string) error { + svc, err := ss.NewSecretService() + if err != nil { + return err + } + for { + item, err := s.findItem(svc, service, "") + if err != nil { + if err == ErrNotFound { + return nil + } + return err + } + err = svc.Delete(item) + if err != nil { + return err + } + } +} + func init() { provider = secretServiceProvider{} } diff --git a/keyring_windows.go b/keyring_windows.go index 738dac4..85641cd 100644 --- a/keyring_windows.go +++ b/keyring_windows.go @@ -1,6 +1,7 @@ package keyring import ( + "strings" "syscall" "github.com/danieljoos/wincred" @@ -59,6 +60,34 @@ func (k windowsKeychain) Delete(service, username string) error { return cred.Delete() } +func (k windowsKeychain) DeleteAll(service string) error { + creds, err := wincred.List() + if err != nil { + return err + } + + prefix := k.credName(service, "") + deletedCount := 0 + + for _, cred := range creds { + if strings.HasPrefix(cred.TargetName, prefix) { + genericCred, err := wincred.GetGenericCredential(cred.TargetName) + if err != nil { + if err != syscall.ERROR_NOT_FOUND { + return err + } + } else { + err := genericCred.Delete() + if err != nil { + return err + } + deletedCount++ + } + } + } + return nil +} + // credName combines service and username to a single string. func (k windowsKeychain) credName(service, username string) string { return service + ":" + username