-
Notifications
You must be signed in to change notification settings - Fork 7
build: implement ios builds #77
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
a1740f0
658989e
fee9052
afd4894
e0baaa3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,195 @@ | ||
| //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 | ||
|
|
||
| import ( | ||
| "errors" | ||
| "fmt" | ||
|
|
||
| gokeychain "github.com/byteness/go-keychain" | ||
| ) | ||
|
|
||
| type keychain struct { | ||
| service string | ||
|
|
||
| passwordFunc PromptFunc | ||
|
|
||
| isSynchronizable bool | ||
| isAccessibleWhenUnlocked bool | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is already the case with
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looking at the code, |
||
| } | ||
|
|
||
| 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 | ||
| } | ||
There was a problem hiding this comment.
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.:Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
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.