Skip to content
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

Adding touch ID support (via new data protection keychain) #136

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
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
9 changes: 9 additions & 0 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,15 @@ type Config struct {
// KeychainPasswordFunc is an optional function used to prompt the user for a password
KeychainPasswordFunc PromptFunc

// Access control options for the (data protection keychain)
KeychainAccessControl []string

// Access constraint for the (data protection keychain)
KeychainAccessConstraint string

// Number of seconds to allow reuse of biometrics without prompting the user
BioMetricsAllowableReuseDuration int // seconds

// FilePasswordFunc is a required function used to prompt the user for a password
FilePasswordFunc PromptFunc

Expand Down
308 changes: 308 additions & 0 deletions data_protection_keychain.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,308 @@
//go:build darwin && cgo
// +build darwin,cgo

package keyring

import (
"errors"
"fmt"

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

type DataProtectionKeychain struct {
service string

authenticationContext *gokeychain.AuthenticationContext

isSynchronizable bool
accessControlFlags gokeychain.AccessControlFlags
accessConstraint gokeychain.Accessible
}

func init() {
supportedBackends[DataProtectionKeychainBackend] = opener(func(cfg Config) (Keyring, error) {
if !gokeychain.CanUseDataProtectionKeychain() {
return nil, errors.New("SecAccessControl is not available on this platform")
}

var authCtxOptions gokeychain.AuthenticationContextOptions

if cfg.BioMetricsAllowableReuseDuration > 0 {
authCtxOptions.AllowableReuseDuration = cfg.BioMetricsAllowableReuseDuration
} else if cfg.BioMetricsAllowableReuseDuration < 0 {
return nil, errors.New("BioMetricsAllowableReuseDuration must be greater than 0")
}

authCtx := gokeychain.CreateAuthenticationContext(authCtxOptions)

accessConstraint, err := mapConstraint(cfg.KeychainAccessConstraint)
if err != nil {
return nil, err
}

accessControlFlags, err := mapStringsToFlags(cfg.KeychainAccessControl)
if err != nil {
return nil, err
}

kc := &DataProtectionKeychain{
service: cfg.ServiceName,

authenticationContext: authCtx,
accessControlFlags: accessControlFlags,
accessConstraint: accessConstraint,
}

if kc.accessConstraint == 0 {
kc.accessConstraint = gokeychain.AccessibleWhenUnlockedThisDeviceOnly
}

return kc, nil
})
}

func (k *DataProtectionKeychain) 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)
query.SetUseDataProtectionKeychain(true)

err := query.SetAuthenticationContext(k.authenticationContext)
if err != nil {
return Item{}, err
}

debugf("Querying item in data protection 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 *DataProtectionKeychain) 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)
query.SetUseDataProtectionKeychain(true)

err := query.SetAuthenticationContext(k.authenticationContext)
if err != nil {
return Metadata{}, err
}

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 *DataProtectionKeychain) updateItem(account string, data []byte) error {
queryItem := gokeychain.NewItem()
queryItem.SetSecClass(gokeychain.SecClassGenericPassword)
queryItem.SetService(k.service)
queryItem.SetAccount(account)
queryItem.SetMatchLimit(gokeychain.MatchLimitOne)
queryItem.SetReturnAttributes(true)
queryItem.SetUseDataProtectionKeychain(true)

err := queryItem.SetAuthenticationContext(k.authenticationContext)
if err != nil {
return err
}

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")
}

updateItem := gokeychain.NewItem()
updateItem.SetData(data)

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

return nil
}

func (k *DataProtectionKeychain) 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)
kcItem.SetUseDataProtectionKeychain(true)

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

kcItem.SetAccessControl(k.accessControlFlags, k.accessConstraint)

debugf("Adding service=%q, label=%q, account=%q", k.service, item.Label, item.Key)

err := gokeychain.AddItem(kcItem)

if err == gokeychain.ErrorDuplicateItem {
debugf("Item already exists, updating item service=%q, account=%q", k.service, item.Key)
err = k.updateItem(item.Key, item.Data)
}

if err != nil {
return err
}

return nil
}

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

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

if err != nil {
return fmt.Errorf("failed to delete item from data protection keychain: %v", err)
}

return nil
}

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

err := query.SetAuthenticationContext(k.authenticationContext)
if err != nil {
return nil, err
}

debugf("Querying keys in data protection 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
}

