lockbox

password manager
Log | Files | Refs | README | LICENSE

commit af1b350bac0db5f0bb5533a4b19e85474c4538aa
parent 772d314ed11a786242b5f4acff88268c87808a7b
Author: Sean Enck <sean@ttypty.com>
Date:   Sat, 16 Jul 2022 10:48:19 -0400

WIP on restructure

Diffstat:
MMakefile | 2+-
Mcmd/lb/main.go | 73+++++++++++++++++++++++++++++++------------------------------------------
Minternal/clipboard/clip.go | 6+++---
Minternal/colors/colors.go | 42++++++++++++++++++++++++------------------
Ainternal/colors/colors_test.go | 64++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/dump/dump.go | 17+++++++++++++++++
Minternal/encrypt/encrypt.go | 3++-
Minternal/encrypt/encrypt_test.go | 9+++++++--
Minternal/hooks/hooks.go | 19++++++++++---------
Minternal/inputs/inputs.go | 11++++++++++-
Ainternal/misc/utils.go | 31+++++++++++++++++++++++++++++++
Minternal/store/store.go | 34+++++++++++++++++++++++-----------
Ainternal/store/store_test.go | 50++++++++++++++++++++++++++++++++++++++++++++++++++
Dinternal/utils.go | 31-------------------------------
14 files changed, 273 insertions(+), 119 deletions(-)

