From 8e31f46841483290f0da135398948c3b83f626e5 Mon Sep 17 00:00:00 2001 From: Dominik Schulz Date: Wed, 14 Jun 2017 17:07:27 +0200 Subject: [PATCH] Split root and sub store packages (#131) Fixes #134 Fixes #133 --- action/action.go | 81 +-- action/clihelper.go | 2 +- action/clone.go | 2 +- action/completion.go | 2 +- action/config.go | 134 +---- action/delete.go | 4 +- action/edit.go | 4 +- action/fsck.go | 6 +- action/init.go | 18 +- action/insert.go | 4 +- action/list.go | 4 +- action/mount.go | 16 +- action/show.go | 14 +- config/config.go | 201 +++++++ password/root_store.go | 582 --------------------- store/err.go | 22 + store/root/config.go | 110 ++++ store/root/fsck.go | 56 ++ store/root/git.go | 13 + store/root/mount.go | 128 +++++ store/root/recipients.go | 77 +++ {password => store/root}/sort.go | 2 +- {password => store/root}/sort_test.go | 23 +- store/root/store.go | 320 +++++++++++ {password => store/root}/store_test.go | 168 +++--- store/root/templates.go | 69 +++ store/root/yaml.go | 24 + store/store.go | 12 + store/sub/config.go | 56 ++ {password => store/sub}/fsck.go | 2 +- {password => store/sub}/git.go | 60 +-- {password => store/sub}/recipients.go | 25 +- {password => store/sub}/recipients_test.go | 15 +- {password => store/sub}/store.go | 179 ++----- store/sub/store_test.go | 32 ++ {password => store/sub}/templates.go | 80 +-- store/sub/yaml.go | 81 +++ tree/{ => simple}/file.go | 2 +- tree/{ => simple}/folder.go | 16 +- tree/simple/tree.go | 47 ++ tree/{ => simple}/tree_test.go | 4 +- tree/tree.go | 55 +- 42 files changed, 1567 insertions(+), 1185 deletions(-) create mode 100644 config/config.go delete mode 100644 password/root_store.go create mode 100644 store/err.go create mode 100644 store/root/config.go create mode 100644 store/root/fsck.go create mode 100644 store/root/git.go create mode 100644 store/root/mount.go create mode 100644 store/root/recipients.go rename {password => store/root}/sort.go (96%) rename {password => store/root}/sort_test.go (51%) create mode 100644 store/root/store.go rename {password => store/root}/store_test.go (90%) create mode 100644 store/root/templates.go create mode 100644 store/root/yaml.go create mode 100644 store/store.go create mode 100644 store/sub/config.go rename {password => store/sub}/fsck.go (99%) rename {password => store/sub}/git.go (82%) rename {password => store/sub}/recipients.go (92%) rename {password => store/sub}/recipients_test.go (89%) rename {password => store/sub}/store.go (73%) create mode 100644 store/sub/store_test.go rename {password => store/sub}/templates.go (62%) create mode 100644 store/sub/yaml.go rename tree/{ => simple}/file.go (98%) rename tree/{ => simple}/folder.go (95%) create mode 100644 tree/simple/tree.go rename tree/{ => simple}/tree_test.go (95%) diff --git a/action/action.go b/action/action.go index 449a617141..0ac5b5844b 100644 --- a/action/action.go +++ b/action/action.go @@ -1,8 +1,6 @@ package action import ( - "fmt" - "io/ioutil" "os" "path/filepath" "strings" @@ -10,16 +8,16 @@ import ( "golang.org/x/crypto/ssh/terminal" "github.com/fatih/color" - "github.com/ghodss/yaml" + "github.com/justwatchcom/gopass/config" "github.com/justwatchcom/gopass/fsutil" "github.com/justwatchcom/gopass/gpg" - "github.com/justwatchcom/gopass/password" + "github.com/justwatchcom/gopass/store/root" ) // Action knows everything to run gopass CLI actions type Action struct { Name string - Store *password.RootStore + Store *root.Store } // New returns a new Action wrapper @@ -32,39 +30,40 @@ func New(v string) *Action { if gdb := os.Getenv("GOPASS_DEBUG"); gdb == "true" { gpg.Debug = true } - pwDir := pwStoreDir("") // try to read config (if it exists) - for _, l := range configLocations() { - if cfg, err := newFromFile(l); err == nil && cfg != nil { - cfg.ImportFunc = askForKeyImport - cfg.FsckFunc = askForConfirmation - cfg.Version = v - color.NoColor = cfg.NoColor - // need this override for our integration tests - if nc := os.Getenv("GOPASS_NOCOLOR"); nc == "true" { - color.NoColor = true - } - // only emit color codes when stdout is a terminal - if !terminal.IsTerminal(int(os.Stdout.Fd())) { - color.NoColor = true - } - return &Action{ - Name: name, - Store: cfg, - } + if cfg, err := config.Load(); err == nil && cfg != nil { + cfg.ImportFunc = askForKeyImport + cfg.FsckFunc = askForConfirmation + cfg.Version = v + color.NoColor = cfg.NoColor + // need this override for our integration tests + if nc := os.Getenv("GOPASS_NOCOLOR"); nc == "true" { + color.NoColor = true + } + // only emit color codes when stdout is a terminal + if !terminal.IsTerminal(int(os.Stdout.Fd())) { + color.NoColor = true + } + store, err := root.New(cfg) + if err != nil { + panic(err) + } + return &Action{ + Name: name, + Store: store, } } - cfg, err := password.NewRootStore(pwDir) + cfg := config.New() + cfg.Path = pwStoreDir("") + cfg.ImportFunc = askForKeyImport + cfg.FsckFunc = askForConfirmation + rs, err := root.New(cfg) if err != nil { panic(err) } - cfg.ImportFunc = askForKeyImport - cfg.FsckFunc = askForConfirmation - cfg.Version = v - color.NoColor = cfg.NoColor // need this override for our integration tests if nc := os.Getenv("GOPASS_NOCOLOR"); nc == "true" { color.NoColor = true @@ -75,30 +74,8 @@ func New(v string) *Action { return &Action{ Name: name, - Store: cfg, - } -} - -// newFromFile creates a new RootStore instance by unmarsahling a config file. -// If the file doesn't exist or fails to unmarshal an error is returned -func newFromFile(cf string) (*password.RootStore, error) { - // deliberately using os.Stat here, a symlinked - // config is OK - if _, err := os.Stat(cf); err != nil { - return nil, err - } - buf, err := ioutil.ReadFile(cf) - if err != nil { - fmt.Printf("Error reading config from %s: %s\n", cf, err) - return nil, err - } - cfg := &password.RootStore{} - err = yaml.Unmarshal(buf, &cfg) - if err != nil { - fmt.Printf("Error reading config from %s: %s\n", cf, err) - return nil, err + Store: rs, } - return cfg, nil } // String implement fmt.Stringer diff --git a/action/clihelper.go b/action/clihelper.go index f3442f6ccb..c8662089da 100644 --- a/action/clihelper.go +++ b/action/clihelper.go @@ -15,7 +15,7 @@ import ( // confirmRecipients asks the user to confirm a given set of recipients func (s *Action) confirmRecipients(name string, recipients []string) ([]string, error) { - if s.Store.NoConfirm { + if s.Store.NoConfirm() { return recipients, nil } for { diff --git a/action/clone.go b/action/clone.go index 643127c691..93e72ddd4b 100644 --- a/action/clone.go +++ b/action/clone.go @@ -47,7 +47,7 @@ func (s *Action) Clone(c *cli.Context) error { } // save new mount in config file - if err := writeConfig(s.Store); err != nil { + if err := s.Store.Config().Save(); err != nil { return fmt.Errorf("Failed to update config: %s", err) } diff --git a/action/completion.go b/action/completion.go index 5a2ddfe3e5..f8fc1cf01b 100644 --- a/action/completion.go +++ b/action/completion.go @@ -78,7 +78,7 @@ func (s *Action) CompletionDMenu(c *cli.Context) error { return err } - content, err := s.Store.First(name) + content, err := s.Store.GetFirstLine(name) if err != nil { return err } diff --git a/action/config.go b/action/config.go index e1cbcce27e..2f354d2edb 100644 --- a/action/config.go +++ b/action/config.go @@ -2,17 +2,8 @@ package action import ( "fmt" - "io/ioutil" - "os" - "path/filepath" - "reflect" "sort" - "strconv" - "strings" - "github.com/ghodss/yaml" - "github.com/justwatchcom/gopass/fsutil" - "github.com/justwatchcom/gopass/password" "github.com/urfave/cli" ) @@ -34,33 +25,17 @@ func (s *Action) Config(c *cli.Context) error { } func (s *Action) printConfigValues(filter ...string) error { - out := make([]string, 0, 10) - o := reflect.ValueOf(s.Store).Elem() - for i := 0; i < o.NumField(); i++ { - jsonArg := o.Type().Field(i).Tag.Get("json") - if jsonArg == "" || jsonArg == "-" { + m := s.Store.Config().ConfigMap() + out := make([]string, 0, len(m)) + for k := range m { + if !contains(filter, k) { continue } - if !contains(filter, jsonArg) { - continue - } - f := o.Field(i) - strVal := "" - switch f.Kind() { - case reflect.String: - strVal = f.String() - case reflect.Bool: - strVal = fmt.Sprintf("%t", f.Bool()) - case reflect.Int: - strVal = fmt.Sprintf("%d", f.Int()) - default: - continue - } - out = append(out, fmt.Sprintf("%s: %s", jsonArg, strVal)) + out = append(out, k) } sort.Strings(out) - for _, line := range out { - fmt.Println(line) + for _, k := range out { + fmt.Printf("%s: %s\n", k, m[k]) } return nil } @@ -78,98 +53,9 @@ func contains(haystack []string, needle string) bool { } func (s *Action) setConfigValue(key, value string) error { - if key == "version" { - return fmt.Errorf("Can not change version") - } - if key != "path" { - value = strings.ToLower(value) - } - o := reflect.ValueOf(s.Store).Elem() - for i := 0; i < o.NumField(); i++ { - jsonArg := o.Type().Field(i).Tag.Get("json") - if jsonArg == "" || jsonArg == "-" { - continue - } - if jsonArg != key { - continue - } - f := o.Field(i) - switch f.Kind() { - case reflect.String: - f.SetString(value) - case reflect.Bool: - if value == "true" { - f.SetBool(true) - } else if value == "false" { - f.SetBool(false) - } else { - return fmt.Errorf("No a bool: %s", value) - } - case reflect.Int: - iv, err := strconv.Atoi(value) - if err != nil { - return err - } - f.SetInt(int64(iv)) - default: - continue - } - } - return writeConfig(s.Store) -} - -// hasConfig is a short hand for checking if the config file exists -func hasConfig() bool { - for _, l := range configLocations() { - if fsutil.IsFile(l) { - return true - } - } - return false -} - -// writeConfig saves the config -func writeConfig(s *password.RootStore) error { - buf, err := yaml.Marshal(s) - if err != nil { + cfg := s.Store.Config() + if err := cfg.SetConfigValue(key, value); err != nil { return err } - cfgLoc := configLocation() - cfgDir := filepath.Dir(cfgLoc) - if !fsutil.IsDir(cfgDir) { - if err := os.MkdirAll(cfgDir, 0700); err != nil { - return err - } - } - if err := ioutil.WriteFile(cfgLoc, buf, 0600); err != nil { - return err - } - return nil -} - -// configLocation returns the location of the config file. Either reading from -// GOPASS_CONFIG or using the default location (~/.gopass.yml) -func configLocation() string { - if cf := os.Getenv("GOPASS_CONFIG"); cf != "" { - return cf - } - if xch := os.Getenv("XDG_CONFIG_HOME"); xch != "" { - return filepath.Join(xch, "gopass", "config.yml") - } - return filepath.Join(os.Getenv("HOME"), ".config", "gopass", "config.yml") -} - -// configLocations returns the possible locations of gopass config files, -// in decreasing priority -func configLocations() []string { - l := []string{} - if cf := os.Getenv("GOPASS_CONFIG"); cf != "" { - l = append(l, cf) - } - if xch := os.Getenv("XDG_CONFIG_HOME"); xch != "" { - l = append(l, filepath.Join(xch, "gopass", "config.yml")) - } - l = append(l, filepath.Join(os.Getenv("HOME"), ".config", "gopass", "config.yml")) - l = append(l, filepath.Join(os.Getenv("HOME"), ".gopass.yml")) - return l + return s.Store.UpdateConfig(cfg) } diff --git a/action/delete.go b/action/delete.go index 5b23c6cd39..4c53d6c46e 100644 --- a/action/delete.go +++ b/action/delete.go @@ -3,7 +3,7 @@ package action import ( "fmt" - "github.com/justwatchcom/gopass/password" + "github.com/justwatchcom/gopass/store" "github.com/urfave/cli" ) @@ -18,7 +18,7 @@ func (s *Action) Delete(c *cli.Context) error { } found, err := s.Store.Exists(name) - if err != nil && err != password.ErrNotFound { + if err != nil && err != store.ErrNotFound { return fmt.Errorf("failed to see if %s exists", name) } diff --git a/action/edit.go b/action/edit.go index 415800f7ba..6359203c53 100644 --- a/action/edit.go +++ b/action/edit.go @@ -9,8 +9,8 @@ import ( "os/exec" "github.com/justwatchcom/gopass/fsutil" - "github.com/justwatchcom/gopass/password" "github.com/justwatchcom/gopass/pwgen" + "github.com/justwatchcom/gopass/store" "github.com/justwatchcom/gopass/tpl" shellquote "github.com/kballard/go-shellquote" "github.com/urfave/cli" @@ -24,7 +24,7 @@ func (s *Action) Edit(c *cli.Context) error { } exists, err := s.Store.Exists(name) - if err != nil && err != password.ErrNotFound { + if err != nil && err != store.ErrNotFound { return fmt.Errorf("failed to see if %s exists", name) } diff --git a/action/fsck.go b/action/fsck.go index 1163e1639e..2bfadd4e5c 100644 --- a/action/fsck.go +++ b/action/fsck.go @@ -17,7 +17,8 @@ func (s *Action) Fsck(c *cli.Context) error { force = false } // make sure config is in the right place - if err := writeConfig(s.Store); err != nil { + // we may have loaded it from one of the fallback locations + if err := s.Store.Config().Save(); err != nil { return err } // clean up any previous config locations @@ -27,5 +28,6 @@ func (s *Action) Fsck(c *cli.Context) error { color.Red("Failed to remove old gopass config %s: %s", oldCfg, err) } } - return s.Store.Fsck(check, force) + _, err := s.Store.Fsck("", check, force) + return err } diff --git a/action/init.go b/action/init.go index f68b8bda9b..34d6230b12 100644 --- a/action/init.go +++ b/action/init.go @@ -27,22 +27,8 @@ func (s *Action) Init(c *cli.Context) error { } func (s *Action) init(alias, path string, nogit bool, keys ...string) error { - if path != "" && alias == "" { - return fmt.Errorf("need mount alias when using path") - } - if !hasConfig() { - // when creating a new config we set some sensible defaults - s.Store.AutoPush = true - s.Store.AutoPull = true - s.Store.AutoImport = false - s.Store.NoConfirm = false - s.Store.PersistKeys = true - s.Store.LoadKeys = false - s.Store.ClipTimeout = 45 - s.Store.ShowSafeContent = false - } if path == "" { - path = s.Store.Path + path = s.Store.Path() } if len(keys) < 1 { @@ -80,7 +66,7 @@ func (s *Action) init(alias, path string, nogit bool, keys ...string) error { fmt.Println("") // write config - if err := writeConfig(s.Store); err != nil { + if err := s.Store.Config().Save(); err != nil { color.Red(fmt.Sprintf("Failed to write config: %s", err)) } diff --git a/action/insert.go b/action/insert.go index c7d587b4cd..80bae5804e 100644 --- a/action/insert.go +++ b/action/insert.go @@ -6,7 +6,7 @@ import ( "io" "os" - "github.com/justwatchcom/gopass/password" + "github.com/justwatchcom/gopass/store" "github.com/urfave/cli" ) @@ -56,7 +56,7 @@ func (s *Action) Insert(c *cli.Context) error { } replacing, err := s.Store.Exists(name) - if err != nil && err != password.ErrNotFound { + if err != nil && err != store.ErrNotFound { return fmt.Errorf("failed to see if %s exists", name) } diff --git a/action/list.go b/action/list.go index 0b4aa4f6ab..bef55b360e 100644 --- a/action/list.go +++ b/action/list.go @@ -31,8 +31,8 @@ func (s *Action) List(c *cli.Context) error { } if subtree := l.FindFolder(filter); subtree != nil { - subtree.Root = true - subtree.Name = filter + subtree.SetRoot(true) + subtree.SetName(filter) if flat { sep := "/" if strings.HasSuffix(filter, "/") { diff --git a/action/mount.go b/action/mount.go index 82638f2a7c..1413ab2054 100644 --- a/action/mount.go +++ b/action/mount.go @@ -4,7 +4,7 @@ import ( "fmt" "github.com/fatih/color" - "github.com/justwatchcom/gopass/tree" + "github.com/justwatchcom/gopass/tree/simple" "github.com/urfave/cli" ) @@ -14,9 +14,9 @@ func (s *Action) MountRemove(c *cli.Context) error { return fmt.Errorf("usage: gopass mount remove [alias]") } if err := s.Store.RemoveMount(c.Args()[0]); err != nil { - return err + color.Yellow("Failed to remove mount: %s", err) } - if err := writeConfig(s.Store); err != nil { + if err := s.Store.Config().Save(); err != nil { return err } @@ -26,12 +26,12 @@ func (s *Action) MountRemove(c *cli.Context) error { // MountsPrint prints all existing mounts func (s *Action) MountsPrint(c *cli.Context) error { - if len(s.Store.Mount) < 1 { + if len(s.Store.Mounts()) < 1 { fmt.Println("No mounts") return nil } - root := tree.New(color.GreenString(fmt.Sprintf("gopass (%s)", s.Store.Path))) - for alias, path := range s.Store.Mount { + root := simple.New(color.GreenString(fmt.Sprintf("gopass (%s)", s.Store.Path()))) + for alias, path := range s.Store.Mounts() { if err := root.AddMount(alias, path); err != nil { fmt.Printf("Failed to add mount: %s\n", err) } @@ -43,7 +43,7 @@ func (s *Action) MountsPrint(c *cli.Context) error { // MountsComplete will print a list of existings mount points for bash // completion func (s *Action) MountsComplete(*cli.Context) { - for alias := range s.Store.Mount { + for alias := range s.Store.Mounts() { fmt.Println(alias) } } @@ -60,7 +60,7 @@ func (s *Action) MountAdd(c *cli.Context) error { if err := s.Store.AddMount(c.Args()[0], c.Args()[1], keys...); err != nil { return err } - if err := writeConfig(s.Store); err != nil { + if err := s.Store.Config().Save(); err != nil { return err } diff --git a/action/show.go b/action/show.go index 4cdc4c42c2..2e8e9f618d 100644 --- a/action/show.go +++ b/action/show.go @@ -5,8 +5,8 @@ import ( "github.com/atotto/clipboard" "github.com/fatih/color" - "github.com/justwatchcom/gopass/password" "github.com/justwatchcom/gopass/qrcon" + "github.com/justwatchcom/gopass/store" "github.com/urfave/cli" ) @@ -37,7 +37,7 @@ func (s *Action) Show(c *cli.Context) error { return err } case qr: - content, err = s.Store.First(name) + content, err = s.Store.GetFirstLine(name) if err != nil { return err } @@ -48,19 +48,19 @@ func (s *Action) Show(c *cli.Context) error { fmt.Println(qr) return nil case clip: - content, err = s.Store.First(name) + content, err = s.Store.GetFirstLine(name) if err != nil { return err } return s.copyToClipboard(name, content) default: - if s.Store.ShowSafeContent && !force { - content, err = s.Store.SafeContent(name) + if s.Store.SafeContent() && !force { + content, err = s.Store.GetBody(name) } else { content, err = s.Store.Get(name) } if err != nil { - if err != password.ErrNotFound { + if err != store.ErrNotFound { return err } color.Yellow("Entry '%s' not found. Starting search...", name) @@ -78,7 +78,7 @@ func (s *Action) copyToClipboard(name string, content []byte) error { return err } - if err := clearClipboard(content, s.Store.ClipTimeout); err != nil { + if err := clearClipboard(content, s.Store.ClipTimeout()); err != nil { return err } diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000000..d6f3c4a144 --- /dev/null +++ b/config/config.go @@ -0,0 +1,201 @@ +package config + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "reflect" + "strconv" + "strings" + + "github.com/ghodss/yaml" + "github.com/justwatchcom/gopass/fsutil" + "github.com/justwatchcom/gopass/store" +) + +// Config is the gopass config structure +type Config struct { + AlwaysTrust bool `json:"alwaystrust"` // always trust public keys when encrypting + AutoImport bool `json:"autoimport"` // import missing public keys w/o asking + AutoPull bool `json:"autopull"` // pull from git before push + AutoPush bool `json:"autopush"` // push to git remote after commit + ClipTimeout int `json:"cliptimeout"` // clear clipboard after seconds + Debug bool `json:"debug"` // enable debug output + FsckFunc store.FsckCallback `json:"-"` + ImportFunc store.ImportCallback `json:"-"` + LoadKeys bool `json:"loadkeys"` // load missing keys from store + Mounts map[string]string `json:"mounts,omitempty"` + NoColor bool `json:"nocolor"` // disable colors in output + NoConfirm bool `json:"noconfirm"` // do not confirm recipients when encrypting + Path string `json:"path"` // path to the root store + PersistKeys bool `json:"persistkeys"` // store recipient keys in store + SafeContent bool `json:"safecontent"` // avoid showing passwords in terminal + Version string `json:"version"` +} + +// New creates a new config with sane default values +func New() *Config { + return &Config{ + AlwaysTrust: true, + AutoImport: true, + AutoPull: true, + AutoPush: true, + ClipTimeout: 45, + Debug: false, + LoadKeys: true, + Mounts: make(map[string]string), + NoColor: false, + NoConfirm: false, + PersistKeys: true, + SafeContent: false, + Version: "", + } +} + +// ConfigMap returns a map of stringified config values for easy printing +func (c *Config) ConfigMap() map[string]string { + m := make(map[string]string, 20) + o := reflect.ValueOf(c).Elem() + for i := 0; i < o.NumField(); i++ { + jsonArg := o.Type().Field(i).Tag.Get("json") + if jsonArg == "" || jsonArg == "-" { + continue + } + f := o.Field(i) + strVal := "" + switch f.Kind() { + case reflect.String: + strVal = f.String() + case reflect.Bool: + strVal = fmt.Sprintf("%t", f.Bool()) + case reflect.Int: + strVal = fmt.Sprintf("%d", f.Int()) + default: + continue + } + m[jsonArg] = strVal + } + return m +} + +// SetConfigValue will try to set the given key to the value in the config struct +func (c *Config) SetConfigValue(key, value string) error { + if key == "version" { + return fmt.Errorf("Can not change version") + } + if key != "path" { + value = strings.ToLower(value) + } + o := reflect.ValueOf(c).Elem() + for i := 0; i < o.NumField(); i++ { + jsonArg := o.Type().Field(i).Tag.Get("json") + if jsonArg == "" || jsonArg == "-" { + continue + } + if jsonArg != key { + continue + } + f := o.Field(i) + switch f.Kind() { + case reflect.String: + f.SetString(value) + case reflect.Bool: + if value == "true" { + f.SetBool(true) + } else if value == "false" { + f.SetBool(false) + } else { + return fmt.Errorf("No a bool: %s", value) + } + case reflect.Int: + iv, err := strconv.Atoi(value) + if err != nil { + return err + } + f.SetInt(int64(iv)) + default: + continue + } + } + return c.Save() +} + +// Load will try to load the config from one of the default locations +func Load() (*Config, error) { + for _, l := range configLocations() { + if cfg, err := load(l); err == nil { + if gdb := os.Getenv("GOPASS_DEBUG"); gdb == "true" { + fmt.Printf("Loaded config from %s: %+v\n", l, cfg) + } + return cfg, err + } + } + return nil, fmt.Errorf("no config found") +} + +func load(cf string) (*Config, error) { + // deliberately using os.Stat here, a symlinked + // config is OK + if _, err := os.Stat(cf); err != nil { + return nil, err + } + buf, err := ioutil.ReadFile(cf) + if err != nil { + fmt.Printf("Error reading config from %s: %s\n", cf, err) + return nil, err + } + cfg := &Config{} + err = yaml.Unmarshal(buf, &cfg) + if err != nil { + fmt.Printf("Error reading config from %s: %s\n", cf, err) + return nil, err + } + return cfg, nil +} + +// Save saves the config +func (c *Config) Save() error { + buf, err := yaml.Marshal(c) + if err != nil { + return err + } + cfgLoc := configLocation() + cfgDir := filepath.Dir(cfgLoc) + if !fsutil.IsDir(cfgDir) { + if err := os.MkdirAll(cfgDir, 0700); err != nil { + return err + } + } + if err := ioutil.WriteFile(cfgLoc, buf, 0600); err != nil { + return err + } + return nil +} + +// configLocation returns the location of the config file. Either reading from +// GOPASS_CONFIG or using the default location (~/.gopass.yml) +func configLocation() string { + if cf := os.Getenv("GOPASS_CONFIG"); cf != "" { + return cf + } + if xch := os.Getenv("XDG_CONFIG_HOME"); xch != "" { + return filepath.Join(xch, "gopass", "config.yml") + } + return filepath.Join(os.Getenv("HOME"), ".config", "gopass", "config.yml") +} + +// configLocations returns the possible locations of gopass config files, +// in decreasing priority +func configLocations() []string { + l := []string{} + if cf := os.Getenv("GOPASS_CONFIG"); cf != "" { + l = append(l, cf) + } + if xch := os.Getenv("XDG_CONFIG_HOME"); xch != "" { + l = append(l, filepath.Join(xch, "gopass", "config.yml")) + } + l = append(l, filepath.Join(os.Getenv("HOME"), ".config", "gopass", "config.yml")) + l = append(l, filepath.Join(os.Getenv("HOME"), ".gopass.yml")) + return l +} diff --git a/password/root_store.go b/password/root_store.go deleted file mode 100644 index fdbff61970..0000000000 --- a/password/root_store.go +++ /dev/null @@ -1,582 +0,0 @@ -package password - -import ( - "encoding/json" - "fmt" - "os" - "sort" - "strings" - - "github.com/fatih/color" - "github.com/justwatchcom/gopass/fsutil" - "github.com/justwatchcom/gopass/gpg" - "github.com/justwatchcom/gopass/tree" -) - -// 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 - 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 -func NewRootStore(path string) (*RootStore, error) { - s := &RootStore{ - Path: path, - Mount: make(map[string]string), - mounts: make(map[string]*Store), - } - if err := s.init(); err != nil { - return nil, err - } - return s, nil -} - -// init checks internal consistency and initializes sub stores -// after unmarshaling -func (r *RootStore) init() error { - if d := os.Getenv("GOPASS_DEBUG"); d == "true" { - r.Debug = true - } - - if r.Mount == nil { - r.Mount = make(map[string]string) - } - if r.mounts == nil { - r.mounts = make(map[string]*Store, len(r.Mount)) - } - if r.Path == "" { - return fmt.Errorf("Path must not be empty") - } - if r.AutoImport { - r.ImportFunc = nil - } - - // create the base store - s, err := NewStore("", fsutil.CleanPath(r.Path), r) - if err != nil { - return err - } - - r.store = s - - // initialize all mounts - for alias, path := range r.Mount { - path = fsutil.CleanPath(path) - if err := r.addMount(alias, path); err != nil { - fmt.Printf("Failed to initialized mount %s (%s): %s. Ignoring\n", alias, path, err) - continue - } - r.Mount[alias] = path - } - - // check for duplicate mounts - if err := r.checkMounts(); err != nil { - return fmt.Errorf("checking mounts failed: %s", err) - } - - // set some defaults - if r.ClipTimeout < 1 { - r.ClipTimeout = 45 - } - - return nil -} - -// Used to avoid recursion in UnmarshalJSON below -// http://attilaolah.eu/2013/11/29/json-decoding-in-go/ -type rootStore RootStore - -// UnmarshalJSON implements a custom JSON unmarshaler -// that will also make sure the store is properly initialized -// after loading -func (r *RootStore) UnmarshalJSON(b []byte) error { - s := rootStore{} - if err := json.Unmarshal(b, &s); err != nil { - return err - } - *r = RootStore(s) - return r.init() -} - -// Initialized checks on disk if .gpg-id was generated and thus returns true. -func (r *RootStore) Initialized() bool { - return r.store.Initialized() -} - -// Init tries to initalize a new password store location matching the object -func (r *RootStore) Init(alias, path string, ids ...string) error { - sub, err := NewStore(alias, fsutil.CleanPath(path), r) - if err != nil { - return err - } - - sub.persistKeys = r.PersistKeys - sub.loadKeys = r.LoadKeys - sub.alwaysTrust = r.AlwaysTrust - return sub.Init(ids...) -} - -// AddMount adds a new mount -func (r *RootStore) AddMount(alias, path string, keys ...string) error { - path = fsutil.CleanPath(path) - if r.Mount == nil { - r.Mount = make(map[string]string, 1) - } - if _, found := r.Mount[alias]; found { - return fmt.Errorf("%s is already mounted", alias) - } - if err := r.addMount(alias, path, keys...); err != nil { - return err - } - r.Mount[alias] = path - - // check for duplicate mounts - return r.checkMounts() -} - -func (r *RootStore) addMount(alias, path string, keys ...string) error { - if alias == "" { - return fmt.Errorf("alias must not be empty") - } - if r.mounts == nil { - r.mounts = make(map[string]*Store, 1) - } - if _, found := r.mounts[alias]; found { - return fmt.Errorf("%s is already mounted", alias) - } - - s, err := NewStore(alias, fsutil.CleanPath(path), r) - if err != nil { - return err - } - - if !s.Initialized() { - if len(keys) < 1 { - return fmt.Errorf("password store %s is not initialized. Try gopass init --alias %s --store %s", alias, alias, path) - } - if err := s.Init(keys...); err != nil { - return err - } - fmt.Printf("Password store %s initialized for:", path) - for _, r := range s.recipients { - color.Yellow(r) - } - } - - r.mounts[alias] = s - return nil -} - -// RemoveMount removes and existing mount -func (r *RootStore) RemoveMount(alias string) error { - if r.Mount == nil { - r.Mount = make(map[string]string) - } - if _, found := r.Mount[alias]; !found { - return fmt.Errorf("%s is not mounted", alias) - } - if _, found := r.mounts[alias]; !found { - fmt.Println(color.YellowString("%s is not initialized", alias)) - } - delete(r.Mount, alias) - delete(r.mounts, alias) - return nil -} - -// mountPoints returns a sorted list of mount points. It encodes the logic that -// the longer a mount point the more specific it is. This allows to "shadow" a -// shorter mount point by a longer one. -func (r *RootStore) mountPoints() []string { - mps := make([]string, 0, len(r.mounts)) - for k := range r.mounts { - mps = append(mps, k) - } - sort.Sort(byLen(mps)) - return mps -} - -// mountPoint returns the most-specific mount point for the given key -func (r *RootStore) mountPoint(name string) string { - for _, mp := range r.mountPoints() { - if strings.HasPrefix(name, mp) { - return mp - } - } - return "" -} - -// getStore returns the Store object at the most-specific mount point for the -// given key -func (r *RootStore) getStore(name string) *Store { - name = strings.TrimSuffix(name, "/") - mp := r.mountPoint(name) - if sub, found := r.mounts[mp]; found { - return sub - } - return r.store -} - -// checkMounts performs some sanity checks on our mounts. At the moment it -// only checks if some path is mounted twice. -func (r *RootStore) checkMounts() error { - paths := make(map[string]string, len(r.mounts)) - for k, v := range r.mounts { - if _, found := paths[v.path]; found { - return fmt.Errorf("Doubly mounted path at %s: %s", v.path, k) - } - paths[v.path] = k - } - return nil -} - -// Format will pretty print all entries in this store and all substores -func (r *RootStore) Format(maxDepth int) (string, error) { - t, err := r.Tree() - if err != nil { - return "", err - } - return t.Format(maxDepth), nil -} - -// List will return a flattened list of all tree entries -func (r *RootStore) List(maxDepth int) ([]string, error) { - t, err := r.Tree() - if err != nil { - return []string{}, err - } - return t.List(maxDepth), nil -} - -// Tree returns the tree representation of the entries -func (r *RootStore) Tree() (*tree.Folder, error) { - root := tree.New("gopass") - addFileFunc := func(in ...string) { - for _, f := range in { - ct := "text/plain" - if strings.HasSuffix(f, ".yaml") { - ct = "text/yaml" - f = strings.TrimSuffix(f, ".yaml") - } else if strings.HasSuffix(f, ".b64") { - ct = "application/octet-stream" - f = strings.TrimSuffix(f, ".b64") - } - if err := root.AddFile(f, ct); err != nil { - fmt.Printf("Failed to add file %s to tree: %s\n", f, err) - continue - } - } - } - addTplFunc := func(in ...string) { - for _, f := range in { - if err := root.AddTemplate(f); err != nil { - fmt.Printf("Failed to add template %s to tree: %s\n", f, err) - continue - } - } - } - mps := r.mountPoints() - sort.Sort(sort.Reverse(byLen(mps))) - for _, alias := range mps { - substore := r.mounts[alias] - if substore == nil { - continue - } - if err := root.AddMount(alias, substore.path); err != nil { - return nil, fmt.Errorf("failed to add mount: %s", err) - } - sf, err := substore.List(alias) - if err != nil { - return nil, fmt.Errorf("failed to add file: %s", err) - } - addFileFunc(sf...) - addTplFunc(substore.ListTemplates(alias)...) - } - - sf, err := r.store.List("") - if err != nil { - return nil, err - } - addFileFunc(sf...) - addTplFunc(r.store.ListTemplates("")...) - - return root, nil -} - -// Get returns the plaintext of a single key -func (r *RootStore) Get(name string) ([]byte, error) { - // forward to substore - store := r.getStore(name) - 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) { - store := r.getStore(name) - return store.First(strings.TrimPrefix(name, store.alias)) -} - -// 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 -func (r *RootStore) Exists(name string) (bool, error) { - store := r.getStore(name) - return store.Exists(strings.TrimPrefix(name, store.alias)) -} - -// IsDir checks if a given key is actually a folder -func (r *RootStore) IsDir(name string) bool { - store := r.getStore(name) - return store.IsDir(strings.TrimPrefix(name, store.alias)) -} - -// Set encodes and write the ciphertext of one entry to disk -func (r *RootStore) Set(name string, content []byte, reason string) error { - store := r.getStore(name) - 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) - return store.SetConfirm(strings.TrimPrefix(name, store.alias), content, reason, cb) -} - -// 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. -func (r *RootStore) Copy(from, to string) error { - subFrom := r.getStore(from) - subTo := r.getStore(to) - - from = strings.TrimPrefix(from, subFrom.alias) - to = strings.TrimPrefix(to, subFrom.alias) - - // cross-store copy - if !subFrom.equals(subTo) { - content, err := subFrom.Get(from) - if err != nil { - return err - } - if err := subTo.Set(to, content, fmt.Sprintf("Copied from %s to %s", from, to)); err != nil { - return err - } - return nil - } - - return subFrom.Copy(from, to) -} - -// Move will move one entry from one location to another. Cross-store moves are -// supported. Moving an entry will decode it from the old location, encode it -// for the destination store with the right set of recipients and remove it -// from the old location afterwards. -func (r *RootStore) Move(from, to string) error { - subFrom := r.getStore(from) - subTo := r.getStore(to) - - from = strings.TrimPrefix(from, subFrom.alias) - - // cross-store move - if !subFrom.equals(subTo) { - to = strings.TrimPrefix(to, subTo.alias) - content, err := subFrom.Get(from) - if err != nil { - return fmt.Errorf("Source %s does not exist in source store %s: %s", from, subFrom.alias, err) - } - if err := subTo.Set(to, content, fmt.Sprintf("Moved from %s to %s", from, to)); err != nil { - return err - } - if err := subFrom.Delete(from); err != nil { - return err - } - return nil - } - - to = strings.TrimPrefix(to, subFrom.alias) - return subFrom.Move(from, to) -} - -// Delete will remove an single entry from the store -func (r *RootStore) Delete(name string) error { - store := r.getStore(name) - sn := strings.TrimPrefix(name, store.alias) - if sn == "" { - return fmt.Errorf("can not delete a mount point. Use `gopass mount remove %s`", store.alias) - } - return store.Delete(sn) -} - -// Prune will remove a subtree from the Store -func (r *RootStore) Prune(tree string) error { - for mp := range r.mounts { - if strings.HasPrefix(mp, tree) { - return fmt.Errorf("can not prune subtree with mounts. Unmount first: `gopass mount remove %s`", mp) - } - } - - store := r.getStore(tree) - return store.Prune(strings.TrimPrefix(tree, store.alias)) -} - -func (r *RootStore) String() string { - ms := make([]string, 0, len(r.mounts)) - for alias, sub := range r.mounts { - ms = append(ms, alias+"="+sub.String()) - } - return fmt.Sprintf("RootStore(Path: %s, Mounts: %+v)", r.store.path, strings.Join(ms, ",")) -} - -// GitInit initializes the git repo -func (r *RootStore) GitInit(store, sk string) error { - return r.getStore(store).GitInit(sk) -} - -// Git runs arbitrary git commands on this store and all substores -func (r *RootStore) Git(store string, args ...string) error { - return r.getStore(store).Git(args...) -} - -// Fsck checks the stores integrity -func (r *RootStore) Fsck(check, force bool) error { - sh := make(map[string]string, 100) - for _, alias := range r.mountPoints() { - // check sub-store integrity - counts, err := r.mounts[alias].Fsck(alias, check, force) - if err != nil { - return err - } - fmt.Println(color.GreenString("[%s] Store (%s) checked (%d OK, %d warnings, %d errors)", alias, r.Mount[alias], counts["ok"], counts["warn"], counts["err"])) - // check shadowing - lst, err := r.mounts[alias].List(alias) - if err != nil { - return err - } - for _, e := range lst { - if a, found := sh[e]; found { - fmt.Println(color.YellowString("Entry %s is being shadowed by %s", e, a)) - } - sh[e] = alias - } - } - - counts, err := r.store.Fsck("root", check, force) - if err != nil { - return err - } - fmt.Println(color.GreenString("[%s] Store checked (%d OK, %d warnings, %d errors)", r.store.path, counts["ok"], counts["warn"], counts["err"])) - // check shadowing - lst, err := r.store.List("") - if err != nil { - return err - } - for _, e := range lst { - if a, found := sh[e]; found { - fmt.Println(color.YellowString("Entry %s is being shadowed by %s", e, a)) - } - sh[e] = "" - } - return nil -} - -// ListRecipients lists all recipients for the given store -func (r *RootStore) ListRecipients(store string) []string { - return r.getStore(store).recipients -} - -// AddRecipient adds a single recipient to the given store -func (r *RootStore) AddRecipient(store, rec string) error { - return r.getStore(store).AddRecipient(rec) -} - -// RemoveRecipient removes a single recipient from the given store -func (r *RootStore) RemoveRecipient(store, rec string) error { - return r.getStore(store).RemoveRecipient(rec) -} - -// RecipientsTree returns a tree view of all stores' recipients -func (r *RootStore) RecipientsTree(pretty bool) (*tree.Folder, error) { - root := tree.New("gopass") - mps := r.mountPoints() - sort.Sort(sort.Reverse(byLen(mps))) - for _, alias := range mps { - substore := r.mounts[alias] - if substore == nil { - continue - } - if err := root.AddMount(alias, substore.path); err != nil { - return nil, fmt.Errorf("failed to add mount: %s", err) - } - for _, r := range substore.recipients { - key := fmt.Sprintf("%s (missing public key)", r) - kl, err := gpg.ListPublicKeys(r) - if err == nil { - if len(kl) > 0 { - if pretty { - key = kl[0].OneLine() - } else { - key = kl[0].Fingerprint - } - } - } - if err := root.AddFile(alias+"/"+key, "gopass/recipient"); err != nil { - fmt.Println(err) - } - } - } - - for _, r := range r.store.recipients { - kl, err := gpg.ListPublicKeys(r) - if err != nil { - fmt.Println(err) - continue - } - if len(kl) < 1 { - fmt.Println("key not found", r) - continue - } - key := kl[0].Fingerprint - if pretty { - key = kl[0].OneLine() - } - if err := root.AddFile(key, "gopass/recipient"); err != nil { - fmt.Println(err) - } - } - return root, nil -} diff --git a/store/err.go b/store/err.go new file mode 100644 index 0000000000..a334d10f2b --- /dev/null +++ b/store/err.go @@ -0,0 +1,22 @@ +package store + +import "fmt" + +var ( + // ErrExistsFailed is returend if we can't check for existence + ErrExistsFailed = fmt.Errorf("Failed to check for existence") + // ErrNotFound is returned if an entry was not found + ErrNotFound = fmt.Errorf("Entry is not in the password store") + // ErrEncrypt is returned if we failed to encrypt an entry + ErrEncrypt = fmt.Errorf("Failed to encrypt") + // ErrDecrypt is returned if we failed to decrypt and entry + ErrDecrypt = fmt.Errorf("Failed to decrypt") + // ErrSneaky is returned if the user passes a possible malicious path to gopass + ErrSneaky = fmt.Errorf("you've attempted to pass a sneaky path to gopass. go home") + // ErrGitInit is returned if git is already initialized + ErrGitInit = fmt.Errorf("git is already initialized") + // ErrGitNotInit is returned if git is not initialized + ErrGitNotInit = fmt.Errorf("git is not initialized") + // ErrGitNoRemote is returned if git has no origin remote + ErrGitNoRemote = fmt.Errorf("git has no remote origin") +) diff --git a/store/root/config.go b/store/root/config.go new file mode 100644 index 0000000000..de9a49e33d --- /dev/null +++ b/store/root/config.go @@ -0,0 +1,110 @@ +package root + +import ( + "fmt" + + "github.com/justwatchcom/gopass/config" +) + +// Config returns this root stores config as a config struct +func (s *Store) Config() *config.Config { + c := &config.Config{ + AlwaysTrust: s.alwaysTrust, + AutoImport: s.autoImport, + AutoPull: s.autoPull, + AutoPush: s.autoPush, + ClipTimeout: s.clipTimeout, + LoadKeys: s.loadKeys, + Mounts: make(map[string]string, len(s.mounts)), + NoColor: s.noColor, + NoConfirm: s.noConfirm, + Path: s.path, + PersistKeys: s.persistKeys, + SafeContent: s.safeContent, + Version: s.version, + } + for alias, sub := range s.mounts { + c.Mounts[alias] = sub.Path() + } + return c +} + +// UpdateConfig updates this root-stores internal config and propagates +// those changes to all substores +func (s *Store) UpdateConfig(cfg *config.Config) error { + if cfg == nil { + return fmt.Errorf("invalid config") + } + s.alwaysTrust = cfg.AlwaysTrust + s.autoImport = cfg.AutoImport + s.autoPull = cfg.AutoPull + s.autoPush = cfg.AutoPush + s.clipTimeout = cfg.ClipTimeout + s.loadKeys = cfg.LoadKeys + s.noColor = cfg.NoColor + s.noConfirm = cfg.NoConfirm + s.path = cfg.Path + s.persistKeys = cfg.PersistKeys + s.safeContent = cfg.SafeContent + + // add any missing mounts + for alias, path := range cfg.Mounts { + if _, found := s.mounts[alias]; !found { + if err := s.addMount(alias, path); err != nil { + return err + } + } + } + + // propagate any config changes to our substores + if err := s.store.UpdateConfig(cfg); err != nil { + return err + } + for _, sub := range s.mounts { + if err := sub.UpdateConfig(cfg); err != nil { + return err + } + } + + return nil +} + +// Path returns the store path +func (s *Store) Path() string { + return s.path +} + +// Alias always returns an empty string +func (s *Store) Alias() string { + return "" +} + +// NoConfirm returns true if no recipients should be confirmed on encryption +func (s *Store) NoConfirm() bool { + return s.noConfirm +} + +// AutoPush returns the value of auto push +func (s *Store) AutoPush() bool { + return s.autoPush +} + +// AutoPull returns the value of auto pull +func (s *Store) AutoPull() bool { + return s.autoPull +} + +// AutoImport returns the value of auto import +func (s *Store) AutoImport() bool { + return s.autoImport +} + +// SafeContent returns the value of safe content +func (s *Store) SafeContent() bool { + return s.safeContent +} + +// ClipTimeout returns the value of clip timeout +func (s *Store) ClipTimeout() int { + return s.clipTimeout +} diff --git a/store/root/fsck.go b/store/root/fsck.go new file mode 100644 index 0000000000..a88f7d357e --- /dev/null +++ b/store/root/fsck.go @@ -0,0 +1,56 @@ +package root + +import ( + "fmt" + + "github.com/fatih/color" +) + +// Fsck checks the stores integrity +func (r *Store) Fsck(prefix string, check, force bool) (map[string]uint64, error) { + rc := make(map[string]uint64, 10) + sh := make(map[string]string, 100) + for _, alias := range r.mountPoints() { + // check sub-store integrity + counts, err := r.mounts[alias].Fsck(alias, check, force) + if err != nil { + return rc, err + } + for k, v := range counts { + rc[k] += v + } + fmt.Println(color.GreenString("[%s] Store (%s) checked (%d OK, %d warnings, %d errors)", alias, r.mounts[alias].Path(), counts["ok"], counts["warn"], counts["err"])) + // check shadowing + lst, err := r.mounts[alias].List(alias) + if err != nil { + return rc, err + } + for _, e := range lst { + if a, found := sh[e]; found { + fmt.Println(color.YellowString("Entry %s is being shadowed by %s", e, a)) + } + sh[e] = alias + } + } + + counts, err := r.store.Fsck("root", check, force) + if err != nil { + return rc, err + } + for k, v := range counts { + rc[k] += v + } + fmt.Println(color.GreenString("[%s] Store checked (%d OK, %d warnings, %d errors)", r.store.Path(), counts["ok"], counts["warn"], counts["err"])) + // check shadowing + lst, err := r.store.List("") + if err != nil { + return rc, err + } + for _, e := range lst { + if a, found := sh[e]; found { + fmt.Println(color.YellowString("Entry %s is being shadowed by %s", e, a)) + } + sh[e] = "" + } + return rc, nil +} diff --git a/store/root/git.go b/store/root/git.go new file mode 100644 index 0000000000..5bb67381c4 --- /dev/null +++ b/store/root/git.go @@ -0,0 +1,13 @@ +package root + +// GitInit initializes the git repo +func (r *Store) GitInit(name, sk string) error { + store := r.getStore(name) + return store.GitInit(store.Alias(), sk) +} + +// Git runs arbitrary git commands on this store and all substores +func (r *Store) Git(name string, args ...string) error { + store := r.getStore(name) + return store.Git(store.Alias(), args...) +} diff --git a/store/root/mount.go b/store/root/mount.go new file mode 100644 index 0000000000..b738464b26 --- /dev/null +++ b/store/root/mount.go @@ -0,0 +1,128 @@ +package root + +import ( + "fmt" + "sort" + "strings" + + "github.com/fatih/color" + "github.com/justwatchcom/gopass/fsutil" + "github.com/justwatchcom/gopass/store/sub" +) + +// AddMount adds a new mount +func (r *Store) AddMount(alias, path string, keys ...string) error { + path = fsutil.CleanPath(path) + if _, found := r.mounts[alias]; found { + return fmt.Errorf("%s is already mounted", alias) + } + if err := r.addMount(alias, path, keys...); err != nil { + return err + } + + // check for duplicate mounts + return r.checkMounts() +} + +func (r *Store) addMount(alias, path string, keys ...string) error { + if alias == "" { + return fmt.Errorf("alias must not be empty") + } + if r.mounts == nil { + r.mounts = make(map[string]*sub.Store, 1) + } + if _, found := r.mounts[alias]; found { + return fmt.Errorf("%s is already mounted", alias) + } + + // propagate our config settings to the sub store + cfg := r.Config() + cfg.Path = fsutil.CleanPath(path) + s, err := sub.New(alias, cfg) + if err != nil { + return err + } + + if !s.Initialized() { + if len(keys) < 1 { + return fmt.Errorf("password store %s is not initialized. Try gopass init --alias %s --store %s", alias, alias, path) + } + if err := s.Init(path, keys...); err != nil { + return err + } + fmt.Printf("Password store %s initialized for:", path) + for _, r := range s.Recipients() { + color.Yellow(r) + } + } + + r.mounts[alias] = s + return nil +} + +// RemoveMount removes and existing mount +func (r *Store) RemoveMount(alias string) error { + if _, found := r.mounts[alias]; !found { + return fmt.Errorf("%s is not mounted", alias) + } + if _, found := r.mounts[alias]; !found { + fmt.Println(color.YellowString("%s is not initialized", alias)) + } + delete(r.mounts, alias) + return nil +} + +// Mounts returns a map of mounts with their paths +func (r *Store) Mounts() map[string]string { + m := make(map[string]string, len(r.mounts)) + for alias, sub := range r.mounts { + m[alias] = sub.Path() + } + return m +} + +// MountPoints returns a sorted list of mount points. It encodes the logic that +// the longer a mount point the more specific it is. This allows to "shadow" a +// shorter mount point by a longer one. +func (r *Store) mountPoints() []string { + mps := make([]string, 0, len(r.mounts)) + for k := range r.mounts { + mps = append(mps, k) + } + sort.Sort(byLen(mps)) + return mps +} + +// mountPoint returns the most-specific mount point for the given key +func (r *Store) mountPoint(name string) string { + for _, mp := range r.mountPoints() { + if strings.HasPrefix(name, mp) { + return mp + } + } + return "" +} + +// getStore returns the Store object at the most-specific mount point for the +// given key +func (r *Store) getStore(name string) *sub.Store { + name = strings.TrimSuffix(name, "/") + mp := r.mountPoint(name) + if sub, found := r.mounts[mp]; found { + return sub + } + return r.store +} + +// checkMounts performs some sanity checks on our mounts. At the moment it +// only checks if some path is mounted twice. +func (r *Store) checkMounts() error { + paths := make(map[string]string, len(r.mounts)) + for k, v := range r.mounts { + if _, found := paths[v.Path()]; found { + return fmt.Errorf("Doubly mounted path at %s: %s", v.Path(), k) + } + paths[v.Path()] = k + } + return nil +} diff --git a/store/root/recipients.go b/store/root/recipients.go new file mode 100644 index 0000000000..e9dbb79cf3 --- /dev/null +++ b/store/root/recipients.go @@ -0,0 +1,77 @@ +package root + +import ( + "fmt" + "sort" + + "github.com/justwatchcom/gopass/gpg" + "github.com/justwatchcom/gopass/tree" + "github.com/justwatchcom/gopass/tree/simple" +) + +// ListRecipients lists all recipients for the given store +func (r *Store) ListRecipients(store string) []string { + return r.getStore(store).Recipients() +} + +// AddRecipient adds a single recipient to the given store +func (r *Store) AddRecipient(store, rec string) error { + return r.getStore(store).AddRecipient(rec) +} + +// RemoveRecipient removes a single recipient from the given store +func (r *Store) RemoveRecipient(store, rec string) error { + return r.getStore(store).RemoveRecipient(rec) +} + +// RecipientsTree returns a tree view of all stores' recipients +func (r *Store) RecipientsTree(pretty bool) (tree.Tree, error) { + root := simple.New("gopass") + mps := r.mountPoints() + sort.Sort(sort.Reverse(byLen(mps))) + for _, alias := range mps { + substore := r.mounts[alias] + if substore == nil { + continue + } + if err := root.AddMount(alias, substore.Path()); err != nil { + return nil, fmt.Errorf("failed to add mount: %s", err) + } + for _, r := range substore.Recipients() { + key := fmt.Sprintf("%s (missing public key)", r) + kl, err := gpg.ListPublicKeys(r) + if err == nil { + if len(kl) > 0 { + if pretty { + key = kl[0].OneLine() + } else { + key = kl[0].Fingerprint + } + } + } + if err := root.AddFile(alias+"/"+key, "gopass/recipient"); err != nil { + fmt.Println(err) + } + } + } + + for _, r := range r.store.Recipients() { + kl, err := gpg.ListPublicKeys(r) + if err != nil { + fmt.Println(err) + continue + } + if len(kl) < 1 { + fmt.Println("key not found", r) + continue + } + key := kl[0].Fingerprint + if pretty { + key = kl[0].OneLine() + } + if err := root.AddFile(key, "gopass/recipient"); err != nil { + fmt.Println(err) + } + } + return root, nil +} diff --git a/password/sort.go b/store/root/sort.go similarity index 96% rename from password/sort.go rename to store/root/sort.go index 74b88c146f..7b87b37c4c 100644 --- a/password/sort.go +++ b/store/root/sort.go @@ -1,4 +1,4 @@ -package password +package root // byLen is a list of mount points (string) that can be sorted by name type byLen []string diff --git a/password/sort_test.go b/store/root/sort_test.go similarity index 51% rename from password/sort_test.go rename to store/root/sort_test.go index f6f0a37bdc..15c0aba657 100644 --- a/password/sort_test.go +++ b/store/root/sort_test.go @@ -1,4 +1,4 @@ -package password +package root import ( "sort" @@ -22,3 +22,24 @@ func TestMountPointSort(t *testing.T) { } } } + +func TestSortByLen(t *testing.T) { + in := []string{ + "a", + "bb", + "ccc", + "dddd", + } + out := []string{ + "dddd", + "ccc", + "bb", + "a", + } + sort.Sort(byLen(in)) + for i, s := range in { + if out[i] != s { + t.Errorf("Mismatch at pos %d (%s - %s)", i, out[i], s) + } + } +} diff --git a/store/root/store.go b/store/root/store.go new file mode 100644 index 0000000000..c80e0e2c4f --- /dev/null +++ b/store/root/store.go @@ -0,0 +1,320 @@ +package root + +import ( + "fmt" + "os" + "sort" + "strings" + + "github.com/justwatchcom/gopass/config" + "github.com/justwatchcom/gopass/fsutil" + "github.com/justwatchcom/gopass/store" + "github.com/justwatchcom/gopass/store/sub" + "github.com/justwatchcom/gopass/tree" + "github.com/justwatchcom/gopass/tree/simple" +) + +// Store is the public facing password store +type Store struct { + alwaysTrust bool // always trust public keys when encrypting + autoImport bool // import missing public keys w/o asking + autoPull bool // pull from git before push + autoPush bool // push to git remote after commit + clipTimeout int // clear clipboard after seconds + debug bool + fsckFunc store.FsckCallback + importFunc store.ImportCallback + loadKeys bool // load missing keys from store + mounts map[string]*sub.Store + noColor bool // disable colors in output + noConfirm bool // do not confirm recipients when encrypting + path string // path to the root store + persistKeys bool // store recipient keys in store + safeContent bool // avoid showing passwords in terminal + store *sub.Store + version string +} + +// New creates a new store +func New(cfg *config.Config) (*Store, error) { + if cfg == nil { + cfg = &config.Config{} + } + if cfg.Path == "" { + return nil, fmt.Errorf("need path") + } + r := &Store{ + alwaysTrust: cfg.AlwaysTrust, + autoImport: cfg.AutoImport, + autoPull: cfg.AutoPull, + autoPush: cfg.AutoPush, + clipTimeout: cfg.ClipTimeout, + debug: cfg.Debug, + fsckFunc: cfg.FsckFunc, + importFunc: cfg.ImportFunc, + loadKeys: cfg.LoadKeys, + mounts: make(map[string]*sub.Store, len(cfg.Mounts)), + noColor: cfg.NoColor, + noConfirm: cfg.NoConfirm, + path: cfg.Path, + persistKeys: cfg.PersistKeys, + safeContent: cfg.SafeContent, + } + + if d := os.Getenv("GOPASS_DEBUG"); d == "true" { + r.debug = true + } + + if r.autoImport { + r.importFunc = nil + } + + // create the base store + subCfg := r.Config() + subCfg.Path = fsutil.CleanPath(r.Path()) + s, err := sub.New("", subCfg) + if err != nil { + return nil, err + } + + r.store = s + + // initialize all mounts + for alias, path := range cfg.Mounts { + path = fsutil.CleanPath(path) + if err := r.addMount(alias, path); err != nil { + fmt.Printf("Failed to initialized mount %s (%s): %s. Ignoring\n", alias, path, err) + continue + } + } + + // check for duplicate mounts + if err := r.checkMounts(); err != nil { + return nil, fmt.Errorf("checking mounts failed: %s", err) + } + + // set some defaults + if r.clipTimeout < 1 { + r.clipTimeout = 45 + } + + return r, nil +} + +// Initialized checks on disk if .gpg-id was generated and thus returns true. +func (r *Store) Initialized() bool { + return r.store.Initialized() +} + +// Init tries to initalize a new password store location matching the object +func (r *Store) Init(alias, path string, ids ...string) error { + cfg := r.Config() + cfg.Path = fsutil.CleanPath(path) + sub, err := sub.New(alias, cfg) + if err != nil { + return err + } + + return sub.Init(path, ids...) +} + +// Format will pretty print all entries in this store and all substores +func (r *Store) Format(maxDepth int) (string, error) { + t, err := r.Tree() + if err != nil { + return "", err + } + return t.Format(maxDepth), nil +} + +// List will return a flattened list of all tree entries +func (r *Store) List(maxDepth int) ([]string, error) { + t, err := r.Tree() + if err != nil { + return []string{}, err + } + return t.List(maxDepth), nil +} + +// Tree returns the tree representation of the entries +func (r *Store) Tree() (tree.Tree, error) { + root := simple.New("gopass") + addFileFunc := func(in ...string) { + for _, f := range in { + ct := "text/plain" + if strings.HasSuffix(f, ".yaml") { + ct = "text/yaml" + f = strings.TrimSuffix(f, ".yaml") + } else if strings.HasSuffix(f, ".b64") { + ct = "application/octet-stream" + f = strings.TrimSuffix(f, ".b64") + } + if err := root.AddFile(f, ct); err != nil { + fmt.Printf("Failed to add file %s to tree: %s\n", f, err) + continue + } + } + } + addTplFunc := func(in ...string) { + for _, f := range in { + if err := root.AddTemplate(f); err != nil { + fmt.Printf("Failed to add template %s to tree: %s\n", f, err) + continue + } + } + } + mps := r.mountPoints() + sort.Sort(sort.Reverse(byLen(mps))) + for _, alias := range mps { + substore := r.mounts[alias] + if substore == nil { + continue + } + if err := root.AddMount(alias, substore.Path()); err != nil { + return nil, fmt.Errorf("failed to add mount: %s", err) + } + sf, err := substore.List(alias) + if err != nil { + return nil, fmt.Errorf("failed to add file: %s", err) + } + addFileFunc(sf...) + addTplFunc(substore.ListTemplates(alias)...) + } + + sf, err := r.store.List("") + if err != nil { + return nil, err + } + addFileFunc(sf...) + addTplFunc(r.store.ListTemplates("")...) + + return root, nil +} + +// Get returns the plaintext of a single key +func (r *Store) Get(name string) ([]byte, error) { + // forward to substore + store := r.getStore(name) + return store.Get(strings.TrimPrefix(name, store.Alias())) +} + +// GetFirstLine returns the first line of the plaintext of a single key +func (r *Store) GetFirstLine(name string) ([]byte, error) { + store := r.getStore(name) + return store.GetFirstLine(strings.TrimPrefix(name, store.Alias())) +} + +// GetBody returns everything but the first line from a key +func (r *Store) GetBody(name string) ([]byte, error) { + store := r.getStore(name) + return store.GetBody(strings.TrimPrefix(name, store.Alias())) +} + +// Exists checks the existence of a single entry +func (r *Store) Exists(name string) (bool, error) { + store := r.getStore(name) + return store.Exists(strings.TrimPrefix(name, store.Alias())) +} + +// IsDir checks if a given key is actually a folder +func (r *Store) IsDir(name string) bool { + store := r.getStore(name) + return store.IsDir(strings.TrimPrefix(name, store.Alias())) +} + +// Set encodes and write the ciphertext of one entry to disk +func (r *Store) Set(name string, content []byte, reason string) error { + store := r.getStore(name) + return store.Set(strings.TrimPrefix(name, store.Alias()), content, reason) +} + +// SetConfirm calls Set with confirmation callback +func (r *Store) SetConfirm(name string, content []byte, reason string, cb store.RecipientCallback) error { + store := r.getStore(name) + return store.SetConfirm(strings.TrimPrefix(name, store.Alias()), content, reason, cb) +} + +// 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. +func (r *Store) Copy(from, to string) error { + subFrom := r.getStore(from) + subTo := r.getStore(to) + + from = strings.TrimPrefix(from, subFrom.Alias()) + to = strings.TrimPrefix(to, subFrom.Alias()) + + // cross-store copy + if !subFrom.Equals(subTo) { + content, err := subFrom.Get(from) + if err != nil { + return err + } + if err := subTo.Set(to, content, fmt.Sprintf("Copied from %s to %s", from, to)); err != nil { + return err + } + return nil + } + + return subFrom.Copy(from, to) +} + +// Move will move one entry from one location to another. Cross-store moves are +// supported. Moving an entry will decode it from the old location, encode it +// for the destination store with the right set of recipients and remove it +// from the old location afterwards. +func (r *Store) Move(from, to string) error { + subFrom := r.getStore(from) + subTo := r.getStore(to) + + from = strings.TrimPrefix(from, subFrom.Alias()) + + // cross-store move + if !subFrom.Equals(subTo) { + to = strings.TrimPrefix(to, subTo.Alias()) + content, err := subFrom.Get(from) + if err != nil { + return fmt.Errorf("Source %s does not exist in source store %s: %s", from, subFrom.Alias(), err) + } + if err := subTo.Set(to, content, fmt.Sprintf("Moved from %s to %s", from, to)); err != nil { + return err + } + if err := subFrom.Delete(from); err != nil { + return err + } + return nil + } + + to = strings.TrimPrefix(to, subFrom.Alias()) + return subFrom.Move(from, to) +} + +// Delete will remove an single entry from the store +func (r *Store) Delete(name string) error { + store := r.getStore(name) + sn := strings.TrimPrefix(name, store.Alias()) + if sn == "" { + return fmt.Errorf("can not delete a mount point. Use `gopass mount remove %s`", store.Alias()) + } + return store.Delete(sn) +} + +// Prune will remove a subtree from the Store +func (r *Store) Prune(tree string) error { + for mp := range r.mounts { + if strings.HasPrefix(mp, tree) { + return fmt.Errorf("can not prune subtree with mounts. Unmount first: `gopass mount remove %s`", mp) + } + } + + store := r.getStore(tree) + return store.Prune(strings.TrimPrefix(tree, store.Alias())) +} + +func (r *Store) String() string { + ms := make([]string, 0, len(r.mounts)) + for alias, sub := range r.mounts { + ms = append(ms, alias+"="+sub.String()) + } + return fmt.Sprintf("Store(Path: %s, Mounts: %+v)", r.store.Path(), strings.Join(ms, ",")) +} diff --git a/password/store_test.go b/store/root/store_test.go similarity index 90% rename from password/store_test.go rename to store/root/store_test.go index 19a5e3a0a8..9a0d723919 100644 --- a/password/store_test.go +++ b/store/root/store_test.go @@ -1,4 +1,4 @@ -package password +package root import ( "io/ioutil" @@ -8,96 +8,10 @@ import ( "strconv" "strings" "testing" -) - -func TestSortByLen(t *testing.T) { - in := []string{ - "a", - "bb", - "ccc", - "dddd", - } - out := []string{ - "dddd", - "ccc", - "bb", - "a", - } - sort.Sort(byLen(in)) - for i, s := range in { - if out[i] != s { - t.Errorf("Mismatch at pos %d (%s - %s)", i, out[i], s) - } - } -} - -func createStore(dir string) ([]string, []string, error) { - recipients := []string{ - "0xDEADBEEF", - "0xFEEDBEEF", - } - list := []string{ - "foo/bar/baz", - "baz/ing/a", - } - sort.Strings(list) - for _, file := range list { - filename := filepath.Join(dir, file+".gpg") - if err := os.MkdirAll(filepath.Dir(filename), 0700); err != nil { - return recipients, list, err - } - if err := ioutil.WriteFile(filename, []byte{}, 0644); err != nil { - return recipients, list, err - } - } - err := ioutil.WriteFile(filepath.Join(dir, gpgID), []byte(strings.Join(recipients, "\n")), 0600) - return recipients, list, err -} - -func maxLenStr(l []string) string { - max := 10 - for _, e := range l { - if len(e) > max { - max = len(e) - } - } - return strconv.Itoa(max) -} -func logLists(t *testing.T, l1, l2 []string) { - tpl := "%3d | %-" + maxLenStr(l1) + "s | %-" + maxLenStr(l2) + "s" - t.Logf(tpl, 0, "L1", "L2") - max := len(l1) - if len(l2) > max { - max = len(l2) - } - for i := 0; i < max; i++ { - e1 := "MISSING" - e2 := "MISSING" - if len(l1) > i { - e1 = l1[i] - } - if len(l2) > i { - e2 = l2[i] - } - t.Logf(tpl, i, e1, e2) - } -} - -func compareLists(t *testing.T, l1, l2 []string) { - if len(l1) != len(l2) { - t.Errorf("len(l1)=%d != len(l2)=%d", len(l1), len(l2)) - logLists(t, l1, l2) - return - } - for i := 0; i < len(l1); i++ { - if l1[i] != l2[i] { - t.Errorf("Mismatch at pos %d: %s - %s", i, l1[i], l2[i]) - logLists(t, l1, l2) - return - } - } -} + "github.com/justwatchcom/gopass/config" + "github.com/justwatchcom/gopass/store/sub" +) func TestSimpleList(t *testing.T) { tempdir, err := ioutil.TempDir("", "gopass-") @@ -113,7 +27,7 @@ func TestSimpleList(t *testing.T) { t.Fatalf("Failed to create store directory: %s", err) } - rs, err := NewRootStore(tempdir) + rs, err := New(&config.Config{Path: tempdir}) if err != nil { t.Fatalf("Failed to create root store: %s", err) } @@ -159,7 +73,7 @@ func TestListMulti(t *testing.T) { ents = append(ents, "sub2/"+k) } sort.Strings(ents) - rs, err := NewRootStore(tempdir + "/root") + rs, err := New(&config.Config{Path: tempdir + "/root"}) if err != nil { t.Fatalf("Failed to create root store: %s", err) } @@ -217,7 +131,7 @@ func TestListNested(t *testing.T) { ents = append(ents, "sub2/sub3/"+k) } sort.Strings(ents) - rs, err := NewRootStore(tempdir + "/root") + rs, err := New(&config.Config{Path: tempdir + "/root"}) if err != nil { t.Fatalf("Failed to create root store: %s", err) } @@ -239,3 +153,71 @@ func TestListNested(t *testing.T) { } compareLists(t, ents, tree.List(0)) } + +func createStore(dir string) ([]string, []string, error) { + recipients := []string{ + "0xDEADBEEF", + "0xFEEDBEEF", + } + list := []string{ + "foo/bar/baz", + "baz/ing/a", + } + sort.Strings(list) + for _, file := range list { + filename := filepath.Join(dir, file+".gpg") + if err := os.MkdirAll(filepath.Dir(filename), 0700); err != nil { + return recipients, list, err + } + if err := ioutil.WriteFile(filename, []byte{}, 0644); err != nil { + return recipients, list, err + } + } + err := ioutil.WriteFile(filepath.Join(dir, sub.GPGID), []byte(strings.Join(recipients, "\n")), 0600) + return recipients, list, err +} + +func maxLenStr(l []string) string { + max := 10 + for _, e := range l { + if len(e) > max { + max = len(e) + } + } + return strconv.Itoa(max) +} + +func logLists(t *testing.T, l1, l2 []string) { + tpl := "%3d | %-" + maxLenStr(l1) + "s | %-" + maxLenStr(l2) + "s" + t.Logf(tpl, 0, "L1", "L2") + max := len(l1) + if len(l2) > max { + max = len(l2) + } + for i := 0; i < max; i++ { + e1 := "MISSING" + e2 := "MISSING" + if len(l1) > i { + e1 = l1[i] + } + if len(l2) > i { + e2 = l2[i] + } + t.Logf(tpl, i, e1, e2) + } +} + +func compareLists(t *testing.T, l1, l2 []string) { + if len(l1) != len(l2) { + t.Errorf("len(l1)=%d != len(l2)=%d", len(l1), len(l2)) + logLists(t, l1, l2) + return + } + for i := 0; i < len(l1); i++ { + if l1[i] != l2[i] { + t.Errorf("Mismatch at pos %d: %s - %s", i, l1[i], l2[i]) + logLists(t, l1, l2) + return + } + } +} diff --git a/store/root/templates.go b/store/root/templates.go new file mode 100644 index 0000000000..3dcf5f6131 --- /dev/null +++ b/store/root/templates.go @@ -0,0 +1,69 @@ +package root + +import ( + "fmt" + "sort" + "strings" + + "github.com/justwatchcom/gopass/tree" + "github.com/justwatchcom/gopass/tree/simple" +) + +// LookupTemplate will lookup and return a template +func (r *Store) LookupTemplate(name string) ([]byte, bool) { + store := r.getStore(name) + return store.LookupTemplate(strings.TrimPrefix(name, store.Alias())) +} + +// TemplateTree returns a tree of all templates +func (r *Store) TemplateTree() (tree.Tree, error) { + root := simple.New("gopass") + mps := r.mountPoints() + sort.Sort(sort.Reverse(byLen(mps))) + for _, alias := range mps { + substore := r.mounts[alias] + if substore == nil { + continue + } + if err := root.AddMount(alias, substore.Path()); err != nil { + return nil, fmt.Errorf("failed to add mount: %s", err) + } + for _, t := range substore.ListTemplates(alias) { + if err := root.AddFile(t, "gopass/template"); err != nil { + fmt.Println(err) + } + } + } + + for _, t := range r.store.ListTemplates("") { + if err := root.AddFile(t, "gopass/template"); err != nil { + fmt.Println(err) + } + } + + return root, nil +} + +// HasTemplate returns true if the template exists +func (r *Store) HasTemplate(name string) bool { + store := r.getStore(name) + return store.HasTemplate(strings.TrimPrefix(name, store.Alias())) +} + +// GetTemplate will return the content of the named template +func (r *Store) GetTemplate(name string) ([]byte, error) { + store := r.getStore(name) + return store.GetTemplate(strings.TrimPrefix(name, store.Alias())) +} + +// SetTemplate will (over)write the content to the template file +func (r *Store) SetTemplate(name string, content []byte) error { + store := r.getStore(name) + return store.SetTemplate(strings.TrimPrefix(name, store.Alias()), content) +} + +// RemoveTemplate will delete the named template if it exists +func (r *Store) RemoveTemplate(name string) error { + store := r.getStore(name) + return store.RemoveTemplate(strings.TrimPrefix(name, store.Alias())) +} diff --git a/store/root/yaml.go b/store/root/yaml.go new file mode 100644 index 0000000000..3ea11cd258 --- /dev/null +++ b/store/root/yaml.go @@ -0,0 +1,24 @@ +package root + +import "strings" + +// 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 *Store) GetKey(name, key string) ([]byte, error) { + store := r.getStore(name) + return store.GetKey(strings.TrimPrefix(name, store.Alias()), key) +} + +// 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 *Store) SetKey(name, key, value string) error { + store := r.getStore(name) + return store.SetKey(strings.TrimPrefix(name, store.Alias()), key, value) +} + +// DeleteKey removes a single key +func (r *Store) DeleteKey(name, key string) error { + store := r.getStore(name) + return store.DeleteKey(strings.TrimPrefix(name, store.Alias()), key) +} diff --git a/store/store.go b/store/store.go new file mode 100644 index 0000000000..26cbc7e6d4 --- /dev/null +++ b/store/store.go @@ -0,0 +1,12 @@ +package store + +// RecipientCallback is a callback to verify the list of recipients +type RecipientCallback func(string, []string) ([]string, error) + +// ImportCallback is a callback to ask the user if he wants to import +// a certain recipients public key into his keystore +type ImportCallback func(string) bool + +// FsckCallback is a callback to ask the user to confirm certain fsck +// corrective actions +type FsckCallback func(string) bool diff --git a/store/sub/config.go b/store/sub/config.go new file mode 100644 index 0000000000..4d8301065e --- /dev/null +++ b/store/sub/config.go @@ -0,0 +1,56 @@ +package sub + +import ( + "fmt" + + "github.com/justwatchcom/gopass/config" +) + +// Config returns this sub stores config as a config struct +func (s *Store) Config() *config.Config { + c := &config.Config{ + AlwaysTrust: s.alwaysTrust, + AutoImport: s.autoImport, + AutoPull: s.autoPull, + AutoPush: s.autoPush, + Debug: s.debug, + FsckFunc: s.fsckFunc, + ImportFunc: s.importFunc, + LoadKeys: s.loadKeys, + Mounts: make(map[string]string), + Path: s.path, + PersistKeys: s.persistKeys, + } + return c +} + +// UpdateConfig updates this sub-stores internal config +func (s *Store) UpdateConfig(cfg *config.Config) error { + if cfg == nil { + return fmt.Errorf("invalid config") + } + s.alwaysTrust = cfg.AlwaysTrust + s.autoImport = cfg.AutoImport + s.autoPull = cfg.AutoPull + s.autoPush = cfg.AutoPush + s.debug = cfg.Debug + s.fsckFunc = cfg.FsckFunc + s.importFunc = cfg.ImportFunc + s.loadKeys = cfg.LoadKeys + s.path = cfg.Path + s.persistKeys = cfg.PersistKeys + + // substores have no mounts + + return nil +} + +// Path returns the value of path +func (s *Store) Path() string { + return s.path +} + +// Alias returns the value of alias +func (s *Store) Alias() string { + return s.alias +} diff --git a/password/fsck.go b/store/sub/fsck.go similarity index 99% rename from password/fsck.go rename to store/sub/fsck.go index 4b88feabfb..8dfa9feec4 100644 --- a/password/fsck.go +++ b/store/sub/fsck.go @@ -1,4 +1,4 @@ -package password +package sub import ( "fmt" diff --git a/password/git.go b/store/sub/git.go similarity index 82% rename from password/git.go rename to store/sub/git.go index 86f85a9cac..3a46814af0 100644 --- a/password/git.go +++ b/store/sub/git.go @@ -1,4 +1,4 @@ -package password +package sub import ( "bytes" @@ -11,32 +11,28 @@ import ( "github.com/fatih/color" "github.com/justwatchcom/gopass/fsutil" + "github.com/justwatchcom/gopass/store" ) -var ( - // ErrGitInit is returned if git is already initialized - ErrGitInit = fmt.Errorf("git is already initialized") - // ErrGitNotInit is returned if git is not initialized - ErrGitNotInit = fmt.Errorf("git is not initialized") - // ErrGitNoRemote is returned if git has no origin remote - ErrGitNoRemote = fmt.Errorf("git has no remote origin") -) +func (s *Store) gitCmd(args ...string) error { + cmd := exec.Command(args[0], args[1:]...) + cmd.Dir = s.path + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + if s.debug { + fmt.Printf("store.GitInit: %s %+v\n", cmd.Path, cmd.Args) + } + return cmd.Run() +} // GitInit initializes this store's git repo and // recursively calls GitInit on all substores. -func (s *Store) GitInit(signKey string) error { +func (s *Store) GitInit(alias, signKey string) error { // the git repo may be empty (i.e. no branches, cloned from a fresh remote) // or already initialized. Only run git init if the folder is completely empty if !s.isGit() { - cmd := exec.Command("git", "init") - cmd.Dir = s.path - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - - if s.debug { - fmt.Printf("store.GitInit: %s %+v\n", cmd.Path, cmd.Args) - } - if err := cmd.Run(); err != nil { + if err := s.gitCmd("git", "init"); err != nil { return fmt.Errorf("Failed to initialize git: %s", err) } } @@ -58,16 +54,12 @@ func (s *Store) GitInit(signKey string) error { fmt.Println(color.YellowString("Warning: Failed to commit .gitattributes to git")) } - cmd := exec.Command("git", "config", "--local", "diff.gpg.binary", "true") - cmd.Dir = s.path - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - - if s.debug { - fmt.Printf("store.GitInit: %s %+v\n", cmd.Path, cmd.Args) + // setup for proper diffs + if err := s.gitCmd("git", "config", "--local", "diff.gpg.binary", "true"); err != nil { + color.Yellow("Error while initializing git: %s\n", err) } - if err := cmd.Run(); err != nil { - color.Yellow("Failed to initialize git: %s\n", err) + if err := s.gitCmd("git", "config", "--local", "diff.gpg.textconv", "gpg --no-tty --decrypt"); err != nil { + color.Yellow("Error while initializing git: %s\n", err) } // set GPG signkey @@ -107,7 +99,7 @@ func (s *Store) gitSetSignKey(sk string) error { } // Git runs arbitrary git commands on this store and all substores -func (s *Store) Git(args ...string) error { +func (s *Store) Git(alias string, args ...string) error { cmd := exec.Command("git", args...) cmd.Dir = s.path cmd.Stdout = os.Stdout @@ -132,7 +124,7 @@ func (s *Store) isGit() bool { // gitAdd adds the listed files to the git index func (s *Store) gitAdd(files ...string) error { if !s.isGit() { - return ErrGitNotInit + return store.ErrGitNotInit } for i := range files { files[i] = strings.TrimPrefix(files[i], s.path+"/") @@ -158,7 +150,7 @@ func (s *Store) gitAdd(files ...string) error { // gitCommit creates a new git commit with the given commit message func (s *Store) gitCommit(msg string) error { if !s.isGit() { - return ErrGitNotInit + return store.ErrGitNotInit } cmd := exec.Command("git", "commit", "-m", msg) @@ -178,7 +170,7 @@ func (s *Store) gitCommit(msg string) error { func (s *Store) gitConfigValue(key string) (string, error) { if !s.isGit() { - return "", ErrGitNotInit + return "", store.ErrGitNotInit } buf := &bytes.Buffer{} @@ -202,7 +194,7 @@ func (s *Store) gitConfigValue(key string) (string, error) { // optional arguments: remote and branch func (s *Store) gitPush(remote, branch string) error { if !s.isGit() { - return ErrGitNotInit + return store.ErrGitNotInit } if remote == "" { @@ -213,7 +205,7 @@ func (s *Store) gitPush(remote, branch string) error { } if v, err := s.gitConfigValue("remote." + remote + ".url"); err != nil || v == "" { - return ErrGitNoRemote + return store.ErrGitNoRemote } if s.autoPull { diff --git a/password/recipients.go b/store/sub/recipients.go similarity index 92% rename from password/recipients.go rename to store/sub/recipients.go index 63700528f7..fd0d02aef5 100644 --- a/password/recipients.go +++ b/store/sub/recipients.go @@ -1,4 +1,4 @@ -package password +package sub import ( "bufio" @@ -14,6 +14,7 @@ import ( "github.com/fatih/color" "github.com/justwatchcom/gopass/fsutil" "github.com/justwatchcom/gopass/gpg" + "github.com/justwatchcom/gopass/store" ) const ( @@ -22,6 +23,11 @@ const ( dirMode = 0700 ) +// Recipients returns the list of recipients of this store +func (s *Store) Recipients() []string { + return s.recipients +} + // AddRecipient adds a new recipient to the list func (s *Store) AddRecipient(id string) error { for _, k := range s.recipients { @@ -39,9 +45,8 @@ func (s *Store) AddRecipient(id string) error { return s.reencrypt("Added Recipient " + id) } -// RemoveRecipient will remove the given recipient from the store +// RemoveRecipient will remove the given recipient from the storefunc (s *Store) RemoveRecipient()id string) error { func (s *Store) RemoveRecipient(id string) error { - // we try to get the public key info for this ID from gpg // but if this key is not available on this machine we // just try to remove it literally keys, err := gpg.ListPublicKeys(id) @@ -137,12 +142,12 @@ func (s *Store) saveRecipients(msg string) error { err := s.gitAdd(s.idFile()) if err == nil { if err := s.gitCommit(msg); err != nil { - if err != ErrGitNotInit { + if err != store.ErrGitNotInit { return err } } } else { - if err != ErrGitNotInit { + if err != store.ErrGitNotInit { return err } } @@ -151,10 +156,10 @@ func (s *Store) saveRecipients(msg string) error { // push to remote repo if s.autoPush { if err := s.gitPush("", ""); err != nil { - if err == ErrGitNotInit { + if err == store.ErrGitNotInit { return nil } - if err == ErrGitNoRemote { + if err == store.ErrGitNoRemote { msg := "Warning: git has no remote. Ignoring auto-push option\n" + "Run: gopass git remote add origin ..." fmt.Println(color.YellowString(msg)) @@ -178,7 +183,7 @@ func (s *Store) saveRecipients(msg string) error { return err } if err := s.gitAdd(path); err != nil { - if err == ErrGitNotInit { + if err == store.ErrGitNotInit { continue } return err @@ -192,10 +197,10 @@ func (s *Store) saveRecipients(msg string) error { // push to remote repo if s.autoPush { if err := s.gitPush("", ""); err != nil { - if err == ErrGitNotInit { + if err == store.ErrGitNotInit { return nil } - if err == ErrGitNoRemote { + if err == store.ErrGitNoRemote { msg := "Warning: git has not remote. Ignoring auto-push option\n" + "Run: gopass git remote add origin ..." fmt.Println(color.YellowString(msg)) diff --git a/password/recipients_test.go b/store/sub/recipients_test.go similarity index 89% rename from password/recipients_test.go rename to store/sub/recipients_test.go index e7d9e6ea62..374d30a5d3 100644 --- a/password/recipients_test.go +++ b/store/sub/recipients_test.go @@ -1,4 +1,4 @@ -package password +package sub import ( "bufio" @@ -10,6 +10,7 @@ import ( "strings" "testing" + "github.com/justwatchcom/gopass/config" "github.com/stretchr/testify/assert" ) @@ -24,7 +25,7 @@ func TestLoadRecipients(t *testing.T) { genRecs, _, err := createStore(tempdir) assert.NoError(t, err) - s, err := NewStore("", tempdir, nil) + s, err := New("", &config.Config{Path: tempdir}) assert.NoError(t, err) recs, err := s.loadRecipients() @@ -43,11 +44,11 @@ func TestSaveRecipients(t *testing.T) { genRecs, _, err := createStore(tempdir) assert.NoError(t, err) - s, err := NewStore("", tempdir, nil) + s, err := New("", &config.Config{Path: tempdir}) assert.NoError(t, err) // remove recipients - _ = os.Remove(filepath.Join(tempdir, gpgID)) + _ = os.Remove(filepath.Join(tempdir, GPGID)) err = s.saveRecipients("test-save-recipients") assert.NoError(t, err) @@ -86,7 +87,7 @@ func TestAddRecipient(t *testing.T) { genRecs, _, err := createStore(tempdir) assert.NoError(t, err) - s, err := NewStore("", tempdir, nil) + s, err := New("", &config.Config{Path: tempdir}) assert.NoError(t, err) newRecp := "A3683834" @@ -114,7 +115,7 @@ func TestRemoveRecipient(t *testing.T) { genRecs, _, err := createStore(tempdir) assert.NoError(t, err) - s, err := NewStore("", tempdir, nil) + s, err := New("", &config.Config{Path: tempdir}) assert.NoError(t, err) if os.Getenv("GOPASS_INTEGRATION_TESTS") != "true" { @@ -137,7 +138,7 @@ func TestListRecipients(t *testing.T) { genRecs, _, err := createStore(tempdir) assert.NoError(t, err) - s, err := NewStore("", tempdir, nil) + s, err := New("", &config.Config{Path: tempdir}) assert.NoError(t, err) assert.NoError(t, err) diff --git a/password/store.go b/store/sub/store.go similarity index 73% rename from password/store.go rename to store/sub/store.go index 39c567260b..a97b6c9f76 100644 --- a/password/store.go +++ b/store/sub/store.go @@ -1,4 +1,4 @@ -package password +package sub import ( "bytes" @@ -7,78 +7,55 @@ import ( "path/filepath" "strings" - "gopkg.in/yaml.v2" - "github.com/fatih/color" + "github.com/justwatchcom/gopass/config" "github.com/justwatchcom/gopass/fsutil" "github.com/justwatchcom/gopass/gpg" + "github.com/justwatchcom/gopass/store" ) const ( - gpgID = ".gpg-id" -) - -var ( - // ErrExistsFailed is returend if we can't check for existence - ErrExistsFailed = fmt.Errorf("Failed to check for existence") - // ErrNotFound is returned if an entry was not found - ErrNotFound = fmt.Errorf("Entry is not in the password store") - // ErrEncrypt is returned if we failed to encrypt an entry - ErrEncrypt = fmt.Errorf("Failed to encrypt") - // ErrDecrypt is returned if we failed to decrypt and entry - ErrDecrypt = fmt.Errorf("Failed to decrypt") - // ErrSneaky is returned if the user passes a possible malicious path to gopass - ErrSneaky = fmt.Errorf("you've attempted to pass a sneaky path to gopass. go home") + // GPGID is the name of the file containing the recipient ids + GPGID = ".gpg-id" ) -// RecipientCallback is a callback to verify the list of recipients -type RecipientCallback func(string, []string) ([]string, error) - -// ImportCallback is a callback to ask the user if he wants to import -// a certain recipients public key into his keystore -type ImportCallback func(string) bool - -// FsckCallback is a callback to ask the user to confirm certain fsck -// corrective actions -type FsckCallback func(string) bool - // Store is password store type Store struct { - recipients []string alias string - path string - autoPush bool - autoPull bool - autoImport bool - persistKeys bool - loadKeys bool alwaysTrust bool - importFunc ImportCallback - fsckFunc FsckCallback + autoImport bool + autoPull bool + autoPush bool debug bool + fsckFunc store.FsckCallback + importFunc store.ImportCallback + loadKeys bool + path string + persistKeys bool + recipients []string } -// NewStore creates a new store, copying settings from the given root store -func NewStore(alias, path string, r *RootStore) (*Store, error) { - if r == nil { - r = &RootStore{} +// New creates a new store, copying settings from the given root store +func New(alias string, cfg *config.Config) (*Store, error) { + if cfg == nil { + cfg = &config.Config{} } - if path == "" { + if cfg.Path == "" { return nil, fmt.Errorf("Need path") } s := &Store{ alias: alias, - path: path, - autoPush: r.AutoPush, - autoPull: r.AutoPull, - autoImport: r.AutoImport, - persistKeys: r.PersistKeys, - loadKeys: r.LoadKeys, - alwaysTrust: r.AlwaysTrust, - importFunc: r.ImportFunc, - fsckFunc: r.FsckFunc, - debug: r.Debug, - recipients: make([]string, 0, 5), + alwaysTrust: cfg.AlwaysTrust, + autoImport: cfg.AutoImport, + autoPull: cfg.AutoPull, + autoPush: cfg.AutoPush, + debug: cfg.Debug, + fsckFunc: cfg.FsckFunc, + importFunc: cfg.ImportFunc, + loadKeys: cfg.LoadKeys, + path: cfg.Path, + persistKeys: cfg.PersistKeys, + recipients: make([]string, 0, 1), } // only try to load recipients if the store / recipients file exist @@ -98,7 +75,7 @@ func (s *Store) Initialized() bool { } // Init tries to initalize a new password store location matching the object -func (s *Store) Init(ids ...string) error { +func (s *Store) Init(path string, ids ...string) error { if s.Initialized() { return fmt.Errorf("Store is already initialized") } @@ -140,7 +117,7 @@ func (s *Store) Init(ids ...string) error { // idFile returns the path to the recipient list for this store func (s *Store) idFile() string { - return fsutil.CleanPath(filepath.Join(s.path, gpgID)) + return fsutil.CleanPath(filepath.Join(s.path, GPGID)) } // mkStoreWalkerFunc create a func to walk a (sub)store, i.e. list it's content @@ -161,7 +138,7 @@ func mkStoreWalkerFunc(alias, folder string, fn func(...string)) func(string, os if path == folder { return nil } - if path == filepath.Join(folder, gpgID) { + if path == filepath.Join(folder, GPGID) { return nil } if info.Mode()&os.ModeSymlink != 0 { @@ -188,8 +165,8 @@ func (s *Store) List(prefix string) ([]string, error) { return lst, err } -// equals returns true if this store has the same on-disk path as the other -func (s *Store) equals(other *Store) bool { +// Equals returns true if this store has the same on-disk path as the other +func (s *Store) Equals(other *Store) bool { if other == nil { return false } @@ -201,45 +178,26 @@ func (s *Store) Get(name string) ([]byte, error) { p := s.passfile(name) if !strings.HasPrefix(p, s.path) { - return []byte{}, ErrSneaky + return []byte{}, store.ErrSneaky } if !fsutil.IsFile(p) { if s.debug { fmt.Printf("File %s not found\n", p) } - return []byte{}, ErrNotFound + return []byte{}, store.ErrNotFound } content, err := gpg.Decrypt(p) if err != nil { - return []byte{}, ErrDecrypt + return []byte{}, store.ErrDecrypt } 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) { +// GetFirstLine returns the first line of the plaintext of a single key +func (s *Store) GetFirstLine(name string) ([]byte, error) { content, err := s.Get(name) if err != nil { return nil, err @@ -253,8 +211,8 @@ func (s *Store) First(name string) ([]byte, error) { return bytes.TrimSpace(lines[0]), nil } -// SafeContent returns everything but the first line -func (s *Store) SafeContent(name string) ([]byte, error) { +// GetBody returns everything but the first line +func (s *Store) GetBody(name string) ([]byte, error) { content, err := s.Get(name) if err != nil { return nil, err @@ -277,7 +235,7 @@ func (s *Store) Exists(name string) (bool, error) { p := s.passfile(name) if !strings.HasPrefix(p, s.path) { - return false, ErrSneaky + return false, store.ErrSneaky } return fsutil.IsFile(p), nil @@ -291,11 +249,11 @@ func (s *Store) Set(name string, content []byte, reason string) error { // SetConfirm encodes and writes the cipertext of one entry to disk. This // method can be passed a callback to confirm the recipients immedeately // before encryption. -func (s *Store) SetConfirm(name string, content []byte, reason string, cb RecipientCallback) error { +func (s *Store) SetConfirm(name string, content []byte, reason string, cb store.RecipientCallback) error { p := s.passfile(name) if !strings.HasPrefix(p, s.path) { - return ErrSneaky + return store.ErrSneaky } if s.IsDir(name) { @@ -315,18 +273,18 @@ func (s *Store) SetConfirm(name string, content []byte, reason string, cb Recipi } if err := gpg.Encrypt(p, content, recipients, s.alwaysTrust); err != nil { - return ErrEncrypt + return store.ErrEncrypt } if err := s.gitAdd(p); err != nil { - if err == ErrGitNotInit { + if err == store.ErrGitNotInit { return nil } return err } if err := s.gitCommit(fmt.Sprintf("Save secret to %s: %s", name, reason)); err != nil { - if err == ErrGitNotInit { + if err == store.ErrGitNotInit { return nil } return err @@ -334,13 +292,13 @@ func (s *Store) SetConfirm(name string, content []byte, reason string, cb Recipi if s.autoPush { if err := s.gitPush("", ""); err != nil { - if err == ErrGitNotInit { + if err == store.ErrGitNotInit { msg := "Warning: git is not initialized for this store. Ignoring auto-push option\n" + "Run: gopass git init" fmt.Println(color.RedString(msg)) return nil } - if err == ErrGitNoRemote { + if err == store.ErrGitNoRemote { msg := "Warning: git has not remote. Ignoring auto-push option\n" + "Run: gopass git remote add origin ..." fmt.Println(color.YellowString(msg)) @@ -352,33 +310,6 @@ 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. @@ -478,14 +409,14 @@ func (s *Store) delete(name string, recurse bool) error { rf := os.Remove if !recurse && !fsutil.IsFile(path) { - return ErrNotFound + return store.ErrNotFound } if recurse && !fsutil.IsFile(path) { path = filepath.Join(s.path, name) rf = os.RemoveAll if !fsutil.IsDir(path) { - return ErrNotFound + return store.ErrNotFound } } @@ -494,13 +425,13 @@ func (s *Store) delete(name string, recurse bool) error { } if err := s.gitAdd(path); err != nil { - if err == ErrGitNotInit { + if err == store.ErrGitNotInit { return nil } return err } if err := s.gitCommit(fmt.Sprintf("Remove %s from store.", name)); err != nil { - if err == ErrGitNotInit { + if err == store.ErrGitNotInit { return nil } return err @@ -508,7 +439,7 @@ func (s *Store) delete(name string, recurse bool) error { if s.autoPush { if err := s.gitPush("", ""); err != nil { - if err == ErrGitNotInit || err == ErrGitNoRemote { + if err == store.ErrGitNotInit || err == store.ErrGitNoRemote { return nil } return err diff --git a/store/sub/store_test.go b/store/sub/store_test.go new file mode 100644 index 0000000000..d3f7904ba7 --- /dev/null +++ b/store/sub/store_test.go @@ -0,0 +1,32 @@ +package sub + +import ( + "io/ioutil" + "os" + "path/filepath" + "sort" + "strings" +) + +func createStore(dir string) ([]string, []string, error) { + recipients := []string{ + "0xDEADBEEF", + "0xFEEDBEEF", + } + list := []string{ + "foo/bar/baz", + "baz/ing/a", + } + sort.Strings(list) + for _, file := range list { + filename := filepath.Join(dir, file+".gpg") + if err := os.MkdirAll(filepath.Dir(filename), 0700); err != nil { + return recipients, list, err + } + if err := ioutil.WriteFile(filename, []byte{}, 0644); err != nil { + return recipients, list, err + } + } + err := ioutil.WriteFile(filepath.Join(dir, GPGID), []byte(strings.Join(recipients, "\n")), 0600) + return recipients, list, err +} diff --git a/password/templates.go b/store/sub/templates.go similarity index 62% rename from password/templates.go rename to store/sub/templates.go index 9b784b4362..bed4aed213 100644 --- a/password/templates.go +++ b/store/sub/templates.go @@ -1,15 +1,15 @@ -package password +package sub import ( "fmt" "io/ioutil" "os" "path/filepath" - "sort" "strings" "github.com/justwatchcom/gopass/fsutil" "github.com/justwatchcom/gopass/tree" + "github.com/justwatchcom/gopass/tree/simple" ) const ( @@ -17,20 +17,15 @@ const ( TemplateFile = ".pass-template" ) -// LookupTemplate will lookup and return a template -func (r *RootStore) LookupTemplate(name string) ([]byte, bool) { - store := r.getStore(name) - return store.LookupTemplate(strings.TrimPrefix(name, store.alias)) -} - // LookupTemplate will lookup and return a template func (s *Store) LookupTemplate(name string) ([]byte, bool) { // chop off one path element until we find something for { - if name == "" || name == "/" { + l1 := len(name) + name = filepath.Dir(name) + if len(name) == l1 { break } - name = filepath.Dir(name) tpl := filepath.Join(s.path, name, TemplateFile) if fsutil.IsFile(tpl) { if content, err := ioutil.ReadFile(tpl); err == nil { @@ -41,35 +36,6 @@ func (s *Store) LookupTemplate(name string) ([]byte, bool) { return []byte{}, false } -// TemplateTree returns a tree of all templates -func (r *RootStore) TemplateTree() (*tree.Folder, error) { - root := tree.New("gopass") - mps := r.mountPoints() - sort.Sort(sort.Reverse(byLen(mps))) - for _, alias := range mps { - substore := r.mounts[alias] - if substore == nil { - continue - } - if err := root.AddMount(alias, substore.path); err != nil { - return nil, fmt.Errorf("failed to add mount: %s", err) - } - for _, t := range substore.ListTemplates(alias) { - if err := root.AddFile(t, "gopass/template"); err != nil { - fmt.Println(err) - } - } - } - - for _, t := range r.store.ListTemplates("") { - if err := root.AddFile(t, "gopass/template"); err != nil { - fmt.Println(err) - } - } - - return root, nil -} - func mkTemplateStoreWalkerFunc(alias, folder string, fn func(...string)) func(string, os.FileInfo, error) error { return func(path string, info os.FileInfo, err error) error { if err != nil { @@ -118,50 +84,38 @@ func (s *Store) ListTemplates(prefix string) []string { return lst } +// TemplateTree returns a tree of all templates +func (s *Store) TemplateTree() (tree.Tree, error) { + root := simple.New("gopass") + for _, t := range s.ListTemplates("") { + if err := root.AddFile(t, "gopass/template"); err != nil { + fmt.Println(err) + } + } + + return root, nil +} + // templatefile returns the name of the given template on disk func (s *Store) templatefile(name string) string { return filepath.Join(s.path, name, TemplateFile) } -// HasTemplate returns true if the template exists -func (r *RootStore) HasTemplate(name string) bool { - store := r.getStore(name) - return store.HasTemplate(strings.TrimPrefix(name, store.alias)) -} - // HasTemplate returns true if the template exists func (s *Store) HasTemplate(name string) bool { return fsutil.IsFile(s.templatefile(name)) } -// GetTemplate will return the content of the named template -func (r *RootStore) GetTemplate(name string) ([]byte, error) { - store := r.getStore(name) - return store.GetTemplate(strings.TrimPrefix(name, store.alias)) -} - // GetTemplate will return the content of the named template func (s *Store) GetTemplate(name string) ([]byte, error) { return ioutil.ReadFile(s.templatefile(name)) } -// SetTemplate will (over)write the content to the template file -func (r *RootStore) SetTemplate(name string, content []byte) error { - store := r.getStore(name) - return store.SetTemplate(strings.TrimPrefix(name, store.alias), content) -} - // SetTemplate will (over)write the content to the template file func (s *Store) SetTemplate(name string, content []byte) error { return ioutil.WriteFile(s.templatefile(name), content, 0600) } -// RemoveTemplate will delete the named template if it exists -func (r *RootStore) RemoveTemplate(name string) error { - store := r.getStore(name) - return store.RemoveTemplate(strings.TrimPrefix(name, store.alias)) -} - // RemoveTemplate will delete the named template if it exists func (s *Store) RemoveTemplate(name string) error { t := s.templatefile(name) diff --git a/store/sub/yaml.go b/store/sub/yaml.go new file mode 100644 index 0000000000..af35c71148 --- /dev/null +++ b/store/sub/yaml.go @@ -0,0 +1,81 @@ +package sub + +import ( + "fmt" + + "github.com/justwatchcom/gopass/store" + yaml "gopkg.in/yaml.v2" +) + +// GetKey returns a single key from a structured secret +func (s *Store) GetKey(name, key string) ([]byte, error) { + content, err := s.GetBody(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") +} + +// 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.GetFirstLine(name) + if err != nil { + first = []byte("\n") + } + body, err := s.GetBody(name) + if err != nil && err != store.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) +} + +// DeleteKey will delete a single key in a YAML structured secret +func (s *Store) DeleteKey(name, key string) error { + var err error + first, err := s.GetFirstLine(name) + if err != nil { + first = []byte("\n") + } + body, err := s.GetBody(name) + if err != nil && err != store.ErrNotFound { + return err + } + + d := make(map[string]string) + if err := yaml.Unmarshal(body, &d); err != nil { + return err + } + + delete(d, key) + + buf, err := yaml.Marshal(d) + if err != nil { + return err + } + + return s.SetConfirm(name, append(first, buf...), fmt.Sprintf("Deleted key %s in %s", key, name), nil) +} diff --git a/tree/file.go b/tree/simple/file.go similarity index 98% rename from tree/file.go rename to tree/simple/file.go index e921457eb7..4349650c86 100644 --- a/tree/file.go +++ b/tree/simple/file.go @@ -1,4 +1,4 @@ -package tree +package simple // File is a leaf node in the tree type File struct { diff --git a/tree/folder.go b/tree/simple/folder.go similarity index 95% rename from tree/folder.go rename to tree/simple/folder.go index 7d30d0ab8a..64c5ee97d7 100644 --- a/tree/folder.go +++ b/tree/simple/folder.go @@ -1,10 +1,12 @@ -package tree +package simple import ( "bytes" "fmt" "path/filepath" "strings" + + "github.com/justwatchcom/gopass/tree" ) // Folder is intermediate tree node @@ -155,7 +157,7 @@ func (f *Folder) getFolder(name string) *Folder { } // FindFolder returns a sub-tree or nil, if the subtree does not exist -func (f *Folder) FindFolder(name string) *Folder { +func (f *Folder) FindFolder(name string) tree.Tree { return f.findFolder(strings.Split(strings.TrimSuffix(name, "/"), "/")) } @@ -220,3 +222,13 @@ func (f *Folder) addTemplate(path []string) error { } return f.getFolder(name).addTemplate(path[1:]) } + +// SetRoot sets the root flag of this folder +func (f *Folder) SetRoot(on bool) { + f.Root = on +} + +// SetName sets the name of this folder +func (f *Folder) SetName(name string) { + f.Name = name +} diff --git a/tree/simple/tree.go b/tree/simple/tree.go new file mode 100644 index 0000000000..fdccc63086 --- /dev/null +++ b/tree/simple/tree.go @@ -0,0 +1,47 @@ +package simple + +import ( + "sort" + + "github.com/fatih/color" +) + +const ( + symEmpty = " " + symBranch = "├── " + symLeaf = "└── " + symVert = "│ " +) + +var ( + colMount = color.New(color.FgRed, color.Bold).SprintfFunc() + colDir = color.New(color.FgBlue, color.Bold).SprintfFunc() + colTpl = color.New(color.FgGreen, color.Bold).SprintfFunc() + colBin = color.New(color.FgYellow, color.Bold).SprintfFunc() + colYaml = color.New(color.FgCyan, color.Bold).SprintfFunc() +) + +// New create a new root folder +func New(name string) *Folder { + f := newFolder(name) + f.Root = true + return f +} + +func sortedFolders(m map[string]*Folder) []string { + keys := make([]string, 0, len(m)) + for k := range m { + keys = append(keys, k) + } + sort.Strings(keys) + return keys +} + +func sortedFiles(m map[string]*File) []string { + keys := make([]string, 0, len(m)) + for k := range m { + keys = append(keys, k) + } + sort.Strings(keys) + return keys +} diff --git a/tree/tree_test.go b/tree/simple/tree_test.go similarity index 95% rename from tree/tree_test.go rename to tree/simple/tree_test.go index e361552511..696c8aa92f 100644 --- a/tree/tree_test.go +++ b/tree/simple/tree_test.go @@ -1,4 +1,4 @@ -package tree +package simple import ( "sort" @@ -62,7 +62,7 @@ func TestFormat(t *testing.T) { got := strings.TrimSpace(root.Format(0)) want := strings.TrimSpace(goldenFormat) if want != got { - t.Errorf("Format mismatch: %s vs %s", want, got) + t.Errorf("Format mismatch:\n---\n%x\n---\n%x\n---", want, got) } } diff --git a/tree/tree.go b/tree/tree.go index 6f0eba3690..3e27b155b2 100644 --- a/tree/tree.go +++ b/tree/tree.go @@ -1,47 +1,14 @@ package tree -import ( - "sort" - - "github.com/fatih/color" -) - -const ( - symEmpty = " " - symBranch = "├── " - symLeaf = "└── " - symVert = "│ " -) - -var ( - colMount = color.New(color.FgRed, color.Bold).SprintfFunc() - colDir = color.New(color.FgBlue, color.Bold).SprintfFunc() - colTpl = color.New(color.FgGreen, color.Bold).SprintfFunc() - colBin = color.New(color.FgYellow, color.Bold).SprintfFunc() - colYaml = color.New(color.FgCyan, color.Bold).SprintfFunc() -) - -// New create a new root folder -func New(name string) *Folder { - f := newFolder(name) - f.Root = true - return f -} - -func sortedFolders(m map[string]*Folder) []string { - keys := make([]string, 0, len(m)) - for k := range m { - keys = append(keys, k) - } - sort.Strings(keys) - return keys -} - -func sortedFiles(m map[string]*File) []string { - keys := make([]string, 0, len(m)) - for k := range m { - keys = append(keys, k) - } - sort.Strings(keys) - return keys +// Tree is tree-like object supporting pretty printing +type Tree interface { + List(int) []string + Format(int) string + String() string + AddFile(string, string) error + AddMount(string, string) error + AddTemplate(string) error + FindFolder(string) Tree + SetRoot(bool) + SetName(string) }