Skip to content

Commit 300c24a

Browse files
committed
store: add Filter function
This patch adds the `Filter` function on the `Store` interface. This allows a store provider from filtering secrets based on partial IDs and attributes. This is necessary to reduce the number of calls made to the underlying store and to standardize how searches happen on a set secrets. In the keychain package the underlying store would prompt the user (in most cases) when a secret's encrypted data is retrieved. Fetching secrets could become an expensive operation, especially when fetching all secrets. Hopefully, the Filter function would allow getting only the necessary secrets required for an application without prompting the user too much. Signed-off-by: Alano Terblanche <18033717+Benehiko@users.noreply.github.com>
1 parent 228d207 commit 300c24a

9 files changed

Lines changed: 419 additions & 89 deletions

File tree

store/examples/secret.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
type secret struct {
1111
AccessToken string
1212
RefreshToken string
13+
Attributes map[string]string
1314
}
1415

1516
var _ secrets.Secret = &secret{}
@@ -27,3 +28,7 @@ func (s *secret) Unmarshal(data []byte) error {
2728
s.AccessToken, s.RefreshToken = string(tokens[0]), string(tokens[1])
2829
return nil
2930
}
31+
32+
func (s *secret) Metadata() map[string]string {
33+
return s.Attributes
34+
}

store/keychain/cmd/main.go

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,8 @@ func newCommand() (*cobra.Command, error) {
4141
}
4242

4343
var e error
44-
for k, v := range secrets {
45-
vv, err := v.Marshal()
46-
if err != nil {
47-
e = errors.Join(e, err)
48-
}
49-
fmt.Printf("\nID: %s\nValue: %s\n", k, vv)
44+
for _, v := range secrets {
45+
fmt.Printf("\nID: %s\n", v)
5046
}
5147
return e
5248
},

store/keychain/keychain_darwin.go

