lockbox

password manager
Log | Files | Refs | README | LICENSE

commit 148519703b7c95fa4de47289d701d57fa581fec0
parent 64dca2e6795efd486955df6b692fef980d715789
Author: Sean Enck <sean@ttypty.com>
Date:   Mon, 27 Mar 2023 20:04:05 -0400

reworking all cli args

Diffstat:
MREADME.md | 10++++++----
Mcmd/main.go | 22+++++++++++++++-------
Minternal/app/core.go | 15+++++----------
Minternal/app/info_test.go | 16++++++++--------
Minternal/app/insert.go | 82++++++++++++++++++++-----------------------------------------------------------
Minternal/app/insert_test.go | 109++++++++++++++++++++++---------------------------------------------------------
Minternal/cli/completions.bash | 15+--------------
Minternal/cli/core.go | 38+++++++++++++++++++++-----------------
Minternal/cli/core_test.go | 4++--
Minternal/totp/core.go | 136+++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------
Ainternal/totp/core_test.go | 56++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtests/run.sh | 9+++++----
12 files changed, 257 insertions(+), 255 deletions(-)

diff --git a/README.md b/README.md @@ -26,7 +26,7 @@ LOCKBOX_KEY="gpg --decrypt /Users/alice/.secrets/key.gpg" LOCKBOX_STORE=/Users/alice/.passwords/secrets.kdbx ``` -Use `lb help -verbose` for additional information about options and environment variables +Use `lb help verbose` for additional information about options and environment variables ## usage @@ -42,7 +42,9 @@ lb clip my/secret/password Create a new entry ``` lb insert my/new/key -# use -m for a multiline entry +# or +lb multiline my/new/multi +# for multiline inserts ``` ### list @@ -70,13 +72,13 @@ lb show my/key/value To get a totp token ``` -lb totp token +lb totp show token # 'token' must contain an entry with the name of LOCKBOX_TOTP ``` The token can be automatically copied to the clipboard too ``` -lb totp -clip token +lb totp clip token ``` ## git integration diff --git a/cmd/main.go b/cmd/main.go @@ -39,8 +39,6 @@ func handleEarly(command string, args []string) (bool, error) { case cli.VersionCommand: fmt.Printf("version: %s\n", version) return true, nil - case cli.TOTPCommand: - return true, totp.Call(args) case cli.ClearCommand: return true, clearClipboard() } @@ -74,12 +72,12 @@ func run() error { return app.List(p) case cli.MoveCommand: return app.Move(p) - case cli.InsertCommand: - insertArgs, err := app.ReadArgs(p) - if err != nil { - return err + case cli.InsertCommand, cli.MultiLineCommand: + mode := app.SingleLineInsert + if command == cli.MultiLineCommand { + mode = app.MultiLineInsert } - return insertArgs.Do(p) + return app.Insert(p, mode) case cli.RemoveCommand: return app.Remove(p) case cli.StatsCommand: @@ -88,6 +86,16 @@ func run() error { return app.ShowClip(p, command == cli.ShowCommand) case cli.HashCommand: return app.Hash(p) + case cli.TOTPCommand: + args, err := totp.NewArguments(sub, inputs.TOTPToken()) + if err != nil { + return err + } + if args.Mode == totp.InsertMode { + p.SetArgs(args.Entry) + return app.Insert(p, app.TOTPInsert) + } + return args.Do(p.Transaction()) default: return fmt.Errorf("unknown command: %s", command) } diff --git a/internal/app/core.go b/internal/app/core.go @@ -59,21 +59,16 @@ func (a *DefaultCommand) Confirm(prompt string) bool { return yesNo } +// SetArgs allow updating the command args +func (a *DefaultCommand) SetArgs(args ...string) { + a.args = args +} + // IsPipe will indicate if we're receiving pipe input func (a *DefaultCommand) IsPipe() bool { return inputs.IsInputFromPipe() } -// TOTPToken will get the configured totp token name -func (a *DefaultCommand) TOTPToken() string { - return inputs.TOTPToken() -} - -// IsNoTOTP indicates if TOTP operations are disabled -func (a *DefaultCommand) IsNoTOTP() (bool, error) { - return inputs.IsNoTOTP() -} - // Input will read user input func (a *DefaultCommand) Input(pipe, multi bool) ([]byte, error) { return inputs.GetUserInputPassword(pipe, multi) diff --git a/internal/app/info_test.go b/internal/app/info_test.go @@ -28,7 +28,7 @@ func TestHelpInfo(t *testing.T) { } old := buf.String() buf = bytes.Buffer{} - ok, err = app.Info(&buf, "help", []string{"-verbose"}) + ok, err = app.Info(&buf, "help", []string{"verbose"}) if !ok || err != nil { t.Errorf("invalid error: %v", err) } @@ -38,7 +38,7 @@ func TestHelpInfo(t *testing.T) { if _, err = app.Info(&buf, "help", []string{"-verb"}); err.Error() != "invalid help option" { t.Errorf("invalid error: %v", err) } - if _, err = app.Info(&buf, "help", []string{"-verbose", "A"}); err.Error() != "invalid help command" { + if _, err = app.Info(&buf, "help", []string{"verbose", "A"}); err.Error() != "invalid help command" { t.Errorf("invalid error: %v", err) } } @@ -54,17 +54,17 @@ func TestBashInfo(t *testing.T) { t.Error("nothing written") } buf = bytes.Buffer{} - ok, err = app.Info(&buf, "bash", []string{"-defaults"}) + ok, err = app.Info(&buf, "bash", []string{"defaults"}) if !ok || err != nil { t.Errorf("invalid error: %v", err) } if buf.String() == "" { t.Error("nothing written") } - if _, err = app.Info(&buf, "bash", []string{"-default"}); err.Error() != "invalid argument" { + if _, err = app.Info(&buf, "bash", []string{"default"}); err.Error() != "invalid argument" { t.Errorf("invalid error: %v", err) } - if _, err = app.Info(&buf, "bash", []string{"test", "-default"}); err.Error() != "invalid argument" { + if _, err = app.Info(&buf, "bash", []string{"test", "default"}); err.Error() != "invalid argument" { t.Errorf("invalid error: %v", err) } } @@ -80,17 +80,17 @@ func TestEnvInfo(t *testing.T) { t.Error("nothing written") } buf = bytes.Buffer{} - ok, err = app.Info(&buf, "env", []string{"-defaults"}) + ok, err = app.Info(&buf, "env", []string{"defaults"}) if !ok || err != nil { t.Errorf("invalid error: %v", err) } if buf.String() == "" { t.Error("nothing written") } - if _, err = app.Info(&buf, "env", []string{"-default"}); err.Error() != "invalid argument" { + if _, err = app.Info(&buf, "env", []string{"default"}); err.Error() != "invalid argument" { t.Errorf("invalid error: %v", err) } - if _, err = app.Info(&buf, "env", []string{"test", "-default"}); err.Error() != "invalid argument" { + if _, err = app.Info(&buf, "env", []string{"test", "default"}); err.Error() != "invalid argument" { t.Errorf("invalid error: %v", err) } } diff --git a/internal/app/insert.go b/internal/app/insert.go @@ -7,79 +7,37 @@ import ( "strings" "github.com/enckse/lockbox/internal/backend" - "github.com/enckse/lockbox/internal/cli" - "github.com/enckse/lockbox/internal/totp" ) type ( + // InsertMode changes how inserts are handled + InsertMode uint // InsertOptions are functions required for insert InsertOptions interface { CommandOptions IsPipe() bool Input(bool, bool) ([]byte, error) - TOTPToken() string - IsNoTOTP() (bool, error) - } - // InsertArgs are parsed insert settings for insert commands - InsertArgs struct { - Entry string - Multi bool } ) -// ReadArgs will read and check insert args -func ReadArgs(cmd InsertOptions) (InsertArgs, error) { - multi := false - isTOTP := false - idx := 0 - noTOTP, err := cmd.IsNoTOTP() - if err != nil { - return InsertArgs{}, err - } - args := cmd.Args() - switch len(args) { - case 0: - return InsertArgs{}, errors.New("insert requires an entry") - case 1: - case 2: - opt := args[0] - switch opt { - case cli.InsertMultiCommand: - multi = true - case cli.InsertTOTPCommand: - if noTOTP { - return InsertArgs{}, totp.ErrNoTOTP - } - isTOTP = true - default: - return InsertArgs{}, errors.New("unknown argument") - } - idx = 1 - default: - return InsertArgs{}, errors.New("too many arguments") - } - entry := args[idx] - if !noTOTP { - totpToken := cmd.TOTPToken() - hasSuffixTOTP := strings.HasSuffix(entry, backend.NewSuffix(totpToken)) - if isTOTP { - if !hasSuffixTOTP { - entry = backend.NewPath(entry, totpToken) - } - } else { - if hasSuffixTOTP { - return InsertArgs{}, errors.New("can not insert totp entry without totp flag") - } - } - - } - return InsertArgs{Multi: multi, Entry: entry}, nil -} +const ( + // SingleLineInsert is a single line entry + SingleLineInsert InsertMode = iota + // MultiLineInsert is a multiline insert + MultiLineInsert + // TOTPInsert is a singleline but from TOTP subcommands + TOTPInsert +) -// Do will execute an insert -func (args InsertArgs) Do(cmd InsertOptions) error { +// Insert will execute an insert +func Insert(cmd InsertOptions, mode InsertMode) error { t := cmd.Transaction() - existing, err := t.Get(args.Entry, backend.BlankValue) + args := cmd.Args() + if len(args) != 1 { + return errors.New("invalid insert, no entry given") + } + entry := args[0] + existing, err := t.Get(entry, backend.BlankValue) if err != nil { return err } @@ -91,12 +49,12 @@ func (args InsertArgs) Do(cmd InsertOptions) error { } } } - password, err := cmd.Input(isPipe, args.Multi) + password, err := cmd.Input(isPipe, mode == MultiLineInsert) if err != nil { return fmt.Errorf("invalid input: %w", err) } p := strings.TrimSpace(string(password)) - if err := t.Insert(args.Entry, p); err != nil { + if err := t.Insert(entry, p); err != nil { return err } if !isPipe { diff --git a/internal/app/insert_test.go b/internal/app/insert_test.go @@ -17,6 +17,7 @@ type ( input func(bool, bool) ([]byte, error) pipe func() bool token func() string + isMulti bool } ) @@ -35,6 +36,7 @@ func (m *mockInsert) IsPipe() bool { } func (m *mockInsert) Input(pipe, multi bool) ([]byte, error) { + m.isMulti = multi return m.input(pipe, multi) } @@ -58,102 +60,33 @@ func (m *mockInsert) Transaction() *backend.Transaction { return m.command.Transaction() } -func TestInsertArgs(t *testing.T) { - m := newMockInsert(t) - m.noTOTP = func() (bool, error) { - return true, nil - } - if _, err := app.ReadArgs(m); err == nil || err.Error() != "insert requires an entry" { - t.Errorf("invalid error: %v", err) - } - m.command.args = []string{"test", "test", "test"} - if _, err := app.ReadArgs(m); err == nil || err.Error() != "too many arguments" { - t.Errorf("invalid error: %v", err) - } - m.command.args = []string{"test"} - r, err := app.ReadArgs(m) - if err != nil { - t.Errorf("invalid error: %v", err) - } - if r.Multi || r.Entry != "test" { - t.Error("invalid parse") - } - m.command.args = []string{"-t", "b"} - if _, err := app.ReadArgs(m); err == nil || err.Error() != "unknown argument" { - t.Errorf("invalid error: %v", err) - } - m.command.args = []string{"-multi", "test3"} - r, err = app.ReadArgs(m) - if err != nil { - t.Errorf("invalid error: %v", err) - } - if !r.Multi || r.Entry != "test3" { - t.Error("invalid parse") - } - m.token = func() string { - return "test3" - } - m.command.args = []string{"-multi", "test/test3"} - r, err = app.ReadArgs(m) - if err != nil { - t.Errorf("invalid error: %v", err) - } - m.noTOTP = func() (bool, error) { - return false, nil - } - if _, err := app.ReadArgs(m); err == nil || err.Error() != "can not insert totp entry without totp flag" { - t.Errorf("invalid error: %v", err) - } - m.command.args = []string{"test/test3"} - if _, err := app.ReadArgs(m); err == nil || err.Error() != "can not insert totp entry without totp flag" { - t.Errorf("invalid error: %v", err) - } - m.command.args = []string{"-totp", "test/test3"} - r, err = app.ReadArgs(m) - if err != nil { - t.Errorf("invalid error: %v", err) - } - if r.Entry != "test/test3" { - t.Error("invalid parse") - } - m.command.args = []string{"-totp", "test"} - r, err = app.ReadArgs(m) - if err != nil { - t.Errorf("invalid error: %v", err) - } - if r.Entry != "test/test3" { - t.Error("invalid parse") - } -} - func TestInsertDo(t *testing.T) { m := newMockInsert(t) - args := app.InsertArgs{} m.pipe = func() bool { return false } - args.Entry = "test/test2" + m.command.args = []string{"test/test2"} m.command.confirm = false m.input = func(bool, bool) ([]byte, error) { return nil, errors.New("failure") } m.command.buf = bytes.Buffer{} - if err := args.Do(m); err == nil || err.Error() != "invalid input: failure" { + if err := app.Insert(m, app.SingleLineInsert); err == nil || err.Error() != "invalid input: failure" { t.Errorf("invalid error: %v", err) } m.command.confirm = false m.pipe = func() bool { return true } - if err := args.Do(m); err == nil || err.Error() != "invalid input: failure" { + if err := app.Insert(m, app.SingleLineInsert); err == nil || err.Error() != "invalid input: failure" { t.Errorf("invalid error: %v", err) } m.input = func(bool, bool) ([]byte, error) { return []byte("TEST"), nil } m.command.confirm = true - args.Entry = "a/b/c" - if err := args.Do(m); err != nil { + m.command.args = []string{"a/b/c"} + if err := app.Insert(m, app.SingleLineInsert); err != nil { t.Errorf("invalid error: %v", err) } if m.command.buf.String() != "" { @@ -163,15 +96,15 @@ func TestInsertDo(t *testing.T) { return false } m.command.buf = bytes.Buffer{} - if err := args.Do(m); err != nil { + if err := app.Insert(m, app.SingleLineInsert); err != nil { t.Errorf("invalid error: %v", err) } if m.command.buf.String() == "" { t.Error("invalid insert") } m.command.buf = bytes.Buffer{} - args.Entry = "test/test2/test1" - if err := args.Do(m); err != nil { + m.command.args = []string{"test/test2/test1"} + if err := app.Insert(m, app.SingleLineInsert); err != nil { t.Errorf("invalid error: %v", err) } if m.command.buf.String() == "" { @@ -179,11 +112,29 @@ func TestInsertDo(t *testing.T) { } m.command.confirm = false m.command.buf = bytes.Buffer{} - args.Entry = "test/test2/test1" - if err := args.Do(m); err != nil { + m.command.args = []string{"test/test2/test1"} + if err := app.Insert(m, app.SingleLineInsert); err != nil { t.Errorf("invalid error: %v", err) } if m.command.buf.String() != "" { t.Error("invalid insert") } + m.isMulti = false + m.command.confirm = true + m.command.buf = bytes.Buffer{} + m.command.args = []string{"test/test2/test1"} + if err := app.Insert(m, app.SingleLineInsert); err != nil { + t.Errorf("invalid error: %v", err) + } + if m.command.buf.String() == "" || m.isMulti { + t.Error("invalid insert") + } + m.command.buf = bytes.Buffer{} + m.command.args = []string{"test/test2/test1"} + if err := app.Insert(m, app.MultiLineInsert); err != nil { + t.Errorf("invalid error: %v", err) + } + if m.command.buf.String() == "" || !m.isMulti { + t.Error("invalid insert") + } } diff --git a/internal/cli/completions.bash b/internal/cli/completions.bash @@ -13,10 +13,7 @@ _{{ $.Executable }}() { if [ "$COMP_CWORD" -eq 2 ]; then case ${COMP_WORDS[1]} in {{- if not $.ReadOnly }} - "{{ $.InsertCommand }}") -{{- range $key, $value := .InsertSubCommands }} - opts="$opts {{ $value }}" -{{- end}} + "{{ $.InsertCommand }}" | "{{ $.MultiLineCommand }}") opts="$opts $({{ $.DoList }})" ;; "{{ $.HelpCommand }}") @@ -32,7 +29,6 @@ _{{ $.Executable }}() { {{- range $key, $value := .TOTPSubCommands }} opts="$opts {{ $value }}" {{- end}} - opts="$opts "$({{ $.DoTOTPList }}) ;; {{- end}} "{{ $.ShowCommand }}" | "{{ $.StatsCommand }}" {{ if not $.ReadOnly }}| "{{ $.RemoveCommand }}" {{end}} {{ if $.CanClip }} | "{{ $.ClipCommand }}" {{end}}) @@ -43,15 +39,6 @@ _{{ $.Executable }}() { if [ "$COMP_CWORD" -eq 3 ]; then case "${COMP_WORDS[1]}" in {{- if not $.ReadOnly }} - "{{ $.InsertCommand }}") - case "${COMP_WORDS[2]}" in -{{- range $key, $value := .InsertSubCommands }} - "{{ $value }}") - opts=$({{ $.DoList }}) - ;; -{{- end }} - esac - ;; "{{ $.MoveCommand }}") opts=$({{ $.DoList }}) ;; diff --git a/internal/cli/core.go b/internal/cli/core.go @@ -40,31 +40,33 @@ const ( // HelpCommand shows usage HelpCommand = "help" // HelpAdvancedCommand shows advanced help - HelpAdvancedCommand = "-verbose" + HelpAdvancedCommand = "verbose" // RemoveCommand removes an entry RemoveCommand = "rm" // EnvCommand shows environment information used by lockbox EnvCommand = "env" - // InsertMultiCommand handles multi-line inserts - InsertMultiCommand = "-multi" - // InsertTOTPCommand is a helper for totp inserts - InsertTOTPCommand = "-totp" // TOTPClipCommand is the argument for copying totp codes to clipboard - TOTPClipCommand = "-clip" + TOTPClipCommand = ClipCommand // TOTPShortCommand is the argument for getting the short version of a code - TOTPShortCommand = "-short" + TOTPShortCommand = "short" // TOTPListCommand will list the totp-enabled entries - TOTPListCommand = "-list" + TOTPListCommand = ListCommand // TOTPOnceCommand will perform like a normal totp request but not refresh - TOTPOnceCommand = "-once" + TOTPOnceCommand = "once" // EnvDefaultsCommand will display the default env variables, not those set - EnvDefaultsCommand = "-defaults" + EnvDefaultsCommand = "defaults" // BashCommand is the command to generate bash completions BashCommand = "bash" // BashDefaultsCommand will generate environment agnostic completions - BashDefaultsCommand = "-defaults" + BashDefaultsCommand = "defaults" // ReKeyCommand will rekey the underlying database ReKeyCommand = "rekey" + // MultiLineCommand handles multi-line inserts (when not piped) + MultiLineCommand = "multiline" + // TOTPShowCommand is for showing the TOTP token + TOTPShowCommand = ShowCommand + // TOTPInsertCommand is for inserting totp tokens + TOTPInsertCommand = InsertCommand ) var ( @@ -83,12 +85,12 @@ type ( CanTOTP bool ReadOnly bool InsertCommand string - InsertSubCommands []string TOTPSubCommands []string TOTPListCommand string RemoveCommand string ClipCommand string ShowCommand string + MultiLineCommand string MoveCommand string TOTPCommand string DoTOTPList string @@ -134,19 +136,19 @@ func BashCompletions(defaults bool) ([]string, error) { Executable: name, InsertCommand: InsertCommand, RemoveCommand: RemoveCommand, - TOTPSubCommands: []string{TOTPShortCommand, TOTPOnceCommand}, + TOTPSubCommands: []string{TOTPShortCommand, TOTPOnceCommand, TOTPShowCommand}, TOTPListCommand: TOTPListCommand, ClipCommand: ClipCommand, ShowCommand: ShowCommand, + MultiLineCommand: MultiLineCommand, StatsCommand: StatsCommand, - InsertSubCommands: []string{InsertMultiCommand, InsertTOTPCommand}, HelpCommand: HelpCommand, HelpAdvancedCommand: HelpAdvancedCommand, TOTPCommand: TOTPCommand, MoveCommand: MoveCommand, DoList: fmt.Sprintf("%s %s", name, ListCommand), DoTOTPList: fmt.Sprintf("%s %s %s", name, TOTPCommand, TOTPListCommand), - Options: []string{EnvCommand, HelpCommand, ListCommand, ShowCommand, VersionCommand, StatsCommand}, + Options: []string{MultiLineCommand, EnvCommand, HelpCommand, ListCommand, ShowCommand, VersionCommand, StatsCommand}, } isReadOnly := false isClip := true @@ -181,6 +183,7 @@ func BashCompletions(defaults bool) ([]string, error) { } if !c.ReadOnly { c.Options = append(c.Options, MoveCommand, RemoveCommand, InsertCommand) + c.TOTPSubCommands = append(c.TOTPSubCommands, TOTPInsertCommand) } if c.CanTOTP { c.Options = append(c.Options, TOTPCommand) @@ -210,18 +213,19 @@ func Usage(verbose bool) ([]string, error) { results = append(results, command(HelpCommand, "", "show this usage information")) results = append(results, subCommand(HelpCommand, HelpAdvancedCommand, "", "display verbose help information")) results = append(results, command(InsertCommand, "entry", "insert a new entry into the store")) - results = append(results, subCommand(InsertCommand, InsertMultiCommand, "entry", "insert a multi-line entry")) - results = append(results, subCommand(InsertCommand, InsertTOTPCommand, "entry", "insert a new totp entry")) results = append(results, command(ListCommand, "", "list entries")) results = append(results, command(MoveCommand, "src dst", "move an entry from source to destination")) + results = append(results, command(MultiLineCommand, "entry", "insert a multiline entry into the store")) results = append(results, command(RemoveCommand, "entry", "remove an entry from the store")) results = append(results, command(ShowCommand, "entry", "show the entry's value")) results = append(results, command(StatsCommand, "entry", "display entry detail information")) results = append(results, command(TOTPCommand, "entry", "display an updating totp generated code")) results = append(results, subCommand(TOTPCommand, TOTPClipCommand, "entry", "copy totp code to clipboard")) + results = append(results, subCommand(TOTPCommand, TOTPInsertCommand, "entry", "insert a new totp entry into the store")) results = append(results, subCommand(TOTPCommand, TOTPListCommand, "", "list entries with totp settings")) results = append(results, subCommand(TOTPCommand, TOTPOnceCommand, "entry", "display the first generated code")) results = append(results, subCommand(TOTPCommand, TOTPShortCommand, "entry", "display the first generated code (no details)")) + results = append(results, subCommand(TOTPCommand, TOTPShowCommand, "entry", "show the totp entry")) results = append(results, command(VersionCommand, "", "display version information")) sort.Strings(results) usage := []string{fmt.Sprintf("%s usage:", name)} diff --git a/internal/cli/core_test.go b/internal/cli/core_test.go @@ -10,11 +10,11 @@ import ( func TestUsage(t *testing.T) { u, _ := cli.Usage(false) - if len(u) != 21 { + if len(u) != 22 { t.Errorf("invalid usage, out of date? %d", len(u)) } u, _ = cli.Usage(true) - if len(u) != 80 { + if len(u) != 81 { t.Errorf("invalid verbose usage, out of date? %d", len(u)) } for _, usage := range u { diff --git a/internal/totp/core.go b/internal/totp/core.go @@ -22,11 +22,12 @@ import ( var ErrNoTOTP = errors.New("TOTP is disabled") type ( - arguments struct { - Clip bool - Once bool - Short bool - List bool + // Mode is the operating mode for TOTP operations + Mode int + // Arguments are the parsed TOTP call arguments + Arguments struct { + Mode Mode + Entry string } totpWrapper struct { opts otp.ValidateOpts @@ -34,6 +35,23 @@ type ( } ) +const ( + // UnknownMode is an unknown command + UnknownMode Mode = iota + // InsertMode is inserting a new totp token + InsertMode + // ShowMode will show the token + ShowMode + // ClipMode will copy to clipboard + ClipMode + // ShortMode will display minimal information to display the token + ShortMode + // ListMode lists the available tokens + ListMode + // OnceMode will only show the token once and exit + OnceMode +) + func clear() { cmd := exec.Command("clear") cmd.Stdout = os.Stdout @@ -54,26 +72,24 @@ func (w totpWrapper) generateCode() (string, error) { return otp.GenerateCodeCustom(w.code, time.Now(), w.opts) } -func display(token string, args arguments) error { +func (args *Arguments) display(tx *backend.Transaction) error { interactive, err := inputs.IsInteractive() if err != nil { return err } - if args.Short { + if args.Mode == ShortMode { interactive = false } - if !interactive && args.Clip { + once := args.Mode == OnceMode + clip := args.Mode == ClipMode + if !interactive && clip { return errors.New("clipboard not available in non-interactive mode") } coloring, err := colors.NewTerminal(colors.Red) if err != nil { return err } - t, err := backend.NewTransaction() - if err != nil { - return err - } - entity, err := t.Get(backend.NewPath(token, inputs.TOTPToken()), backend.SecretValue) + entity, err := tx.Get(backend.NewPath(args.Entry, inputs.TOTPToken()), backend.SecretValue) if err != nil { return err } @@ -102,13 +118,13 @@ func display(token string, args arguments) error { first := true running := 0 lastSecond := -1 - if !args.Clip { - if !args.Once { + if !clip { + if !once { clear() } } clipboard := platform.Clipboard{} - if args.Clip { + if clip { clipboard, err = platform.NewClipboard() if err != nil { return err @@ -153,27 +169,30 @@ func display(token string, args arguments) error { } expires := fmt.Sprintf("%s%s (%s)%s", startColor, now.Format("15:04:05"), leftString, endColor) outputs := []string{expires} - if !args.Clip { - outputs = append(outputs, fmt.Sprintf("%s\n %s", token, code)) - if !args.Once { + if !clip { + outputs = append(outputs, fmt.Sprintf("%s\n %s", args.Entry, code)) + if !once { outputs = append(outputs, "-> CTRL+C to exit") } } else { fmt.Printf("-> %s\n", expires) return clipboard.CopyTo(code) } - if !args.Once { + if !once { clear() } fmt.Printf("%s\n", strings.Join(outputs, "\n\n")) - if args.Once { + if once { return nil } } } -// Call handles UI for TOTP tokens. -func Call(args []string) error { +// Do will perform the TOTP operation +func (args *Arguments) Do(tx *backend.Transaction) error { + if args == nil || args.Mode == UnknownMode { + return errors.New("unknown totp mode") + } off, err := inputs.IsNoTOTP() if err != nil { return err @@ -181,17 +200,8 @@ func Call(args []string) error { if off { return ErrNoTOTP } - if len(args) > 2 || len(args) < 1 { - return errors.New("invalid arguments, subkey and entry required") - } - cmd := args[0] - options := parseArgs(cmd) - if options.List { - t, err := backend.NewTransaction() - if err != nil { - return err - } - e, err := t.QueryCallback(backend.QueryOptions{Mode: backend.SuffixMode, Criteria: backend.NewSuffix(inputs.TOTPToken())}) + if args.Mode == ListMode { + e, err := tx.QueryCallback(backend.QueryOptions{Mode: backend.SuffixMode, Criteria: backend.NewSuffix(inputs.TOTPToken())}) if err != nil { return err } @@ -200,20 +210,50 @@ func Call(args []string) error { } return nil } - if len(args) == 2 { - if !options.Clip && !options.Short && !options.Once { - return errors.New("invalid sub command") - } - cmd = args[1] - } - return display(cmd, options) + return args.display(tx) } -func parseArgs(arg string) arguments { - args := arguments{} - args.Clip = arg == cli.TOTPClipCommand - args.Once = arg == cli.TOTPOnceCommand - args.Short = arg == cli.TOTPShortCommand - args.List = arg == cli.TOTPListCommand - return args +// NewArguments will parse the input arguments +func NewArguments(args []string, tokenType string) (*Arguments, error) { + if len(args) == 0 { + return nil, errors.New("not enough arguments for totp") + } + if strings.TrimSpace(tokenType) == "" { + return nil, errors.New("invalid token type, not set?") + } + opts := &Arguments{Mode: UnknownMode} + sub := args[0] + needs := true + switch sub { + case cli.TOTPListCommand: + needs = false + if len(args) != 1 { + return nil, errors.New("list takes no arguments") + } + opts.Mode = ListMode + case cli.TOTPInsertCommand: + opts.Mode = InsertMode + case cli.TOTPShowCommand: + opts.Mode = ShowMode + case cli.TOTPClipCommand: + opts.Mode = ClipMode + case cli.TOTPShortCommand: + opts.Mode = ShortMode + case cli.TOTPOnceCommand: + opts.Mode = OnceMode + default: + return nil, errors.New("unknown totp command") + } + if needs { + if len(args) != 2 { + return nil, errors.New("missing entry") + } + opts.Entry = args[1] + if opts.Mode == InsertMode { + if !strings.HasSuffix(opts.Entry, tokenType) { + opts.Entry = backend.NewPath(opts.Entry, tokenType) + } + } + } + return opts, nil } diff --git a/internal/totp/core_test.go b/internal/totp/core_test.go @@ -0,0 +1,56 @@ +package totp_test + +import ( + "testing" + + "github.com/enckse/lockbox/internal/totp" +) + +func TestNewArgumentsErrors(t *testing.T) { + if _, err := totp.NewArguments(nil, ""); err == nil || err.Error() != "not enough arguments for totp" { + t.Errorf("invalid error: %v", err) + } + if _, err := totp.NewArguments([]string{"test"}, ""); err == nil || err.Error() != "invalid token type, not set?" { + t.Errorf("invalid error: %v", err) + } + if _, err := totp.NewArguments([]string{"test"}, "a"); err == nil || err.Error() != "unknown totp command" { + t.Errorf("invalid error: %v", err) + } + if _, err := totp.NewArguments([]string{"ls", "test"}, "a"); err == nil || err.Error() != "list takes no arguments" { + t.Errorf("invalid error: %v", err) + } + if _, err := totp.NewArguments([]string{"show"}, "a"); err == nil || err.Error() != "missing entry" { + t.Errorf("invalid error: %v", err) + } +} + +func TestNewArguments(t *testing.T) { + args, _ := totp.NewArguments([]string{"ls"}, "test") + if args.Mode != totp.ListMode || args.Entry != "" { + t.Error("invalid args") + } + args, _ = totp.NewArguments([]string{"show", "test"}, "test") + if args.Mode != totp.ShowMode || args.Entry != "test" { + t.Error("invalid args") + } + args, _ = totp.NewArguments([]string{"clip", "test"}, "test") + if args.Mode != totp.ClipMode || args.Entry != "test" { + t.Error("invalid args") + } + args, _ = totp.NewArguments([]string{"short", "test"}, "test") + if args.Mode != totp.ShortMode || args.Entry != "test" { + t.Error("invalid args") + } + args, _ = totp.NewArguments([]string{"once", "test"}, "test") + if args.Mode != totp.OnceMode || args.Entry != "test" { + t.Error("invalid args") + } + args, _ = totp.NewArguments([]string{"insert", "test2"}, "test") + if args.Mode != totp.InsertMode || args.Entry != "test2/test" { + t.Errorf("invalid args: %s", args.Entry) + } + args, _ = totp.NewArguments([]string{"insert", "test2/test"}, "test") + if args.Mode != totp.InsertMode || args.Entry != "test2/test" { + t.Errorf("invalid args: %s", args.Entry) + } +} diff --git a/tests/run.sh b/tests/run.sh @@ -22,6 +22,7 @@ _execute() { echo test |${LB_BINARY} insert /keys/k/one echo test |${LB_BINARY} insert keys/aa/b//s///e printf "test3\ntest4\n" |${LB_BINARY} insert keys2/k/three + printf "test3\ntest4\n" |${LB_BINARY} multiline keys2/k/three ${LB_BINARY} ls echo y |${LB_BINARY} rm keys/k/one echo @@ -30,10 +31,10 @@ _execute() { ${LB_BINARY} show keys/k/one2 ${LB_BINARY} show keys2/k/three ${LB_BINARY} stats keys2/k/three - echo 5ae472abqdekjqykoyxk7hvc2leklq5n |${LB_BINARY} insert -totp test/k - echo 5ae472abqdekjqykoyxk7hvc2leklq5n |${LB_BINARY} insert -totp test/k/totp - ${LB_BINARY} totp -list - ${LB_BINARY} totp test/k + echo 5ae472abqdekjqykoyxk7hvc2leklq5n |${LB_BINARY} totp insert test/k + echo 5ae472abqdekjqykoyxk7hvc2leklq5n |${LB_BINARY} totp insert test/k/totp + ${LB_BINARY} totp ls + ${LB_BINARY} totp show test/k ${LB_BINARY} hash "$LOCKBOX_STORE" echo y |${LB_BINARY} rm keys2/k/three echo