From 09299f8e7dc022234d99ae585a29c258f77115ec Mon Sep 17 00:00:00 2001 From: Dominik Schulz Date: Tue, 27 Mar 2018 17:11:03 +0200 Subject: [PATCH] Refactor action package (#731) --- README.md | 2 +- commands.go | 44 ++--- commands_test.go | 5 + pkg/action/audit.go | 4 +- pkg/action/binary.go | 238 ------------------------- pkg/action/binary/binary.go | 19 ++ pkg/action/{ => binary}/binary_test.go | 111 +++++++----- pkg/action/binary/cat.go | 57 ++++++ pkg/action/binary/copy.go | 172 ++++++++++++++++++ pkg/action/binary/sum.go | 34 ++++ pkg/action/clone.go | 2 + pkg/action/copy.go | 2 +- pkg/action/{ => create}/create.go | 115 +++++++----- pkg/action/{ => create}/create_test.go | 68 +++---- pkg/action/delete.go | 2 + pkg/action/edit.go | 4 + pkg/action/find.go | 11 +- pkg/action/find_test.go | 4 +- pkg/action/fsck.go | 1 + pkg/action/generate.go | 8 +- pkg/action/git.go | 25 ++- pkg/action/grep.go | 11 +- pkg/action/history.go | 3 +- pkg/action/list.go | 8 +- pkg/action/otp.go | 3 +- pkg/action/show.go | 9 + pkg/action/sync.go | 1 + pkg/action/update.go | 2 +- pkg/agent/doc.go | 8 + pkg/audit/audit.go | 1 + tests/copy_test.go | 4 +- tests/find_test.go | 2 +- tests/mockstore/store.go | 194 ++++++++++++++++++++ 33 files changed, 738 insertions(+), 436 deletions(-) delete mode 100644 pkg/action/binary.go create mode 100644 pkg/action/binary/binary.go rename pkg/action/{ => binary}/binary_test.go (61%) create mode 100644 pkg/action/binary/cat.go create mode 100644 pkg/action/binary/copy.go create mode 100644 pkg/action/binary/sum.go rename pkg/action/{ => create}/create.go (72%) rename pkg/action/{ => create}/create_test.go (78%) create mode 100644 pkg/agent/doc.go create mode 100644 tests/mockstore/store.go diff --git a/README.md b/README.md index 474b62dce9..b8c1e66cd0 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ The slightly more awesome Standard Unix Password Manager for Teams. Written in G ## Demo -[![asciicast](https://asciinema.org/a/101688.png)](https://asciinema.org/a/101688) +[![asciicast](https://asciinema.org/a/172749.png)](https://asciinema.org/a/172749) ## Features diff --git a/commands.go b/commands.go index 01943b2dca..26d63d8de6 100644 --- a/commands.go +++ b/commands.go @@ -5,6 +5,8 @@ import ( "fmt" ap "github.com/justwatchcom/gopass/pkg/action" + "github.com/justwatchcom/gopass/pkg/action/binary" + "github.com/justwatchcom/gopass/pkg/action/create" "github.com/justwatchcom/gopass/pkg/action/xc" "github.com/justwatchcom/gopass/pkg/agent" "github.com/justwatchcom/gopass/pkg/agent/client" @@ -73,10 +75,10 @@ func getCommands(ctx context.Context, action *ap.Action, app *cli.App) []cli.Com Description: "" + "This command will decrypt all secrets and check the passwords against the public " + "havibeenpwned.com v2 API or Dumps. " + - "To use the dumps you need to download the dumps from https://haveibeenpwned.com/passwords first. This is a very expensive operation for advanced users." + - "Most users should probably use the API." + - "If you want to use the dumps you need to use 7z to extract the dump: 7z x pwned-passwords-2.0.txt.7z." + - "To speed up processing you should sort them and use the --sorted flag: cat pwned-passwords-2.0.txt | LANG=C sort -S 10G --parallel=4 | gzip --fast > pwned-passwords-2.0.txt.gz", + "To use the dumps you need to download the dumps from https://haveibeenpwned.com/passwords first. Be sure to grap the one that says '(ordered by hash)'. " + + "This is a very expensive operation for advanced users. " + + "Most users should probably use the API. " + + "If you want to use the dumps you need to use 7z to extract the dump: 7z x pwned-passwords-ordered-2.0.txt.7z.", Before: func(c *cli.Context) error { return action.Initialized(withGlobalFlags(ctx, c), c) }, Action: func(c *cli.Context) error { return action.HIBP(withGlobalFlags(ctx, c), c) @@ -115,7 +117,7 @@ func getCommands(ctx context.Context, action *ap.Action, app *cli.App) []cli.Com "to a secret.", Before: func(c *cli.Context) error { return action.Initialized(withGlobalFlags(ctx, c), c) }, Action: func(c *cli.Context) error { - return action.BinaryCat(withGlobalFlags(ctx, c), c) + return binary.Cat(withGlobalFlags(ctx, c), c, action.Store) }, BashComplete: func(c *cli.Context) { action.Complete(ctx, c) }, }, @@ -129,7 +131,7 @@ func getCommands(ctx context.Context, action *ap.Action, app *cli.App) []cli.Com Aliases: []string{"sha", "sha256"}, Before: func(c *cli.Context) error { return action.Initialized(withGlobalFlags(ctx, c), c) }, Action: func(c *cli.Context) error { - return action.BinarySum(withGlobalFlags(ctx, c), c) + return binary.Sum(withGlobalFlags(ctx, c), c, action.Store) }, BashComplete: func(c *cli.Context) { action.Complete(ctx, c) }, }, @@ -145,7 +147,7 @@ func getCommands(ctx context.Context, action *ap.Action, app *cli.App) []cli.Com Before: func(c *cli.Context) error { return action.Initialized(withGlobalFlags(ctx, c), c) }, Aliases: []string{"cp"}, Action: func(c *cli.Context) error { - return action.BinaryCopy(withGlobalFlags(ctx, c), c) + return binary.Copy(withGlobalFlags(ctx, c), c, action.Store) }, BashComplete: func(c *cli.Context) { action.Complete(ctx, c) }, Flags: []cli.Flag{ @@ -169,7 +171,7 @@ func getCommands(ctx context.Context, action *ap.Action, app *cli.App) []cli.Com Before: func(c *cli.Context) error { return action.Initialized(withGlobalFlags(ctx, c), c) }, Aliases: []string{"mv"}, Action: func(c *cli.Context) error { - return action.BinaryMove(withGlobalFlags(ctx, c), c) + return binary.Move(withGlobalFlags(ctx, c), c, action.Store) }, BashComplete: func(c *cli.Context) { action.Complete(ctx, c) }, Flags: []cli.Flag{ @@ -283,7 +285,7 @@ func getCommands(ctx context.Context, action *ap.Action, app *cli.App) []cli.Com "This command starts a wizard to aid in creation of new secrets.", Before: func(c *cli.Context) error { return action.Initialized(withGlobalFlags(ctx, c), c) }, Action: func(c *cli.Context) error { - return action.Create(withGlobalFlags(ctx, c), c) + return create.Create(withGlobalFlags(ctx, c), c, action.Store) }, }, { @@ -539,14 +541,6 @@ func getCommands(ctx context.Context, action *ap.Action, app *cli.App) []cli.Com Name: "store", Usage: "Store to operate on", }, - cli.StringFlag{ - Name: "remote", - Usage: "Git remote to add", - }, - cli.StringFlag{ - Name: "url", - Usage: "Git URL", - }, }, }, }, @@ -564,14 +558,6 @@ func getCommands(ctx context.Context, action *ap.Action, app *cli.App) []cli.Com Name: "store", Usage: "Store to operate on", }, - cli.StringFlag{ - Name: "origin", - Usage: "Git Origin to push to", - }, - cli.StringFlag{ - Name: "branch", - Usage: "Git branch to push", - }, }, }, { @@ -587,14 +573,6 @@ func getCommands(ctx context.Context, action *ap.Action, app *cli.App) []cli.Com Name: "store", Usage: "Store to operate on", }, - cli.StringFlag{ - Name: "origin", - Usage: "Git Origin to push to", - }, - cli.StringFlag{ - Name: "branch", - Usage: "Git branch to push", - }, }, }, }, diff --git a/commands_test.go b/commands_test.go index 23d245ebf1..2f5f0fc4b6 100644 --- a/commands_test.go +++ b/commands_test.go @@ -20,6 +20,8 @@ import ( "github.com/urfave/cli" ) +// commandsWithError is a list of commands that return an error when +// invoked without arguments var commandsWithError = map[string]struct{}{ ".audit": {}, ".audit.hibp": {}, @@ -35,6 +37,9 @@ var commandsWithError = map[string]struct{}{ ".find": {}, ".generate": {}, ".grep": {}, + ".git.remote.add": {}, + ".git.push": {}, + ".git.pull": {}, ".history": {}, ".init": {}, ".insert": {}, diff --git a/pkg/action/audit.go b/pkg/action/audit.go index fd9bbc22e5..970ae8c0ce 100644 --- a/pkg/action/audit.go +++ b/pkg/action/audit.go @@ -12,6 +12,8 @@ import ( func (s *Action) Audit(ctx context.Context, c *cli.Context) error { filter := c.Args().First() + out.Print(ctx, "Auditing passwords for common flaws ...") + t, err := s.Store.Tree(ctx) if err != nil { return ExitError(ctx, ExitList, err, "failed to get store tree: %s", err) @@ -19,7 +21,7 @@ func (s *Action) Audit(ctx context.Context, c *cli.Context) error { if filter != "" { subtree, err := t.FindFolder(filter) if err != nil { - return err + return ExitError(ctx, ExitUnknown, err, "failed to find subtree: %s", err) } t = subtree } diff --git a/pkg/action/binary.go b/pkg/action/binary.go deleted file mode 100644 index 7e981c6925..0000000000 --- a/pkg/action/binary.go +++ /dev/null @@ -1,238 +0,0 @@ -package action - -import ( - "bytes" - "context" - "crypto/sha256" - "encoding/base64" - "fmt" - "io" - "io/ioutil" - "os" - "strings" - - "github.com/justwatchcom/gopass/pkg/fsutil" - "github.com/justwatchcom/gopass/pkg/out" - "github.com/justwatchcom/gopass/pkg/store/secret" - "github.com/justwatchcom/gopass/pkg/store/sub" - "github.com/pkg/errors" - "github.com/urfave/cli" -) - -const ( - // BinarySuffix is the suffix that is appended to binaries in the store - BinarySuffix = ".b64" -) - -// BinaryCat prints to or reads from STDIN/STDOUT -func (s *Action) BinaryCat(ctx context.Context, c *cli.Context) error { - name := c.Args().First() - if name == "" { - return ExitError(ctx, ExitNoName, nil, "need a name") - } - if !strings.HasSuffix(name, BinarySuffix) { - name += BinarySuffix - } - - // handle pipe to stdin - info, err := os.Stdin.Stat() - if err != nil { - return ExitError(ctx, ExitIO, err, "failed to stat stdin: %s", err) - } - - // if content is piped to stdin, read and save it - if info.Mode()&os.ModeCharDevice == 0 { - content := &bytes.Buffer{} - - if written, err := io.Copy(content, os.Stdin); err != nil { - return ExitError(ctx, ExitIO, err, "Failed to copy after %d bytes: %s", written, err) - } - - return s.Store.Set( - sub.WithReason(ctx, "Read secret from STDIN"), - name, - secret.New("", base64.StdEncoding.EncodeToString(content.Bytes())), - ) - } - - buf, err := s.binaryGet(ctx, name) - if err != nil { - return ExitError(ctx, ExitDecrypt, err, "failed to read secret: %s", err) - } - - out.Yellow(ctx, string(buf)) - return nil -} - -// BinarySum decodes binary content and computes the SHA256 checksum -func (s *Action) BinarySum(ctx context.Context, c *cli.Context) error { - name := c.Args().First() - if name == "" { - return ExitError(ctx, ExitUsage, nil, "Usage: %s binary sha256 name", s.Name) - } - if !strings.HasSuffix(name, BinarySuffix) { - name += BinarySuffix - } - - buf, err := s.binaryGet(ctx, name) - if err != nil { - return ExitError(ctx, ExitDecrypt, err, "failed to read secret: %s", err) - } - - h := sha256.New() - _, _ = h.Write(buf) - out.Yellow(ctx, "%x", h.Sum(nil)) - - return nil -} - -// BinaryCopy copies either from the filesystem to the store or from the store -// to the filesystem -func (s *Action) BinaryCopy(ctx context.Context, c *cli.Context) error { - from := c.Args().Get(0) - to := c.Args().Get(1) - - if err := s.binaryCopy(ctx, from, to, false); err != nil { - return ExitError(ctx, ExitUnknown, err, "%s", err) - } - return nil -} - -// BinaryMove works like BinaryCopy but will remove (shred/wipe) the source -// after a successful copy. Mostly useful for securely moving secrets into -// the store if they are no longer needed / wanted on disk afterwards -func (s *Action) BinaryMove(ctx context.Context, c *cli.Context) error { - from := c.Args().Get(0) - to := c.Args().Get(1) - - if err := s.binaryCopy(ctx, from, to, true); err != nil { - return ExitError(ctx, ExitUnknown, err, "%s", err) - } - return nil -} - -// binaryCopy implements the control flow for copy and move. We support two -// workflows: -// 1. From the filesystem to the store -// 2. From the store to the filesystem -// -// Copying secrets in the store must be done through the regular copy command -func (s *Action) binaryCopy(ctx context.Context, from, to string, deleteSource bool) error { - if from == "" || to == "" { - op := "copy" - if deleteSource { - op = "move" - } - return errors.Errorf("Usage: %s binary %s from to", s.Name, op) - } - switch { - case fsutil.IsFile(from) && fsutil.IsFile(to): - // copying from on file to another file is not supported - return errors.New("ambiquity detected. Only from or to can be a file") - case s.Store.Exists(ctx, from+BinarySuffix) && s.Store.Exists(ctx, to+BinarySuffix): - // copying from one secret to another secret is not supported - return errors.New("ambiquity detected. Either from or to must be a file") - case fsutil.IsFile(from) && !fsutil.IsFile(to): - return s.binaryCopyFromFileToStore(ctx, from, to, deleteSource) - case !fsutil.IsFile(from): - return s.binaryCopyFromStoreToFile(ctx, from, to, deleteSource) - default: - return errors.Errorf("ambiquity detected. Unhandled case. Please report a bug") - } -} -func (s *Action) binaryCopyFromFileToStore(ctx context.Context, from, to string, deleteSource bool) error { - // if the source is a file the destination must no to avoid ambiquities - // if necessary this can be resolved by using a absolute path for the file - // and a relative one for the secret - if !strings.HasSuffix(to, BinarySuffix) { - to += BinarySuffix - } - - // copy from FS to store - buf, err := ioutil.ReadFile(from) - if err != nil { - return errors.Wrapf(err, "failed to read file from '%s'", from) - } - - if err := s.Store.Set(sub.WithReason(ctx, fmt.Sprintf("Copied data from %s to %s", from, to)), to, secret.New("", base64.StdEncoding.EncodeToString(buf))); err != nil { - return errors.Wrapf(err, "failed to save buffer to store") - } - - if !deleteSource { - return nil - } - - // it's important that we return if the validation fails, because - // in that case we don't want to shred our (only) copy of this data! - if err := s.binaryValidate(ctx, buf, to); err != nil { - return errors.Wrapf(err, "failed to validate written data") - } - if err := fsutil.Shred(from, 8); err != nil { - return errors.Wrapf(err, "failed to shred data") - } - return nil -} - -func (s *Action) binaryCopyFromStoreToFile(ctx context.Context, from, to string, deleteSource bool) error { - // if the source is no file we assume it's a secret and to is a filename - // (which may already exist or not) - if !strings.HasSuffix(from, BinarySuffix) { - from += BinarySuffix - } - - // copy from store to FS - buf, err := s.binaryGet(ctx, from) - if err != nil { - return errors.Wrapf(err, "failed to read data from '%s'", from) - } - if err := ioutil.WriteFile(to, buf, 0600); err != nil { - return errors.Wrapf(err, "failed to write data to '%s'", to) - } - - if !deleteSource { - return nil - } - - // as before: if validation of the written data fails, we MUST NOT - // delete the (only) source - if err := s.binaryValidate(ctx, buf, from); err != nil { - return errors.Wrapf(err, "failed to validate the written data") - } - if err := s.Store.Delete(ctx, from); err != nil { - return errors.Wrapf(err, "failed to delete '%s' from the store", from) - } - return nil -} - -func (s *Action) binaryValidate(ctx context.Context, buf []byte, name string) error { - h := sha256.New() - _, _ = h.Write(buf) - fileSum := fmt.Sprintf("%x", h.Sum(nil)) - - h.Reset() - - var err error - buf, err = s.binaryGet(ctx, name) - if err != nil { - return errors.Wrapf(err, "failed to read '%s' from the store", name) - } - _, _ = h.Write(buf) - storeSum := fmt.Sprintf("%x", h.Sum(nil)) - - if fileSum != storeSum { - return errors.Errorf("Hashsum mismatch (file: %s, store: %s)", fileSum, storeSum) - } - return nil -} - -func (s *Action) binaryGet(ctx context.Context, name string) ([]byte, error) { - sec, err := s.Store.Get(ctx, name) - if err != nil { - return nil, errors.Wrapf(err, "failed to read '%s' from the store", name) - } - buf, err := base64.StdEncoding.DecodeString(sec.Body()) - if err != nil { - return nil, errors.Wrapf(err, "failed to encode to base64") - } - return buf, nil -} diff --git a/pkg/action/binary/binary.go b/pkg/action/binary/binary.go new file mode 100644 index 0000000000..cb3eb71bb0 --- /dev/null +++ b/pkg/action/binary/binary.go @@ -0,0 +1,19 @@ +package binary + +import ( + "context" + + "github.com/justwatchcom/gopass/pkg/store" +) + +const ( + // Suffix is the suffix that is appended to binaries in the store + Suffix = ".b64" +) + +type storer interface { + Get(context.Context, string) (store.Secret, error) + Set(context.Context, string, store.Secret) error + Exists(context.Context, string) bool + Delete(context.Context, string) error +} diff --git a/pkg/action/binary_test.go b/pkg/action/binary/binary_test.go similarity index 61% rename from pkg/action/binary_test.go rename to pkg/action/binary/binary_test.go index 75313c3de3..21457a860e 100644 --- a/pkg/action/binary_test.go +++ b/pkg/action/binary/binary_test.go @@ -1,4 +1,4 @@ -package action +package binary import ( "bytes" @@ -11,22 +11,27 @@ import ( "github.com/justwatchcom/gopass/pkg/ctxutil" "github.com/justwatchcom/gopass/pkg/out" - "github.com/justwatchcom/gopass/tests/gptest" + "github.com/justwatchcom/gopass/tests/mockstore" "github.com/stretchr/testify/assert" "github.com/urfave/cli" ) func TestBinary(t *testing.T) { - u := gptest.NewUnitTester(t) - defer u.Remove() + tempdir, err := ioutil.TempDir("", "gopass-") + assert.NoError(t, err) + defer func() { + _ = os.RemoveAll(tempdir) + }() + + store := mockstore.New("") ctx := context.Background() ctx = ctxutil.WithAlwaysYes(ctx, true) ctx = out.WithHidden(ctx, true) - act, err := newMock(ctx, u) - assert.NoError(t, err) app := cli.NewApp() + fs := flag.NewFlagSet("default", flag.ContinueOnError) + c := cli.NewContext(app, fs, nil) buf := &bytes.Buffer{} out.Stdout = buf @@ -34,30 +39,32 @@ func TestBinary(t *testing.T) { out.Stdout = os.Stdout }() - infile := filepath.Join(u.Dir, "input.txt") + infile := filepath.Join(tempdir, "input.txt") assert.NoError(t, ioutil.WriteFile(infile, []byte("0xDEADBEEF"), 0644)) - assert.NoError(t, act.binaryCopy(ctx, infile, "bar", true)) + assert.NoError(t, binaryCopy(ctx, c, infile, "bar", true, store)) - // no arg - fs := flag.NewFlagSet("default", flag.ContinueOnError) - c := cli.NewContext(app, fs, nil) - assert.Error(t, act.BinaryCat(ctx, c)) - assert.Error(t, act.BinaryCopy(ctx, c)) - assert.Error(t, act.BinaryMove(ctx, c)) - assert.Error(t, act.BinarySum(ctx, c)) + assert.Error(t, Cat(ctx, c, store)) + assert.Error(t, Copy(ctx, c, store)) + assert.Error(t, Move(ctx, c, store)) + assert.Error(t, Sum(ctx, c, store)) } func TestBinaryCat(t *testing.T) { - u := gptest.NewUnitTester(t) - defer u.Remove() + tempdir, err := ioutil.TempDir("", "gopass-") + assert.NoError(t, err) + defer func() { + _ = os.RemoveAll(tempdir) + }() + + store := mockstore.New("") ctx := context.Background() ctx = ctxutil.WithAlwaysYes(ctx, true) ctx = out.WithHidden(ctx, true) - act, err := newMock(ctx, u) - assert.NoError(t, err) app := cli.NewApp() + fs := flag.NewFlagSet("default", flag.ContinueOnError) + c := cli.NewContext(app, fs, nil) buf := &bytes.Buffer{} out.Stdout = buf @@ -65,79 +72,87 @@ func TestBinaryCat(t *testing.T) { out.Stdout = os.Stdout }() - infile := filepath.Join(u.Dir, "input.txt") + infile := filepath.Join(tempdir, "input.txt") assert.NoError(t, ioutil.WriteFile(infile, []byte("0xDEADBEEF"), 0644)) - assert.NoError(t, act.binaryCopy(ctx, infile, "bar", true)) + assert.NoError(t, binaryCopy(ctx, c, infile, "bar", true, store)) // binary cat bar - fs := flag.NewFlagSet("default", flag.ContinueOnError) + fs = flag.NewFlagSet("default", flag.ContinueOnError) assert.NoError(t, fs.Parse([]string{"bar"})) - c := cli.NewContext(app, fs, nil) - assert.NoError(t, act.BinaryCat(ctx, c)) + c = cli.NewContext(app, fs, nil) + assert.NoError(t, Cat(ctx, c, store)) } func TestBinaryCopy(t *testing.T) { - u := gptest.NewUnitTester(t) - defer u.Remove() + tempdir, err := ioutil.TempDir("", "gopass-") + assert.NoError(t, err) + defer func() { + _ = os.RemoveAll(tempdir) + }() + + store := mockstore.New("") ctx := context.Background() ctx = ctxutil.WithAlwaysYes(ctx, true) ctx = out.WithHidden(ctx, true) - act, err := newMock(ctx, u) - assert.NoError(t, err) app := cli.NewApp() + fs := flag.NewFlagSet("default", flag.ContinueOnError) + c := cli.NewContext(app, fs, nil) buf := &bytes.Buffer{} out.Stdout = buf - stdout = buf defer func() { out.Stdout = os.Stdout - stdout = os.Stdout }() - infile := filepath.Join(u.Dir, "input.txt") + infile := filepath.Join(tempdir, "input.txt") assert.NoError(t, ioutil.WriteFile(infile, []byte("0xDEADBEEF"), 0644)) - assert.NoError(t, act.binaryCopy(ctx, infile, "bar", true)) + assert.NoError(t, binaryCopy(ctx, c, infile, "bar", true, store)) - outfile := filepath.Join(u.Dir, "output.txt") + outfile := filepath.Join(tempdir, "output.txt") // binary copy bar tempdir/bar - fs := flag.NewFlagSet("default", flag.ContinueOnError) + fs = flag.NewFlagSet("default", flag.ContinueOnError) assert.NoError(t, fs.Parse([]string{"bar", outfile})) - c := cli.NewContext(app, fs, nil) - assert.NoError(t, act.BinaryCopy(ctx, c)) + c = cli.NewContext(app, fs, nil) + assert.NoError(t, Copy(ctx, c, store)) // binary copy tempdir/bar tempdir/bar fs = flag.NewFlagSet("default", flag.ContinueOnError) assert.NoError(t, fs.Parse([]string{outfile, outfile})) c = cli.NewContext(app, fs, nil) - assert.Error(t, act.BinaryCopy(ctx, c)) + assert.Error(t, Copy(ctx, c, store)) // binary copy bar bar fs = flag.NewFlagSet("default", flag.ContinueOnError) assert.NoError(t, fs.Parse([]string{"bar", "bar"})) c = cli.NewContext(app, fs, nil) - assert.Error(t, act.BinaryCopy(ctx, c)) + assert.Error(t, Copy(ctx, c, store)) // binary move tempdir/bar bar2 fs = flag.NewFlagSet("default", flag.ContinueOnError) assert.NoError(t, fs.Parse([]string{outfile, "bar2"})) c = cli.NewContext(app, fs, nil) - assert.NoError(t, act.BinaryMove(ctx, c)) + assert.NoError(t, Move(ctx, c, store)) } func TestBinarySum(t *testing.T) { - u := gptest.NewUnitTester(t) - defer u.Remove() + tempdir, err := ioutil.TempDir("", "gopass-") + assert.NoError(t, err) + defer func() { + _ = os.RemoveAll(tempdir) + }() + + store := mockstore.New("") ctx := context.Background() ctx = ctxutil.WithAlwaysYes(ctx, true) ctx = out.WithHidden(ctx, true) - act, err := newMock(ctx, u) - assert.NoError(t, err) app := cli.NewApp() + fs := flag.NewFlagSet("default", flag.ContinueOnError) + c := cli.NewContext(app, fs, nil) buf := &bytes.Buffer{} out.Stdout = buf @@ -145,13 +160,13 @@ func TestBinarySum(t *testing.T) { out.Stdout = os.Stdout }() - infile := filepath.Join(u.Dir, "input.txt") + infile := filepath.Join(tempdir, "input.txt") assert.NoError(t, ioutil.WriteFile(infile, []byte("0xDEADBEEF"), 0644)) - assert.NoError(t, act.binaryCopy(ctx, infile, "bar", true)) + assert.NoError(t, binaryCopy(ctx, c, infile, "bar", true, store)) // binary sum bar - fs := flag.NewFlagSet("default", flag.ContinueOnError) + fs = flag.NewFlagSet("default", flag.ContinueOnError) assert.NoError(t, fs.Parse([]string{"bar"})) - c := cli.NewContext(app, fs, nil) - assert.NoError(t, act.BinarySum(ctx, c)) + c = cli.NewContext(app, fs, nil) + assert.NoError(t, Sum(ctx, c, store)) } diff --git a/pkg/action/binary/cat.go b/pkg/action/binary/cat.go new file mode 100644 index 0000000000..bccfc5f4eb --- /dev/null +++ b/pkg/action/binary/cat.go @@ -0,0 +1,57 @@ +package binary + +import ( + "bytes" + "context" + "encoding/base64" + "io" + "os" + "strings" + + "github.com/justwatchcom/gopass/pkg/action" + "github.com/justwatchcom/gopass/pkg/out" + "github.com/justwatchcom/gopass/pkg/store/secret" + "github.com/justwatchcom/gopass/pkg/store/sub" + "github.com/urfave/cli" +) + +// Cat prints to or reads from STDIN/STDOUT +func Cat(ctx context.Context, c *cli.Context, store storer) error { + name := c.Args().First() + if name == "" { + return action.ExitError(ctx, action.ExitNoName, nil, "Usage: %s binary cat ", c.App.Name) + } + + if !strings.HasSuffix(name, Suffix) { + name += Suffix + } + + // handle pipe to stdin + info, err := os.Stdin.Stat() + if err != nil { + return action.ExitError(ctx, action.ExitIO, err, "failed to stat stdin: %s", err) + } + + // if content is piped to stdin, read and save it + if info.Mode()&os.ModeCharDevice == 0 { + content := &bytes.Buffer{} + + if written, err := io.Copy(content, os.Stdin); err != nil { + return action.ExitError(ctx, action.ExitIO, err, "Failed to copy after %d bytes: %s", written, err) + } + + return store.Set( + sub.WithReason(ctx, "Read secret from STDIN"), + name, + secret.New("", base64.StdEncoding.EncodeToString(content.Bytes())), + ) + } + + buf, err := binaryGet(ctx, name, store) + if err != nil { + return action.ExitError(ctx, action.ExitDecrypt, err, "failed to read secret: %s", err) + } + + out.Yellow(ctx, string(buf)) + return nil +} diff --git a/pkg/action/binary/copy.go b/pkg/action/binary/copy.go new file mode 100644 index 0000000000..241c4bc9e8 --- /dev/null +++ b/pkg/action/binary/copy.go @@ -0,0 +1,172 @@ +package binary + +import ( + "context" + "crypto/sha256" + "encoding/base64" + "fmt" + "io/ioutil" + "strings" + + "github.com/justwatchcom/gopass/pkg/action" + "github.com/justwatchcom/gopass/pkg/fsutil" + "github.com/justwatchcom/gopass/pkg/store/secret" + "github.com/justwatchcom/gopass/pkg/store/sub" + "github.com/pkg/errors" + "github.com/urfave/cli" +) + +// Copy copies either from the filesystem to the store or from the store +// to the filesystem +func Copy(ctx context.Context, c *cli.Context, store storer) error { + from := c.Args().Get(0) + to := c.Args().Get(1) + + // argument checking is in s.binaryCopy + if err := binaryCopy(ctx, c, from, to, false, store); err != nil { + return action.ExitError(ctx, action.ExitUnknown, err, "%s", err) + } + return nil +} + +// Move works like Copy but will remove (shred/wipe) the source +// after a successful copy. Mostly useful for securely moving secrets into +// the store if they are no longer needed / wanted on disk afterwards +func Move(ctx context.Context, c *cli.Context, store storer) error { + from := c.Args().Get(0) + to := c.Args().Get(1) + + // argument checking is in s.binaryCopy + if err := binaryCopy(ctx, c, from, to, true, store); err != nil { + return action.ExitError(ctx, action.ExitUnknown, err, "%s", err) + } + return nil +} + +// binaryCopy implements the control flow for copy and move. We support two +// workflows: +// 1. From the filesystem to the store +// 2. From the store to the filesystem +// +// Copying secrets in the store must be done through the regular copy command +func binaryCopy(ctx context.Context, c *cli.Context, from, to string, deleteSource bool, store storer) error { + if from == "" || to == "" { + op := "copy" + if deleteSource { + op = "move" + } + return errors.Errorf("Usage: %s %s from to", c.App.Name, op) + } + + switch { + case fsutil.IsFile(from) && fsutil.IsFile(to): + // copying from on file to another file is not supported + return errors.New("ambiquity detected. Only from or to can be a file") + case store.Exists(ctx, from+Suffix) && store.Exists(ctx, to+Suffix): + // copying from one secret to another secret is not supported + return errors.New("ambiquity detected. Either from or to must be a file") + case fsutil.IsFile(from) && !fsutil.IsFile(to): + return binaryCopyFromFileToStore(ctx, from, to, deleteSource, store) + case !fsutil.IsFile(from): + return binaryCopyFromStoreToFile(ctx, from, to, deleteSource, store) + default: + return errors.Errorf("ambiquity detected. Unhandled case. Please report a bug") + } +} + +func binaryCopyFromFileToStore(ctx context.Context, from, to string, deleteSource bool, store storer) error { + // if the source is a file the destination must no to avoid ambiquities + // if necessary this can be resolved by using a absolute path for the file + // and a relative one for the secret + if !strings.HasSuffix(to, Suffix) { + to += Suffix + } + + // copy from FS to store + buf, err := ioutil.ReadFile(from) + if err != nil { + return errors.Wrapf(err, "failed to read file from '%s'", from) + } + + if err := store.Set(sub.WithReason(ctx, fmt.Sprintf("Copied data from %s to %s", from, to)), to, secret.New("", base64.StdEncoding.EncodeToString(buf))); err != nil { + return errors.Wrapf(err, "failed to save buffer to store") + } + + if !deleteSource { + return nil + } + + // it's important that we return if the validation fails, because + // in that case we don't want to shred our (only) copy of this data! + if err := binaryValidate(ctx, buf, to, store); err != nil { + return errors.Wrapf(err, "failed to validate written data") + } + if err := fsutil.Shred(from, 8); err != nil { + return errors.Wrapf(err, "failed to shred data") + } + return nil +} + +func binaryCopyFromStoreToFile(ctx context.Context, from, to string, deleteSource bool, store storer) error { + // if the source is no file we assume it's a secret and to is a filename + // (which may already exist or not) + if !strings.HasSuffix(from, Suffix) { + from += Suffix + } + + // copy from store to FS + buf, err := binaryGet(ctx, from, store) + if err != nil { + return errors.Wrapf(err, "failed to read data from '%s'", from) + } + if err := ioutil.WriteFile(to, buf, 0600); err != nil { + return errors.Wrapf(err, "failed to write data to '%s'", to) + } + + if !deleteSource { + return nil + } + + // as before: if validation of the written data fails, we MUST NOT + // delete the (only) source + if err := binaryValidate(ctx, buf, from, store); err != nil { + return errors.Wrapf(err, "failed to validate the written data") + } + if err := store.Delete(ctx, from); err != nil { + return errors.Wrapf(err, "failed to delete '%s' from the store", from) + } + return nil +} + +func binaryValidate(ctx context.Context, buf []byte, name string, store storer) error { + h := sha256.New() + _, _ = h.Write(buf) + fileSum := fmt.Sprintf("%x", h.Sum(nil)) + + h.Reset() + + var err error + buf, err = binaryGet(ctx, name, store) + if err != nil { + return errors.Wrapf(err, "failed to read '%s' from the store", name) + } + _, _ = h.Write(buf) + storeSum := fmt.Sprintf("%x", h.Sum(nil)) + + if fileSum != storeSum { + return errors.Errorf("Hashsum mismatch (file: %s, store: %s)", fileSum, storeSum) + } + return nil +} + +func binaryGet(ctx context.Context, name string, store storer) ([]byte, error) { + sec, err := store.Get(ctx, name) + if err != nil { + return nil, errors.Wrapf(err, "failed to read '%s' from the store", name) + } + buf, err := base64.StdEncoding.DecodeString(sec.Body()) + if err != nil { + return nil, errors.Wrapf(err, "failed to encode to base64") + } + return buf, nil +} diff --git a/pkg/action/binary/sum.go b/pkg/action/binary/sum.go new file mode 100644 index 0000000000..732af903fc --- /dev/null +++ b/pkg/action/binary/sum.go @@ -0,0 +1,34 @@ +package binary + +import ( + "context" + "crypto/sha256" + "strings" + + "github.com/justwatchcom/gopass/pkg/action" + "github.com/justwatchcom/gopass/pkg/out" + "github.com/urfave/cli" +) + +// Sum decodes binary content and computes the SHA256 checksum +func Sum(ctx context.Context, c *cli.Context, store storer) error { + name := c.Args().First() + if name == "" { + return action.ExitError(ctx, action.ExitUsage, nil, "Usage: %s sha256 name", c.App.Name) + } + + if !strings.HasSuffix(name, Suffix) { + name += Suffix + } + + buf, err := binaryGet(ctx, name, store) + if err != nil { + return action.ExitError(ctx, action.ExitDecrypt, err, "failed to read secret: %s", err) + } + + h := sha256.New() + _, _ = h.Write(buf) + out.Yellow(ctx, "%x", h.Sum(nil)) + + return nil +} diff --git a/pkg/action/clone.go b/pkg/action/clone.go index b4db72eb23..d3c0a6080b 100644 --- a/pkg/action/clone.go +++ b/pkg/action/clone.go @@ -129,6 +129,8 @@ func (s *Action) cloneGetGitConfig(ctx context.Context, name string) (string, st return username, email, nil } +// detectCryptoBackend tries to detect the crypto backend used in a cloned repo +// This detection is very shallow and doesn't support all backends, yet func detectCryptoBackend(ctx context.Context, path string) backend.CryptoBackend { if fsutil.IsFile(filepath.Join(path, xc.IDFile)) { return backend.XC diff --git a/pkg/action/copy.go b/pkg/action/copy.go index 3813e9efff..6074577d17 100644 --- a/pkg/action/copy.go +++ b/pkg/action/copy.go @@ -13,7 +13,7 @@ func (s *Action) Copy(ctx context.Context, c *cli.Context) error { force := c.Bool("force") if len(c.Args()) != 2 { - return ExitError(ctx, ExitUsage, nil, "Usage: %s cp old-path new-path", s.Name) + return ExitError(ctx, ExitUsage, nil, "Usage: %s cp ", s.Name) } from := c.Args()[0] diff --git a/pkg/action/create.go b/pkg/action/create/create.go similarity index 72% rename from pkg/action/create.go rename to pkg/action/create/create.go index 97d34ed795..6af65510e9 100644 --- a/pkg/action/create.go +++ b/pkg/action/create/create.go @@ -1,4 +1,4 @@ -package action +package create import ( "context" @@ -9,11 +9,13 @@ import ( "strings" "github.com/fatih/color" + "github.com/justwatchcom/gopass/pkg/action" "github.com/justwatchcom/gopass/pkg/clipboard" "github.com/justwatchcom/gopass/pkg/cui" "github.com/justwatchcom/gopass/pkg/fsutil" "github.com/justwatchcom/gopass/pkg/out" "github.com/justwatchcom/gopass/pkg/pwgen" + "github.com/justwatchcom/gopass/pkg/store" "github.com/justwatchcom/gopass/pkg/store/secret" "github.com/justwatchcom/gopass/pkg/store/sub" "github.com/justwatchcom/gopass/pkg/termio" @@ -21,8 +23,25 @@ import ( "github.com/urfave/cli" ) +const ( + defaultLength = 24 +) + +type storer interface { + //Get(context.Context, string) (store.Secret, error) + Set(context.Context, string, store.Secret) error + Exists(context.Context, string) bool + //Delete(context.Context, string) error + MountPoints() []string +} + +type creator struct { + store storer +} + // Create displays the password creation wizard -func (s *Action) Create(ctx context.Context, c *cli.Context) error { +func Create(ctx context.Context, c *cli.Context, store storer) error { + s := creator{store: store} acts := make(cui.Actions, 0, 5) acts = append(acts, cui.Action{Name: "Website Login", Fn: s.createWebsite}) acts = append(acts, cui.Action{Name: "PIN Code (numerical)", Fn: s.createPIN}) @@ -36,10 +55,12 @@ func (s *Action) Create(ctx context.Context, c *cli.Context) error { case "show": return acts.Run(ctx, c, sel) default: - return ExitError(ctx, ExitAborted, nil, "user aborted") + return action.ExitError(ctx, action.ExitAborted, nil, "user aborted") } } +// extractHostname tries to extract the hostname from a URL in a filepath-safe +// way for use in the name of a secret func extractHostname(in string) string { if in == "" { return "" @@ -57,7 +78,8 @@ func extractHostname(in string) string { return fsutil.CleanFilename(in) } -func (s *Action) createWebsite(ctx context.Context, c *cli.Context) error { +// createWebsite walks through the website credential creation wizard +func (s *creator) createWebsite(ctx context.Context, c *cli.Context) error { var ( urlStr string username string @@ -72,9 +94,10 @@ func (s *Action) createWebsite(ctx context.Context, c *cli.Context) error { if err != nil { return err } + // the hostname is used as part of the name hostname := extractHostname(urlStr) if hostname == "" { - return ExitError(ctx, ExitUnknown, err, "Can not parse URL '%s'. Please use 'gopass edit' to manually create the secret", urlStr) + return action.ExitError(ctx, action.ExitUnknown, err, "Can not parse URL '%s'. Please use 'gopass edit' to manually create the secret", urlStr) } username, err = termio.AskForString(ctx, "Please enter the Username/Login", "") @@ -101,7 +124,7 @@ func (s *Action) createWebsite(ctx context.Context, c *cli.Context) error { comment, _ = termio.AskForString(ctx, "Comments (optional)", "") // select store - store = cui.AskForStore(ctx, s.Store) + store = cui.AskForStore(ctx, s.store) // generate name, ask for override if already taken if store != "" { @@ -109,34 +132,33 @@ func (s *Action) createWebsite(ctx context.Context, c *cli.Context) error { } name := fmt.Sprintf("%swebsites/%s/%s", store, fsutil.CleanFilename(hostname), fsutil.CleanFilename(username)) - if s.Store.Exists(ctx, name) { + if s.store.Exists(ctx, name) { name, err = termio.AskForString(ctx, "Secret already exists, please choose another path", name) if err != nil { return err } } - out.Yellow(ctx, "Note: You may be asked for your GPG passphrase to sign the commit") - sec := secret.New(password, "") _ = sec.SetValue("url", urlStr) _ = sec.SetValue("username", username) _ = sec.SetValue("comment", comment) - if err := s.Store.Set(sub.WithReason(ctx, "Created new entry"), name, sec); err != nil { - return ExitError(ctx, ExitEncrypt, err, "failed to set '%s': %s", name, err) + if err := s.store.Set(sub.WithReason(ctx, "Created new entry"), name, sec); err != nil { + return action.ExitError(ctx, action.ExitEncrypt, err, "failed to set '%s': %s", name, err) } return s.createPrintOrCopy(ctx, c, name, password, genPw) } -func (s *Action) createPrintOrCopy(ctx context.Context, c *cli.Context, name, password string, genPw bool) error { +// createPrintOrCopy will display the created password (or copy to clipboard) +func (s *creator) createPrintOrCopy(ctx context.Context, c *cli.Context, name, password string, genPw bool) error { if !genPw { return nil } if c.Bool("print") { fmt.Fprintf( - stdout, + out.Stdout, "The generated password for %s is:\n%s\n", name, color.YellowString(password), ) @@ -144,12 +166,13 @@ func (s *Action) createPrintOrCopy(ctx context.Context, c *cli.Context, name, pa } if err := clipboard.CopyTo(ctx, name, []byte(password)); err != nil { - return ExitError(ctx, ExitIO, err, "failed to copy to clipboard: %s", err) + return action.ExitError(ctx, action.ExitIO, err, "failed to copy to clipboard: %s", err) } return nil } -func (s *Action) createPIN(ctx context.Context, c *cli.Context) error { +// createPIN will walk through the numerical password (PIN) wizard +func (s *creator) createPIN(ctx context.Context, c *cli.Context) error { var ( authority string application string @@ -165,14 +188,14 @@ func (s *Action) createPIN(ctx context.Context, c *cli.Context) error { return err } if authority == "" { - return ExitError(ctx, ExitUnknown, nil, "Authority must not be empty") + return action.ExitError(ctx, action.ExitUnknown, nil, "Authority must not be empty") } application, err = termio.AskForString(ctx, "Please enter the entity (e.g. Credit Card) this PIN is for", "") if err != nil { return err } if application == "" { - return ExitError(ctx, ExitUnknown, nil, "Application must not be empty") + return action.ExitError(ctx, action.ExitUnknown, nil, "Application must not be empty") } genPw, err = termio.AskForBool(ctx, "Do you want to generate a new PIN?", true) if err != nil { @@ -192,14 +215,14 @@ func (s *Action) createPIN(ctx context.Context, c *cli.Context) error { comment, _ = termio.AskForString(ctx, "Comments (optional)", "") // select store - store = cui.AskForStore(ctx, s.Store) + store = cui.AskForStore(ctx, s.store) // generate name, ask for override if already taken if store != "" { store += "/" } name := fmt.Sprintf("%spins/%s/%s", store, fsutil.CleanFilename(authority), fsutil.CleanFilename(application)) - if s.Store.Exists(ctx, name) { + if s.store.Exists(ctx, name) { name, err = termio.AskForString(ctx, "Secret already exists, please choose another path", name) if err != nil { return err @@ -208,14 +231,15 @@ func (s *Action) createPIN(ctx context.Context, c *cli.Context) error { sec := secret.New(password, "") _ = sec.SetValue("application", application) _ = sec.SetValue("comment", comment) - if err := s.Store.Set(sub.WithReason(ctx, "Created new entry"), name, sec); err != nil { - return ExitError(ctx, ExitEncrypt, err, "failed to set '%s': %s", name, err) + if err := s.store.Set(sub.WithReason(ctx, "Created new entry"), name, sec); err != nil { + return action.ExitError(ctx, action.ExitEncrypt, err, "failed to set '%s': %s", name, err) } return s.createPrintOrCopy(ctx, c, name, password, genPw) } -func (s *Action) createAWS(ctx context.Context, c *cli.Context) error { +// createAWS will walk through the AWS credential creation wizard +func (s *creator) createAWS(ctx context.Context, c *cli.Context) error { var ( account string username string @@ -231,14 +255,14 @@ func (s *Action) createAWS(ctx context.Context, c *cli.Context) error { return err } if account == "" { - return ExitError(ctx, ExitUnknown, nil, "Account must not be empty") + return action.ExitError(ctx, action.ExitUnknown, nil, "Account must not be empty") } username, err = termio.AskForString(ctx, "Please enter the name of the AWS IAM User this key belongs to", "") if err != nil { return err } if username == "" { - return ExitError(ctx, ExitUnknown, nil, "Username must not be empty") + return action.ExitError(ctx, action.ExitUnknown, nil, "Username must not be empty") } accesskey, err = termio.AskForString(ctx, "Please enter the Access Key ID (AWS_ACCESS_KEY_ID)", "") if err != nil { @@ -251,14 +275,14 @@ func (s *Action) createAWS(ctx context.Context, c *cli.Context) error { region, _ = termio.AskForString(ctx, "Please enter the default Region (AWS_DEFAULT_REGION) (optional)", "") // select store - store = cui.AskForStore(ctx, s.Store) + store = cui.AskForStore(ctx, s.store) // generate name, ask for override if already taken if store != "" { store += "/" } name := fmt.Sprintf("%saws/iam/%s/%s", store, fsutil.CleanFilename(account), fsutil.CleanFilename(username)) - if s.Store.Exists(ctx, name) { + if s.store.Exists(ctx, name) { name, err = termio.AskForString(ctx, "Secret already exists, please choose another path", name) if err != nil { return err @@ -269,13 +293,14 @@ func (s *Action) createAWS(ctx context.Context, c *cli.Context) error { _ = sec.SetValue("username", username) _ = sec.SetValue("accesskey", accesskey) _ = sec.SetValue("region", region) - if err := s.Store.Set(sub.WithReason(ctx, "Created new entry"), name, sec); err != nil { - return ExitError(ctx, ExitEncrypt, err, "failed to set '%s': %s", name, err) + if err := s.store.Set(sub.WithReason(ctx, "Created new entry"), name, sec); err != nil { + return action.ExitError(ctx, action.ExitEncrypt, err, "failed to set '%s': %s", name, err) } return nil } -func (s *Action) createGCP(ctx context.Context, c *cli.Context) error { +// createGCP will walk through the GCP credential creation wizard +func (s *creator) createGCP(ctx context.Context, c *cli.Context) error { var ( project string username string @@ -303,7 +328,7 @@ func (s *Action) createGCP(ctx context.Context, c *cli.Context) error { } } if username == "" { - return ExitError(ctx, ExitUnknown, nil, "Username must not be empty") + return action.ExitError(ctx, action.ExitUnknown, nil, "Username must not be empty") } if project == "" { project, err = termio.AskForString(ctx, "Please enter the name of this GCP project", "") @@ -312,30 +337,31 @@ func (s *Action) createGCP(ctx context.Context, c *cli.Context) error { } } if project == "" { - return ExitError(ctx, ExitUnknown, nil, "Project must not be empty") + return action.ExitError(ctx, action.ExitUnknown, nil, "Project must not be empty") } // select store - store = cui.AskForStore(ctx, s.Store) + store = cui.AskForStore(ctx, s.store) // generate name, ask for override if already taken if store != "" { store += "/" } name := fmt.Sprintf("%sgcp/iam/%s/%s", store, fsutil.CleanFilename(project), fsutil.CleanFilename(username)) - if s.Store.Exists(ctx, name) { + if s.store.Exists(ctx, name) { name, err = termio.AskForString(ctx, "Secret already exists, please choose another path", name) if err != nil { return err } } sec := secret.New("", string(buf)) - if err := s.Store.Set(sub.WithReason(ctx, "Created new entry"), name, sec); err != nil { - return ExitError(ctx, ExitEncrypt, err, "failed to set '%s': %s", name, err) + if err := s.store.Set(sub.WithReason(ctx, "Created new entry"), name, sec); err != nil { + return action.ExitError(ctx, action.ExitEncrypt, err, "failed to set '%s': %s", name, err) } return nil } +// extractGCPInfo will extract the GCP details from the given json blob func extractGCPInfo(buf []byte) (string, string, error) { var m map[string]string if err := json.Unmarshal(buf, &m); err != nil { @@ -353,7 +379,8 @@ func extractGCPInfo(buf []byte) (string, string, error) { return username, p[0], nil } -func (s *Action) createGeneric(ctx context.Context, c *cli.Context) error { +// createGeneric will walk through the generic secret wizard +func (s *creator) createGeneric(ctx context.Context, c *cli.Context) error { var ( shortname string password string @@ -367,7 +394,7 @@ func (s *Action) createGeneric(ctx context.Context, c *cli.Context) error { return err } if shortname == "" { - return ExitError(ctx, ExitUnknown, nil, "Name must not be empty") + return action.ExitError(ctx, action.ExitUnknown, nil, "Name must not be empty") } genPw, err = termio.AskForBool(ctx, "Do you want to generate a new password?", true) if err != nil { @@ -386,14 +413,14 @@ func (s *Action) createGeneric(ctx context.Context, c *cli.Context) error { } // select store - store = cui.AskForStore(ctx, s.Store) + store = cui.AskForStore(ctx, s.store) // generate name, ask for override if already taken if store != "" { store += "/" } name := fmt.Sprintf("%smisc/%s", store, fsutil.CleanFilename(shortname)) - if s.Store.Exists(ctx, name) { + if s.store.Exists(ctx, name) { name, err = termio.AskForString(ctx, "Secret already exists, please choose another path", name) if err != nil { return err @@ -415,14 +442,15 @@ func (s *Action) createGeneric(ctx context.Context, c *cli.Context) error { } _ = sec.SetValue(key, val) } - if err := s.Store.Set(sub.WithReason(ctx, "Created new entry"), name, sec); err != nil { - return ExitError(ctx, ExitEncrypt, err, "failed to set '%s': %s", name, err) + if err := s.store.Set(sub.WithReason(ctx, "Created new entry"), name, sec); err != nil { + return action.ExitError(ctx, action.ExitEncrypt, err, "failed to set '%s': %s", name, err) } return s.createPrintOrCopy(ctx, c, name, password, genPw) } -func (s *Action) createGeneratePassword(ctx context.Context) (string, error) { +// createGeneratePasssword will walk through the password generation steps +func (s *creator) createGeneratePassword(ctx context.Context) (string, error) { xkcd, err := termio.AskForBool(ctx, "Do you want an rememberable password?", true) if err != nil { return "", err @@ -450,7 +478,8 @@ func (s *Action) createGeneratePassword(ctx context.Context) (string, error) { return pwgen.GeneratePassword(length, symbols), nil } -func (s *Action) createGeneratePIN(ctx context.Context) (string, error) { +// createGeneratePIN will walk through the PIN generation steps +func (s *creator) createGeneratePIN(ctx context.Context) (string, error) { length, err := termio.AskForInt(ctx, "How long should the PIN be?", 4) if err != nil { return "", err diff --git a/pkg/action/create_test.go b/pkg/action/create/create_test.go similarity index 78% rename from pkg/action/create_test.go rename to pkg/action/create/create_test.go index 6465b7bc31..a65b3639d3 100644 --- a/pkg/action/create_test.go +++ b/pkg/action/create/create_test.go @@ -1,4 +1,4 @@ -package action +package create import ( "bytes" @@ -13,7 +13,7 @@ import ( "github.com/justwatchcom/gopass/pkg/ctxutil" "github.com/justwatchcom/gopass/pkg/out" "github.com/justwatchcom/gopass/pkg/termio" - "github.com/justwatchcom/gopass/tests/gptest" + "github.com/justwatchcom/gopass/tests/mockstore" "github.com/stretchr/testify/assert" "github.com/urfave/cli" ) @@ -32,13 +32,10 @@ func TestExtractHostname(t *testing.T) { } func TestCreate(t *testing.T) { - u := gptest.NewUnitTester(t) - defer u.Remove() + store := mockstore.New("") ctx := context.Background() ctx = ctxutil.WithAlwaysYes(ctx, true) - act, err := newMock(ctx, u) - assert.NoError(t, err) buf := &bytes.Buffer{} out.Stdout = buf @@ -51,26 +48,21 @@ func TestCreate(t *testing.T) { fs := flag.NewFlagSet("default", flag.ContinueOnError) c := cli.NewContext(app, fs, nil) - assert.Error(t, act.Create(ctx, c)) + assert.Error(t, Create(ctx, c, store)) buf.Reset() } func TestCreateWebsite(t *testing.T) { - u := gptest.NewUnitTester(t) - defer u.Remove() + s := creator{mockstore.New("")} ctx := context.Background() ctx = ctxutil.WithInteractive(ctx, false) - act, err := newMock(ctx, u) - assert.NoError(t, err) buf := &bytes.Buffer{} out.Stdout = buf - stdout = buf termio.Stdout = buf defer func() { out.Stdout = os.Stdout - stdout = os.Stdout termio.Stdout = os.Stdout }() @@ -92,7 +84,7 @@ y fs := flag.NewFlagSet("default", flag.ContinueOnError) c := cli.NewContext(app, fs, nil) - assert.NoError(t, act.createWebsite(ctx, c)) + assert.NoError(t, s.createWebsite(ctx, c)) buf.Reset() // try to create the same entry twice @@ -107,31 +99,26 @@ y fs = flag.NewFlagSet("default", flag.ContinueOnError) c = cli.NewContext(app, fs, nil) - assert.NoError(t, act.createWebsite(ctx, c)) + assert.NoError(t, s.createWebsite(ctx, c)) buf.Reset() } func TestCreatePIN(t *testing.T) { - u := gptest.NewUnitTester(t) - defer u.Remove() + s := creator{mockstore.New("")} ctx := context.Background() ctx = ctxutil.WithInteractive(ctx, false) - act, err := newMock(ctx, u) - assert.NoError(t, err) buf := &bytes.Buffer{} out.Stdout = buf - stdout = buf termio.Stdout = buf defer func() { - stdout = os.Stdout out.Stdout = os.Stdout termio.Stdout = os.Stdout }() ctx = ctxutil.WithAlwaysYes(ctx, true) - pw, err := act.createGeneratePIN(ctx) + pw, err := s.createGeneratePIN(ctx) assert.NoError(t, err) if len(pw) < 4 || len(pw) > 4 { t.Errorf("PIN should have 4 characters") @@ -155,25 +142,20 @@ y fs := flag.NewFlagSet("default", flag.ContinueOnError) c := cli.NewContext(app, fs, nil) - assert.NoError(t, act.createPIN(ctx, c)) + assert.NoError(t, s.createPIN(ctx, c)) buf.Reset() } func TestCreateGeneric(t *testing.T) { - u := gptest.NewUnitTester(t) - defer u.Remove() + s := creator{mockstore.New("")} ctx := context.Background() ctx = ctxutil.WithAlwaysYes(ctx, true) - act, err := newMock(ctx, u) - assert.NoError(t, err) buf := &bytes.Buffer{} out.Stdout = buf - stdout = buf termio.Stdout = buf defer func() { - stdout = os.Stdout out.Stdout = os.Stdout termio.Stdout = os.Stdout }() @@ -196,27 +178,22 @@ y fs := flag.NewFlagSet("default", flag.ContinueOnError) c := cli.NewContext(app, fs, nil) - assert.NoError(t, act.createGeneric(ctx, c)) + assert.NoError(t, s.createGeneric(ctx, c)) buf.Reset() } func TestCreateAWS(t *testing.T) { - u := gptest.NewUnitTester(t) - defer u.Remove() + s := creator{mockstore.New("")} ctx := context.Background() ctx = ctxutil.WithAlwaysYes(ctx, true) ctx = ctxutil.WithTerminal(ctx, false) - act, err := newMock(ctx, u) - assert.NoError(t, err) buf := &bytes.Buffer{} out.Stdout = buf - stdout = buf termio.Stdout = buf defer func() { out.Stdout = os.Stdout - stdout = os.Stdout termio.Stdout = os.Stdout }() @@ -239,30 +216,31 @@ SECRETKEY fs := flag.NewFlagSet("default", flag.ContinueOnError) c := cli.NewContext(app, fs, nil) - assert.NoError(t, act.createAWS(ctx, c)) + assert.NoError(t, s.createAWS(ctx, c)) buf.Reset() } func TestCreateGCP(t *testing.T) { - u := gptest.NewUnitTester(t) - defer u.Remove() + tempdir, err := ioutil.TempDir("", "gopass-") + assert.NoError(t, err) + defer func() { + _ = os.RemoveAll(tempdir) + }() + + s := creator{mockstore.New("")} ctx := context.Background() ctx = ctxutil.WithAlwaysYes(ctx, true) - act, err := newMock(ctx, u) - assert.NoError(t, err) buf := &bytes.Buffer{} out.Stdout = buf - stdout = buf termio.Stdout = buf defer func() { out.Stdout = os.Stdout - stdout = os.Stdout termio.Stdout = os.Stdout }() - tf := filepath.Join(u.Dir, "service-account.json") + tf := filepath.Join(tempdir, "service-account.json") assert.NoError(t, ioutil.WriteFile(tf, []byte(`{"client_email": "foobar@example.org"}`), 0600)) // provide values on redirected stdin input := tf @@ -280,6 +258,6 @@ func TestCreateGCP(t *testing.T) { fs := flag.NewFlagSet("default", flag.ContinueOnError) c := cli.NewContext(app, fs, nil) - assert.NoError(t, act.createGCP(ctx, c)) + assert.NoError(t, s.createGCP(ctx, c)) buf.Reset() } diff --git a/pkg/action/delete.go b/pkg/action/delete.go index 91473cc041..c3b9aaa67e 100644 --- a/pkg/action/delete.go +++ b/pkg/action/delete.go @@ -20,6 +20,7 @@ func (s *Action) Delete(ctx context.Context, c *cli.Context) error { return ExitError(ctx, ExitUsage, nil, "Usage: %s rm name", s.Name) } + // specifying a key is optional key := c.Args().Get(1) if !force { // don't check if it's force anyway @@ -54,6 +55,7 @@ func (s *Action) Delete(ctx context.Context, c *cli.Context) error { return nil } +// deleteKeyFromYAML deletes a single key from YAML func (s *Action) deleteKeyFromYAML(ctx context.Context, name, key string) error { sec, err := s.Store.Get(ctx, name) if err != nil { diff --git a/pkg/action/edit.go b/pkg/action/edit.go index 89a21d9649..b18c61ac81 100644 --- a/pkg/action/edit.go +++ b/pkg/action/edit.go @@ -24,6 +24,7 @@ func (s *Action) Edit(ctx context.Context, c *cli.Context) error { ed := editor.Path(c) + // get existing content or generate new one from a template var content []byte var changed bool if s.Store.Exists(ctx, name) { @@ -46,6 +47,7 @@ func (s *Action) Edit(ctx context.Context, c *cli.Context) error { } } + // invoke the editor to let the user edit the content nContent, err := editor.Invoke(ctx, ed, content) if err != nil { return ExitError(ctx, ExitUnknown, err, "failed to invoke editor: %s", err) @@ -61,10 +63,12 @@ func (s *Action) Edit(ctx context.Context, c *cli.Context) error { out.Red(ctx, "WARNING: Invalid YAML: %s", err) } + // if the secret has a password, we check it's strength if pw := nSec.Password(); pw != "" { audit.Single(ctx, pw) } + // write result (back) to store if err := s.Store.Set(sub.WithReason(ctx, fmt.Sprintf("Edited with %s", ed)), name, nSec); err != nil { return ExitError(ctx, ExitEncrypt, err, "failed to encrypt secret %s: %s", name, err) } diff --git a/pkg/action/find.go b/pkg/action/find.go index 00d357490d..61ea109588 100644 --- a/pkg/action/find.go +++ b/pkg/action/find.go @@ -18,16 +18,18 @@ func (s *Action) Find(ctx context.Context, c *cli.Context) error { ctx = WithClip(ctx, c.Bool("clip")) if !c.Args().Present() { - return ExitError(ctx, ExitUsage, nil, "Usage: %s find arg", s.Name) + return ExitError(ctx, ExitUsage, nil, "Usage: %s find ", s.Name) } - l, err := s.Store.List(ctx, 0) + // get all existing entries + haystack, err := s.Store.List(ctx, 0) if err != nil { return ExitError(ctx, ExitList, err, "failed to list store: %s", err) } + // filter our the ones from the haystack matching the needle needle := strings.ToLower(c.Args().First()) - choices := filter(l, needle) + choices := filter(haystack, needle) // if we have an exact match print it if len(choices) == 1 { @@ -38,7 +40,7 @@ func (s *Action) Find(ctx context.Context, c *cli.Context) error { // if we don't have a match yet try a fuzzy search if len(choices) < 1 && ctxutil.IsFuzzySearch(ctx) { // try fuzzy match - cm := closestmatch.New(l, []int{2}) + cm := closestmatch.New(haystack, []int{2}) choices = cm.ClosestN(needle, 5) } @@ -59,6 +61,7 @@ func (s *Action) Find(ctx context.Context, c *cli.Context) error { return s.findSelection(ctx, c, choices, needle) } +// findSelection runs a wizard that lets the user select an entry func (s *Action) findSelection(ctx context.Context, c *cli.Context, choices []string, needle string) error { sort.Strings(choices) act, sel := cui.GetSelection(ctx, "Found secrets - Please select an entry", "<↑/↓> to change the selection, <→> to show, <←> to copy, to sync, to quit", choices) diff --git a/pkg/action/find_test.go b/pkg/action/find_test.go index f96e42085f..9dd0b64b89 100644 --- a/pkg/action/find_test.go +++ b/pkg/action/find_test.go @@ -39,8 +39,8 @@ func TestFind(t *testing.T) { // find c := cli.NewContext(app, flag.NewFlagSet("default", flag.ContinueOnError), nil) - if err := act.Find(ctx, c); err == nil || err.Error() != "Usage: action.test find arg" { - t.Errorf("Should fail") + if err := act.Find(ctx, c); err == nil || err.Error() != "Usage: action.test find " { + t.Errorf("Should fail: %s", err) } // find fo diff --git a/pkg/action/fsck.go b/pkg/action/fsck.go index 4e3a357ee7..206c1ffc0b 100644 --- a/pkg/action/fsck.go +++ b/pkg/action/fsck.go @@ -26,6 +26,7 @@ func (s *Action) Fsck(ctx context.Context, c *cli.Context) error { } } + // the main work in done by the sub stores if err := s.Store.Fsck(ctx, c.Args().Get(0)); err != nil { return ExitError(ctx, ExitFsck, err, "fsck found errors: %s", err) } diff --git a/pkg/action/generate.go b/pkg/action/generate.go index 4ff0506382..8f98e0a16b 100644 --- a/pkg/action/generate.go +++ b/pkg/action/generate.go @@ -27,7 +27,7 @@ var ( reNumber = regexp.MustCompile(`^\d+$`) ) -// Generate & save a password +// Generate and save a password func (s *Action) Generate(ctx context.Context, c *cli.Context) error { force := c.Bool("force") edit := c.Bool("edit") @@ -90,6 +90,8 @@ func keyAndLength(c *cli.Context) (string, string) { return key, length } +// generateCopyOrPrint will print the password to the screen or copy to the +// clipboard func (s *Action) generateCopyOrPrint(ctx context.Context, c *cli.Context, name, key, password string) error { if c.Bool("print") { if key != "" { @@ -110,6 +112,7 @@ func (s *Action) generateCopyOrPrint(ctx context.Context, c *cli.Context, name, return nil } +// generatePassword will run through the password generation steps func (s *Action) generatePassword(ctx context.Context, c *cli.Context, length string) (string, error) { if c.Bool("xkcd") || c.IsSet("xkcdsep") { return s.generatePasswordXKCD(ctx, c, length) @@ -144,6 +147,8 @@ func (s *Action) generatePassword(ctx context.Context, c *cli.Context, length st return pwgen.GeneratePassword(pwlen, symbols), nil } +// generatePasswordXKCD walks through the steps necessary to create an XKCD-style +// password func (s *Action) generatePasswordXKCD(ctx context.Context, c *cli.Context, length string) (string, error) { xkcdSeparator := " " if c.IsSet("xkcdsep") { @@ -174,6 +179,7 @@ func (s *Action) generatePasswordXKCD(ctx context.Context, c *cli.Context, lengt return xkcdgen.RandomLengthDelim(pwlen, xkcdSeparator, c.String("xkcdlang")) } +// generateSetPassword will update or create a secret func (s *Action) generateSetPassword(ctx context.Context, name, key, password string) (context.Context, error) { // set a single key in a yaml doc if key != "" { diff --git a/pkg/action/git.go b/pkg/action/git.go index 7f0791da6a..9150a406f3 100644 --- a/pkg/action/git.go +++ b/pkg/action/git.go @@ -77,23 +77,36 @@ func (s *Action) getUserData(ctx context.Context, store, un, ue string) (string, // GitAddRemote adds a new git remote func (s *Action) GitAddRemote(ctx context.Context, c *cli.Context) error { store := c.String("store") - remote := c.String("remote") - url := c.String("url") + remote := c.Args().Get(0) + url := c.Args().Get(1) + + if remote == "" || url == "" { + return ExitError(ctx, ExitUsage, nil, "Usage: %s git remote add ", s.Name) + } + return s.Store.GitAddRemote(ctx, store, remote, url) } // GitPull pulls from a git remote func (s *Action) GitPull(ctx context.Context, c *cli.Context) error { store := c.String("store") - origin := c.String("origin") - branch := c.String("branch") + origin := c.Args().Get(0) + branch := c.Args().Get(1) + + if origin == "" || branch == "" { + return ExitError(ctx, ExitUsage, nil, "Usage: %s git pull ", s.Name) + } return s.Store.GitPull(ctx, store, origin, branch) } // GitPush pushes to a git remote func (s *Action) GitPush(ctx context.Context, c *cli.Context) error { store := c.String("store") - origin := c.String("origin") - branch := c.String("branch") + origin := c.Args().Get(0) + branch := c.Args().Get(1) + + if origin == "" || branch == "" { + return ExitError(ctx, ExitUsage, nil, "Usage: %s git push ", s.Name) + } return s.Store.GitPush(ctx, store, origin, branch) } diff --git a/pkg/action/grep.go b/pkg/action/grep.go index 1c4a4cfc97..ce6974b788 100644 --- a/pkg/action/grep.go +++ b/pkg/action/grep.go @@ -15,24 +15,25 @@ func (s *Action) Grep(ctx context.Context, c *cli.Context) error { return ExitError(ctx, ExitUsage, nil, "Usage: %s grep arg", s.Name) } - search := c.Args().First() + // get the search term + needle := c.Args().First() - l, err := s.Store.List(ctx, 0) + haystack, err := s.Store.List(ctx, 0) if err != nil { return ExitError(ctx, ExitList, err, "failed to list store: %s", err) } - for _, v := range l { + for _, v := range haystack { sec, err := s.Store.Get(ctx, v) if err != nil { out.Red(ctx, "failed to decrypt %s: %v", v, err) continue } - if strings.Contains(sec.Password(), search) { + if strings.Contains(sec.Password(), needle) { out.Print(ctx, "%s:\n%s", color.BlueString(v), sec.Password()) } - if strings.Contains(sec.Body(), search) { + if strings.Contains(sec.Body(), needle) { out.Print(ctx, "%s:\n%s", color.BlueString(v), sec.Body()) } } diff --git a/pkg/action/history.go b/pkg/action/history.go index 9ceffac474..5896568e48 100644 --- a/pkg/action/history.go +++ b/pkg/action/history.go @@ -11,11 +11,10 @@ import ( // History displays the history of a given secret func (s *Action) History(ctx context.Context, c *cli.Context) error { name := c.Args().Get(0) - showPassword := c.Bool("password") if name == "" { - return ExitError(ctx, ExitUsage, nil, "Usage: %s history [name]", s.Name) + return ExitError(ctx, ExitUsage, nil, "Usage: %s history ", s.Name) } if !s.Store.Exists(ctx, name) { diff --git a/pkg/action/list.go b/pkg/action/list.go index 5469fc8f00..e21e62e02f 100644 --- a/pkg/action/list.go +++ b/pkg/action/list.go @@ -19,7 +19,8 @@ import ( "github.com/urfave/cli" ) -// List all secrets as a tree +// List all secrets as a tree. If the filter argument is non-empty +// display only those that have this prefix func (s *Action) List(ctx context.Context, c *cli.Context) error { filter := c.Args().First() flat := c.Bool("flat") @@ -44,6 +45,7 @@ func (s *Action) listFiltered(ctx context.Context, l tree.Tree, limit int, flat, return nil } + // SetRoot formats the root entry properly subtree.SetRoot(true) subtree.SetName(filter) if flat { @@ -73,6 +75,8 @@ func (s *Action) listFiltered(ctx context.Context, l tree.Tree, limit int, flat, return nil } +// redirectPager returns a redirected io.Writer if the output would exceed +// the terminal size func redirectPager(ctx context.Context, subtree tree.Tree) (io.Writer, *bytes.Buffer) { if ctxutil.IsNoPager(ctx) { return stdout, nil @@ -89,6 +93,7 @@ func redirectPager(ctx context.Context, subtree tree.Tree) (io.Writer, *bytes.Bu return buf, buf } +// listAll will unconditionally list all entries, used if no filter is given func (s *Action) listAll(ctx context.Context, l tree.Tree, limit int, flat bool) error { if flat { for _, e := range l.List(limit) { @@ -109,6 +114,7 @@ func (s *Action) listAll(ctx context.Context, l tree.Tree, limit int, flat bool) return nil } +// pager invokes the default pager with the given content func (s *Action) pager(ctx context.Context, buf io.Reader) error { pager := os.Getenv("PAGER") if pager == "" { diff --git a/pkg/action/otp.go b/pkg/action/otp.go index be2a3b8cf3..47f9a73e78 100644 --- a/pkg/action/otp.go +++ b/pkg/action/otp.go @@ -22,8 +22,9 @@ const ( func (s *Action) OTP(ctx context.Context, c *cli.Context) error { name := c.Args().First() if name == "" { - return ExitError(ctx, ExitUsage, nil, "usage: %s otp [name]", s.Name) + return ExitError(ctx, ExitUsage, nil, "Usage: %s otp ", s.Name) } + qrf := c.String("qr") clip := c.Bool("clip") diff --git a/pkg/action/show.go b/pkg/action/show.go index 577ed5c5b9..475f911078 100644 --- a/pkg/action/show.go +++ b/pkg/action/show.go @@ -15,6 +15,11 @@ import ( "github.com/urfave/cli" ) +const ( + // BinarySuffix is the suffix that is appended to binaries in the store + BinarySuffix = ".b64" +) + // Show the content of a secret file func (s *Action) Show(ctx context.Context, c *cli.Context) error { name := c.Args().First() @@ -38,6 +43,7 @@ func (s *Action) Show(ctx context.Context, c *cli.Context) error { return nil } +// show displays the given secret/key func (s *Action) show(ctx context.Context, c *cli.Context, name, key string, recurse bool) error { if name == "" { return ExitError(ctx, ExitUsage, nil, "Usage: %s show [name]", s.Name) @@ -67,6 +73,7 @@ func (s *Action) show(ctx context.Context, c *cli.Context, name, key string, rec return s.showHandleOutput(ctx, name, key, sec) } +// showHandleRevision displays a single revision func (s *Action) showHandleRevision(ctx context.Context, c *cli.Context, name, key, revision string) error { sec, err := s.Store.GetRevision(ctx, name, revision) if err != nil { @@ -76,6 +83,7 @@ func (s *Action) showHandleRevision(ctx context.Context, c *cli.Context, name, k return s.showHandleOutput(ctx, name, key, sec) } +// showHandleOutput displays a secret func (s *Action) showHandleOutput(ctx context.Context, name, key string, sec store.Secret) error { var content string @@ -120,6 +128,7 @@ func (s *Action) showHandleOutput(ctx context.Context, name, key string, sec sto return nil } +// showHandleError handles errors retrieving secrets func (s *Action) showHandleError(ctx context.Context, c *cli.Context, name string, recurse bool, err error) error { if err != store.ErrNotFound || !recurse || !ctxutil.IsTerminal(ctx) { return ExitError(ctx, ExitUnknown, err, "failed to retrieve secret '%s': %s", name, err) diff --git a/pkg/action/sync.go b/pkg/action/sync.go index fccd7b8885..5bdbdc6ff0 100644 --- a/pkg/action/sync.go +++ b/pkg/action/sync.go @@ -61,6 +61,7 @@ func (s *Action) sync(ctx context.Context, c *cli.Context, store string) error { return nil } +// syncMount syncs a single mount func (s *Action) syncMount(ctx context.Context, mp string) error { ctxno := out.WithNewline(ctx, false) name := mp diff --git a/pkg/action/update.go b/pkg/action/update.go index 2e634ddc47..780319b3d8 100644 --- a/pkg/action/update.go +++ b/pkg/action/update.go @@ -8,7 +8,7 @@ import ( "github.com/urfave/cli" ) -// Update will start hte interactive update assistant +// Update will start the interactive update assistant func (s *Action) Update(ctx context.Context, c *cli.Context) error { pre := c.Bool("pre") diff --git a/pkg/agent/doc.go b/pkg/agent/doc.go new file mode 100644 index 0000000000..291dc8df7f --- /dev/null +++ b/pkg/agent/doc.go @@ -0,0 +1,8 @@ +// Package agent contains a long running background process to aide +// gopass in caching credentials. Since gopass is a one-off application +// it can not store much state in memory. It's lost once gopass quits. +// However certain operations require frequently entering credentials, +// like passphrases for custom crypto backend or encrypted config stores. +// This package implements an agent, similar to the GPG or SSH agents, +// that can ask for and cache credentials. +package agent diff --git a/pkg/audit/audit.go b/pkg/audit/audit.go index ad34083a97..10babd6a33 100644 --- a/pkg/audit/audit.go +++ b/pkg/audit/audit.go @@ -1,3 +1,4 @@ +// Package audit contains the password-strength auditing implementation package audit import ( diff --git a/tests/copy_test.go b/tests/copy_test.go index fa8efcb7f0..ce0f3bf11f 100644 --- a/tests/copy_test.go +++ b/tests/copy_test.go @@ -18,11 +18,11 @@ func TestCopy(t *testing.T) { out, err := ts.run("copy") assert.Error(t, err) - assert.Equal(t, "\nError: Usage: "+filepath.Base(ts.Binary)+" cp old-path new-path\n", out) + assert.Equal(t, "\nError: Usage: "+filepath.Base(ts.Binary)+" cp \n", out) out, err = ts.run("copy foo") assert.Error(t, err) - assert.Equal(t, "\nError: Usage: "+filepath.Base(ts.Binary)+" cp old-path new-path\n", out) + assert.Equal(t, "\nError: Usage: "+filepath.Base(ts.Binary)+" cp \n", out) out, err = ts.run("copy foo bar") assert.Error(t, err) diff --git a/tests/find_test.go b/tests/find_test.go index 7508312eaf..ee2640e191 100644 --- a/tests/find_test.go +++ b/tests/find_test.go @@ -15,7 +15,7 @@ func TestFind(t *testing.T) { out, err := ts.run("find") assert.Error(t, err) - assert.Equal(t, "\nError: Usage: "+filepath.Base(ts.Binary)+" find arg\n", out) + assert.Equal(t, "\nError: Usage: "+filepath.Base(ts.Binary)+" find \n", out) _, err = ts.run("config safecontent false") assert.NoError(t, err) diff --git a/tests/mockstore/store.go b/tests/mockstore/store.go new file mode 100644 index 0000000000..86573e8d89 --- /dev/null +++ b/tests/mockstore/store.go @@ -0,0 +1,194 @@ +package mockstore + +import ( + "context" + "fmt" + + "github.com/justwatchcom/gopass/pkg/backend" + "github.com/justwatchcom/gopass/pkg/backend/crypto/plain" + "github.com/justwatchcom/gopass/pkg/backend/rcs/noop" + "github.com/justwatchcom/gopass/pkg/backend/storage/kv/inmem" + "github.com/justwatchcom/gopass/pkg/store" + "github.com/justwatchcom/gopass/pkg/store/secret" + "github.com/justwatchcom/gopass/pkg/tree" +) + +type MockStore struct { + alias string + storage backend.Storage +} + +func New(alias string) *MockStore { + return &MockStore{ + alias: alias, + storage: inmem.New(), + } +} + +func (m *MockStore) String() string { + return "mockstore" +} + +func (m *MockStore) GetTemplate(context.Context, string) ([]byte, error) { + return []byte{}, nil +} + +func (m *MockStore) HasTemplate(context.Context, string) bool { + return false +} + +func (m *MockStore) ListTemplates(context.Context, string) []string { + return nil +} + +func (m *MockStore) LookupTemplate(context.Context, string) ([]byte, bool) { + return []byte{}, false +} + +func (m *MockStore) RemoveTemplate(context.Context, string) error { + return nil +} + +func (m *MockStore) SetTemplate(context.Context, string, []byte) error { + return nil +} + +func (m *MockStore) TemplateTree(context.Context) (tree.Tree, error) { + return nil, nil +} + +func (m *MockStore) AddRecipient(context.Context, string) error { + return nil +} + +func (m *MockStore) GetRecipients(context.Context, string) ([]string, error) { + return nil, nil +} + +func (m *MockStore) RemoveRecipient(context.Context, string) error { + return nil +} + +func (m *MockStore) SaveRecipients(context.Context) error { + return nil +} + +func (m *MockStore) Recipients(context.Context) []string { + return nil +} + +func (m *MockStore) ImportMissingPublicKeys(context.Context) error { + return nil +} + +func (m *MockStore) ExportMissingPublicKeys(context.Context, []string) (bool, error) { + return false, nil +} + +func (m *MockStore) Fsck(context.Context, string) error { + return nil +} + +func (m *MockStore) Path() string { + return "" +} + +func (m *MockStore) URL() string { + return "mockstore://" +} + +func (m *MockStore) RCS() backend.RCS { + return noop.New() +} + +func (m *MockStore) Crypto() backend.Crypto { + return plain.New() +} + +func (m *MockStore) Storage() backend.Storage { + return m.storage +} + +func (m *MockStore) GitInit(context.Context, string, string) error { + return nil +} + +func (m *MockStore) Alias() string { + return m.alias +} + +func (m *MockStore) Copy(ctx context.Context, from string, to string) error { + content, _ := m.storage.Get(ctx, from) + m.storage.Set(ctx, to, content) + return nil +} + +func (m *MockStore) Delete(ctx context.Context, name string) error { + return m.storage.Delete(ctx, name) +} + +func (m *MockStore) Equals(other store.Store) bool { + return false +} + +func (m *MockStore) Exists(ctx context.Context, name string) bool { + return m.storage.Exists(ctx, name) +} + +func (m *MockStore) Get(ctx context.Context, name string) (store.Secret, error) { + content, err := m.storage.Get(ctx, name) + if err != nil { + return nil, err + } + return secret.Parse(content) +} + +func (m *MockStore) GetRevision(context.Context, string, string) (store.Secret, error) { + return nil, fmt.Errorf("not supported") +} + +func (m *MockStore) Init(context.Context, string, ...string) error { + return nil +} + +func (m *MockStore) Initialized(context.Context) bool { + return true +} + +func (m *MockStore) IsDir(ctx context.Context, name string) bool { + return m.storage.IsDir(ctx, name) +} + +func (m *MockStore) List(context.Context, string) ([]string, error) { + return nil, nil +} + +func (m *MockStore) ListRevisions(context.Context, string) ([]backend.Revision, error) { + return nil, nil +} + +func (m *MockStore) Move(ctx context.Context, from string, to string) error { + content, _ := m.storage.Get(ctx, from) + m.storage.Set(ctx, to, content) + return m.storage.Delete(ctx, from) +} + +func (m *MockStore) Set(ctx context.Context, name string, sec store.Secret) error { + buf, err := sec.Bytes() + if err != nil { + return err + } + return m.storage.Set(ctx, name, buf) +} + +func (m *MockStore) Prune(context.Context, string) error { + return fmt.Errorf("not supported") +} + +func (m *MockStore) Valid() bool { + return true +} + +func (m *MockStore) MountPoints() []string { + return nil +}