commit 148519703b7c95fa4de47289d701d57fa581fec0
parent 64dca2e6795efd486955df6b692fef980d715789
Author: Sean Enck <sean@ttypty.com>
Date: Mon, 27 Mar 2023 20:04:05 -0400
reworking all cli args
Diffstat:
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