Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add DeleteAll methods to keyring #112

Merged
merged 3 commits into from
Sep 23, 2024
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
7 changes: 7 additions & 0 deletions keyring.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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)
}
22 changes: 22 additions & 0 deletions keyring_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,28 @@ 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 {
// if service is empty, do nothing otherwise it might accidentally delete all secrets
if service == "" {
return ErrNotFound
}
// 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{}
}
4 changes: 4 additions & 0 deletions keyring_fallback.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
9 changes: 9 additions & 0 deletions keyring_mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}
Expand Down
35 changes: 35 additions & 0 deletions keyring_mock_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
51 changes: 51 additions & 0 deletions keyring_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,3 +130,54 @@ 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)
}
}

// TestDeleteAll with empty service name
func TestDeleteAllEmptyService(t *testing.T) {
err := Set(service, user, password)

if err != nil {
t.Errorf("Should not fail, got: %s", err)
}
_ = DeleteAll("")
_, err = Get(service, user)
if err == ErrNotFound {
t.Errorf("Should not have deleted secret from another service")
}
}
53 changes: 53 additions & 0 deletions keyring_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,31 @@ func (s secretServiceProvider) findItem(svc *ss.SecretService, service, user str
return results[0], nil
}

// findServiceItems looksup all items by service.
func (s secretServiceProvider) findServiceItems(svc *ss.SecretService, service string) ([]dbus.ObjectPath, error) {
collection := svc.GetLoginCollection()

search := map[string]string{
"service": service,
}

err := svc.Unlock(collection.Path())
if err != nil {
return []dbus.ObjectPath{}, err
}

results, err := svc.SearchItems(collection, search)
if err != nil {
return []dbus.ObjectPath{}, err
}

if len(results) == 0 {
return []dbus.ObjectPath{}, ErrNotFound
}

return results, nil
}

// Get gets a secret from the keyring given a service name and a user.
func (s secretServiceProvider) Get(service, user string) (string, error) {
svc, err := ss.NewSecretService()
Expand Down Expand Up @@ -124,6 +149,34 @@ 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 {
// if service is empty, do nothing otherwise it might accidentally delete all secrets
if service == "" {
return ErrNotFound
}

svc, err := ss.NewSecretService()
if err != nil {
return err
}
// find all items for the service
items, err := s.findServiceItems(svc, service)
if err != nil {
if err == ErrNotFound {
return nil
}
return err
}
for _, item := range items {
err = svc.Delete(item)
if err != nil {
return err
}
}
return nil
}

func init() {
provider = secretServiceProvider{}
}
34 changes: 34 additions & 0 deletions keyring_windows.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package keyring

import (
"strings"
"syscall"

"github.com/danieljoos/wincred"
Expand Down Expand Up @@ -59,6 +60,39 @@ func (k windowsKeychain) Delete(service, username string) error {
return cred.Delete()
}

func (k windowsKeychain) DeleteAll(service string) error {
// if service is empty, do nothing otherwise it might accidentally delete all secrets
if service == "" {
return ErrNotFound
}

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
Expand Down