Skip to content

Commit 3ac7bbc

Browse files
authored
Merge pull request #47 from docker/keychain-fix-darwin-list
store/keychain: fix macOS GetAll secrets
2 parents bf50eb5 + 5f74bfe commit 3ac7bbc

1 file changed

Lines changed: 67 additions & 15 deletions

File tree

store/keychain/keychain_darwin.go

Lines changed: 67 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,15 @@ var (
1313
ErrAuthFailed = errors.New("user incorrectly enterered their credentials")
1414
)
1515

16-
func newKeychainItem[T store.Secret](id store.ID, k *keychainStore[T]) kc.Item {
16+
// newKeychainItem creates a new keychain item with valid default parameters.
17+
//
18+
// It uses a generic password class, which is suitable for most use cases.
19+
//
20+
// By default, the item will only return one secret when queried.
21+
//
22+
// The id parameter can be empty, in which case the item will search based on
23+
// the service name and group, but not the item label or account.
24+
func newKeychainItem[T store.Secret](id string, k *keychainStore[T]) kc.Item {
1725
item := kc.NewItem()
1826
// generic password is used here as we don't know what we are storing
1927
// the main difference between a generic and internet password is the
@@ -23,27 +31,27 @@ func newKeychainItem[T store.Secret](id store.ID, k *keychainStore[T]) kc.Item {
2331
// set this to MatchLimitAll if you want to retrieve all items
2432
item.SetMatchLimit(kc.MatchLimitOne)
2533
item.SetAccessible(kc.AccessibleAfterFirstUnlock)
26-
item.SetReturnData(true)
2734
item.SetReturnAttributes(true)
2835

2936
item.SetService(k.serviceName)
3037
item.SetAccessGroup(k.serviceGroup)
3138

32-
if id.String() != "" {
33-
item.SetLabel(k.itemLabel(id))
34-
item.SetAccount(id.String())
39+
if id != "" {
40+
item.SetAccount(id)
3541
}
3642

3743
return item
3844
}
3945

40-
func (k *keychainStore[T]) Delete(ctx context.Context, id store.ID) error {
46+
// getItemWithData retrieves a keychain item with its data.
47+
//
48+
// It uses the SetReturnData attribute to query for an item with its data.
49+
// It cannot be used with MatchLimitAll, as it will return an error.
50+
// https://developer.apple.com/documentation/security/secitemcopymatching(_:_:)#Discussion
51+
func getItemWithData[T store.Secret](id string, k *keychainStore[T]) (*kc.QueryResult, error) {
4152
item := newKeychainItem(id, k)
42-
return mapKeychainError(kc.DeleteItem(item))
43-
}
53+
item.SetReturnData(true)
4454

45-
func (k *keychainStore[T]) Get(ctx context.Context, id store.ID) (store.Secret, error) {
46-
item := newKeychainItem(id, k)
4755
results, err := kc.QueryItem(item)
4856
if err != nil {
4957
return nil, mapKeychainError(err)
@@ -52,40 +60,84 @@ func (k *keychainStore[T]) Get(ctx context.Context, id store.ID) (store.Secret,
5260
return nil, store.ErrCredentialNotFound
5361
}
5462

63+
return &results[0], nil
64+
}
65+
66+
func (k *keychainStore[T]) Delete(ctx context.Context, id store.ID) error {
67+
if err := id.Valid(); err != nil {
68+
return err
69+
}
70+
71+
item := newKeychainItem(id.String(), k)
72+
return mapKeychainError(kc.DeleteItem(item))
73+
}
74+
75+
func (k *keychainStore[T]) Get(ctx context.Context, id store.ID) (store.Secret, error) {
76+
if err := id.Valid(); err != nil {
77+
return nil, err
78+
}
79+
80+
result, err := getItemWithData(id.String(), k)
81+
if err != nil {
82+
return nil, err
83+
}
84+
5585
secret := k.factory()
56-
if err := secret.Unmarshal(results[0].Data); err != nil {
86+
if err := secret.Unmarshal(result.Data); err != nil {
5787
return nil, err
5888
}
5989
return secret, nil
6090
}
6191

6292
func (k *keychainStore[T]) GetAll(ctx context.Context) (map[store.ID]store.Secret, error) {
63-
item := newKeychainItem(store.ID(""), k)
93+
item := newKeychainItem("", k)
94+
95+
// We use the MatchLimitAll attribute to query for multiple items from the
96+
// store. It cannot be used with item.SetReturnData.
97+
// https://developer.apple.com/documentation/security/secitemcopymatching(_:_:)#Discussion
6498
item.SetMatchLimit(kc.MatchLimitAll)
6599

66100
results, err := kc.QueryItem(item)
67101
if err != nil {
68102
return nil, mapKeychainError(err)
69103
}
104+
70105
creds := make(map[store.ID]store.Secret, len(results))
71106
for _, result := range results {
107+
id, err := store.ParseID(result.Account)
108+
if err != nil {
109+
return nil, err
110+
}
111+
112+
i, err := getItemWithData(id.String(), k)
113+
if err != nil {
114+
return nil, err
115+
}
116+
72117
secret := k.factory()
73-
if err := secret.Unmarshal(result.Data); err != nil {
118+
if err := secret.Unmarshal(i.Data); err != nil {
74119
return nil, err
75120
}
76-
id := store.ID(result.Label)
77121
creds[id] = secret
78122
}
79123
return creds, nil
80124
}
81125

82126
func (k *keychainStore[T]) Save(ctx context.Context, id store.ID, secret store.Secret) error {
127+
if err := id.Valid(); err != nil {
128+
return err
129+
}
130+
83131
data, err := secret.Marshal()
84132
if err != nil {
85133
return err
86134
}
87-
item := newKeychainItem(id, k)
135+
item := newKeychainItem(id.String(), k)
88136
item.SetData(data)
137+
// only creation of a secret needs the label attribute.
138+
// it is a user-friendly name for the item, which is displayed in the keychain UI.
139+
// https://developer.apple.com/documentation/security/ksecattrlabel
140+
item.SetLabel(k.itemLabel(id))
89141
return mapKeychainError(kc.AddItem(item))
90142
}
91143

0 commit comments

Comments
 (0)