From 9eca1dfe385e6a870a603e5db6aa27bebec0bde7 Mon Sep 17 00:00:00 2001 From: milesbryant Date: Thu, 15 Sep 2022 17:31:40 +0100 Subject: [PATCH 1/3] WIP for touchid support based on https://github.com/99designs/aws-vault/pull/131 --- config.go | 1 + go.mod | 1 + go.sum | 2 ++ keychain.go | 101 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 105 insertions(+) diff --git a/config.go b/config.go index 590af7c..e99ea0d 100644 --- a/config.go +++ b/config.go @@ -55,4 +55,5 @@ type Config struct { // WinCredPrefix is a string prefix to prepend to the key name WinCredPrefix string + UseBiometrics bool } diff --git a/go.mod b/go.mod index c421d71..8288e82 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ 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/lox/go-touchid v0.0.0-20170712105233-619cc8e578d0 github.com/mtibben/percent v0.2.1 github.com/stretchr/testify v1.7.0 golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a diff --git a/go.sum b/go.sum index 09fbcb2..c3b7f2b 100644 --- a/go.sum +++ b/go.sum @@ -14,6 +14,8 @@ github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c/go.mod h1:NM github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lox/go-touchid v0.0.0-20170712105233-619cc8e578d0 h1:m81erW+1MD5vl3lKQ/+TYPHJ6Y9/C1COqxXPE51FkDk= +github.com/lox/go-touchid v0.0.0-20170712105233-619cc8e578d0/go.mod h1:EHbIQzfC3kdWFI81pLOFjssnolF+ALfmVf8PUdWBxo4= github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs= github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= diff --git a/keychain.go b/keychain.go index 8850922..ef726be 100644 --- a/keychain.go +++ b/keychain.go @@ -8,6 +8,14 @@ import ( "fmt" gokeychain "github.com/99designs/go-keychain" + + "github.com/lox/go-touchid" +) + +const ( + biometricsAccount = "com.99designs.aws-vault.biometrics" + biometricsService = "aws-vault" + biometricsLabel = "Passphrase for %s" ) type keychain struct { @@ -19,6 +27,9 @@ type keychain struct { isSynchronizable bool isAccessibleWhenUnlocked bool isTrusted bool + + isTouchIDAuthenticated bool + useTouchID bool } func init() { @@ -31,6 +42,9 @@ func init() { // KeychainAccessibleWhenUnlocked is a shorthand for setting the accessibility value. // See: https://developer.apple.com/documentation/security/ksecattraccessiblewhenunlocked isAccessibleWhenUnlocked: cfg.KeychainAccessibleWhenUnlocked, + + isTouchIDAuthenticated: false, + useTouchID: cfg.UseBiometrics, } if cfg.KeychainName != "" { kc.path = cfg.KeychainName + ".keychain" @@ -271,6 +285,9 @@ func (k *keychain) createOrOpen() (gokeychain.Keychain, error) { debugf("Checking keychain status") err := kc.Status() if err == nil { + if k.useTouchID { + return k.openWithTouchID() + } debugf("Keychain status returned nil, keychain exists") return kc, nil } @@ -294,3 +311,87 @@ func (k *keychain) createOrOpen() (gokeychain.Keychain, error) { debugf("Creating keychain %s with provided password", k.path) return gokeychain.NewKeychain(k.path, passphrase) } + +func (k *keychain) openWithTouchID() (gokeychain.Keychain, error) { + if k.isTouchIDAuthenticated { + return gokeychain.NewWithPath(k.path), nil + } + + debugf("checking with touchid") + ok, err := touchid.Authenticate("unlock " + k.path) + if err != nil { + return gokeychain.Keychain{}, fmt.Errorf("failed to authenticate with biometrics: %v", err) + } + if !ok { + return gokeychain.Keychain{}, fmt.Errorf("failed to authenticate with biometrics") + } + + k.isTouchIDAuthenticated = true + + debugf("looking up password in login.keychain") + query := gokeychain.NewItem() + query.SetSecClass(gokeychain.SecClassGenericPassword) + query.SetService(biometricsService) + query.SetAccount(biometricsAccount) + query.SetLabel(fmt.Sprintf(biometricsLabel, k.path)) + query.SetMatchLimit(gokeychain.MatchLimitOne) + query.SetReturnData(true) + + results, err := gokeychain.QueryItem(query) + if err != nil { + return gokeychain.Keychain{}, fmt.Errorf("failed to query keychain: %v", err) + } + + if len(results) != 1 { + kc, err := k.setupTouchID() + if err != nil { + return gokeychain.Keychain{}, fmt.Errorf("failed to setup touchid: %v", err) + } + return kc, nil + } else { + debugf("found password in login.keychain, unlocking %s with stored password", k.path) + if err := gokeychain.UnlockAtPath(k.path, string(results[0].Data)); err != nil { + return gokeychain.Keychain{}, fmt.Errorf("failed to unlock keychain: %v", err) + } + passPhrase := string(results[0].Data) + return gokeychain.NewKeychain(k.path, passPhrase) + } + + return gokeychain.NewWithPath(k.path), nil +} + +func (k *keychain) setupTouchID() (gokeychain.Keychain, error) { + fmt.Println("\nTo use Touch ID for authentication, your keychain password needs to be stored in your login keychain.\n" + + "You will be prompted for your password.\n") + passphrase, err := k.passwordFunc(fmt.Sprintf("Enter passphrase for %q", k.path)) + if err != nil { + return gokeychain.Keychain{}, fmt.Errorf("failed to get password: %v", err) + } + + fmt.Println() + debugf("locking keychain %s", k.path) + if err := gokeychain.LockAtPath(k.path); err != nil { + return gokeychain.Keychain{}, fmt.Errorf("failed to lock keychain: %v", err) + } + + debugf("unlocking keychain %s", k.path) + if err := gokeychain.UnlockAtPath(k.path, passphrase); err != nil { + return gokeychain.Keychain{}, fmt.Errorf("failed to unlock keychain: %v", err) + } + + item := gokeychain.NewItem() + item.SetSecClass(gokeychain.SecClassGenericPassword) + item.SetService(biometricsService) + item.SetAccount(biometricsAccount) + item.SetLabel(fmt.Sprintf(biometricsLabel, k.path)) + item.SetData([]byte(passphrase)) + item.SetSynchronizable(gokeychain.SynchronizableNo) + item.SetAccessible(gokeychain.AccessibleWhenUnlocked) + + debugf("Adding service=%q, account=%q to osx keychain %s", biometricsService, biometricsAccount, k.path) + if err := gokeychain.AddItem(item); err != nil { + return gokeychain.Keychain{}, fmt.Errorf("failed to add item to keychain: %v", err) + } + + return gokeychain.NewKeychain(k.path, passphrase) +} From d4fb1c426d0bb5825ef72e63f51db49a7d49ef6e Mon Sep 17 00:00:00 2001 From: milesbryant Date: Thu, 15 Sep 2022 20:22:07 +0100 Subject: [PATCH 2/3] Handle passwordFunc being nil --- go.mod | 1 + go.sum | 8 ++++++++ keychain.go | 23 ++++++++++++++++++++--- 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 8288e82..37a7e73 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/lox/go-touchid v0.0.0-20170712105233-619cc8e578d0 github.com/mtibben/percent v0.2.1 github.com/stretchr/testify v1.7.0 + golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 ) diff --git a/go.sum b/go.sum index c3b7f2b..7885107 100644 --- a/go.sum +++ b/go.sum @@ -28,12 +28,20 @@ github.com/stretchr/objx v0.3.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoH github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 h1:Y/gsMcFOcR+6S6f3YeMKl5g+dZMEWqcz5Czj/GWYbkM= +golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a h1:ppl5mZgokTT8uPkmYOyEUmPTr3ypaKkg5eFOGrAmxxE= golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 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= diff --git a/keychain.go b/keychain.go index ef726be..aab2ff2 100644 --- a/keychain.go +++ b/keychain.go @@ -6,8 +6,10 @@ package keyring import ( "errors" "fmt" + "os" gokeychain "github.com/99designs/go-keychain" + "golang.org/x/crypto/ssh/terminal" "github.com/lox/go-touchid" ) @@ -363,9 +365,24 @@ func (k *keychain) openWithTouchID() (gokeychain.Keychain, error) { func (k *keychain) setupTouchID() (gokeychain.Keychain, error) { fmt.Println("\nTo use Touch ID for authentication, your keychain password needs to be stored in your login keychain.\n" + "You will be prompted for your password.\n") - passphrase, err := k.passwordFunc(fmt.Sprintf("Enter passphrase for %q", k.path)) - if err != nil { - return gokeychain.Keychain{}, fmt.Errorf("failed to get password: %v", err) + + var passphrase string + if k.passwordFunc == nil { + debugf("Creating keychain %s with prompt", k.path) + fmt.Printf("Password for %q: ", k.path) + passphraseBytes, err := terminal.ReadPassword(int(os.Stdin.Fd())) + if err != nil { + return gokeychain.Keychain{}, fmt.Errorf("failed to read password: %v", err) + } + + passphrase = string(passphraseBytes) + return gokeychain.NewKeychainWithPrompt(k.path) + } else { + var err error + passphrase, err = k.passwordFunc(fmt.Sprintf("Enter passphrase for %q", k.path)) + if err != nil { + return gokeychain.Keychain{}, fmt.Errorf("failed to get password: %v", err) + } } fmt.Println() From 35a1afea02b0df25198c4b90dbe404fe24bd6f5e Mon Sep 17 00:00:00 2001 From: milesbryant Date: Fri, 16 Sep 2022 13:04:41 +0100 Subject: [PATCH 3/3] Tidy up and get it working! --- config.go | 8 ++++++ keychain.go | 70 ++++++++++++++++++++++++++++++++--------------------- 2 files changed, 51 insertions(+), 27 deletions(-) diff --git a/config.go b/config.go index e99ea0d..5c6dae8 100644 --- a/config.go +++ b/config.go @@ -55,5 +55,13 @@ type Config struct { // WinCredPrefix is a string prefix to prepend to the key name WinCredPrefix string + + // UseBiometrics is whether to use biometrics (where available) to unlock the keychain UseBiometrics bool + + // TouchIDAccount is the name of the account that we store the unlock password in keychain + TouchIDAccount string + + // TouchIDService is the name of the service that we store the unlock password in keychain + TouchIDService string } diff --git a/keychain.go b/keychain.go index aab2ff2..09e0a45 100644 --- a/keychain.go +++ b/keychain.go @@ -15,9 +15,7 @@ import ( ) const ( - biometricsAccount = "com.99designs.aws-vault.biometrics" - biometricsService = "aws-vault" - biometricsLabel = "Passphrase for %s" + touchIDLabel = "Passphrase for %s" ) type keychain struct { @@ -32,6 +30,8 @@ type keychain struct { isTouchIDAuthenticated bool useTouchID bool + touchIDAccount string + touchIDService string } func init() { @@ -46,7 +46,19 @@ func init() { isAccessibleWhenUnlocked: cfg.KeychainAccessibleWhenUnlocked, isTouchIDAuthenticated: false, - useTouchID: cfg.UseBiometrics, + } + if cfg.UseBiometrics { + switch { + case cfg.TouchIDAccount == "": + return kc, fmt.Errorf("TouchIDAccount must be non-empty if UseBiometrics is true") + + case cfg.TouchIDService == "": + return kc, fmt.Errorf("TouchIDService must be non-empty if UseBiometrics is true") + } + + kc.useTouchID = true + kc.touchIDAccount = cfg.TouchIDAccount + kc.touchIDService = cfg.TouchIDService } if cfg.KeychainName != "" { kc.path = cfg.KeychainName + ".keychain" @@ -316,6 +328,7 @@ func (k *keychain) createOrOpen() (gokeychain.Keychain, error) { func (k *keychain) openWithTouchID() (gokeychain.Keychain, error) { if k.isTouchIDAuthenticated { + // already unlocked, return keychain return gokeychain.NewWithPath(k.path), nil } @@ -330,12 +343,12 @@ func (k *keychain) openWithTouchID() (gokeychain.Keychain, error) { k.isTouchIDAuthenticated = true - debugf("looking up password in login.keychain") + debugf("looking up %s password in login.keychain", k.path) query := gokeychain.NewItem() query.SetSecClass(gokeychain.SecClassGenericPassword) - query.SetService(biometricsService) - query.SetAccount(biometricsAccount) - query.SetLabel(fmt.Sprintf(biometricsLabel, k.path)) + query.SetService(k.touchIDService) + query.SetAccount(k.touchIDAccount) + query.SetLabel(fmt.Sprintf(touchIDLabel, k.path)) query.SetMatchLimit(gokeychain.MatchLimitOne) query.SetReturnData(true) @@ -344,27 +357,31 @@ func (k *keychain) openWithTouchID() (gokeychain.Keychain, error) { return gokeychain.Keychain{}, fmt.Errorf("failed to query keychain: %v", err) } + var passphrase string if len(results) != 1 { - kc, err := k.setupTouchID() + // touch ID was never set up, let's do it now + var err error + passphrase, err = k.setupTouchID() if err != nil { return gokeychain.Keychain{}, fmt.Errorf("failed to setup touchid: %v", err) } - return kc, nil } else { debugf("found password in login.keychain, unlocking %s with stored password", k.path) - if err := gokeychain.UnlockAtPath(k.path, string(results[0].Data)); err != nil { + passphrase = string(results[0].Data) + + // try unlocking with the passphrase we found + if err := gokeychain.UnlockAtPath(k.path, passphrase); err != nil { return gokeychain.Keychain{}, fmt.Errorf("failed to unlock keychain: %v", err) } - passPhrase := string(results[0].Data) - return gokeychain.NewKeychain(k.path, passPhrase) } + // either way we've unlocked the keychain so we should be able to return it return gokeychain.NewWithPath(k.path), nil } -func (k *keychain) setupTouchID() (gokeychain.Keychain, error) { - fmt.Println("\nTo use Touch ID for authentication, your keychain password needs to be stored in your login keychain.\n" + - "You will be prompted for your password.\n") +func (k *keychain) setupTouchID() (string, error) { + fmt.Printf("\nTo use Touch ID for authentication, the aws-vault keychain password needs to be stored in your login keychain.\n" + + "You will be prompted for the password you use to unlock aws-vault.\n\n") var passphrase string if k.passwordFunc == nil { @@ -372,43 +389,42 @@ func (k *keychain) setupTouchID() (gokeychain.Keychain, error) { fmt.Printf("Password for %q: ", k.path) passphraseBytes, err := terminal.ReadPassword(int(os.Stdin.Fd())) if err != nil { - return gokeychain.Keychain{}, fmt.Errorf("failed to read password: %v", err) + return "", fmt.Errorf("failed to read password: %v", err) } passphrase = string(passphraseBytes) - return gokeychain.NewKeychainWithPrompt(k.path) } else { var err error passphrase, err = k.passwordFunc(fmt.Sprintf("Enter passphrase for %q", k.path)) if err != nil { - return gokeychain.Keychain{}, fmt.Errorf("failed to get password: %v", err) + return "", fmt.Errorf("failed to get password: %v", err) } } fmt.Println() debugf("locking keychain %s", k.path) if err := gokeychain.LockAtPath(k.path); err != nil { - return gokeychain.Keychain{}, fmt.Errorf("failed to lock keychain: %v", err) + return "", fmt.Errorf("failed to lock keychain: %v", err) } debugf("unlocking keychain %s", k.path) if err := gokeychain.UnlockAtPath(k.path, passphrase); err != nil { - return gokeychain.Keychain{}, fmt.Errorf("failed to unlock keychain: %v", err) + return "", fmt.Errorf("failed to unlock keychain: %v", err) } item := gokeychain.NewItem() item.SetSecClass(gokeychain.SecClassGenericPassword) - item.SetService(biometricsService) - item.SetAccount(biometricsAccount) - item.SetLabel(fmt.Sprintf(biometricsLabel, k.path)) + item.SetService(k.touchIDService) + item.SetAccount(k.touchIDAccount) + item.SetLabel(fmt.Sprintf(touchIDLabel, k.path)) item.SetData([]byte(passphrase)) item.SetSynchronizable(gokeychain.SynchronizableNo) item.SetAccessible(gokeychain.AccessibleWhenUnlocked) - debugf("Adding service=%q, account=%q to osx keychain %s", biometricsService, biometricsAccount, k.path) + debugf("Adding service=%q, account=%q to osx keychain %s", k.touchIDService, k.touchIDAccount, k.path) if err := gokeychain.AddItem(item); err != nil { - return gokeychain.Keychain{}, fmt.Errorf("failed to add item to keychain: %v", err) + return "", fmt.Errorf("failed to add item to keychain: %v", err) } - return gokeychain.NewKeychain(k.path, passphrase) + return passphrase, nil }