Skip to content
Open
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
3 changes: 1 addition & 2 deletions keychain.go → keychain_darwin.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
//go:build darwin && cgo
// +build darwin,cgo
//go:build darwin && !ios && cgo

package keyring

Expand Down
191 changes: 191 additions & 0 deletions keychain_ios.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
//go:build ios && cgo
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You comment notes that iOS 15.0+ is effectively required due to a Go runtime dependency on SecTrustCopyCertificateChain. This is a meaningful constraint for downstream users, but it's not captured anywhere in the code (no comment, no doc, no README update). It would be worth at minimum adding a comment near the build tag, e.g.:

//go:build ios && cgo
// NOTE: Due to Go's crypto library requiring SecTrustCopyCertificateChain,
// this requires iOS 15.0 or later.


package keyring

import (
"errors"
"fmt"

gokeychain "github.com/byteness/go-keychain"
)

type keychain struct {
service string

passwordFunc PromptFunc

isSynchronizable bool
isAccessibleWhenUnlocked bool
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't seem to be initialised?

The keychain struct includes isSynchronizable, and Set does check it — but the init() function that constructs the struct never sets it from cfg. Looking at the Darwin implementation, cfg.KeychainSynchronizable is presumably used to populate this field. The iOS init currently omits this, meaning isSynchronizable will always be false even if a caller configures it. This looks like an oversight.

}

func init() {
supportedBackends[KeychainBackend] = opener(func(cfg Config) (Keyring, error) {
kc := &keychain{
service: cfg.ServiceName,
passwordFunc: cfg.KeychainPasswordFunc,

// Set the isAccessibleWhenUnlocked to the boolean value of
// KeychainAccessibleWhenUnlocked is a shorthand for setting the accessibility value.
Comment on lines +27 to +28
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this part of the same sentance, reads a bit weird 🤔

// See: https://developer.apple.com/documentation/security/ksecattraccessiblewhenunlocked
isAccessibleWhenUnlocked: cfg.KeychainAccessibleWhenUnlocked,
}
return kc, nil
})
}

func (k *keychain) Get(key string) (Item, error) {
query := gokeychain.NewItem()
query.SetSecClass(gokeychain.SecClassGenericPassword)
query.SetService(k.service)
query.SetAccount(key)
query.SetMatchLimit(gokeychain.MatchLimitOne)
query.SetReturnAttributes(true)
query.SetReturnData(true)

debugf("Querying keychain for service=%q, account=%q", k.service, key)
results, err := gokeychain.QueryItem(query)
if err == gokeychain.ErrorItemNotFound || len(results) == 0 {
debugf("No results found")
return Item{}, ErrKeyNotFound
}

if err != nil {
debugf("Error: %#v", err)
return Item{}, err
}

item := Item{
Key: key,
Data: results[0].Data,
Label: results[0].Label,
Description: results[0].Description,
}

debugf("Found item %q", results[0].Label)
return item, nil
}

func (k *keychain) GetMetadata(key string) (Metadata, error) {
query := gokeychain.NewItem()
query.SetSecClass(gokeychain.SecClassGenericPassword)
query.SetService(k.service)
query.SetAccount(key)
query.SetMatchLimit(gokeychain.MatchLimitOne)
query.SetReturnAttributes(true)
query.SetReturnData(false)
query.SetReturnRef(true)

debugf("Querying keychain for metadata of service=%q, account=%q", k.service, key)
results, err := gokeychain.QueryItem(query)
if err == gokeychain.ErrorItemNotFound || len(results) == 0 {
debugf("No results found")
return Metadata{}, ErrKeyNotFound
} else if err != nil {
debugf("Error: %#v", err)
return Metadata{}, err
}

md := Metadata{
Item: &Item{
Key: key,
Label: results[0].Label,
Description: results[0].Description,
},
ModificationTime: results[0].ModificationDate,
}

debugf("Found metadata for %q", md.Item.Label)

return md, nil
}

func (k *keychain) updateItem(kcItem gokeychain.Item, account string) error {
queryItem := gokeychain.NewItem()
queryItem.SetSecClass(gokeychain.SecClassGenericPassword)
queryItem.SetService(k.service)
queryItem.SetAccount(account)
queryItem.SetMatchLimit(gokeychain.MatchLimitOne)
queryItem.SetReturnAttributes(true)

results, err := gokeychain.QueryItem(queryItem)
if err != nil {
return fmt.Errorf("Failed to query keychain: %v", err)
}
if len(results) == 0 {
return errors.New("no results")
}

if err := gokeychain.UpdateItem(queryItem, kcItem); err != nil {
return fmt.Errorf("Failed to update item in keychain: %v", err)
}

return nil
}

func (k *keychain) Set(item Item) error {
kcItem := gokeychain.NewItem()
kcItem.SetSecClass(gokeychain.SecClassGenericPassword)
kcItem.SetService(k.service)
kcItem.SetAccount(item.Key)
kcItem.SetLabel(item.Label)
kcItem.SetDescription(item.Description)
kcItem.SetData(item.Data)

if k.isSynchronizable && !item.KeychainNotSynchronizable {
kcItem.SetSynchronizable(gokeychain.SynchronizableYes)
}

if k.isAccessibleWhenUnlocked {
kcItem.SetAccessible(gokeychain.AccessibleWhenUnlocked)
}

err := gokeychain.AddItem(kcItem)

if err == gokeychain.ErrorDuplicateItem {
debugf("Item already exists, updating")
err = k.updateItem(kcItem, item.Key)
}

if err != nil {
return err
}

return nil
}

func (k *keychain) Remove(key string) error {
item := gokeychain.NewItem()
item.SetSecClass(gokeychain.SecClassGenericPassword)
item.SetService(k.service)
item.SetAccount(key)

debugf("Removing keychain item service=%q, account=%q", k.service, key)
err := gokeychain.DeleteItem(item)
if err == gokeychain.ErrorItemNotFound {
return ErrKeyNotFound
}

return err
}

func (k *keychain) Keys() ([]string, error) {
query := gokeychain.NewItem()
query.SetSecClass(gokeychain.SecClassGenericPassword)
query.SetService(k.service)
query.SetMatchLimit(gokeychain.MatchLimitAll)
query.SetReturnAttributes(true)

debugf("Querying keychain for service=%q", k.service)
results, err := gokeychain.QueryItem(query)
if err != nil {
return nil, err
}

debugf("Found %d results", len(results))
accountNames := make([]string, len(results))
for idx, r := range results {
accountNames[idx] = r.Account
}

return accountNames, nil
}
2 changes: 1 addition & 1 deletion keyring.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const (
var backendOrder = []BackendType{
// Windows
WinCredBackend,
// MacOS
// MacOS & iOS
KeychainBackend,
// Linux
SecretServiceBackend,
Expand Down