commit 99786ce7ac838b8cfd144c986d18675c58b8842a
parent ab74338ba8796f44b7b2d810691a0125da2d39f5
Author: Sean Enck <sean@ttypty.com>
Date: Sun, 21 Aug 2022 10:08:54 -0400
all libexec is gone
Diffstat:
7 files changed, 441 insertions(+), 441 deletions(-)
diff --git a/Makefile b/Makefile
@@ -1,21 +1,21 @@
VERSION := development
DESTDIR :=
BUILD := bin/
-TARGETS := $(BUILD)lb $(BUILD)lb-totp
+TARGET := $(BUILD)lb
MAIN := $(DESTDIR)bin/lb
TESTDIR := $(sort $(dir $(wildcard internal/**/*_test.go)))
.PHONY: $(TESTDIR)
-all: $(TARGETS)
+all: $(TARGET)
-$(TARGETS): cmd/**/* internal/**/*.go go.*
- go build -ldflags '-X main.version=$(VERSION) -X main.mainExe=$(MAIN)' -trimpath -buildmode=pie -mod=readonly -modcacherw -o $@ cmd/$(shell basename $@)/main.go
+$(TARGET): cmd/main.go internal/**/*.go go.*
+ go build -ldflags '-X main.version=$(VERSION) -X main.mainExe=$(MAIN)' -trimpath -buildmode=pie -mod=readonly -modcacherw -o $@ cmd/main.go
$(TESTDIR):
cd $@ && go test
-check: $(TARGETS) $(TESTDIR)
+check: $(TARGET) $(TESTDIR)
cd tests && make BUILD=../$(BUILD)
clean:
diff --git a/cmd/lb-totp/main.go b/cmd/lb-totp/main.go
@@ -1,228 +0,0 @@
-// support TOTP tokens in lockbox.
-package main
-
-import (
- "errors"
- "fmt"
- "os"
- "os/exec"
- "path/filepath"
- "sort"
- "strconv"
- "strings"
- "time"
-
- "github.com/enckse/lockbox/internal/cli"
- "github.com/enckse/lockbox/internal/colors"
- "github.com/enckse/lockbox/internal/encrypt"
- "github.com/enckse/lockbox/internal/inputs"
- "github.com/enckse/lockbox/internal/misc"
- "github.com/enckse/lockbox/internal/platform"
- "github.com/enckse/lockbox/internal/store"
- otp "github.com/pquerna/otp/totp"
-)
-
-var (
- mainExe = ""
-)
-
-type (
- colorWhen struct {
- start int
- end int
- }
-)
-
-func clear() {
- cmd := exec.Command("clear")
- cmd.Stdout = os.Stdout
- if err := cmd.Run(); err != nil {
- fmt.Printf("unable to clear screen: %v\n", err)
- }
-}
-
-func totpEnv() string {
- return inputs.EnvOrDefault(inputs.TotpEnv, "totp")
-}
-
-func colorWhenRules() ([]colorWhen, error) {
- envTime := os.Getenv(inputs.ColorBetweenEnv)
- if envTime == "" {
- return []colorWhen{
- colorWhen{start: 0, end: 5},
- colorWhen{start: 30, end: 35},
- }, nil
- }
- var rules []colorWhen
- for _, item := range strings.Split(envTime, ",") {
- line := strings.TrimSpace(item)
- if line == "" {
- continue
- }
- parts := strings.Split(line, ":")
- if len(parts) != 2 {
- return nil, fmt.Errorf("invalid colorization rule found: %s", line)
- }
- s, err := strconv.Atoi(parts[0])
- if err != nil {
- return nil, err
- }
- e, err := strconv.Atoi(parts[1])
- if err != nil {
- return nil, err
- }
- if s < 0 || e < 0 || e < s || s > 59 || e > 59 {
- return nil, fmt.Errorf("invalid time found for colorization rule: %s", line)
- }
- rules = append(rules, colorWhen{start: s, end: e})
- }
- if len(rules) == 0 {
- return nil, errors.New("invalid colorization rules for totp, none found")
- }
- return rules, nil
-}
-
-func display(token string, args cli.Arguments) error {
- interactive, err := inputs.IsInteractive()
- if err != nil {
- return err
- }
- if args.Short {
- interactive = false
- }
- if !interactive && args.Clip {
- return errors.New("clipboard not available in non-interactive mode")
- }
- coloring, err := colors.NewTerminal(colors.Red)
- if err != nil {
- return err
- }
- f := store.NewFileSystemStore()
- tok := filepath.Join(strings.TrimSpace(token), totpEnv())
- pathing := f.NewPath(tok)
- if !misc.PathExists(pathing) {
- return errors.New("object does not exist")
- }
- val, err := encrypt.FromFile(pathing)
- if err != nil {
- return err
- }
- exe := inputs.EnvOrDefault(inputs.ExeEnv, mainExe)
- totpToken := string(val)
- if !interactive {
- code, err := otp.GenerateCode(totpToken, time.Now())
- if err != nil {
- return err
- }
- fmt.Println(code)
- return nil
- }
- first := true
- running := 0
- lastSecond := -1
- if !args.Clip {
- if !args.Once {
- clear()
- }
- }
- clipboard := platform.Clipboard{}
- if args.Clip {
- clipboard, err = platform.NewClipboard()
- if err != nil {
- misc.Die("invalid clipboard", err)
- }
- }
- colorRules, err := colorWhenRules()
- if err != nil {
- misc.Die("invalid totp output coloring rules", err)
- }
- for {
- if !first {
- time.Sleep(500 * time.Millisecond)
- }
- first = false
- running++
- if running > 120 {
- fmt.Println("exiting (timeout)")
- return nil
- }
- now := time.Now()
- last := now.Second()
- if last == lastSecond {
- continue
- }
- lastSecond = last
- left := 60 - last
- code, err := otp.GenerateCode(totpToken, now)
- if err != nil {
- return err
- }
- startColor := ""
- endColor := ""
- for _, when := range colorRules {
- if left < when.end && left >= when.start {
- startColor = coloring.Start
- endColor = coloring.End
- }
- }
- leftString := fmt.Sprintf("%d", left)
- if len(leftString) < 2 {
- leftString = "0" + leftString
- }
- 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", tok, code))
- if !args.Once {
- outputs = append(outputs, "-> CTRL+C to exit")
- }
- } else {
- fmt.Printf("-> %s\n", expires)
- clipboard.CopyTo(code, exe)
- return nil
- }
- if !args.Once {
- clear()
- }
- fmt.Printf("%s\n", strings.Join(outputs, "\n\n"))
- if args.Once {
- return nil
- }
- }
-}
-
-func main() {
- args := os.Args
- if len(args) > 3 || len(args) < 2 {
- misc.Die("subkey required", errors.New("invalid arguments"))
- }
- cmd := args[1]
- options := cli.ParseArgs(cmd)
- if options.List {
- f := store.NewFileSystemStore()
- token := f.NewFile(totpEnv())
- results, err := f.List(store.ViewOptions{ErrorOnEmpty: true, Filter: func(path string) string {
- if filepath.Base(path) == token {
- return filepath.Dir(f.CleanPath(path))
- }
- return ""
- }})
- if err != nil {
- misc.Die("invalid list response", err)
- }
- sort.Strings(results)
- for _, entry := range results {
- fmt.Println(entry)
- }
- return
- }
- if len(args) == 3 {
- if !options.Clip && !options.Short && !options.Once {
- misc.Die("subcommand not supported", errors.New("invalid sub command"))
- }
- cmd = args[2]
- }
- if err := display(cmd, options); err != nil {
- misc.Die("failed to show totp token", err)
- }
-}
diff --git a/cmd/lb/main.go b/cmd/lb/main.go
@@ -1,204 +0,0 @@
-// provides the binary runs or calls lockbox commands.
-package main
-
-import (
- "errors"
- "fmt"
- "os"
- "path/filepath"
-
- "github.com/enckse/lockbox/internal/cli"
- "github.com/enckse/lockbox/internal/dump"
- "github.com/enckse/lockbox/internal/encrypt"
- "github.com/enckse/lockbox/internal/hooks"
- "github.com/enckse/lockbox/internal/inputs"
- "github.com/enckse/lockbox/internal/misc"
- "github.com/enckse/lockbox/internal/platform"
- "github.com/enckse/lockbox/internal/store"
- "github.com/enckse/lockbox/internal/subcommands"
-)
-
-var (
- version = "development"
- libExec = ""
-)
-
-type (
- callbackFunction func([]string) error
-)
-
-func getEntry(fs store.FileSystem, args []string, idx int) string {
- if len(args) != idx+1 {
- misc.Die("invalid entry given", errors.New("specific entry required"))
- }
- return fs.NewPath(args[idx])
-}
-
-func internalCallback(name string) callbackFunction {
- switch name {
- case "gitdiff":
- return subcommands.GitDiff
- case "rekey":
- return subcommands.Rekey
- case "rw":
- return subcommands.ReadWrite
- }
- return nil
-}
-
-func main() {
- args := os.Args
- if len(args) < 2 {
- misc.Die("missing arguments", errors.New("requires subcommand"))
- }
- command := args[1]
- switch command {
- case "ls", "list", "find":
- opts := subcommands.ListFindOptions{Find: command == "find", Search: "", Store: store.NewFileSystemStore()}
- if opts.Find {
- if len(args) < 3 {
- misc.Die("find requires an argument to search for", errors.New("search term required"))
- }
- opts.Search = args[2]
- }
- files, err := subcommands.ListFindCallback(opts)
- if err != nil {
- misc.Die("unable to list files", err)
- }
- for _, f := range files {
- fmt.Println(f)
- }
- case "version":
- fmt.Printf("version: %s\n", version)
- case "insert":
- options := cli.Arguments{}
- idx := 2
- switch len(args) {
- case 2:
- misc.Die("insert missing required arguments", errors.New("entry required"))
- case 3:
- case 4:
- options = cli.ParseArgs(args[2])
- if !options.Multi {
- misc.Die("multi-line insert must be after 'insert'", errors.New("invalid command"))
- }
- idx = 3
- default:
- misc.Die("too many arguments", errors.New("insert can only perform one operation"))
- }
- isPipe := inputs.IsInputFromPipe()
- entry := getEntry(store.NewFileSystemStore(), args, idx)
- if misc.PathExists(entry) {
- if !isPipe {
- if !confirm("overwrite existing") {
- return
- }
- }
- } else {
- dir := filepath.Dir(entry)
- if !misc.PathExists(dir) {
- if err := os.MkdirAll(dir, 0755); err != nil {
- misc.Die("failed to create directory structure", err)
- }
- }
- }
- password, err := inputs.GetUserInputPassword(isPipe, options.Multi)
- if err != nil {
- misc.Die("invalid input", err)
- }
- if err := encrypt.ToFile(entry, password); err != nil {
- misc.Die("unable to encrypt object", err)
- }
- fmt.Println("")
- hooks.Run(hooks.Insert, hooks.PostStep)
- case "rm":
- entry := getEntry(store.NewFileSystemStore(), args, 2)
- if !misc.PathExists(entry) {
- misc.Die("does not exists", errors.New("can not delete unknown entry"))
- }
- if confirm("remove entry") {
- os.Remove(entry)
- hooks.Run(hooks.Remove, hooks.PostStep)
- }
- case "show", "clip", "dump":
- fs := store.NewFileSystemStore()
- opts := subcommands.DisplayOptions{Dump: command == "dump", Show: command == "show", Glob: getEntry(fs, []string{"***"}, 0), Store: fs}
- opts.Show = opts.Show || opts.Dump
- startEntry := 2
- options := cli.Arguments{}
- if opts.Dump {
- if len(args) > 2 {
- options = cli.ParseArgs(args[2])
- if options.Yes {
- startEntry = 3
- }
- }
- }
- opts.Entry = getEntry(fs, args, startEntry)
- var err error
- dumpData, err := subcommands.DisplayCallback(opts)
- if err != nil {
- misc.Die("display command failed to retrieve data", err)
- }
- if opts.Dump {
- if !options.Yes {
- if !confirm("dump data to stdout as plaintext") {
- return
- }
- }
- d, err := dump.Marshal(dumpData)
- if err != nil {
- misc.Die("failed to marshal items", err)
- }
- fmt.Println(string(d))
- return
- }
- clipboard := platform.Clipboard{}
- exe := ""
- if !opts.Show {
- clipboard, err = platform.NewClipboard()
- if err != nil {
- misc.Die("unable to get clipboard", err)
- }
- exe, err = os.Executable()
- if err != nil {
- misc.Die("unable to get executable", err)
- }
- }
- for _, obj := range dumpData {
- if opts.Show {
- if obj.Path != "" {
- fmt.Println(obj.Path)
- }
- fmt.Println(obj.Value)
- continue
- }
- clipboard.CopyTo(obj.Value, exe)
- }
- case "clear":
- if err := subcommands.ClearClipboardCallback(); err != nil {
- misc.Die("failed to handle clipboard clear", err)
- }
- default:
- a := args[2:]
- callback := internalCallback(command)
- if callback != nil {
- if err := callback(a); err != nil {
- misc.Die(fmt.Sprintf("%s command failure", command), err)
- }
- return
- }
- lib := inputs.EnvOrDefault(inputs.LibExecEnv, libExec)
- if err := subcommands.LibExecCallback(subcommands.LibExecOptions{Directory: lib, Command: command, Args: a}); err != nil {
- misc.Die("subcommand failed", err)
- }
- }
-}
-
-func confirm(prompt string) bool {
- yesNo, err := inputs.ConfirmYesNoPrompt(prompt)
- if err != nil {
- misc.Die("failed to get response", err)
- }
- return yesNo
-}
diff --git a/cmd/main.go b/cmd/main.go
@@ -0,0 +1,202 @@
+// provides the binary runs or calls lockbox commands.
+package main
+
+import (
+ "errors"
+ "fmt"
+ "os"
+ "path/filepath"
+
+ "github.com/enckse/lockbox/internal/cli"
+ "github.com/enckse/lockbox/internal/dump"
+ "github.com/enckse/lockbox/internal/encrypt"
+ "github.com/enckse/lockbox/internal/hooks"
+ "github.com/enckse/lockbox/internal/inputs"
+ "github.com/enckse/lockbox/internal/misc"
+ "github.com/enckse/lockbox/internal/platform"
+ "github.com/enckse/lockbox/internal/store"
+ "github.com/enckse/lockbox/internal/subcommands"
+)
+
+var (
+ version = "development"
+)
+
+type (
+ callbackFunction func([]string) error
+)
+
+func getEntry(fs store.FileSystem, args []string, idx int) string {
+ if len(args) != idx+1 {
+ misc.Die("invalid entry given", errors.New("specific entry required"))
+ }
+ return fs.NewPath(args[idx])
+}
+
+func internalCallback(name string) callbackFunction {
+ switch name {
+ case "gitdiff":
+ return subcommands.GitDiff
+ case "rekey":
+ return subcommands.Rekey
+ case "rw":
+ return subcommands.ReadWrite
+ case "totp":
+ return subcommands.TOTP
+ }
+ return nil
+}
+
+func main() {
+ args := os.Args
+ if len(args) < 2 {
+ misc.Die("missing arguments", errors.New("requires subcommand"))
+ }
+ command := args[1]
+ switch command {
+ case "ls", "list", "find":
+ opts := subcommands.ListFindOptions{Find: command == "find", Search: "", Store: store.NewFileSystemStore()}
+ if opts.Find {
+ if len(args) < 3 {
+ misc.Die("find requires an argument to search for", errors.New("search term required"))
+ }
+ opts.Search = args[2]
+ }
+ files, err := subcommands.ListFindCallback(opts)
+ if err != nil {
+ misc.Die("unable to list files", err)
+ }
+ for _, f := range files {
+ fmt.Println(f)
+ }
+ case "version":
+ fmt.Printf("version: %s\n", version)
+ case "insert":
+ options := cli.Arguments{}
+ idx := 2
+ switch len(args) {
+ case 2:
+ misc.Die("insert missing required arguments", errors.New("entry required"))
+ case 3:
+ case 4:
+ options = cli.ParseArgs(args[2])
+ if !options.Multi {
+ misc.Die("multi-line insert must be after 'insert'", errors.New("invalid command"))
+ }
+ idx = 3
+ default:
+ misc.Die("too many arguments", errors.New("insert can only perform one operation"))
+ }
+ isPipe := inputs.IsInputFromPipe()
+ entry := getEntry(store.NewFileSystemStore(), args, idx)
+ if misc.PathExists(entry) {
+ if !isPipe {
+ if !confirm("overwrite existing") {
+ return
+ }
+ }
+ } else {
+ dir := filepath.Dir(entry)
+ if !misc.PathExists(dir) {
+ if err := os.MkdirAll(dir, 0755); err != nil {
+ misc.Die("failed to create directory structure", err)
+ }
+ }
+ }
+ password, err := inputs.GetUserInputPassword(isPipe, options.Multi)
+ if err != nil {
+ misc.Die("invalid input", err)
+ }
+ if err := encrypt.ToFile(entry, password); err != nil {
+ misc.Die("unable to encrypt object", err)
+ }
+ fmt.Println("")
+ hooks.Run(hooks.Insert, hooks.PostStep)
+ case "rm":
+ entry := getEntry(store.NewFileSystemStore(), args, 2)
+ if !misc.PathExists(entry) {
+ misc.Die("does not exists", errors.New("can not delete unknown entry"))
+ }
+ if confirm("remove entry") {
+ os.Remove(entry)
+ hooks.Run(hooks.Remove, hooks.PostStep)
+ }
+ case "show", "clip", "dump":
+ fs := store.NewFileSystemStore()
+ opts := subcommands.DisplayOptions{Dump: command == "dump", Show: command == "show", Glob: getEntry(fs, []string{"***"}, 0), Store: fs}
+ opts.Show = opts.Show || opts.Dump
+ startEntry := 2
+ options := cli.Arguments{}
+ if opts.Dump {
+ if len(args) > 2 {
+ options = cli.ParseArgs(args[2])
+ if options.Yes {
+ startEntry = 3
+ }
+ }
+ }
+ opts.Entry = getEntry(fs, args, startEntry)
+ var err error
+ dumpData, err := subcommands.DisplayCallback(opts)
+ if err != nil {
+ misc.Die("display command failed to retrieve data", err)
+ }
+ if opts.Dump {
+ if !options.Yes {
+ if !confirm("dump data to stdout as plaintext") {
+ return
+ }
+ }
+ d, err := dump.Marshal(dumpData)
+ if err != nil {
+ misc.Die("failed to marshal items", err)
+ }
+ fmt.Println(string(d))
+ return
+ }
+ clipboard := platform.Clipboard{}
+ exe := ""
+ if !opts.Show {
+ clipboard, err = platform.NewClipboard()
+ if err != nil {
+ misc.Die("unable to get clipboard", err)
+ }
+ exe, err = os.Executable()
+ if err != nil {
+ misc.Die("unable to get executable", err)
+ }
+ }
+ for _, obj := range dumpData {
+ if opts.Show {
+ if obj.Path != "" {
+ fmt.Println(obj.Path)
+ }
+ fmt.Println(obj.Value)
+ continue
+ }
+ clipboard.CopyTo(obj.Value, exe)
+ }
+ case "clear":
+ if err := subcommands.ClearClipboardCallback(); err != nil {
+ misc.Die("failed to handle clipboard clear", err)
+ }
+ default:
+ a := args[2:]
+ callback := internalCallback(command)
+ if callback != nil {
+ if err := callback(a); err != nil {
+ misc.Die(fmt.Sprintf("%s command failure", command), err)
+ }
+ return
+ }
+ misc.Die("unknown command", errors.New(command))
+ }
+}
+
+func confirm(prompt string) bool {
+ yesNo, err := inputs.ConfirmYesNoPrompt(prompt)
+ if err != nil {
+ misc.Die("failed to get response", err)
+ }
+ return yesNo
+}
diff --git a/internal/inputs/env.go b/internal/inputs/env.go
@@ -14,8 +14,6 @@ const (
interactiveEnv = prefixKey + "INTERACTIVE"
// TotpEnv allows for overriding of the special name for totp entries.
TotpEnv = prefixKey + "TOTP"
- // ExeEnv allows for installing lb to various locations.
- ExeEnv = prefixKey + "EXE"
// KeyModeEnv indicates what the KEY value is (e.g. command, plaintext).
KeyModeEnv = prefixKey + "KEYMODE"
// KeyEnv is the key value used by the lockbox store.
diff --git a/internal/subcommands/totp.go b/internal/subcommands/totp.go
@@ -0,0 +1,232 @@
+// Package subcommands handles TOTP tokens.
+package subcommands
+
+import (
+ "errors"
+ "fmt"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "sort"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/enckse/lockbox/internal/cli"
+ "github.com/enckse/lockbox/internal/colors"
+ "github.com/enckse/lockbox/internal/encrypt"
+ "github.com/enckse/lockbox/internal/inputs"
+ "github.com/enckse/lockbox/internal/misc"
+ "github.com/enckse/lockbox/internal/platform"
+ "github.com/enckse/lockbox/internal/store"
+ otp "github.com/pquerna/otp/totp"
+)
+
+var (
+ mainExe = ""
+)
+
+type (
+ colorWhen struct {
+ start int
+ end int
+ }
+)
+
+func clear() {
+ cmd := exec.Command("clear")
+ cmd.Stdout = os.Stdout
+ if err := cmd.Run(); err != nil {
+ fmt.Printf("unable to clear screen: %v\n", err)
+ }
+}
+
+func totpEnv() string {
+ return inputs.EnvOrDefault(inputs.TotpEnv, "totp")
+}
+
+func colorWhenRules() ([]colorWhen, error) {
+ envTime := os.Getenv(inputs.ColorBetweenEnv)
+ if envTime == "" {
+ return []colorWhen{
+ colorWhen{start: 0, end: 5},
+ colorWhen{start: 30, end: 35},
+ }, nil
+ }
+ var rules []colorWhen
+ for _, item := range strings.Split(envTime, ",") {
+ line := strings.TrimSpace(item)
+ if line == "" {
+ continue
+ }
+ parts := strings.Split(line, ":")
+ if len(parts) != 2 {
+ return nil, fmt.Errorf("invalid colorization rule found: %s", line)
+ }
+ s, err := strconv.Atoi(parts[0])
+ if err != nil {
+ return nil, err
+ }
+ e, err := strconv.Atoi(parts[1])
+ if err != nil {
+ return nil, err
+ }
+ if s < 0 || e < 0 || e < s || s > 59 || e > 59 {
+ return nil, fmt.Errorf("invalid time found for colorization rule: %s", line)
+ }
+ rules = append(rules, colorWhen{start: s, end: e})
+ }
+ if len(rules) == 0 {
+ return nil, errors.New("invalid colorization rules for totp, none found")
+ }
+ return rules, nil
+}
+
+func display(token string, args cli.Arguments) error {
+ interactive, err := inputs.IsInteractive()
+ if err != nil {
+ return err
+ }
+ if args.Short {
+ interactive = false
+ }
+ if !interactive && args.Clip {
+ return errors.New("clipboard not available in non-interactive mode")
+ }
+ coloring, err := colors.NewTerminal(colors.Red)
+ if err != nil {
+ return err
+ }
+ f := store.NewFileSystemStore()
+ tok := filepath.Join(strings.TrimSpace(token), totpEnv())
+ pathing := f.NewPath(tok)
+ if !misc.PathExists(pathing) {
+ return errors.New("object does not exist")
+ }
+ val, err := encrypt.FromFile(pathing)
+ if err != nil {
+ return err
+ }
+ exe, err := os.Executable()
+ if err != nil {
+ return err
+ }
+ totpToken := string(val)
+ if !interactive {
+ code, err := otp.GenerateCode(totpToken, time.Now())
+ if err != nil {
+ return err
+ }
+ fmt.Println(code)
+ return nil
+ }
+ first := true
+ running := 0
+ lastSecond := -1
+ if !args.Clip {
+ if !args.Once {
+ clear()
+ }
+ }
+ clipboard := platform.Clipboard{}
+ if args.Clip {
+ clipboard, err = platform.NewClipboard()
+ if err != nil {
+ misc.Die("invalid clipboard", err)
+ }
+ }
+ colorRules, err := colorWhenRules()
+ if err != nil {
+ misc.Die("invalid totp output coloring rules", err)
+ }
+ for {
+ if !first {
+ time.Sleep(500 * time.Millisecond)
+ }
+ first = false
+ running++
+ if running > 120 {
+ fmt.Println("exiting (timeout)")
+ return nil
+ }
+ now := time.Now()
+ last := now.Second()
+ if last == lastSecond {
+ continue
+ }
+ lastSecond = last
+ left := 60 - last
+ code, err := otp.GenerateCode(totpToken, now)
+ if err != nil {
+ return err
+ }
+ startColor := ""
+ endColor := ""
+ for _, when := range colorRules {
+ if left < when.end && left >= when.start {
+ startColor = coloring.Start
+ endColor = coloring.End
+ }
+ }
+ leftString := fmt.Sprintf("%d", left)
+ if len(leftString) < 2 {
+ leftString = "0" + leftString
+ }
+ 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", tok, code))
+ if !args.Once {
+ outputs = append(outputs, "-> CTRL+C to exit")
+ }
+ } else {
+ fmt.Printf("-> %s\n", expires)
+ clipboard.CopyTo(code, exe)
+ return nil
+ }
+ if !args.Once {
+ clear()
+ }
+ fmt.Printf("%s\n", strings.Join(outputs, "\n\n"))
+ if args.Once {
+ return nil
+ }
+ }
+}
+
+// TOTP handles UI for TOTP tokens.
+func TOTP(args []string) error {
+ if len(args) > 2 || len(args) < 1 {
+ return errors.New("invalid arguments, subkey and entry required")
+ }
+ cmd := args[0]
+ options := cli.ParseArgs(cmd)
+ if options.List {
+ f := store.NewFileSystemStore()
+ token := f.NewFile(totpEnv())
+ results, err := f.List(store.ViewOptions{ErrorOnEmpty: true, Filter: func(path string) string {
+ if filepath.Base(path) == token {
+ return filepath.Dir(f.CleanPath(path))
+ }
+ return ""
+ }})
+ if err != nil {
+ return err
+ }
+ sort.Strings(results)
+ for _, entry := range results {
+ fmt.Println(entry)
+ }
+ return nil
+ }
+ if len(args) == 2 {
+ if !options.Clip && !options.Short && !options.Once {
+ return errors.New("invalid sub command")
+ }
+ cmd = args[1]
+ }
+ if err := display(cmd, options); err != nil {
+ return err
+ }
+ return nil
+}
diff --git a/tests/run.sh b/tests/run.sh
@@ -43,8 +43,8 @@ _run() {
"$BIN/lb" show keys2/three
echo "y" | "$BIN/lb" dump keys2/three
echo "5ae472abqdekjqykoyxk7hvc2leklq5n" | "$BIN/lb" insert test/totp
- "$BIN/lb-totp" -list
- "$BIN/lb-totp" test | tr '[:digit:]' 'X'
+ "$BIN/lb" "totp" -list
+ "$BIN/lb" "totp" test | tr '[:digit:]' 'X'
"$BIN/lb" "gitdiff" bin/lb/keys/one.lb bin/lb/keys/one2.lb
yes 2>/dev/null | "$BIN/lb" rm keys2/three
echo