lockbox

password manager
Log | Files | Refs | README | LICENSE

commit b6ebda8d29faa567af5e59f50d398cad77c198d3
parent f808fd7a48ff09a27b4f1a4eecb15b3cdfea6419
Author: Sean Enck <sean@ttypty.com>
Date:   Wed,  4 Jun 2025 22:47:06 -0400

support totp find

Diffstat:
Mcmd/lb/main_test.go | 1+
Mcmd/lb/tests/expected.log | 1+
Minternal/app/commands/core.go | 2++
Minternal/app/completions/core.go | 2++
Minternal/app/completions/shell/bash.sh | 1+
Minternal/app/completions/shell/fish.sh | 5+++--
Minternal/app/completions/shell/zsh.sh | 1+
Minternal/app/help/core.go | 1+
Minternal/app/help/core_test.go | 4++--
Minternal/app/totp.go | 24++++++++++++++++++++++--
Minternal/app/totp_test.go | 24++++++++++++++++++++++++
11 files changed, 60 insertions(+), 6 deletions(-)

diff --git a/cmd/lb/main_test.go b/cmd/lb/main_test.go @@ -228,6 +228,7 @@ func test(profile string) error { r.run("echo 5ae472abqdekjqykoyxk7hvc2leklq5n |", "totp insert test/k") r.run("echo 5ae472abqdekjqykoyxk7hvc2leklq5n |", "totp insert test/k/totp") r.run("", "totp ls") + r.run("", "totp find test") r.run("", "totp show test/k") r.run("", "totp once test/k") r.run("", "totp minimal test/k") diff --git a/cmd/lb/tests/expected.log b/cmd/lb/tests/expected.log @@ -39,6 +39,7 @@ test4 } test/k +test/k XXXXXX XXXXXX XXXXXX diff --git a/internal/app/commands/core.go b/internal/app/commands/core.go @@ -38,6 +38,8 @@ const ( TOTPMinimal = "minimal" // TOTPList will list the totp-enabled entries TOTPList = List + // TOTPFind allows a filter search of TOTP listed entries + TOTPFind = Find // TOTPOnce will perform like a normal totp request but not refresh TOTPOnce = "once" // CompletionsBash is the command to generate bash completions diff --git a/internal/app/completions/core.go b/internal/app/completions/core.go @@ -19,6 +19,7 @@ type ( Template struct { InsertCommand string TOTPListCommand string + TOTPFindCommand string RemoveCommand string ClipCommand string ShowCommand string @@ -104,6 +105,7 @@ func Generate(completionType, exe string) ([]string, error) { InsertCommand: commands.Insert, RemoveCommand: commands.Remove, TOTPListCommand: commands.TOTPList, + TOTPFindCommand: commands.TOTPFind, ClipCommand: commands.Clip, ShowCommand: commands.Show, MultiLineCommand: commands.MultiLine, diff --git a/internal/app/completions/shell/bash.sh b/internal/app/completions/shell/bash.sh @@ -40,6 +40,7 @@ _{{ $.Executable }}() { ;; "{{ $.TOTPCommand }}") opts="{{ $.TOTPListCommand }} " + opts="{{ $.TOTPFindCommand }} " {{- range $key, $value := .TOTPSubCommands }} if {{ $value.Conditional }}; then opts="$opts {{ $value.Key }}" diff --git a/internal/app/completions/shell/fish.sh b/internal/app/completions/shell/fish.sh @@ -24,7 +24,8 @@ function {{ $.Executable }}-completion end end if {{ $.Conditionals.Not.CanTOTP }} - set -f totps "" + set -f totpbase "{{ $.TOTPFindCommand }} {{ $.TOTPListCommand }}" + set -f totps " $totpbase" {{- range $idx, $value := $.TOTPSubCommands }} {{- if gt $idx 0 }} set -f totps " $totps" @@ -35,7 +36,7 @@ function {{ $.Executable }}-completion {{- end }} complete -c {{ $.Executable }} -n "__fish_seen_subcommand_from {{ $.TOTPCommand }}; and not __fish_seen_subcommand_from $totps" -a "$totps" if {{ $.Conditionals.Not.AskMode }} - complete -c {{ $.Executable }} -n "__fish_seen_subcommand_from {{ $.TOTPCommand }}; and __fish_seen_subcommand_from $totps; and test (count (commandline -opc)) -lt 4" -a "({{ $.DoTOTPList }})" + complete -c {{ $.Executable }} -n "__fish_seen_subcommand_from {{ $.TOTPCommand }}; and __fish_seen_subcommand_from $totps; and not __fish_seen_subcommand_from $totpbase; and test (count (commandline -opc)) -lt 4" -a "({{ $.DoTOTPList }})" end end if {{ $.Conditionals.Not.CanClip }} diff --git a/internal/app/completions/shell/zsh.sh b/internal/app/completions/shell/zsh.sh @@ -65,6 +65,7 @@ _{{ $.Executable }}() { case "$len" in 3) compadd "$@" {{ $.TOTPListCommand }} + compadd "$@" {{ $.TOTPFindCommand }} {{- range $key, $value := .TOTPSubCommands }} if {{ $value.Conditional }}; then compadd "$@" {{ $value.Key }} diff --git a/internal/app/help/core.go b/internal/app/help/core.go @@ -107,6 +107,7 @@ func Usage(verbose bool, exe string) ([]string, error) { results = append(results, subCommand(commands.TOTP, commands.TOTPOnce, isEntry, "display the first generated code")) results = append(results, subCommand(commands.TOTP, commands.TOTPMinimal, isEntry, "display one generated code (no details)")) results = append(results, subCommand(commands.TOTP, commands.TOTPShow, isEntry, "show the totp entry")) + results = append(results, subCommand(commands.TOTP, commands.TOTPFind, isFilter, "find matching entries with totp settings")) results = append(results, command(commands.Version, "", "display version information")) sort.Strings(results) usage := []string{fmt.Sprintf("%s usage:", exe)} diff --git a/internal/app/help/core_test.go b/internal/app/help/core_test.go @@ -9,11 +9,11 @@ import ( func TestUsage(t *testing.T) { u, _ := help.Usage(false, "lb") - if len(u) != 28 { + if len(u) != 29 { t.Errorf("invalid usage, out of date? %d", len(u)) } u, _ = help.Usage(true, "lb") - if len(u) != 102 { + if len(u) != 103 { t.Errorf("invalid verbose usage, out of date? %d", len(u)) } for _, usage := range u { diff --git a/internal/app/totp.go b/internal/app/totp.go @@ -4,6 +4,7 @@ package app import ( "errors" "fmt" + "regexp" "slices" "strconv" "strings" @@ -60,6 +61,8 @@ const ( MinimalTOTPMode // ListTOTPMode lists the available tokens ListTOTPMode + // FindTOTPMode is list but with a regexp filter + FindTOTPMode // OnceTOTPMode will only show the token once and exit OnceTOTPMode ) @@ -222,17 +225,32 @@ func (args *TOTPArguments) Do(opts TOTPOptions) error { if !opts.CanTOTP() { return ErrNoTOTP } - if args.Mode == ListTOTPMode { + if args.Mode == ListTOTPMode || args.Mode == FindTOTPMode { e, err := opts.app.Transaction().QueryCallback(backend.QueryOptions{Mode: backend.SuffixMode, Criteria: backend.NewSuffix(args.token)}) if err != nil { return err } writer := opts.app.Writer() + printer := func(entity backend.Entity) { + fmt.Fprintf(writer, "%s\n", entity.Directory()) + } + filter := printer + if args.Mode == FindTOTPMode { + re, err := regexp.Compile(args.Entry) + if err != nil { + return err + } + filter = func(entity backend.Entity) { + if re.MatchString(entity.Path) { + printer(entity) + } + } + } for entry, err := range e { if err != nil { return err } - fmt.Fprintf(writer, "%s\n", entry.Directory()) + filter(entry) } return nil } @@ -258,6 +276,8 @@ func NewTOTPArguments(args []string, tokenType string) (*TOTPArguments, error) { return nil, errors.New("list takes no arguments") } opts.Mode = ListTOTPMode + case commands.TOTPFind: + opts.Mode = FindTOTPMode case commands.TOTPInsert: opts.Mode = InsertTOTPMode case commands.TOTPShow: diff --git a/internal/app/totp_test.go b/internal/app/totp_test.go @@ -100,6 +100,10 @@ func TestNewTOTPArguments(t *testing.T) { if args.Mode != app.ListTOTPMode || args.Entry != "" { t.Error("invalid args") } + args, _ = app.NewTOTPArguments([]string{"find", "tesst"}, "test") + if args.Mode != app.FindTOTPMode || args.Entry == "" { + t.Error("invalid args") + } args, _ = app.NewTOTPArguments([]string{"show", "test"}, "test") if args.Mode != app.ShowTOTPMode || args.Entry != "test" { t.Error("invalid args") @@ -268,3 +272,23 @@ func TestParseWindows(t *testing.T) { t.Errorf("invalid error: %v", err) } } + +func TestTOTPFind(t *testing.T) { + setupTOTP(t) + args, _ := app.NewTOTPArguments([]string{"find", "test"}, "totp") + m, opts := newMock(t) + if err := args.Do(opts); err != nil { + t.Errorf("invalid error: %v", err) + } + if m.buf.String() != "test/test2\ntest/test3\n" { + t.Errorf("invalid list: %s", m.buf.String()) + } + m.buf.Reset() + args, _ = app.NewTOTPArguments([]string{"find", "[zzzz]"}, "totp") + if err := args.Do(opts); err != nil { + t.Errorf("invalid error: %v", err) + } + if m.buf.String() != "" { + t.Errorf("invalid list: %s", m.buf.String()) + } +}