From 42eca3138bc0449e1b12d145ff8f3c30dfa32267 Mon Sep 17 00:00:00 2001 From: Dominik Schulz Date: Tue, 13 Jun 2017 08:04:49 +0200 Subject: [PATCH] Add YAML read and write support Thanks to @bumi / #114 for some inspiration. --- Makefile | 2 +- action/init.go | 2 +- action/insert.go | 14 ++++++++ action/show.go | 60 ++++++++++++++++++-------------- main.go | 1 + password/root_store.go | 69 +++++++++++++++++++++---------------- password/store.go | 78 ++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 169 insertions(+), 57 deletions(-) diff --git a/Makefile b/Makefile index 784bc1c962..17c8162b78 100644 --- a/Makefile +++ b/Makefile @@ -77,7 +77,7 @@ test: for PKG in $(PACKAGES); do go test -cover -coverprofile $$GOPATH/src/$$PKG/coverage.out $$PKG || exit 1; done; .PHONY: test-integration -test-integration: build +test-integration: clean build cd tests && GOPASS_BINARY=$(PWD)/$(EXECUTABLE)-$(GOOS)-$(GOARCH) GOPASS_TEST_DIR=$(PWD)/tests go test -v .PHONY: install diff --git a/action/init.go b/action/init.go index 5bcd55a475..e0a40cc85b 100644 --- a/action/init.go +++ b/action/init.go @@ -36,7 +36,7 @@ func (s *Action) init(alias, path string, nogit bool, keys ...string) error { s.Store.PersistKeys = true s.Store.LoadKeys = false s.Store.ClipTimeout = 45 - s.Store.SafeContent = false + s.Store.ShowSafeContent = false } if path == "" { path = s.Store.Path diff --git a/action/insert.go b/action/insert.go index ef327a2098..c7d587b4cd 100644 --- a/action/insert.go +++ b/action/insert.go @@ -15,6 +15,7 @@ func (s *Action) Insert(c *cli.Context) error { echo := c.Bool("echo") multiline := c.Bool("multiline") force := c.Bool("force") + confirm := s.confirmRecipients if force { confirm = nil @@ -24,6 +25,19 @@ func (s *Action) Insert(c *cli.Context) error { if name == "" { return fmt.Errorf("provide a secret name") } + key := c.Args().Get(1) + value := c.Args().Get(2) + + if key != "" { + if value == "" { + content, err := askForPassword(name+"/"+key, nil) + if err != nil { + return fmt.Errorf("failed to ask for password: %v", err) + } + value = string(content) + } + return s.Store.SetKey(name, key, value) + } info, err := os.Stdin.Stat() if err != nil { diff --git a/action/show.go b/action/show.go index 779e9a2f94..4cdc4c42c2 100644 --- a/action/show.go +++ b/action/show.go @@ -1,7 +1,6 @@ package action import ( - "bytes" "fmt" "github.com/atotto/clipboard" @@ -14,6 +13,8 @@ import ( // Show the content of a secret file func (s *Action) Show(c *cli.Context) error { name := c.Args().First() + key := c.Args().Get(1) + clip := c.Bool("clip") force := c.Bool("force") qr := c.Bool("qr") @@ -26,38 +27,45 @@ func (s *Action) Show(c *cli.Context) error { return s.List(c) } - if clip || qr { - content, err := s.Store.First(name) + var content []byte + var err error + + switch { + case key != "": + content, err = s.Store.GetKey(name, key) if err != nil { return err } - - if qr { - qr, err := qrcon.QRCode(string(content)) - if err != nil { - return err - } - fmt.Println(qr) - return nil + case qr: + content, err = s.Store.First(name) + if err != nil { + return err } - return s.copyToClipboard(name, content) - } - - content, err := s.Store.Get(name) - if err != nil { - if err != password.ErrNotFound { + qr, err := qrcon.QRCode(string(content)) + if err != nil { return err } - color.Yellow("Entry '%s' not found. Starting search...", name) - return s.Find(c) - } - - if s.Store.SafeContent && !force { - lines := bytes.SplitN(content, []byte("\n"), 2) - if len(lines) < 2 || len(bytes.TrimSpace(lines[1])) == 0 { - return fmt.Errorf("no safe content to display, you can force display with show -f") + fmt.Println(qr) + return nil + case clip: + content, err = s.Store.First(name) + if err != nil { + return err + } + return s.copyToClipboard(name, content) + default: + if s.Store.ShowSafeContent && !force { + content, err = s.Store.SafeContent(name) + } else { + content, err = s.Store.Get(name) + } + if err != nil { + if err != password.ErrNotFound { + return err + } + color.Yellow("Entry '%s' not found. Starting search...", name) + return s.Find(c) } - content = lines[1] } color.Yellow(string(content)) diff --git a/main.go b/main.go index b4ca4a7512..57a6c0f6e6 100644 --- a/main.go +++ b/main.go @@ -173,6 +173,7 @@ func main() { Usage: "Insert a new secret or edit an existing secret using $EDITOR.", Before: action.Initialized, Action: action.Edit, + Aliases: []string{"set"}, BashComplete: action.Complete, }, { diff --git a/password/root_store.go b/password/root_store.go index e37ccf71b9..fdbff61970 100644 --- a/password/root_store.go +++ b/password/root_store.go @@ -1,7 +1,6 @@ package password import ( - "bytes" "encoding/json" "fmt" "os" @@ -16,24 +15,24 @@ import ( // RootStore is the public facing password store type RootStore struct { - AutoPush bool `json:"autopush"` // push to git remote after commit - AutoPull bool `json:"autopull"` // pull from git before push - AutoImport bool `json:"autoimport"` // import missing public keys w/o asking - AlwaysTrust bool `json:"alwaystrust"` // always trust public keys when encrypting - NoConfirm bool `json:"noconfirm"` // do not confirm recipients when encrypting - PersistKeys bool `json:"persistkeys"` // store recipient keys in store - LoadKeys bool `json:"loadkeys"` // load missing keys from store - ClipTimeout int `json:"cliptimeout"` // clear clipboard after seconds - NoColor bool `json:"nocolor"` // disable colors in output - Path string `json:"path"` // path to the root store - SafeContent bool `json:"safecontent"` // avoid showing passwords in terminal - Mount map[string]string `json:"mounts,omitempty"` - Version string `json:"version"` - ImportFunc ImportCallback `json:"-"` - FsckFunc FsckCallback `json:"-"` - Debug bool `json:"-"` - store *Store - mounts map[string]*Store + AutoPush bool `json:"autopush"` // push to git remote after commit + AutoPull bool `json:"autopull"` // pull from git before push + AutoImport bool `json:"autoimport"` // import missing public keys w/o asking + AlwaysTrust bool `json:"alwaystrust"` // always trust public keys when encrypting + NoConfirm bool `json:"noconfirm"` // do not confirm recipients when encrypting + PersistKeys bool `json:"persistkeys"` // store recipient keys in store + LoadKeys bool `json:"loadkeys"` // load missing keys from store + ClipTimeout int `json:"cliptimeout"` // clear clipboard after seconds + NoColor bool `json:"nocolor"` // disable colors in output + Path string `json:"path"` // path to the root store + ShowSafeContent bool `json:"safecontent"` // avoid showing passwords in terminal + Mount map[string]string `json:"mounts,omitempty"` + Version string `json:"version"` + ImportFunc ImportCallback `json:"-"` + FsckFunc FsckCallback `json:"-"` + Debug bool `json:"-"` + store *Store + mounts map[string]*Store } // NewRootStore creates a new store @@ -327,19 +326,24 @@ func (r *RootStore) Get(name string) ([]byte, error) { return store.Get(strings.TrimPrefix(name, store.alias)) } +// GetKey will return a single named entry from a structured document (YAML) +// in secret name. If no such key exists or yaml decoding fails it will +// return an error +func (r *RootStore) GetKey(name, key string) ([]byte, error) { + store := r.getStore(name) + return store.GetKey(strings.TrimPrefix(name, store.alias), key) +} + // First returns the first line of the plaintext of a single key func (r *RootStore) First(name string) ([]byte, error) { - content, err := r.Get(name) - if err != nil { - return nil, err - } - - lines := bytes.Split(content, []byte("\n")) - if len(lines) < 1 { - return nil, fmt.Errorf("no content to return the first line from") - } + store := r.getStore(name) + return store.First(strings.TrimPrefix(name, store.alias)) +} - return bytes.TrimSpace(lines[0]), nil +// SafeContent returns everything but the first line from a key +func (r *RootStore) SafeContent(name string) ([]byte, error) { + store := r.getStore(name) + return store.SafeContent(strings.TrimPrefix(name, store.alias)) } // Exists checks the existence of a single entry @@ -360,6 +364,13 @@ func (r *RootStore) Set(name string, content []byte, reason string) error { return store.Set(strings.TrimPrefix(name, store.alias), content, reason) } +// SetKey sets a single key in structured document (YAML) to the given +// value. If the secret name is non-empty but no YAML it will return an error. +func (r *RootStore) SetKey(name, key, value string) error { + store := r.getStore(name) + return store.SetKey(strings.TrimPrefix(name, store.alias), key, value) +} + // SetConfirm calls Set with confirmation callback func (r *RootStore) SetConfirm(name string, content []byte, reason string, cb RecipientCallback) error { store := r.getStore(name) diff --git a/password/store.go b/password/store.go index cdd5e7e2f7..39c567260b 100644 --- a/password/store.go +++ b/password/store.go @@ -1,11 +1,14 @@ package password import ( + "bytes" "fmt" "os" "path/filepath" "strings" + "gopkg.in/yaml.v2" + "github.com/fatih/color" "github.com/justwatchcom/gopass/fsutil" "github.com/justwatchcom/gopass/gpg" @@ -216,6 +219,54 @@ func (s *Store) Get(name string) ([]byte, error) { return content, nil } +// GetKey returns a single key from a structured secret +func (s *Store) GetKey(name, key string) ([]byte, error) { + content, err := s.SafeContent(name) + if err != nil { + return nil, err + } + + d := make(map[string]string) + if err := yaml.Unmarshal(content, &d); err != nil { + return nil, err + } + + if v, found := d[key]; found { + return []byte(v), nil + } + + return nil, fmt.Errorf("key not found") +} + +// First returns the first line of the plaintext of a single key +func (s *Store) First(name string) ([]byte, error) { + content, err := s.Get(name) + if err != nil { + return nil, err + } + + lines := bytes.Split(content, []byte("\n")) + if len(lines) < 1 { + return nil, fmt.Errorf("no content to return the first line from") + } + + return bytes.TrimSpace(lines[0]), nil +} + +// SafeContent returns everything but the first line +func (s *Store) SafeContent(name string) ([]byte, error) { + content, err := s.Get(name) + if err != nil { + return nil, err + } + + lines := bytes.SplitN(content, []byte("\n"), 2) + if len(lines) < 2 || len(bytes.TrimSpace(lines[1])) < 1 { + return nil, fmt.Errorf("no safe content to display, you can force display with show -f") + } + return lines[1], nil +} + // IsDir returns true if the entry is folder inside the store func (s *Store) IsDir(name string) bool { return fsutil.IsDir(filepath.Join(s.path, name)) @@ -301,6 +352,33 @@ func (s *Store) SetConfirm(name string, content []byte, reason string, cb Recipi return nil } +// SetKey will update a single key in a YAML structured secret +func (s *Store) SetKey(name, key, value string) error { + var err error + first, err := s.First(name) + if err != nil { + first = []byte("\n") + } + body, err := s.SafeContent(name) + if err != nil && err != ErrNotFound { + return err + } + + d := make(map[string]string) + if err := yaml.Unmarshal(body, &d); err != nil { + return err + } + + d[key] = value + + buf, err := yaml.Marshal(d) + if err != nil { + return err + } + + return s.SetConfirm(name, append(first, buf...), fmt.Sprintf("Updated key %s in %s", key, name), nil) +} + // Copy will copy one entry to another location. Multi-store copies are // supported. Each entry has to be decoded and encoded for the destination // to make sure it's encrypted for the right set of recipients.