From 73d3e2c985ada0a884e5893434e7cab31f0a1f1b Mon Sep 17 00:00:00 2001 From: Lachlan Donald Date: Sat, 5 Aug 2017 14:48:23 +1000 Subject: [PATCH 1/6] Add a biometrics flag --- cli/global.go | 7 +++++++ vendor/github.com/99designs/keyring/keyring.go | 10 ++++++---- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/cli/global.go b/cli/global.go index 0a62bfdb6..62dda0cbb 100644 --- a/cli/global.go +++ b/cli/global.go @@ -26,6 +26,7 @@ var GlobalFlags struct { Debug bool Backend string PromptDriver string + Biometrics bool } func ConfigureGlobals(app *kingpin.Application) { @@ -42,6 +43,9 @@ func ConfigureGlobals(app *kingpin.Application) { OverrideDefaultFromEnvar("AWS_VAULT_PROMPT"). EnumVar(&GlobalFlags.PromptDriver, promptsAvailable...) + app.Flag("biometrics", "Use biometric authentication if supported"). + BoolVar(&GlobalFlags.Biometrics) + app.PreAction(func(c *kingpin.ParseContext) (err error) { if !GlobalFlags.Debug { log.SetOutput(ioutil.Discard) @@ -49,6 +53,9 @@ func ConfigureGlobals(app *kingpin.Application) { if keyringImpl == nil { keyringImpl, err = keyring.Open(KeyringName, GlobalFlags.Backend) } + if globals.Biometrics { + keyring.UseBiometricsIfAvailable = true + } if awsConfigFile == nil { awsConfigFile, err = vault.NewConfigFromEnv() } diff --git a/vendor/github.com/99designs/keyring/keyring.go b/vendor/github.com/99designs/keyring/keyring.go index 2a52c72be..831946532 100644 --- a/vendor/github.com/99designs/keyring/keyring.go +++ b/vendor/github.com/99designs/keyring/keyring.go @@ -3,12 +3,14 @@ package keyring import "errors" const ( - SecretServiceBackend string = "secret-service" - KeychainBackend string = "keychain" - KWalletBackend string = "kwallet" - FileBackend string = "file" + SecretServiceBackend string = "secret-service" + KeychainBackend string = "keychain" + KWalletBackend string = "kwallet" + FileBackend string = "file" ) +var UseBiometricsIfAvailable bool + var DefaultBackend = FileBackend var supportedBackends = map[string]opener{} From 296ee907f40742524bfb94858e471672d0b6a261 Mon Sep 17 00:00:00 2001 From: Lachlan Donald Date: Sat, 5 Aug 2017 16:19:07 +1000 Subject: [PATCH 2/6] Initial implementation of touch id support --- cli/exec.go | 1 + cli/global.go | 2 +- .../github.com/99designs/keyring/keychain.go | 101 +++++++++++++++++- .../github.com/99designs/keyring/keyring.go | 4 +- vendor/github.com/lox/go-touchid/README.md | 26 +++++ vendor/github.com/lox/go-touchid/touchid.go | 56 ++++++++++ vendor/vendor.json | 6 ++ 7 files changed, 191 insertions(+), 5 deletions(-) create mode 100644 vendor/github.com/lox/go-touchid/README.md create mode 100644 vendor/github.com/lox/go-touchid/touchid.go diff --git a/cli/exec.go b/cli/exec.go index 8d4577957..c84b12b5e 100644 --- a/cli/exec.go +++ b/cli/exec.go @@ -112,6 +112,7 @@ func ExecCommand(app *kingpin.Application, input ExecCommandInput) { val, err := creds.Get() if err != nil { app.Fatalf(vault.FormatCredentialError(input.Profile, profiles, err)) + return } if input.StartServer { diff --git a/cli/global.go b/cli/global.go index 62dda0cbb..ac51eb725 100644 --- a/cli/global.go +++ b/cli/global.go @@ -54,7 +54,7 @@ func ConfigureGlobals(app *kingpin.Application) { keyringImpl, err = keyring.Open(KeyringName, GlobalFlags.Backend) } if globals.Biometrics { - keyring.UseBiometricsIfAvailable = true + keyring.Config.UseBiometrics = true } if awsConfigFile == nil { awsConfigFile, err = vault.NewConfigFromEnv() diff --git a/vendor/github.com/99designs/keyring/keychain.go b/vendor/github.com/99designs/keyring/keychain.go index 910d3f486..29ae75d96 100644 --- a/vendor/github.com/99designs/keyring/keychain.go +++ b/vendor/github.com/99designs/keyring/keychain.go @@ -3,16 +3,29 @@ package keyring import ( + "errors" "fmt" "log" + "os" + + "golang.org/x/crypto/ssh/terminal" gokeychain "github.com/keybase/go-keychain" + touchid "github.com/lox/go-touchid" +) + +const ( + keychainAccessGroup = "ACE1234DEF.com.99designs.aws-vault" + biometricsAccount = "com.99designs.aws-vault.biometrics" + biometricsService = "aws-vault" + biometricsLabel = "Passphrase for %s" ) type keychain struct { - path string - service string - passphrase string + path string + service string + passphrase string + authenticated bool } func init() { @@ -45,6 +58,7 @@ func (k *keychain) Get(key string) (Item, error) { query.SetReturnData(true) query.SetMatchSearchList(kc) + log.Printf("Querying service=%q, account=%q in osx keychain %s", k.service, key, k.path) results, err := gokeychain.QueryItem(query) if err == gokeychain.ErrorItemNotFound || len(results) == 0 { return Item{}, ErrKeyNotFound @@ -145,11 +159,92 @@ func (k *keychain) Keys() ([]string, error) { return accountNames, nil } +func (k *keychain) setupBiometrics() error { + fmt.Println("\nTo use biometrics for authentication, your keychain password needs to be stored in your login keychain.\n" + + "You will be prompted for your password.\n") + + fmt.Printf("Password for %q: ", k.path) + passphrase, err := terminal.ReadPassword(int(os.Stdin.Fd())) + if err != nil { + return err + } + + fmt.Println() + + // needs to be locked first in-case it's already unlocked. if so, an incorrect password can be stored + log.Printf("Locking keychain %s", k.path) + gokeychain.LockAtPath(k.path) + + log.Printf("Unlocking keychain %s", k.path) + if err := gokeychain.UnlockAtPath(k.path, string(passphrase)); err != nil { + return err + } + + k.passphrase = string(passphrase) + + item := gokeychain.NewItem() + item.SetSecClass(gokeychain.SecClassGenericPassword) + item.SetService(biometricsService) + item.SetAccount(biometricsAccount) + item.SetLabel(fmt.Sprintf(biometricsLabel, k.path)) + item.SetData(passphrase) + item.SetSynchronizable(gokeychain.SynchronizableNo) + item.SetAccessible(gokeychain.AccessibleWhenUnlocked) + + log.Printf("Adding service=%q, account=%q to osx keychain %s", biometricsService, biometricsAccount, k.path) + return gokeychain.AddItem(item) +} + +func (k *keychain) openWithBiometrics() (gokeychain.Keychain, error) { + if !k.authenticated { + log.Printf("Checking touchid") + ok, err := touchid.Authenticate("unlock " + k.path) + if !ok || err != nil { + return gokeychain.Keychain{}, errors.New("Authentication with biometrics failed") + } + + k.authenticated = true + + log.Printf("Looking up password stored 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{}, err + } + + if len(results) != 1 { + err := k.setupBiometrics() + if err != nil { + return gokeychain.Keychain{}, err + } + } else { + log.Printf("Found passphrase 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{}, err + } + k.passphrase = string(results[0].Data) + } + } + + return gokeychain.NewWithPath(k.path), nil +} + func (k *keychain) createOrOpen() (gokeychain.Keychain, error) { kc := gokeychain.NewWithPath(k.path) err := kc.Status() if err == nil { + if Config.UseBiometrics { + log.Printf("Opening %s with biometrics", k.path) + return k.openWithBiometrics() + } return kc, nil } diff --git a/vendor/github.com/99designs/keyring/keyring.go b/vendor/github.com/99designs/keyring/keyring.go index 831946532..90f318923 100644 --- a/vendor/github.com/99designs/keyring/keyring.go +++ b/vendor/github.com/99designs/keyring/keyring.go @@ -9,7 +9,9 @@ const ( FileBackend string = "file" ) -var UseBiometricsIfAvailable bool +var Config struct { + UseBiometrics bool +} var DefaultBackend = FileBackend diff --git a/vendor/github.com/lox/go-touchid/README.md b/vendor/github.com/lox/go-touchid/README.md new file mode 100644 index 000000000..e5a7d0a8b --- /dev/null +++ b/vendor/github.com/lox/go-touchid/README.md @@ -0,0 +1,26 @@ +# Authenticating with TouchID + +```golang +package main + +import ( + "log" + + touchid "github.com/lox/go-touchid" +) + +func main() { + ok, err := touchid.Authenticate("access llamas") + if err != nil { + log.Fatal(err) + } + + if ok { + log.Printf("Authenticated") + } else { + log.Fatal("Failed to authenticate") + } +} +``` + +![Screenshot](https://lachlan.me/s/9TMZWTYGikXoeCHm8RBgi8Bb0o4R1Bz6uI.png) diff --git a/vendor/github.com/lox/go-touchid/touchid.go b/vendor/github.com/lox/go-touchid/touchid.go new file mode 100644 index 000000000..19caad419 --- /dev/null +++ b/vendor/github.com/lox/go-touchid/touchid.go @@ -0,0 +1,56 @@ +package touchid + +/* +#cgo CFLAGS: -x objective-c -fmodules -fblocks +#cgo LDFLAGS: -framework CoreFoundation -framework LocalAuthentication -framework Foundation +#include +#include +#import + +int Authenticate(char const* reason) { + LAContext *myContext = [[LAContext alloc] init]; + NSError *authError = nil; + dispatch_semaphore_t sema = dispatch_semaphore_create(0); + NSString *nsReason = [NSString stringWithUTF8String:reason]; + __block int result = 0; + + if ([myContext canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics error:&authError]) { + [myContext evaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics + localizedReason:nsReason + reply:^(BOOL success, NSError *error) { + if (success) { + result = 1; + } else { + result = 2; + } + dispatch_semaphore_signal(sema); + }]; + } + + dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER); + dispatch_release(sema); + return result; +} +*/ +import ( + "C" +) +import ( + "errors" + "unsafe" +) + +func Authenticate(reason string) (bool, error) { + reasonStr := C.CString(reason) + defer C.free(unsafe.Pointer(reasonStr)) + + result := C.Authenticate(reasonStr) + switch result { + case 1: + return true, nil + case 2: + return false, nil + } + + return false, errors.New("Error occurred accessing biometrics") +} diff --git a/vendor/vendor.json b/vendor/vendor.json index 190b0ce5a..f61d20d3a 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -257,6 +257,12 @@ "revision": "efeae48c0b272ac15a6c0b53129285b2b0ed828d", "revisionTime": "2017-08-10T04:31:23Z" }, + { + "checksumSHA1": "kEyCzFEzVLWWRauNn0Nzxg2pg6Q=", + "path": "github.com/lox/go-touchid", + "revision": "619cc8e578d0ef916aa29c806117c370f9d621cb", + "revisionTime": "2017-07-12T10:52:33Z" + }, { "checksumSHA1": "AXacfEchaUqT5RGmPmMXsOWRhv8=", "path": "github.com/mitchellh/go-homedir", From 974e0d901f7bbc11835532932984db8c822cd49b Mon Sep 17 00:00:00 2001 From: Lachlan Donald Date: Tue, 8 Aug 2017 17:13:03 +1000 Subject: [PATCH 3/6] Fix bad merge --- vendor/github.com/99designs/keyring/keychain.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/vendor/github.com/99designs/keyring/keychain.go b/vendor/github.com/99designs/keyring/keychain.go index 29ae75d96..17d88005c 100644 --- a/vendor/github.com/99designs/keyring/keychain.go +++ b/vendor/github.com/99designs/keyring/keychain.go @@ -15,10 +15,9 @@ import ( ) const ( - keychainAccessGroup = "ACE1234DEF.com.99designs.aws-vault" - biometricsAccount = "com.99designs.aws-vault.biometrics" - biometricsService = "aws-vault" - biometricsLabel = "Passphrase for %s" + biometricsAccount = "com.99designs.aws-vault.biometrics" + biometricsService = "aws-vault" + biometricsLabel = "Passphrase for %s" ) type keychain struct { From 701eecafbdeb592676e56b9fafc496756113a22e Mon Sep 17 00:00:00 2001 From: Lachlan Donald Date: Tue, 8 Aug 2017 17:16:48 +1000 Subject: [PATCH 4/6] Set a specific xcode environment for touchid stuff --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index fdef53859..e5cd6fbbe 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,6 @@ language: go +osx_image: xcode8.3 install: ./scripts/ci_install.sh go: From ba5a63e683facdff8eeb756202bc2333b98d8b92 Mon Sep 17 00:00:00 2001 From: Lachlan Donald Date: Wed, 9 Aug 2017 11:42:31 +1000 Subject: [PATCH 5/6] Add a bit to the readme about how to do self-signed certs --- Makefile | 2 +- README.md | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 82c73e42b..facf12df3 100644 --- a/Makefile +++ b/Makefile @@ -30,7 +30,7 @@ $(BIN)-windows-386.exe: $(SRC) GOOS=windows GOARCH=386 go build -o $@ -ldflags="$(FLAGS)" . release: $(BIN)-linux-amd64 $(BIN)-darwin-amd64 $(BIN)-windows-386.exe - codesign -s $(CERT) $(BIN)-darwin-amd64 + codesign -s "$(CERT)" $(BIN)-darwin-amd64 clean: rm -f $(BIN)-*-* diff --git a/README.md b/README.md index d8338b807..f89c96c6b 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,19 @@ Developed with golang, to install run: go get github.com/99designs/aws-vault ``` +## Self-signing your binary + +Binaries that call Keychain need to be signed, otherwise they always show the "allow access" prompt. Releases are signed by 99designs certificates, but if you are actively developing and want to mimic the behaviour of a signed release you can generate a self-signed code signing certificate. + +Check out Apple's guide on it [here](http://web.archive.org/web/20090119080759/http://developer.apple.com/documentation/Security/Conceptual/CodeSigningGuide/Procedures/chapter_3_section_2.html), or find it in `Keychain Access > Certificate Assistant > Create Certificate > Code Signing Certificate`. + +You can then sign your binary like this: + +```bash +make build +codesign -s "Name of my certificate" ./aws-vault +``` + ## References and Inspiration * https://github.com/pda/aws-keychain From 6b90d34963adf6bf501d70d1874ae787207aa3b9 Mon Sep 17 00:00:00 2001 From: Lachlan Donald Date: Wed, 9 Aug 2017 11:45:54 +1000 Subject: [PATCH 6/6] Read biometrics defaults from AWS_VAULT_BIOMETRICS --- cli/global.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/global.go b/cli/global.go index ac51eb725..85e6ed5e5 100644 --- a/cli/global.go +++ b/cli/global.go @@ -44,6 +44,7 @@ func ConfigureGlobals(app *kingpin.Application) { EnumVar(&GlobalFlags.PromptDriver, promptsAvailable...) app.Flag("biometrics", "Use biometric authentication if supported"). + OverrideDefaultFromEnvar("AWS_VAULT_BIOMETRICS"). BoolVar(&GlobalFlags.Biometrics) app.PreAction(func(c *kingpin.ParseContext) (err error) { @@ -61,5 +62,4 @@ func ConfigureGlobals(app *kingpin.Application) { } return err }) - }