Skip to content

Commit

Permalink
Merge pull request #112 from avallete/avallete/feat-add-delete-all
Browse files Browse the repository at this point in the history
feat: add DeleteAll methods to keyring
  • Loading branch information
mikkeloscar authored Sep 23, 2024
2 parents de351c5 + a07326c commit ead676f
Show file tree
Hide file tree
Showing 8 changed files with 215 additions and 0 deletions.
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

0 comments on commit ead676f

Please sign in to comment.