func mapStringsToFlags(strings []string) (gokeychain.AccessControlFlags, error) {
var flags gokeychain.AccessControlFlags

flagMap := map[string]gokeychain.AccessControlFlags{
"UserPresence": gokeychain.AccessControlFlagsUserPresence,
"BiometryAny": gokeychain.AccessControlFlagsBiometryAny,
"BiometryCurrentSet": gokeychain.AccessControlFlagsBiometryCurrentSet,
"DevicePasscode": gokeychain.AccessControlFlagsDevicePasscode,
"Watch": gokeychain.AccessControlFlagsWatch,
"Or": gokeychain.AccessControlFlagsOr,
"And": gokeychain.AccessControlFlagsAnd,
"PrivateKeyUsage": gokeychain.AccessControlFlagsPrivateKeyUsage,
"ApplicationPassword": gokeychain.AccessControlFlagsApplicationPassword,
}

for _, flagString := range strings {
if flag, exists := flagMap[flagString]; exists {
flags |= flag // Combine flags using bitwise OR
} else {
return 0, fmt.Errorf("invalid access control flag: %s", flagString)
}
}

return flags, nil
}

func mapConstraint(constraint string) (gokeychain.Accessible, error) {
switch constraint {
case "AccessibleWhenUnlocked":
return gokeychain.AccessibleWhenUnlocked, nil
case "AccessibleAfterFirstUnlock":
return gokeychain.AccessibleAfterFirstUnlock, nil
case "AccessibleAfterFirstUnlockThisDeviceOnly":
return gokeychain.AccessibleAfterFirstUnlockThisDeviceOnly, nil
case "AccessibleWhenPasscodeSetThisDeviceOnly":
return gokeychain.AccessibleWhenPasscodeSetThisDeviceOnly, nil
case "AccessibleWhenUnlockedThisDeviceOnly":
return gokeychain.AccessibleWhenUnlockedThisDeviceOnly, nil
// @deprecated
// https://developer.apple.com/documentation/security/ksecattraccessiblealwaysthisdeviceonly
// https://developer.apple.com/documentation/security/ksecattraccessiblealways
case "AccessibleAccessibleAlwaysThisDeviceOnly":
case "AccessibleAlways":
return 0, fmt.Errorf("AccessibleAlways and AccessibleAccessibleAlwaysThisDeviceOnly have been deprecated, use AccessibleWhenUnlockedThisDeviceOnly instead")
case "":
return gokeychain.AccessibleDefault, nil
default:
return 0, fmt.Errorf("invalid access constraint: %s", constraint)
}

return 0, nil
}
8 changes: 5 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,17 @@ require (
github.com/dvsekhvalnov/jose2go v1.5.0
github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2
github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c
github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6
github.com/mtibben/percent v0.2.1
github.com/stretchr/testify v1.7.0
github.com/stretchr/testify v1.8.4
golang.org/x/sys v0.3.0
golang.org/x/term v0.3.0
)

replace github.com/keybase/go-keychain => github.com/alexw23/go-keychain v0.0.0-20240507145345-41efe171240e

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/stretchr/objx v0.3.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
13 changes: 7 additions & 6 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 h1:/vQbFIOMbk2FiG/kXiLl8BRyzTWDw7gX/Hz7Dd5eDMs=
github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN7oaIRCjzsZ2dE+yG5k+rsdt3qcwykqK6HVGcKwsw4=
github.com/alexw23/go-keychain v0.0.0-20240507145345-41efe171240e h1:tr4NMs+H918AUrqOpYkjfeZDg7554ufuQXS/Ego/JRU=
github.com/alexw23/go-keychain v0.0.0-20240507145345-41efe171240e/go.mod h1:3VeWNIJaW+O5xpRQbPp0Ybqu1vJd/pm7s2F473HRrkw=
github.com/danieljoos/wincred v1.1.2 h1:QLdCxFs1/Yl4zduvBdcHB8goaYk9RARS2SgLLRuAyr0=
github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnGqR5Vl2tAx0=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
Expand All @@ -21,11 +23,10 @@ github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLA
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.3.0 h1:NGXK3lHquSN08v5vWalVI/L8XU9hdzE/G6xsrze47As=
github.com/stretchr/objx v0.3.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
Expand All @@ -35,5 +36,5 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b h1:QRR6H1YWRnHb4Y/HeNFCTJLFVxaq6wH4YuVdsUOr75U=
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
Loading