Lines changed: 65 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package keychain
33
import (
44
"context"
55
"errors"
6+
"strings"
67

78
kc "github.com/keybase/go-keychain"
89

@@ -94,7 +95,7 @@ func (k *keychainStore[T]) Get(_ context.Context, id store.ID) (store.Secret, er
9495
return secret, nil
9596
}
9697

97-
func (k *keychainStore[T]) GetAll(context.Context) (map[store.ID]store.Secret, error) {
98+
func (k *keychainStore[T]) GetAll(context.Context) ([]store.ID, error) {
9899
item := newKeychainItem("", k)
99100

100101
// We use the MatchLimitAll attribute to query for multiple items from the
@@ -107,23 +108,13 @@ func (k *keychainStore[T]) GetAll(context.Context) (map[store.ID]store.Secret, e
107108
return nil, mapKeychainError(err)
108109
}
109110

110-
creds := make(map[store.ID]store.Secret, len(results))
111+
creds := make([]store.ID, len(results))
111112
for _, result := range results {
112113
id, err := store.ParseID(result.Account)
113114
if err != nil {
114-
return nil, err
115-
}
116-
117-
i, err := getItemWithData(id.String(), k)
118-
if err != nil {
119-
return nil, err
120-
}
121-
122-
secret := k.factory()
123-
if err := secret.Unmarshal(i.Data); err != nil {
124-
return nil, err
115+
continue
125116
}
126-
creds[id] = secret
117+
creds = append(creds, id)
127118
}
128119
return creds, nil
129120
}
@@ -143,9 +134,69 @@ func (k *keychainStore[T]) Save(_ context.Context, id store.ID, secret store.Sec
143134
// it is a user-friendly name for the item, which is displayed in the keychain UI.
144135
// https://developer.apple.com/documentation/security/ksecattrlabel
145136
item.SetLabel(k.itemLabel(id))
137+
138+
parts := strings.SplitSeq(id.String(), "/")
139+
for p := range parts {
140+
if p == "" {
141+
continue
142+
}
143+
item.SetString(p, p)
144+
}
145+
146+
for k, v := range secret.Metadata() {
147+
item.SetString(k, v)
148+
}
149+
146150
return mapKeychainError(kc.AddItem(item))
147151
}
148152

153+
func (k *keychainStore[T]) Filter(ctx context.Context, id store.ID, attributes map[string]string) (map[store.ID]store.Secret, error) {
154+
item := newKeychainItem("", k)
155+
156+
// We use the MatchLimitAll attribute to query for multiple items from the
157+
// store. It cannot be used with item.SetReturnData.
158+
// https://developer.apple.com/documentation/security/secitemcopymatching(_:_:)#Discussion
159+
item.SetMatchLimit(kc.MatchLimitAll)
160+
161+
parts := strings.SplitSeq(id.String(), "/")
162+
for p := range parts {
163+
if p == "" {
164+
continue
165+
}
166+
item.SetString(p, p)
167+
}
168+
169+
for k, v := range attributes {
170+
item.SetString(k, v)
171+
}
172+
173+
results, err := kc.QueryItem(item)
174+
if err != nil {
175+
return nil, mapKeychainError(err)
176+
}
177+
178+
creds := make(map[store.ID]store.Secret)
179+
for _, result := range results {
180+
id, err := store.ParseID(result.Account)
181+
if err != nil {
182+
continue
183+
}
184+
185+
i, err := getItemWithData(id.String(), k)
186+
if err != nil {
187+
return nil, err
188+
}
189+
190+
secret := k.factory()
191+
if err := secret.Unmarshal(i.Data); err != nil {
192+
return nil, err
193+
}
194+
creds[id] = secret
195+
}
196+
197+
return creds, nil
198+
}
199+
149200
func mapKeychainError(err error) error {
150201
if err == nil {
151202
return nil

store/keychain/keychain_linux.go

Lines changed: 96 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ import (
77
"context"
88
"errors"
99
"fmt"
10+
"maps"
1011
"slices"
12+
"strings"
1113

1214
"github.com/keybase/dbus"
1315
kc "github.com/keybase/go-keychain/secretservice"
@@ -208,7 +210,7 @@ func (k *keychainStore[T]) Get(_ context.Context, id store.ID) (store.Secret, er
208210
return secret, nil
209211
}
210212

211-
func (k *keychainStore[T]) GetAll(context.Context) (map[store.ID]store.Secret, error) {
213+
func (k *keychainStore[T]) GetAll(context.Context) ([]store.ID, error) {
212214
service, err := kc.NewService()
213215
if err != nil {
214216
return nil, err
@@ -245,32 +247,23 @@ func (k *keychainStore[T]) GetAll(context.Context) (map[store.ID]store.Secret, e
245247
return nil, store.ErrCredentialNotFound
246248
}
247249

248-
credentials := make(map[store.ID]store.Secret, len(itemPaths))
250+
credentials := make([]store.ID, 0)
249251
for _, itemPath := range itemPaths {
250-
value, err := service.GetSecret(itemPath, *session)
251-
if err != nil {
252-
return nil, err
253-
}
254-
255252
attributes, err := service.GetAttributes(itemPath)
256253
if err != nil {
257254
return nil, err
258255
}
259256

260257
attrID, ok := attributes["id"]
261258
if !ok {
262-
return nil, errors.New("secret attributes does not contain `id` field")
259+
continue
263260
}
264261

265-
secret := k.factory()
266-
if err := secret.Unmarshal(value); err != nil {
267-
return nil, err
268-
}
269262
secretID, err := store.ParseID(attrID)
270263
if err != nil {
271-
return nil, err
264+
continue
272265
}
273-
credentials[secretID] = secret
266+
credentials = append(credentials, secretID)
274267
}
275268

276269
return credentials, nil
@@ -318,6 +311,17 @@ func (k *keychainStore[T]) Save(_ context.Context, id store.ID, secret store.Sec
318311
}
319312

320313
attributes := newItemAttributes(id.String(), k)
314+
315+
parts := strings.SplitSeq(id.String(), "/")
316+
for p := range parts {
317+
if p == "" {
318+
continue
319+
}
320+
attributes[p] = p
321+
}
322+
323+
maps.Copy(attributes, secret.Metadata())
324+
321325
label := k.itemLabel(id)
322326
properties := kc.NewSecretProperties(label, attributes)
323327

@@ -328,3 +332,81 @@ func (k *keychainStore[T]) Save(_ context.Context, id store.ID, secret store.Sec
328332

329333
return nil
330334
}
335+
336+
func (k *keychainStore[T]) Filter(ctx context.Context, id store.ID, attributes map[string]string) (map[store.ID]store.Secret, error) {
337+
service, err := kc.NewService()
338+
if err != nil {
339+
return nil, err
340+
}
341+
342+
session, err := service.OpenSession(kc.AuthenticationDHAES)
343+
if err != nil {
344+
return nil, err
345+
}
346+
defer service.CloseSession(session)
347+
348+
objectPath, err := getDefaultCollection(service)
349+
if err != nil {
350+
return nil, err
351+
}
352+
353+
err = isCollectionUnlocked(objectPath, service)
354+
if err != nil && !errors.Is(err, errCollectionLocked) {
355+
return nil, err
356+
}
357+
if errors.Is(err, errCollectionLocked) {
358+
if err := service.Unlock([]dbus.ObjectPath{objectPath}); err != nil {
359+
return nil, err
360+
}
361+
}
362+
363+
itemAttr := newItemAttributes("", k)
364+
maps.Copy(itemAttr, attributes)
365+
366+
parts := strings.SplitSeq(id.String(), "/")
367+
for p := range parts {
368+
if p == "" {
369+
continue
370+
}
371+
itemAttr[p] = p
372+
}
373+
374+
itemPaths, err := service.SearchCollection(objectPath, itemAttr)
375+
if err != nil {
376+
return nil, fmt.Errorf("failed to search collection: %w", err)
377+
}
378+
379+
if len(itemPaths) == 0 {
380+
return nil, store.ErrCredentialNotFound
381+
}
382+
383+
credentials := make(map[store.ID]store.Secret, len(itemPaths))
384+
for _, itemPath := range itemPaths {
385+
value, err := service.GetSecret(itemPath, *session)
386+
if err != nil {
387+
return nil, err
388+
}
389+
390+
attributes, err := service.GetAttributes(itemPath)
391+
if err != nil {
392+
return nil, err
393+
}
394+
395+
attrID, ok := attributes["id"]
396+
if !ok {
397+
return nil, errors.New("secret attributes does not contain `id` field")
398+
}
399+
400+
secret := k.factory()
401+
if err := secret.Unmarshal(value); err != nil {
402+
return nil, err
403+
}
404+
secretID, err := store.ParseID(attrID)
405+
if err != nil {
406+
return nil, err
407+
}
408+
credentials[secretID] = secret
409+
}
410+
411+
return credentials, nil
412+
}

0 commit comments

Comments
 (0)