From b0dbc1270042c5656f584e0c0cc82f5bac02fd72 Mon Sep 17 00:00:00 2001 From: Lachlan Donald Date: Tue, 3 May 2016 13:21:45 +1000 Subject: [PATCH 1/2] Add a basic file backend --- keyring/array.go | 3 +- keyring/file.go | 155 +++++++++++++++++++++++++++++++++++++++ keyring/file_test.go | 33 +++++++++ keyring/keychain.go | 2 + keyring/keychain_test.go | 4 - keyring/keyring.go | 40 +++++----- main.go | 13 ++-- 7 files changed, 219 insertions(+), 31 deletions(-) create mode 100644 keyring/file.go create mode 100644 keyring/file_test.go diff --git a/keyring/array.go b/keyring/array.go index 9169c9b24..917073064 100644 --- a/keyring/array.go +++ b/keyring/array.go @@ -7,9 +7,8 @@ type arrayKeyring struct { func (k *arrayKeyring) Get(key string) (Item, error) { if i, ok := k.items[key]; ok { return i, nil - } else { - return Item{}, ErrKeyNotFound } + return Item{}, ErrKeyNotFound } func (k *arrayKeyring) Set(i Item) error { diff --git a/keyring/file.go b/keyring/file.go new file mode 100644 index 000000000..f99c99d01 --- /dev/null +++ b/keyring/file.go @@ -0,0 +1,155 @@ +package keyring + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "os" + "os/user" + "path/filepath" + "time" + + jose "github.com/dvsekhvalnov/jose2go" + "golang.org/x/crypto/ssh/terminal" +) + +type passwordFunc func(string) (string, error) + +func terminalPrompt(prompt string) (string, error) { + fmt.Printf("%s: ", prompt) + b, err := terminal.ReadPassword(1) + if err != nil { + return "", err + } + fmt.Println() + return string(b), nil +} + +func init() { + supportedBackends[FileBackend] = opener(func(name string) (Keyring, error) { + return &fileKeyring{ + PasswordFunc: terminalPrompt, + }, nil + }) +} + +type fileKeyring struct { + Dir string + PasswordFunc passwordFunc + password string +} + +func (k *fileKeyring) dir() (string, error) { + dir := k.Dir + if dir == "" { + usr, err := user.Current() + if err != nil { + return dir, err + } + dir = usr.HomeDir + "/.awsvault/keys/" + } + + stat, err := os.Stat(dir) + if os.IsNotExist(err) { + os.MkdirAll(dir, 0700) + } else if err != nil && !stat.IsDir() { + err = fmt.Errorf("%s is a file, not a directory", dir) + } + + return dir, nil +} + +func (k *fileKeyring) unlock() error { + dir, err := k.dir() + if err != nil { + return err + } + + if k.password == "" { + pwd, err := k.PasswordFunc(fmt.Sprintf("Enter passphrase to unlock %s", dir)) + if err != nil { + return err + } + k.password = pwd + } + + return nil +} + +func (k *fileKeyring) Get(key string) (Item, error) { + dir, err := k.dir() + if err != nil { + return Item{}, err + } + + bytes, err := ioutil.ReadFile(filepath.Join(dir, key)) + if os.IsNotExist(err) { + return Item{}, ErrKeyNotFound + } else if err != nil { + return Item{}, err + } + + if err = k.unlock(); err != nil { + return Item{}, err + } + + payload, _, err := jose.Decode(string(bytes), k.password) + if err != nil { + return Item{}, err + } + + var decoded Item + err = json.Unmarshal([]byte(payload), &decoded) + + return decoded, err +} + +func (k *fileKeyring) Set(i Item) error { + bytes, err := json.Marshal(i) + if err != nil { + return err + } + + dir, err := k.dir() + if err != nil { + return err + } + + if err = k.unlock(); err != nil { + return err + } + + token, err := jose.Encrypt(string(bytes), jose.PBES2_HS256_A128KW, jose.A256GCM, k.password, + jose.Headers(map[string]interface{}{ + "created": time.Now().String(), + })) + if err != nil { + return err + } + + return ioutil.WriteFile(filepath.Join(dir, i.Key), []byte(token), 0600) +} + +func (k *fileKeyring) Remove(key string) error { + dir, err := k.dir() + if err != nil { + return err + } + + return os.Remove(filepath.Join(dir, key)) +} + +func (k *fileKeyring) Keys() ([]string, error) { + dir, err := k.dir() + if err != nil { + return nil, err + } + + var keys = []string{} + files, _ := ioutil.ReadDir(dir) + for _, f := range files { + keys = append(keys, f.Name()) + } + + return keys, nil +} diff --git a/keyring/file_test.go b/keyring/file_test.go new file mode 100644 index 000000000..878b14262 --- /dev/null +++ b/keyring/file_test.go @@ -0,0 +1,33 @@ +package keyring + +import ( + "os" + "testing" +) + +func TestFileKeyringSetWhenEmpty(t *testing.T) { + k := &fileKeyring{ + Dir: os.TempDir(), + PasswordFunc: passwordFunc(func(string) (string, error) { + return "no more secrets", nil + }), + } + item := Item{Key: "llamas", Data: []byte("llamas are great")} + + if err := k.Set(item); err != nil { + t.Fatal(err) + } + + foundItem, err := k.Get("llamas") + if err != nil { + t.Fatal(err) + } + + if string(foundItem.Data) != "llamas are great" { + t.Fatalf("Value stored was not the value retrieved: %q", foundItem.Data) + } + + if foundItem.Key != "llamas" { + t.Fatalf("Key wasn't persisted: %q", foundItem.Key) + } +} diff --git a/keyring/keychain.go b/keyring/keychain.go index 3e4e4c8cf..fa8cc5568 100644 --- a/keyring/keychain.go +++ b/keyring/keychain.go @@ -43,6 +43,8 @@ func init() { return &keychain{Path: usr.HomeDir + "/Library/Keychains/" + name + ".keychain", Service: name}, nil }) + + DefaultBackend = KeychainBackend } func (k *keychain) Get(key string) (Item, error) { diff --git a/keyring/keychain_test.go b/keyring/keychain_test.go index 878c65a5a..642d986a1 100644 --- a/keyring/keychain_test.go +++ b/keyring/keychain_test.go @@ -20,10 +20,6 @@ func TestOSXKeychainKeyringSet(t *testing.T) { Description: "A freetext description", Data: []byte("llamas are great"), TrustSelf: true, - Metadata: map[string]string{ - "llamas": "rock", - "alpacas": "rock", - }, } if err := k.Set(item); err != nil { diff --git a/keyring/keyring.go b/keyring/keyring.go index e3689f8a6..272b6c04e 100644 --- a/keyring/keyring.go +++ b/keyring/keyring.go @@ -2,35 +2,35 @@ package keyring import "errors" -type backend string - const ( - KeychainBackend backend = "osxkeychain" - KWalletBackend backend = "kwallet" + KeychainBackend string = "keychain" + KWalletBackend string = "kwallet" + FileBackend string = "file" ) -var supportedBackends = map[backend]opener{} +var DefaultBackend = FileBackend -func Open(name string, prefer ...backend) (Keyring, error) { - if len(prefer) == 0 { - for b := range supportedBackends { - prefer = append(prefer, b) - } - } +var supportedBackends = map[string]opener{} - for _, b := range prefer { - for supported, f := range supportedBackends { - if b == supported { - return f(name) - } - } +func SupportedBackends() []string { + b := []string{} + for k := range supportedBackends { + b = append(b, k) } - - return nil, ErrNoAvailImpl + return b } type opener func(name string) (Keyring, error) +func Open(name string, backend string) (Keyring, error) { + op, ok := supportedBackends[backend] + if !ok { + return nil, ErrNoAvailImpl + } + + return op(name) +} + type Item struct { Key string Data []byte @@ -46,5 +46,5 @@ type Keyring interface { Keys() ([]string, error) } -var ErrNoAvailImpl = errors.New("No keyring implementation for your platform available.") +var ErrNoAvailImpl = errors.New("Specified keyring backend not available") var ErrKeyNotFound = errors.New("The specified item could not be found in the keyring.") diff --git a/main.go b/main.go index 87adca55c..fc7fc3317 100644 --- a/main.go +++ b/main.go @@ -31,9 +31,12 @@ func (w logWriter) Write(b []byte) (int, error) { func main() { var ( - prompts = prompt.Available() + prompts = prompt.Available() + backends = keyring.SupportedBackends() + debug = kingpin.Flag("debug", "Show debugging output").Bool() - promptDriver = kingpin.Flag("prompt", fmt.Sprintf("Prompt driver to use %v", prompts)).Default("terminal").OverrideDefaultFromEnvar("AWS_VAULT_PROMPT").Enum(prompts...) + backend = kingpin.Flag("backend", fmt.Sprintf("Secret backend to use %v", backends)).Default(keyring.DefaultBackend).OverrideDefaultFromEnvar("AWS_VAULT_BACKEND").Enum(backends...) + promptDriver = kingpin.Flag("prompt", fmt.Sprintf("Prompt driver to use %v", prompts)).Default("terminal").OverrideDefaultFromEnvar("AWS_VAULT_PROMPT").Enum(prompts...) add = kingpin.Command("add", "Adds credentials, prompts if none provided") addProfile = add.Arg("profile", "Name of the profile").Required().String() addFromEnv = add.Flag("env", "Read the credentials from the environment").Bool() @@ -68,13 +71,13 @@ func main() { Exit: os.Exit, } - keyring, err := keyring.Open("aws-vault") + cmd := kingpin.Parse() + + keyring, err := keyring.Open("aws-vault", *backend) if err != nil { ui.Error.Fatal(err) } - cmd := kingpin.Parse() - if *debug { ui.Debug = log.New(os.Stderr, "DEBUG ", log.LstdFlags) log.SetFlags(0) From b215e91bf57893eec4481322f6f5332a6469d213 Mon Sep 17 00:00:00 2001 From: Lachlan Donald Date: Tue, 3 May 2016 13:26:16 +1000 Subject: [PATCH 2/2] Update glide deps --- glide.lock | 34 +++++++++++++++++++++++----------- glide.yaml | 3 +-- 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/glide.lock b/glide.lock index 49d73b99a..0227dda9a 100644 --- a/glide.lock +++ b/glide.lock @@ -1,16 +1,16 @@ -hash: f2a10b0d49616eeea455f5e31df7bf26544beacb460b62352962ef69a6989c53 -updated: 2016-04-23T21:46:39.827232759-07:00 +hash: 90a5a45c153cf550f5147d1df57e9876fbd792f21b200de7e66103783618f91b +updated: 2016-05-03T13:25:54.040402684+10:00 imports: - name: github.com/alecthomas/template - version: "" + version: b867cc6ab45cece8143cfcc6fc9c77cf3f2c23c0 subpackages: - parse - name: github.com/alecthomas/units - version: "" + version: 6b4e7dc5e3143b85ea77909c72caf89416fc2915 - name: github.com/aulanov/go.dbus - version: "" + version: 25c3068a42a0b50b877953fb249dbcffc6bd1bca - name: github.com/aws/aws-sdk-go - version: "" + version: 376badeb12ede342ca2f41fd7b45027aa3f00264 subpackages: - aws - internal/endpoints @@ -31,14 +31,26 @@ imports: - aws/corehandlers - internal/protocol/query/queryutil - aws/ec2metadata -- name: github.com/bgentry/speakeasy - version: "" +- name: github.com/dvsekhvalnov/jose2go + version: 6387d3c1f5abd8443b223577d5a7e0f4e0e5731f + subpackages: + - aes + - arrays + - base64url + - compact + - kdf + - keys/ecc + - padding - name: github.com/skratchdot/open-golang - version: "" + version: c8748311a7528d0ba7330d302adbc5a677ef9c9e subpackages: - open - name: github.com/vaughan0/go-ini - version: "" + version: a98ad7ee00ec53921f08832bc06ecf7fd600e6a1 +- name: golang.org/x/crypto + version: 285fb2ed20d1dc450fc743a1b3ff7c36bef372b9 + subpackages: + - ssh/terminal - name: gopkg.in/alecthomas/kingpin.v2 - version: "" + version: 639879d6110b1b0409410c7b737ef0bb18325038 devImports: [] diff --git a/glide.yaml b/glide.yaml index dfb74afc6..f3c58f3a0 100644 --- a/glide.yaml +++ b/glide.yaml @@ -19,8 +19,6 @@ import: - aws/awserr - aws/credentials - service/iam -- package: github.com/bgentry/speakeasy - version: 200c3052657e041d0b8abab4ad65e0ba64a9f8c4 - package: github.com/skratchdot/open-golang version: c8748311a7528d0ba7330d302adbc5a677ef9c9e subpackages: @@ -29,3 +27,4 @@ import: version: a98ad7ee00ec53921f08832bc06ecf7fd600e6a1 - package: gopkg.in/alecthomas/kingpin.v2 version: 639879d6110b1b0409410c7b737ef0bb18325038 +- package: github.com/dvsekhvalnov/jose2go