Skip to content
Merged
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
195 changes: 195 additions & 0 deletions keychain_ios.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
//go:build ios && cgo
Copy link
Copy Markdown
Contributor

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.

Copy link
Copy Markdown
Author

@gBasil gBasil Mar 21, 2026

Choose a reason for hiding this comment

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

The thing is, I'm not very familiar with building in Go, especially with CGO and Apple SDKs, so I'm not 100% certain that you strictly need to use a newer iOS version, but I personally haven't been able to figure out how to get around this. I'm sure it must be possible though. Thing is, I want to be able to build for older iOS, as I made this PR with the goal of helping get another project to compile for an iOS version below 15.

I do think a comment would be a good idea, perhaps even in the README, since that's far more likely to be read. But it should be warned somewhere that macOS 12.0+ is required too, since the API is only available there.

// NOTE: Due to newer versions of 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
Copy Markdown
Contributor

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.

Copy link
Copy Markdown
Author

@gBasil gBasil Mar 21, 2026

Choose a reason for hiding this comment

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

This is already the case with keychain.go. When making this PR, I simply copied over the original keyring.go, and removed everything that was either unused or macOS-specific. This slipped by me, but it also mirrors the behavior on mac, and the behavior that was already present.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Looking at the code, cfg.KeychainSynchronizable doesn't seem to actually be used anywhere, which I think is the root oversight. I've added support for it to both iOS & macOS.

}

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

isSynchronizable: cfg.KeychainSynchronizable,

// Set isAccessibleWhenUnlocked to the boolean value of KeychainAccessibleWhenUnlocked,
// which is a shorthand for setting the accessibility value.
// 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
}
9 changes: 5 additions & 4 deletions keychain.go → keychain_macos.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 Expand Up @@ -41,8 +40,10 @@ func init() {
service: cfg.ServiceName,
passwordFunc: cfg.KeychainPasswordFunc,

// Set the isAccessibleWhenUnlocked to the boolean value of
// KeychainAccessibleWhenUnlocked is a shorthand for setting the accessibility value.
isSynchronizable: cfg.KeychainSynchronizable,

// Set isAccessibleWhenUnlocked to the boolean value of KeychainAccessibleWhenUnlocked,
// which is a shorthand for setting the accessibility value.
// See: https://developer.apple.com/documentation/security/ksecattraccessiblewhenunlocked
isAccessibleWhenUnlocked: cfg.KeychainAccessibleWhenUnlocked,

Expand Down
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