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

Add a basic file backend #63

Merged
merged 2 commits into from
Jun 21, 2016
Merged
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
34 changes: 23 additions & 11 deletions glide.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions glide.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -29,3 +27,4 @@ import:
version: a98ad7ee00ec53921f08832bc06ecf7fd600e6a1
- package: gopkg.in/alecthomas/kingpin.v2
version: 639879d6110b1b0409410c7b737ef0bb18325038
- package: github.com/dvsekhvalnov/jose2go
3 changes: 1 addition & 2 deletions keyring/array.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
155 changes: 155 additions & 0 deletions keyring/file.go
Original file line number Diff line number Diff line change
@@ -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
})
Copy link
Member

@mtibben mtibben Jun 21, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It may be better to keep use of the supportedBackends in keyring.go to keep the scope small

}

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
}
33 changes: 33 additions & 0 deletions keyring/file_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
2 changes: 2 additions & 0 deletions keyring/keychain.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
4 changes: 0 additions & 4 deletions keyring/keychain_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
40 changes: 20 additions & 20 deletions keyring/keyring.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.")
13 changes: 8 additions & 5 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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)
Expand Down