From a1740f05e3e1041ee934d45755a84f4a60be0a36 Mon Sep 17 00:00:00 2001 From: Basil Date: Thu, 19 Mar 2026 23:22:47 +0000 Subject: [PATCH 1/5] build: implement ios builds --- keychain.go => keychain_darwin.go | 3 +- keychain_ios.go | 191 ++++++++++++++++++++++++++++++ keyring.go | 2 +- 3 files changed, 193 insertions(+), 3 deletions(-) rename keychain.go => keychain_darwin.go (99%) create mode 100644 keychain_ios.go diff --git a/keychain.go b/keychain_darwin.go similarity index 99% rename from keychain.go rename to keychain_darwin.go index 76482bc..932eef2 100644 --- a/keychain.go +++ b/keychain_darwin.go @@ -1,5 +1,4 @@ -//go:build darwin && cgo -// +build darwin,cgo +//go:build darwin && !ios && cgo package keyring diff --git a/keychain_ios.go b/keychain_ios.go new file mode 100644 index 0000000..fea70fc --- /dev/null +++ b/keychain_ios.go @@ -0,0 +1,191 @@ +//go:build ios && cgo + +package keyring + +import ( + "errors" + "fmt" + + gokeychain "github.com/byteness/go-keychain" +) + +type keychain struct { + service string + + passwordFunc PromptFunc + + isSynchronizable bool + isAccessibleWhenUnlocked bool +} + +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. + // 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 +} diff --git a/keyring.go b/keyring.go index ca5865c..47a4981 100644 --- a/keyring.go +++ b/keyring.go @@ -35,7 +35,7 @@ const ( var backendOrder = []BackendType{ // Windows WinCredBackend, - // MacOS + // MacOS & iOS KeychainBackend, // Linux SecretServiceBackend, From 658989e9a8d86bd3810def06a21e201068ee10a9 Mon Sep 17 00:00:00 2001 From: Basil Date: Sat, 21 Mar 2026 19:06:09 +0000 Subject: [PATCH 2/5] add note to keychain_ios.go about system requirements --- keychain_ios.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/keychain_ios.go b/keychain_ios.go index fea70fc..9af4cf6 100644 --- a/keychain_ios.go +++ b/keychain_ios.go @@ -1,4 +1,6 @@ //go:build ios && cgo +// NOTE: Due to newer versions of Go's crypto library requiring SecTrustCopyCertificateChain, +// this requires iOS 15.0 or later. package keyring From fee9052fc180706febb306b6ab8b9d8471fb4b41 Mon Sep 17 00:00:00 2001 From: Basil Date: Sat, 21 Mar 2026 19:06:37 +0000 Subject: [PATCH 3/5] rename keychain_darwin.go to keychain_macos.go --- keychain_darwin.go => keychain_macos.go | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename keychain_darwin.go => keychain_macos.go (100%) diff --git a/keychain_darwin.go b/keychain_macos.go similarity index 100% rename from keychain_darwin.go rename to keychain_macos.go From afd48943df203f54a8acad129f19cecb7dc6b167 Mon Sep 17 00:00:00 2001 From: Basil Date: Sat, 21 Mar 2026 19:16:08 +0000 Subject: [PATCH 4/5] fix KeychainSynchronizable not being used --- keychain_ios.go | 2 ++ keychain_macos.go | 2 ++ 2 files changed, 4 insertions(+) diff --git a/keychain_ios.go b/keychain_ios.go index 9af4cf6..8a75350 100644 --- a/keychain_ios.go +++ b/keychain_ios.go @@ -26,6 +26,8 @@ func init() { service: cfg.ServiceName, passwordFunc: cfg.KeychainPasswordFunc, + isSynchronizable: cfg.KeychainSynchronizable, + // Set the isAccessibleWhenUnlocked to the boolean value of // KeychainAccessibleWhenUnlocked is a shorthand for setting the accessibility value. // See: https://developer.apple.com/documentation/security/ksecattraccessiblewhenunlocked diff --git a/keychain_macos.go b/keychain_macos.go index 932eef2..d3404e3 100644 --- a/keychain_macos.go +++ b/keychain_macos.go @@ -40,6 +40,8 @@ func init() { service: cfg.ServiceName, passwordFunc: cfg.KeychainPasswordFunc, + isSynchronizable: cfg.KeychainSynchronizable, + // Set the isAccessibleWhenUnlocked to the boolean value of // KeychainAccessibleWhenUnlocked is a shorthand for setting the accessibility value. // See: https://developer.apple.com/documentation/security/ksecattraccessiblewhenunlocked From e0baaa35d0e6ac1d645d911413478a8c4c888d4c Mon Sep 17 00:00:00 2001 From: Basil Date: Sat, 21 Mar 2026 19:18:22 +0000 Subject: [PATCH 5/5] fix confusing wording in keychain comments --- keychain_ios.go | 4 ++-- keychain_macos.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/keychain_ios.go b/keychain_ios.go index 8a75350..822bcd0 100644 --- a/keychain_ios.go +++ b/keychain_ios.go @@ -28,8 +28,8 @@ func init() { isSynchronizable: cfg.KeychainSynchronizable, - // Set the isAccessibleWhenUnlocked to the boolean value of - // KeychainAccessibleWhenUnlocked is a shorthand for setting the accessibility value. + // 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, } diff --git a/keychain_macos.go b/keychain_macos.go index d3404e3..ad66bf5 100644 --- a/keychain_macos.go +++ b/keychain_macos.go @@ -42,8 +42,8 @@ func init() { isSynchronizable: cfg.KeychainSynchronizable, - // Set the isAccessibleWhenUnlocked to the boolean value of - // KeychainAccessibleWhenUnlocked is a shorthand for setting the accessibility value. + // 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,