Skip to content

Commit

Permalink
Add YAML read and write support (gopasspw#125)
Browse files Browse the repository at this point in the history
Thanks to @bumi / gopasspw#114 for some inspiration.
  • Loading branch information
dominikschulz authored Jun 14, 2017
1 parent 51415fa commit 776a767
Show file tree
Hide file tree
Showing 7 changed files with 169 additions and 57 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion action/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,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
Expand Down
14 changes: 14 additions & 0 deletions action/insert.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand Down
60 changes: 34 additions & 26 deletions action/show.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package action

import (
"bytes"
"fmt"

"github.com/atotto/clipboard"
Expand All @@ -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")
Expand All @@ -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))
Expand Down
1 change: 1 addition & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
{
Expand Down
69 changes: 40 additions & 29 deletions password/root_store.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package password

import (
"bytes"
"encoding/json"
"fmt"
"os"
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down
78 changes: 78 additions & 0 deletions password/store.go
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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.
Expand Down

0 comments on commit 776a767

Please sign in to comment.