diff --git a/Makefile b/Makefile @@ -11,7 +11,7 @@ SOURCE := $(shell find . -type f -name "*.go") all: $(TARGETS) -$(TARGETS): $(SOURCE) +$(TARGETS): $(SOURCE) go.* go build -ldflags '-X main.version=$(VERSION) -X main.libExec=$(LIBEXEC) -X main.mainExe=$(MAIN)' -trimpath -buildmode=pie -mod=readonly -modcacherw -o $@ cmd/$(shell basename $@)/main.go $(TESTDIR): $(TARGETS) diff --git a/cmd/lb/main.go b/cmd/lb/main.go @@ -11,13 +11,10 @@ import ( "strings" "time" - "github.com/enckse/lockbox/internal" "github.com/enckse/lockbox/internal/cli" "github.com/enckse/lockbox/internal/clipboard" -) - -const ( - postStep = "post" + "github.com/enckse/lockbox/internal/misc" + "github.com/enckse/lockbox/internal/store" ) var ( @@ -25,25 +22,17 @@ var ( libExec = "" ) -type ( - // Dump represents the output structure from a JSON dump. - Dump struct { - Path string `json:"path,omitempty"` - Value string `json:"value"` - } -) - func getEntry(store string, args []string, idx int) string { if len(args) != idx+1 { - internal.Die("invalid entry given", errors.New("specific entry required")) + misc.Die("invalid entry given", errors.New("specific entry required")) } - return filepath.Join(store, args[idx]) + internal.Extension + return filepath.Join(store, args[idx]) + store.Extension } func getExecutable() string { exe, err := os.Executable() if err != nil { - internal.Die("unable to get exe", err) + misc.Die("unable to get exe", err) } return exe } @@ -51,7 +40,7 @@ func getExecutable() string { func main() { args := os.Args if len(args) < 2 { - internal.Die("missing arguments", errors.New("requires subcommand")) + misc.Die("missing arguments", errors.New("requires subcommand")) } command := args[1] store := internal.GetStore() @@ -61,13 +50,13 @@ func main() { searchTerm := "" if isFind { if len(args) < 3 { - internal.Die("find requires an argument to search for", errors.New("search term required")) + misc.Die("find requires an argument to search for", errors.New("search term required")) } searchTerm = args[2] } files, err := internal.List(store, true) if err != nil { - internal.Die("unable to list files", err) + misc.Die("unable to list files", err) } for _, f := range files { if isFind { @@ -84,16 +73,16 @@ func main() { idx := 2 switch len(args) { case 2: - internal.Die("insert missing required arguments", errors.New("entry required")) + misc.Die("insert missing required arguments", errors.New("entry required")) case 3: case 4: options = cli.ParseArgs(args[2]) if !options.Multi { - internal.Die("multi-line insert must be after 'insert'", errors.New("invalid command")) + misc.Die("multi-line insert must be after 'insert'", errors.New("invalid command")) } idx = 3 default: - internal.Die("too many arguments", errors.New("insert can only perform one operation")) + misc.Die("too many arguments", errors.New("insert can only perform one operation")) } isPipe := internal.IsInputFromPipe() entry := getEntry(store, args, idx) @@ -107,7 +96,7 @@ func main() { dir := filepath.Dir(entry) if !internal.PathExists(dir) { if err := os.MkdirAll(dir, 0755); err != nil { - internal.Die("failed to create directory structure", err) + misc.Die("failed to create directory structure", err) } } } @@ -115,32 +104,32 @@ func main() { if !options.Multi && !isPipe { input, err := internal.ConfirmInputsMatch("password") if err != nil { - internal.Die("password input failed", err) + misc.Die("password input failed", err) } password = input } else { input, err := internal.Stdin(false) if err != nil { - internal.Die("failed to read stdin", err) + misc.Die("failed to read stdin", err) } password = input } if password == "" { - internal.Die("empty password provided", errors.New("password can NOT be empty")) + misc.Die("empty password provided", errors.New("password can NOT be empty")) } l, err := internal.NewLockbox(internal.LockboxOptions{File: entry}) if err != nil { - internal.Die("unable to make lockbox model instance", err) + misc.Die("unable to make lockbox model instance", err) } if err := l.Encrypt([]byte(password)); err != nil { - internal.Die("failed to save password", err) + misc.Die("failed to save password", err) } fmt.Println("") internal.Hooks(internal.InsertHook, internal.PostHookStep) case "rm": entry := getEntry(store, args, 2) if !internal.PathExists(entry) { - internal.Die("does not exists", errors.New("can not delete unknown entry")) + misc.Die("does not exists", errors.New("can not delete unknown entry")) } if confirm("remove entry") { os.Remove(entry) @@ -165,13 +154,13 @@ func main() { if inEntry == getEntry(store, []string{"***"}, 0) { all, err := internal.List(store, false) if err != nil { - internal.Die("unable to get all files", err) + misc.Die("unable to get all files", err) } entries = all } else { matches, err := filepath.Glob(inEntry) if err != nil { - internal.Die("bad glob", err) + misc.Die("bad glob", err) } entries = matches } @@ -179,33 +168,33 @@ func main() { isGlob := len(entries) > 1 if isGlob { if !isShow { - internal.Die("cannot glob to clipboard", errors.New("bad glob request")) + misc.Die("cannot glob to clipboard", errors.New("bad glob request")) } sort.Strings(entries) } startColor, endColor, err := internal.GetColor(internal.ColorRed) if err != nil { - internal.Die("unable to get color for terminal", err) + misc.Die("unable to get color for terminal", err) } dumpData := []Dump{} clip := clipboard.Commands{} if !isShow { clip, err = clipboard.NewCommands() if err != nil { - internal.Die("unable to get clipboard", err) + misc.Die("unable to get clipboard", err) } } for _, entry := range entries { if !internal.PathExists(entry) { - internal.Die("invalid entry", errors.New("entry not found")) + misc.Die("invalid entry", errors.New("entry not found")) } l, err := internal.NewLockbox(internal.LockboxOptions{File: entry}) if err != nil { - internal.Die("unable to make lockbox model instance", err) + misc.Die("unable to make lockbox model instance", err) } decrypt, err := l.Decrypt() if err != nil { - internal.Die("unable to decrypt", err) + misc.Die("unable to decrypt", err) } value := strings.TrimSpace(string(decrypt)) dump := Dump{} @@ -245,7 +234,7 @@ func main() { } b, err := json.MarshalIndent(d, "", " ") if err != nil { - internal.Die("failed to marshal dump item", err) + misc.Die("failed to marshal dump item", err) } fmt.Println(string(b)) } @@ -255,11 +244,11 @@ func main() { idx := 0 val, err := internal.Stdin(false) if err != nil { - internal.Die("unable to read value to clear", err) + misc.Die("unable to read value to clear", err) } clip, err := clipboard.NewCommands() if err != nil { - internal.Die("unable to get paste command", err) + misc.Die("unable to get paste command", err) } var args []string if len(clip.Paste) > 1 { @@ -290,7 +279,7 @@ func main() { c.Stdout = os.Stdout c.Stderr = os.Stderr if err := c.Run(); err != nil { - internal.Die("bad command", err) + misc.Die("bad command", err) } } } @@ -298,7 +287,7 @@ func main() { func confirm(prompt string) bool { yesNo, err := internal.ConfirmYesNoPrompt(prompt) if err != nil { - internal.Die("failed to get response", err) + misc.Die("failed to get response", err) } return yesNo } diff --git a/internal/clipboard/clip.go b/internal/clipboard/clip.go @@ -8,7 +8,7 @@ import ( "path/filepath" "strings" - "github.com/enckse/lockbox/internal" + "github.com/enckse/lockbox/internal/misc" ) const ( @@ -89,7 +89,7 @@ func pipeTo(command, value string, wait bool, args ...string) { cmd := exec.Command(command, args...) stdin, err := cmd.StdinPipe() if err != nil { - internal.Die("unable to get stdin pipe", err) + misc.Die("unable to get stdin pipe", err) } go func() { @@ -105,6 +105,6 @@ func pipeTo(command, value string, wait bool, args ...string) { ran = cmd.Start() } if ran != nil { - internal.Die("failed to run command", ran) + misc.Die("failed to run command", ran) } } diff --git a/internal/colors/colors.go b/internal/colors/colors.go @@ -1,40 +1,46 @@ -package internal +package colors import ( "errors" -) - -type ( - // Color are terminal colors for dumb terminal coloring. - Color int + "github.com/enckse/lockbox/internal/inputs" ) const ( termBeginRed = "\033[1;31m" termEndRed = "\033[0m" - // ColorRed will get red terminal coloring. - ColorRed = iota + // Red will get red terminal coloring. + Red = iota +) + +type ( + // Color are terminal colors for dumb terminal coloring. + Color int + // Terminal represents terminal coloring information. + Terminal struct { + Start string + End string + } ) -// GetColor will retrieve start/end terminal coloration indicators. -func GetColor(color Color) (string, string, error) { - if color != ColorRed { - return "", "", errors.New("bad color") +// NewTerminal will retrieve start/end terminal coloration indicators. +func NewTerminal(color Color) (Terminal, error) { + if color != Red { + return Terminal{}, errors.New("bad color") } - interactive, err := IsInteractive() + interactive, err := inputs.IsInteractive() if err != nil { - return "", "", err + return Terminal{}, err } colors := interactive if colors { - isColored, err := isYesNoEnv(false, "LOCKBOX_NOCOLOR") + isColored, err := inputs.IsColorEnabled() if err != nil { - return "", "", err + return Terminal{}, err } colors = !isColored } if colors { - return termBeginRed, termEndRed, nil + return Terminal{Start: termBeginRed, End: termEndRed}, nil } - return "", "", nil + return Terminal{}, nil } diff --git a/internal/colors/colors_test.go b/internal/colors/colors_test.go @@ -0,0 +1,64 @@ +package colors + +import ( + "os" + "testing" +) + +func TestHasColoring(t *testing.T) { + os.Setenv("LOCKBOX_INTERACTIVE", "yes") + os.Setenv("LOCKBOX_NOCOLOR", "no") + term, err := NewTerminal(Red) + if err != nil { + t.Errorf("color was valid: %v", err) + } + if term.Start != termBeginRed || term.End != termEndRed { + t.Error("bad resulting color") + } +} + +func TestBadColor(t *testing.T) { + _, err := NewTerminal(Color(5)) + if err == nil || err.Error() != "bad color" { + t.Errorf("invalid color error: %v", err) + } +} + +func TestNoColoring(t *testing.T) { + os.Setenv("LOCKBOX_INTERACTIVE", "no") + os.Setenv("LOCKBOX_NOCOLOR", "yes") + term, err := NewTerminal(Red) + if err != nil { + t.Errorf("color was valid: %v", err) + } + if term.Start != "" || term.End != "" { + t.Error("should have no color") + } + os.Setenv("LOCKBOX_INTERACTIVE", "yes") + os.Setenv("LOCKBOX_NOCOLOR", "yes") + term, err = NewTerminal(Red) + if err != nil { + t.Errorf("color was valid: %v", err) + } + if term.Start != "" || term.End != "" { + t.Error("should have no color") + } + os.Setenv("LOCKBOX_INTERACTIVE", "no") + os.Setenv("LOCKBOX_NOCOLOR", "no") + term, err = NewTerminal(Red) + if err != nil { + t.Errorf("color was valid: %v", err) + } + if term.Start != "" || term.End != "" { + t.Error("should have no color") + } + os.Setenv("LOCKBOX_INTERACTIVE", "yes") + os.Setenv("LOCKBOX_NOCOLOR", "no") + term, err = NewTerminal(Red) + if err != nil { + t.Errorf("color was valid: %v", err) + } + if term.Start == "" || term.End == "" { + t.Error("should have color") + } +} diff --git a/internal/dump/dump.go b/internal/dump/dump.go @@ -0,0 +1,17 @@ +package dump + +import ( + "encoding/json" +) + +type ( + // ExportEntity represents the output structure from a JSON dump. + ExportEntity struct { + Path string `json:"path,omitempty"` + Value string `json:"value"` + } +) + +func Marshal(entities []ExportEntity) ([]byte, error) { + return json.MarshalIndent(entities, "", " ") +} diff --git a/internal/encrypt/encrypt.go b/internal/encrypt/encrypt.go @@ -12,6 +12,7 @@ import ( "github.com/google/shlex" "golang.org/x/crypto/nacl/secretbox" + "github.com/enckse/lockbox/internal/inputs" ) const ( @@ -116,7 +117,7 @@ func (l Lockbox) Encrypt(datum []byte) error { } data := datum if data == nil { - b, err := getStdin(false) + b, err := inputs.RawStdin(false) if err != nil { return err } diff --git a/internal/encrypt/encrypt_test.go b/internal/encrypt/encrypt_test.go @@ -4,16 +4,21 @@ import ( "os" "path/filepath" "testing" + "github.com/enckse/lockbox/internal/misc" ) func setupData(t *testing.T) string { os.Setenv("LOCKBOX_KEYMODE", "") os.Setenv("LOCKBOX_KEY", "") - if !PathExists("bin") { + if misc.PathExists("bin") { + if err := os.RemoveAll("bin"); err != nil { + t.Errorf("unable to cleanup dir: %v") + } + } + if err := os.MkdirAll("bin", 0755); err != nil { t.Errorf("failed to setup bin directory: %v", err) } - } return filepath.Join("bin", "test.lb") } diff --git a/internal/hooks/hooks.go b/internal/hooks/hooks.go @@ -1,32 +1,33 @@ -package internal +package hooks import ( "errors" "os" "os/exec" "path/filepath" + "github.com/enckse/lockbox/internal/misc" ) type ( // HookAction are specific steps that may call a hook. - HookAction string + Action string // HookStep is the step, during command execution, when the hook was called. - HookStep string + Step string ) const ( // RemoveHook is called when a store entry is removed. - RemoveHook HookAction = "remove" + Remove Action = "remove" // InsertHook is called when a store entry is inserted. - InsertHook HookAction = "insert" + Insert Action = "insert" // PostHookStep is a hook running at the end of a command. - PostHookStep HookStep = "post" + PostStep Step = "post" ) -// Hooks executes any configured hooks. -func Hooks(action HookAction, step HookStep) error { +// Run executes any configured hooks. +func Run(action Action, step Step) error { hookDir := os.Getenv("LOCKBOX_HOOKDIR") - if !PathExists(hookDir) { + if !misc.PathExists(hookDir) { return nil } dirs, err := os.ReadDir(hookDir) diff --git a/internal/inputs/inputs.go b/internal/inputs/inputs.go @@ -1,4 +1,4 @@ -package internal +package inputs import ( "bufio" @@ -23,6 +23,10 @@ func isYesNoEnv(defaultValue bool, env string) (bool, error) { return false, fmt.Errorf("invalid yes/no env value for %s", env) } +func IsColorEnabled() (bool, error) { + return isYesNoEnv(false, "LOCKBOX_NOCOLOR") +} + // IsInteractive indicates if running as a user UI experience. func IsInteractive() (bool, error) { return isYesNoEnv(true, "LOCKBOX_INTERACTIVE") @@ -104,6 +108,11 @@ func ConfirmYesNoPrompt(prompt string) (bool, error) { return resp == "Y" || resp == "y", nil } +// RawStdin will get raw stdin data. +func RawStdin() ([]byte, error) { + return getStdin(false) +} + func getStdin(one bool) ([]byte, error) { scanner := bufio.NewScanner(os.Stdin) var b bytes.Buffer diff --git a/internal/misc/utils.go b/internal/misc/utils.go @@ -0,0 +1,31 @@ +package misc + +import ( + "fmt" + "os" +) + +// LogError will log an error to stderr. +func LogError(message string, err error) { + msg := message + if err != nil { + msg = fmt.Sprintf("%s (%v)", msg, err) + } + fmt.Fprintln(os.Stderr, msg) +} + +// Die will print messages and exit. +func Die(message string, err error) { + LogError(message, err) + os.Exit(1) +} + +// PathExists indicates if a path exists. +func PathExists(path string) bool { + if _, err := os.Stat(path); err != nil { + if os.IsNotExist(err) { + return false + } + } + return true +} diff --git a/internal/store/store.go b/internal/store/store.go @@ -1,4 +1,4 @@ -package internal +package store import ( "errors" @@ -7,6 +7,7 @@ import ( "path/filepath" "sort" "strings" + "github.com/enckse/lockbox/internal/misc" ) const ( @@ -14,25 +15,36 @@ const ( Extension = ".lb" ) -// GetStore gets the lockbox directory. -func GetStore() string { +type ( + // FileSystem represents a filesystem store. + FileSystem struct { + path string + } + ViewOptions struct { + Display bool + } + +) + +// NewFileSystemStore gets the lockbox directory (filesystem-based) store. +func NewFileSystemStore() string { return os.Getenv("LOCKBOX_STORE") } -// List will get all lockbox files in a directory store. -func List(store string, display bool) ([]string, error) { +// List will get all lockbox files in a store. +func (s FileSystem) List(options ViewOptions) ([]string, error) { var results []string - if !PathExists(store) { - return nil, errors.New("store does not exists") + if !misc.PathExists(s.path) { + return nil, errors.New("store does not exist") } - err := filepath.Walk(store, func(path string, info fs.FileInfo, err error) error { + err := filepath.Walk(s.path, func(path string, info fs.FileInfo, err error) error { if err != nil { return err } if strings.HasSuffix(path, Extension) { usePath := path - if display { - usePath = strings.TrimPrefix(usePath, store) + if options.Display { + usePath = strings.TrimPrefix(usePath, s.path) usePath = strings.TrimPrefix(usePath, "/") usePath = strings.TrimSuffix(usePath, Extension) } @@ -44,7 +56,7 @@ func List(store string, display bool) ([]string, error) { if err != nil { return nil, err } - if display { + if options.Display { sort.Strings(results) } return results, nil diff --git a/internal/store/store_test.go b/internal/store/store_test.go @@ -0,0 +1,50 @@ +package store + +import ( + "os" + "path/filepath" + "testing" + "github.com/enckse/lockbox/internal/misc" +) + +func TestListErrors(t *testing.T) { + _, err := FileSystem{path: "aaa"}.List(ViewOptions{}) + if err == nil || err.Error() != "store does not exist" { + t.Errorf("invalid store error: %v", err) + } +} + +func TestList(t *testing.T) { + testStore := "bin" + if misc.PathExists(testStore) { + if err := os.RemoveAll(testStore); err != nil { + t.Errorf("invalid error on remove: %v", err) + } + } + if err := os.MkdirAll(filepath.Join(testStore, "sub"), 0755); err != nil { + t.Errorf("unable to makedir: %v", err) + } + for _, path := range []string{"test", "test2", "aaa", "sub/aaaaajk", "sub/12lkjafav"} { + if err := os.WriteFile(filepath.Join(testStore, path+Extension), []byte(""), 0644); err != nil { + t.Errorf("failed to write %s: %v", path, err) + } + } + s := FileSystem{path: testStore} + res, err := s.List(ViewOptions{}) + if err != nil { + t.Errorf("unable to list: %v", err) + } + if len(res) != 5 { + t.Error("mismatched results") + } + res, err = s.List(ViewOptions{Display: true}) + if err != nil { + t.Errorf("unable to list: %v", err) + } + if len(res) != 5 { + t.Error("mismatched results") + } + if res[0] != "aaa" || res[1] != "sub/12lkjafav" || res[2] != "sub/aaaaajk" || res[3] != "test" || res[4] != "test2" { + t.Errorf("not sorted: %v", res) + } +} diff --git a/internal/utils.go b/internal/utils.go @@ -1,31 +0,0 @@ -package internal - -import ( - "fmt" - "os" -) - -// LogError will log an error to stderr. -func LogError(message string, err error) { - msg := message - if err != nil { - msg = fmt.Sprintf("%s (%v)", msg, err) - } - fmt.Fprintln(os.Stderr, msg) -} - -// Die will print messages and exit. -func Die(message string, err error) { - LogError(message, err) - os.Exit(1) -} - -// PathExists indicates if a path exists. -func PathExists(path string) bool { - if _, err := os.Stat(path); err != nil { - if os.IsNotExist(err) { - return false - } - } - return true -}