Skip to content

Commit ead676f

Browse files
authored
Merge pull request #112 from avallete/avallete/feat-add-delete-all
feat: add DeleteAll methods to keyring
2 parents de351c5 + a07326c commit ead676f

File tree

8 files changed

+215
-0
lines changed

8 files changed

+215
-0
lines changed

keyring.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ type Keyring interface {
2525
Get(service, user string) (string, error)
2626
// Delete secret from keyring.
2727
Delete(service, user string) error
28+
// DeleteAll deletes all secrets for a given service
29+
DeleteAll(service string) error
2830
}
2931

3032
// Set password in keyring for user.
@@ -41,3 +43,8 @@ func Get(service, user string) (string, error) {
4143
func Delete(service, user string) error {
4244
return provider.Delete(service, user)
4345
}
46+
47+
// DeleteAll deletes all secrets for a given service
48+
func DeleteAll(service string) error {
49+
return provider.DeleteAll(service)
50+
}

keyring_darwin.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,28 @@ func (k macOSXKeychain) Delete(service, username string) error {
113113
return err
114114
}
115115

116+
// DeleteAll deletes all secrets for a given service
117+
func (k macOSXKeychain) DeleteAll(service string) error {
118+
// if service is empty, do nothing otherwise it might accidentally delete all secrets
119+
if service == "" {
120+
return ErrNotFound
121+
}
122+
// Delete each secret in a while loop until there is no more left
123+
// under the service
124+
for {
125+
out, err := exec.Command(
126+
execPathKeychain,
127+
"delete-generic-password",
128+
"-s", service).CombinedOutput()
129+
if strings.Contains(string(out), "could not be found") {
130+
return nil
131+
} else if err != nil {
132+
return err
133+
}
134+
}
135+
136+
}
137+
116138
func init() {
117139
provider = macOSXKeychain{}
118140
}

keyring_fallback.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,7 @@ func (fallbackServiceProvider) Get(service, user string) (string, error) {
2121
func (fallbackServiceProvider) Delete(service, user string) error {
2222
return ErrUnsupportedPlatform
2323
}
24+
25+
func (fallbackServiceProvider) DeleteAll(service string) error {
26+
return ErrUnsupportedPlatform
27+
}

keyring_mock.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,15 @@ func (m *mockProvider) Delete(service, user string) error {
5050
return ErrNotFound
5151
}
5252

53+
// DeleteAll deletes all secrets for a given service
54+
func (m *mockProvider) DeleteAll(service string) error {
55+
if m.mockError != nil {
56+
return m.mockError
57+
}
58+
delete(m.mockStore, service)
59+
return nil
60+
}
61+
5362
// MockInit sets the provider to a mocked memory store
5463
func MockInit() {
5564
provider = &mockProvider{}

keyring_mock_test.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,41 @@ func TestMockWithError(t *testing.T) {
7676
assertError(t, err, mp.mockError)
7777
}
7878

79+
// TestMockDeleteAll tests deleting all secrets for a given service.
80+
func TestMockDeleteAll(t *testing.T) {
81+
mp := mockProvider{}
82+
83+
// Set up multiple secrets for the same service
84+
err := mp.Set(service, user, password)
85+
if err != nil {
86+
t.Errorf("Should not fail, got: %s", err)
87+
}
88+
89+
err = mp.Set(service, user+"2", password+"2")
90+
if err != nil {
91+
t.Errorf("Should not fail, got: %s", err)
92+
}
93+
94+
// Delete all secrets for the service
95+
err = mp.DeleteAll(service)
96+
if err != nil {
97+
t.Errorf("Should not fail, got: %s", err)
98+
}
99+
100+
// Verify that all secrets for the service are deleted
101+
_, err = mp.Get(service, user)
102+
assertError(t, err, ErrNotFound)
103+
104+
_, err = mp.Get(service, user+"2")
105+
assertError(t, err, ErrNotFound)
106+
107+
// Verify that DeleteAll on an empty service doesn't cause an error
108+
err = mp.DeleteAll(service)
109+
if err != nil {
110+
t.Errorf("Should not fail on empty service, got: %s", err)
111+
}
112+
}
113+
79114
func assertError(t *testing.T, err error, expected error) {
80115
if err != expected {
81116
t.Errorf("Expected error %s, got %s", expected, err)

keyring_test.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,3 +130,54 @@ func TestDeleteNonExisting(t *testing.T) {
130130
t.Errorf("Expected error ErrNotFound, got %s", err)
131131
}
132132
}
133+
134+
// TestDeleteAll tests deleting all secrets for a given service.
135+
func TestDeleteAll(t *testing.T) {
136+
// Set up multiple secrets for the same service
137+
err := Set(service, user, password)
138+
if err != nil {
139+
t.Errorf("Should not fail, got: %s", err)
140+
}
141+
142+
err = Set(service, user+"2", password+"2")
143+
if err != nil {
144+
t.Errorf("Should not fail, got: %s", err)
145+
}
146+
147+
// Delete all secrets for the service
148+
err = DeleteAll(service)
149+
if err != nil {
150+
t.Errorf("Should not fail, got: %s", err)
151+
}
152+
153+
// Verify that all secrets for the service are deleted
154+
_, err = Get(service, user)
155+
if err != ErrNotFound {
156+
t.Errorf("Expected error ErrNotFound, got %s", err)
157+
}
158+
159+
_, err = Get(service, user+"2")
160+
if err != ErrNotFound {
161+
t.Errorf("Expected error ErrNotFound, got %s", err)
162+
}
163+
164+
// Verify that DeleteAll on an empty service doesn't cause an error
165+
err = DeleteAll(service)
166+
if err != nil {
167+
t.Errorf("Should not fail on empty service, got: %s", err)
168+
}
169+
}
170+
171+
// TestDeleteAll with empty service name
172+
func TestDeleteAllEmptyService(t *testing.T) {
173+
err := Set(service, user, password)
174+
175+
if err != nil {
176+
t.Errorf("Should not fail, got: %s", err)
177+
}
178+
_ = DeleteAll("")
179+
_, err = Get(service, user)
180+
if err == ErrNotFound {
181+
t.Errorf("Should not have deleted secret from another service")
182+
}
183+
}

keyring_unix.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,31 @@ func (s secretServiceProvider) findItem(svc *ss.SecretService, service, user str
7676
return results[0], nil
7777
}
7878

79+
// findServiceItems looksup all items by service.
80+
func (s secretServiceProvider) findServiceItems(svc *ss.SecretService, service string) ([]dbus.ObjectPath, error) {
81+
collection := svc.GetLoginCollection()
82+
83+
search := map[string]string{
84+
"service": service,
85+
}
86+
87+
err := svc.Unlock(collection.Path())
88+
if err != nil {
89+
return []dbus.ObjectPath{}, err
90+
}
91+
92+
results, err := svc.SearchItems(collection, search)
93+
if err != nil {
94+
return []dbus.ObjectPath{}, err
95+
}
96+
97+
if len(results) == 0 {
98+
return []dbus.ObjectPath{}, ErrNotFound
99+
}
100+
101+
return results, nil
102+
}
103+
79104
// Get gets a secret from the keyring given a service name and a user.
80105
func (s secretServiceProvider) Get(service, user string) (string, error) {
81106
svc, err := ss.NewSecretService()
@@ -124,6 +149,34 @@ func (s secretServiceProvider) Delete(service, user string) error {
124149
return svc.Delete(item)
125150
}
126151

152+
// DeleteAll deletes all secrets for a given service
153+
func (s secretServiceProvider) DeleteAll(service string) error {
154+
// if service is empty, do nothing otherwise it might accidentally delete all secrets
155+
if service == "" {
156+
return ErrNotFound
157+
}
158+
159+
svc, err := ss.NewSecretService()
160+
if err != nil {
161+
return err
162+
}
163+
// find all items for the service
164+
items, err := s.findServiceItems(svc, service)
165+
if err != nil {
166+
if err == ErrNotFound {
167+
return nil
168+
}
169+
return err
170+
}
171+
for _, item := range items {
172+
err = svc.Delete(item)
173+
if err != nil {
174+
return err
175+
}
176+
}
177+
return nil
178+
}
179+
127180
func init() {
128181
provider = secretServiceProvider{}
129182
}

keyring_windows.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package keyring
22

33
import (
4+
"strings"
45
"syscall"
56

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

63+
func (k windowsKeychain) DeleteAll(service string) error {
64+
// if service is empty, do nothing otherwise it might accidentally delete all secrets
65+
if service == "" {
66+
return ErrNotFound
67+
}
68+
69+
creds, err := wincred.List()
70+
if err != nil {
71+
return err
72+
}
73+
74+
prefix := k.credName(service, "")
75+
deletedCount := 0
76+
77+
for _, cred := range creds {
78+
if strings.HasPrefix(cred.TargetName, prefix) {
79+
genericCred, err := wincred.GetGenericCredential(cred.TargetName)
80+
if err != nil {
81+
if err != syscall.ERROR_NOT_FOUND {
82+
return err
83+
}
84+
} else {
85+
err := genericCred.Delete()
86+
if err != nil {
87+
return err
88+
}
89+
deletedCount++
90+
}
91+
}
92+
}
93+
return nil
94+
}
95+
6296
// credName combines service and username to a single string.
6397
func (k windowsKeychain) credName(service, username string) string {
6498
return service + ":" + username

0 commit comments

Comments
 (0)