lockbox

password manager
Log | Files | Refs | README | LICENSE

commit e0ec888a9a9e52f284d80e3ac2a470adfb4fc4ec
parent a67d62dde8daa86f3ea90059910a6198d8b49036
Author: Sean Enck <sean@ttypty.com>
Date:   Tue, 12 Jul 2022 18:55:34 -0400

updating lockbox to be internally maintained without stock and adding hooks

Diffstat:
Mcmd/lb-diff/main.go | 5++---
Mcmd/lb-pwgen/main.go | 24++++++++++++------------
Mcmd/lb-rekey/main.go | 11+++++------
Mcmd/lb-rw/main.go | 9++++-----
Mcmd/lb-totp/main.go | 17++++++++---------
Mcmd/lb/main.go | 85++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
Mgo.mod | 1-
Mgo.sum | 2--
Minternal/clip.go | 14++++++--------
Minternal/encdec.go | 11+++++------
Minternal/utils.go | 70++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
11 files changed, 160 insertions(+), 89 deletions(-)

diff --git a/cmd/lb-diff/main.go b/cmd/lb-diff/main.go @@ -5,18 +5,17 @@ import ( "os" "voidedtech.com/lockbox/internal" - "voidedtech.com/stock" ) func main() { args := os.Args l, err := internal.NewLockbox("", "", args[len(args)-1]) if err != nil { - stock.Die("unable to make lockbox model instance", err) + internal.Die("unable to make lockbox model instance", err) } result, err := l.Decrypt() if err != nil { - stock.Die("unable to read file", err) + internal.Die("unable to read file", err) } if result != nil { fmt.Println(string(result)) diff --git a/cmd/lb-pwgen/main.go b/cmd/lb-pwgen/main.go @@ -11,7 +11,7 @@ import ( "strings" "time" - "voidedtech.com/stock" + "voidedtech.com/lockbox/internal" ) const ( @@ -41,15 +41,15 @@ func main() { var paths []string parts := strings.Split(src, ":") for _, p := range parts { - if stock.PathExists(p) { + if internal.PathExists(p) { info, err := os.Stat(p) if err != nil { - stock.Die("unable to stat", err) + internal.Die("unable to stat", err) } if info.IsDir() { files, err := os.ReadDir(p) if err != nil { - stock.Die("failed to read directory", err) + internal.Die("failed to read directory", err) } var results []string for _, f := range files { @@ -62,7 +62,7 @@ func main() { } } if len(paths) == 0 { - stock.Die("no paths found for generation", stock.NewBasicError("unable to read paths")) + internal.Die("no paths found for generation", internal.NewLockboxError("unable to read paths")) } result := "" l := *length @@ -92,37 +92,37 @@ func main() { name = newValue case transformModeSed: if len(sedPattern) == 0 { - stock.Die("unable to use sed transform without pattern", stock.NewBasicError("set PWGEN_SED")) + internal.Die("unable to use sed transform without pattern", internal.NewLockboxError("set PWGEN_SED")) } cmd := exec.Command("sed", "-e", sedPattern) stdin, err := cmd.StdinPipe() if err != nil { - stock.Die("unable to attach stdin to sed", err) + internal.Die("unable to attach stdin to sed", err) } var stdout bytes.Buffer var stderr bytes.Buffer cmd.Stdout = &stdout cmd.Stderr = &stderr if err := cmd.Start(); err != nil { - stock.Die("failed to run sed", err) + internal.Die("failed to run sed", err) } if _, err := io.WriteString(stdin, name); err != nil { stdin.Close() - stock.Die("write to stdin failed for sed", err) + internal.Die("write to stdin failed for sed", err) } stdin.Close() if err := cmd.Wait(); err != nil { - stock.Die("sed failed", err) + internal.Die("sed failed", err) } errors := strings.TrimSpace(stderr.String()) if len(errors) > 0 { - stock.Die("sed stderr failure", stock.NewBasicError(errors)) + internal.Die("sed stderr failure", internal.NewLockboxError(errors)) } name = strings.TrimSpace(stdout.String()) case transformModeNone: break default: - stock.Die("unknown transform mode", stock.NewBasicError(transform)) + internal.Die("unknown transform mode", internal.NewLockboxError(transform)) } result += name } diff --git a/cmd/lb-rekey/main.go b/cmd/lb-rekey/main.go @@ -6,7 +6,6 @@ import ( "strings" "voidedtech.com/lockbox/internal" - "voidedtech.com/stock" ) func main() { @@ -17,24 +16,24 @@ func main() { flag.Parse() found, err := internal.Find(internal.GetStore(), false) if err != nil { - stock.Die("failed finding entries", err) + internal.Die("failed finding entries", err) } for _, file := range found { fmt.Printf("rekeying: %s\n", file) in, err := internal.NewLockbox(*inKey, *inMode, file) if err != nil { - stock.Die("unable to make input lockbox", err) + internal.Die("unable to make input lockbox", err) } decrypt, err := in.Decrypt() if err != nil { - stock.Die("failed to process file decryption", err) + internal.Die("failed to process file decryption", err) } out, err := internal.NewLockbox(*outKey, *outMode, file) if err != nil { - stock.Die("unable to make output lockbox", err) + internal.Die("unable to make output lockbox", err) } if err := out.Encrypt([]byte(strings.TrimSpace(string(decrypt)))); err != nil { - stock.Die("failed to encrypt file", err) + internal.Die("failed to encrypt file", err) } } } diff --git a/cmd/lb-rw/main.go b/cmd/lb-rw/main.go @@ -5,7 +5,6 @@ import ( "fmt" "voidedtech.com/lockbox/internal" - "voidedtech.com/stock" ) func main() { @@ -16,20 +15,20 @@ func main() { flag.Parse() l, err := internal.NewLockbox(*key, *keyMode, *file) if err != nil { - stock.Die("unable to make lockbox model instance", err) + internal.Die("unable to make lockbox model instance", err) } switch *mode { case "encrypt": if err := l.Encrypt(nil); err != nil { - stock.Die("failed to encrypt", err) + internal.Die("failed to encrypt", err) } case "decrypt": results, err := l.Decrypt() if err != nil { - stock.Die("failed to decrypt", err) + internal.Die("failed to decrypt", err) } fmt.Println(string(results)) default: - stock.Die("invalid mode", stock.NewBasicError("bad mode")) + internal.Die("invalid mode", internal.NewLockboxError("bad mode")) } } diff --git a/cmd/lb-totp/main.go b/cmd/lb-totp/main.go @@ -11,7 +11,6 @@ import ( otp "github.com/pquerna/otp/totp" "voidedtech.com/lockbox/internal" - "voidedtech.com/stock" ) func getEnv() string { @@ -32,7 +31,7 @@ func list() ([]string, error) { } } if len(results) == 0 { - return nil, stock.NewBasicError("no objects found") + return nil, internal.NewLockboxError("no objects found") } return results, nil } @@ -54,7 +53,7 @@ func display(token string, clip, once, short bool) error { interactive = false } if !interactive && clip { - return stock.NewBasicError("clipboard not available in non-interactive mode") + return internal.NewLockboxError("clipboard not available in non-interactive mode") } redStart, redEnd, err := internal.GetColor(internal.ColorRed) if err != nil { @@ -62,8 +61,8 @@ func display(token string, clip, once, short bool) error { } tok := strings.TrimSpace(token) store := filepath.Join(getEnv(), tok+internal.Extension) - if !stock.PathExists(store) { - return stock.NewBasicError("object does not exist") + if !internal.PathExists(store) { + return internal.NewLockboxError("object does not exist") } l, err := internal.NewLockbox("", "", store) if err != nil { @@ -146,13 +145,13 @@ func display(token string, clip, once, short bool) error { func main() { args := os.Args if len(args) > 3 || len(args) < 2 { - stock.Die("subkey required", stock.NewBasicError("invalid arguments")) + internal.Die("subkey required", internal.NewLockboxError("invalid arguments")) } cmd := args[1] if cmd == "-list" || cmd == "-ls" { result, err := list() if err != nil { - stock.Die("invalid list response", err) + internal.Die("invalid list response", err) } sort.Strings(result) for _, entry := range result { @@ -165,7 +164,7 @@ func main() { short := false if len(args) == 3 { if cmd != "-c" && cmd != "clip" && cmd != "-once" && cmd != "-short" { - stock.Die("subcommand not supported", stock.NewBasicError("invalid sub command")) + internal.Die("subcommand not supported", internal.NewLockboxError("invalid sub command")) } clip = cmd == "-clip" || cmd == "-c" once = cmd == "-once" @@ -173,6 +172,6 @@ func main() { cmd = args[2] } if err := display(cmd, clip, once, short); err != nil { - stock.Die("failed to show totp token", err) + internal.Die("failed to show totp token", err) } } diff --git a/cmd/lb/main.go b/cmd/lb/main.go @@ -10,7 +10,6 @@ import ( "time" "voidedtech.com/lockbox/internal" - "voidedtech.com/stock" ) var ( @@ -19,15 +18,41 @@ var ( func getEntry(store string, args []string, idx int) string { if len(args) != idx+1 { - stock.Die("invalid entry given", stock.NewBasicError("specific entry required")) + internal.Die("invalid entry given", internal.NewLockboxError("specific entry required")) } return filepath.Join(store, args[idx]) + internal.Extension } +func hooks() { + hookDir := os.Getenv("LOCKBOX_HOOKDIR") + if !internal.PathExists(hookDir) { + return + } + dirs, err := os.ReadDir(hookDir) + if err != nil { + internal.Die("unable to read hookdir", err) + } + for _, d := range dirs { + if !d.IsDir() { + if d.Type() & 0111 == 011 { + name := d.Name() + cmd := exec.Command(filepath.Join(hookDir, name)) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + internal.Die(fmt.Sprintf("hook failed: %s", name), err) + } + continue + } + } + internal.Die("invalid hook", internal.NewLockboxError("hook is not file and/or has wrong mode")) + } +} + func main() { args := os.Args if len(args) < 2 { - stock.Die("missing arguments", stock.NewBasicError("requires subcommand")) + internal.Die("missing arguments", internal.NewLockboxError("requires subcommand")) } command := args[1] store := internal.GetStore() @@ -37,13 +62,13 @@ func main() { searchTerm := "" if isFind { if len(args) < 3 { - stock.Die("find requires an argument to search for", stock.NewBasicError("search term required")) + internal.Die("find requires an argument to search for", internal.NewLockboxError("search term required")) } searchTerm = args[2] } files, err := internal.Find(store, true) if err != nil { - stock.Die("unable to list files", err) + internal.Die("unable to list files", err) } for _, f := range files { if isFind { @@ -60,20 +85,20 @@ func main() { idx := 2 switch len(args) { case 2: - stock.Die("insert missing required arguments", stock.NewBasicError("entry required")) + internal.Die("insert missing required arguments", internal.NewLockboxError("entry required")) case 3: case 4: multi = args[2] == "-m" if !multi { - stock.Die("multi-line insert must be after 'insert'", stock.NewBasicError("invalid command")) + internal.Die("multi-line insert must be after 'insert'", internal.NewLockboxError("invalid command")) } idx = 3 default: - stock.Die("too many arguments", stock.NewBasicError("insert can only perform one operation")) + internal.Die("too many arguments", internal.NewLockboxError("insert can only perform one operation")) } isPipe := internal.IsInputFromPipe() entry := getEntry(store, args, idx) - if stock.PathExists(entry) { + if internal.PathExists(entry) { if !isPipe { if !confirm("overwrite existing") { return @@ -81,9 +106,9 @@ func main() { } } else { dir := filepath.Dir(entry) - if !stock.PathExists(dir) { + if !internal.PathExists(dir) { if err := os.MkdirAll(dir, 0755); err != nil { - stock.Die("failed to create directory structure", err) + internal.Die("failed to create directory structure", err) } } } @@ -91,34 +116,36 @@ func main() { if !multi && !isPipe { input, err := internal.ConfirmInput() if err != nil { - stock.Die("password input failed", err) + internal.Die("password input failed", err) } password = input } else { input, err := internal.Stdin(false) if err != nil { - stock.Die("failed to read stdin", err) + internal.Die("failed to read stdin", err) } password = input } if password == "" { - stock.Die("empty password provided", stock.NewBasicError("password can NOT be empty")) + internal.Die("empty password provided", internal.NewLockboxError("password can NOT be empty")) } l, err := internal.NewLockbox("", "", entry) if err != nil { - stock.Die("unable to make lockbox model instance", err) + internal.Die("unable to make lockbox model instance", err) } if err := l.Encrypt([]byte(password)); err != nil { - stock.Die("failed to save password", err) + internal.Die("failed to save password", err) } fmt.Println("") + hooks() case "rm": entry := getEntry(store, args, 2) - if !stock.PathExists(entry) { - stock.Die("does not exists", stock.NewBasicError("can not delete unknown entry")) + if !internal.PathExists(entry) { + internal.Die("does not exists", internal.NewLockboxError("can not delete unknown entry")) } if confirm("remove entry") { os.Remove(entry) + hooks() } case "show", "-c", "clip": inEntry := getEntry(store, args, 2) @@ -127,32 +154,32 @@ func main() { if strings.Contains(inEntry, "*") { matches, err := filepath.Glob(inEntry) if err != nil { - stock.Die("bad glob", err) + internal.Die("bad glob", err) } entries = matches } isGlob := len(entries) > 1 if isGlob { if !isShow { - stock.Die("cannot glob to clipboard", stock.NewBasicError("bad glob request")) + internal.Die("cannot glob to clipboard", internal.NewLockboxError("bad glob request")) } sort.Strings(entries) } startColor, endColor, err := internal.GetColor(internal.ColorRed) if err != nil { - stock.Die("unable to get color for terminal", err) + internal.Die("unable to get color for terminal", err) } for _, entry := range entries { - if !stock.PathExists(entry) { - stock.Die("invalid entry", stock.NewBasicError("entry not found")) + if !internal.PathExists(entry) { + internal.Die("invalid entry", internal.NewLockboxError("entry not found")) } l, err := internal.NewLockbox("", "", entry) if err != nil { - stock.Die("unable to make lockbox model instance", err) + internal.Die("unable to make lockbox model instance", err) } decrypt, err := l.Decrypt() if err != nil { - stock.Die("unable to decrypt", err) + internal.Die("unable to decrypt", err) } value := strings.TrimSpace(string(decrypt)) if isShow { @@ -173,11 +200,11 @@ func main() { idx := 0 val, err := internal.Stdin(false) if err != nil { - stock.Die("unable to read value to clear", err) + internal.Die("unable to read value to clear", err) } _, paste, err := internal.GetClipboardCommand() if err != nil { - stock.Die("unable to get paste command", err) + internal.Die("unable to get paste command", err) } var args []string if len(paste) > 1 { @@ -204,7 +231,7 @@ func main() { c.Stdout = os.Stdout c.Stderr = os.Stderr if err := c.Run(); err != nil { - stock.Die("bad command", err) + internal.Die("bad command", err) } } } @@ -213,7 +240,7 @@ func confirm(prompt string) bool { fmt.Printf("%s? (y/N) ", prompt) resp, err := internal.Stdin(true) if err != nil { - stock.Die("failed to get response", err) + internal.Die("failed to get response", err) } return resp == "Y" || resp == "y" } diff --git a/go.mod b/go.mod @@ -6,7 +6,6 @@ require ( github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/pquerna/otp v1.3.0 golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838 - voidedtech.com/stock v0.0.0-20211014234009-93c0ed43354e ) require ( diff --git a/go.sum b/go.sum @@ -23,5 +23,3 @@ golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -voidedtech.com/stock v0.0.0-20211014234009-93c0ed43354e h1:feU8+uf5lbdKC9Z4+5+x7KObXW6mjTZK+ZqnY/d6oZA= -voidedtech.com/stock v0.0.0-20211014234009-93c0ed43354e/go.mod h1:fDeTx9Bymp++UZEUI+pxljhMDzibXQvRKTcg1h+5tw4= diff --git a/internal/clip.go b/internal/clip.go @@ -5,8 +5,6 @@ import ( "os" "os/exec" "strings" - - "voidedtech.com/stock" ) const ( @@ -32,14 +30,14 @@ func GetClipboardCommand() ([]string, []string, error) { case "Linux": if strings.TrimSpace(os.Getenv("WAYLAND_DISPLAY")) == "" { if strings.TrimSpace(os.Getenv("DISPLAY")) == "" { - return nil, nil, stock.NewBasicError("unable to detect linux clipboard mode") + return nil, nil, NewLockboxError("unable to detect linux clipboard mode") } env = xClipMode } else { env = waylandClipMode } default: - return nil, nil, stock.NewBasicError("unable to detect clipboard mode") + return nil, nil, NewLockboxError("unable to detect clipboard mode") } } switch env { @@ -50,9 +48,9 @@ func GetClipboardCommand() ([]string, []string, error) { case waylandClipMode: return []string{"wl-copy"}, []string{"wl-paste"}, nil case "off": - return nil, nil, stock.NewBasicError("clipboard is turned off") + return nil, nil, NewLockboxError("clipboard is turned off") } - return nil, nil, stock.NewBasicError("unable to get clipboard command(s)") + return nil, nil, NewLockboxError("unable to get clipboard command(s)") } // CopyToClipboard will copy to clipboard, if non-empty will clear later. @@ -77,7 +75,7 @@ func pipeTo(command, value string, wait bool, args ...string) { cmd := exec.Command(command, args...) stdin, err := cmd.StdinPipe() if err != nil { - stock.Die("unable to get stdin pipe", err) + Die("unable to get stdin pipe", err) } go func() { @@ -93,6 +91,6 @@ func pipeTo(command, value string, wait bool, args ...string) { ran = cmd.Start() } if ran != nil { - stock.Die("failed to run command", ran) + Die("failed to run command", ran) } } diff --git a/internal/encdec.go b/internal/encdec.go @@ -11,7 +11,6 @@ import ( "github.com/google/shlex" "golang.org/x/crypto/nacl/secretbox" - "voidedtech.com/stock" ) const ( @@ -48,11 +47,11 @@ func NewLockbox(key, keyMode, file string) (Lockbox, error) { } if len(b) == 0 { - return Lockbox{}, stock.NewBasicError("key is empty") + return Lockbox{}, NewLockboxError("key is empty") } if len(b) > keyLength { - return Lockbox{}, stock.NewBasicError("key is too large for use") + return Lockbox{}, NewLockboxError("key is too large for use") } for len(b) < keyLength { @@ -80,7 +79,7 @@ func getKey(keyMode, name string) ([]byte, error) { case PlainKeyMode: data = []byte(name) default: - return nil, stock.NewBasicError("unknown keymode") + return nil, NewLockboxError("unknown keymode") } return []byte(strings.TrimSpace(string(data))), nil } @@ -98,7 +97,7 @@ func (l Lockbox) Encrypt(datum []byte) error { } data := datum if data == nil { - b, err := stock.Stdin(false) + b, err := getStdin(false) if err != nil { return err } @@ -126,7 +125,7 @@ func (l Lockbox) Decrypt() ([]byte, error) { copy(nonce[:], encrypted[:nonceLength]) decrypted, ok := secretbox.Open(nil, encrypted[nonceLength:], &nonce, &l.secret) if !ok { - return nil, stock.NewBasicError("decrypt not ok") + return nil, NewLockboxError("decrypt not ok") } padding := int(decrypted[0]) diff --git a/internal/utils.go b/internal/utils.go @@ -1,6 +1,8 @@ package internal import ( + "bufio" + "bytes" "fmt" "io/fs" "os" @@ -8,13 +10,14 @@ import ( "sort" "strings" "syscall" - - "voidedtech.com/stock" ) type ( // Color are terminal colors for dumb terminal coloring. Color int + LockboxError struct { + message string + } ) const ( @@ -37,7 +40,7 @@ func isYesNoEnv(defaultValue bool, env string) (bool, error) { case "yes": return true, nil } - return false, stock.NewBasicError(fmt.Sprintf("invalid yes/no env value for %s", env)) + return false, NewLockboxError(fmt.Sprintf("invalid yes/no env value for %s", env)) } // IsInteractive indicates if running as a user UI experience. @@ -48,7 +51,7 @@ func IsInteractive() (bool, error) { // GetColor will retrieve start/end terminal coloration indicators. func GetColor(color Color) (string, string, error) { if color != ColorRed { - return "", "", stock.NewBasicError("bad color") + return "", "", NewLockboxError("bad color") } interactive, err := IsInteractive() if err != nil { @@ -76,8 +79,8 @@ func GetStore() string { // Find will find all lockbox files in a directory store. func Find(store string, display bool) ([]string, error) { var results []string - if !stock.PathExists(store) { - return nil, stock.NewBasicError("store does not exists") + if !PathExists(store) { + return nil, NewLockboxError("store does not exists") } err := filepath.Walk(store, func(path string, info fs.FileInfo, err error) error { if err != nil { @@ -150,14 +153,14 @@ func ConfirmInput() (string, error) { return "", err } if first != second { - return "", stock.NewBasicError("passwords do NOT match") + return "", NewLockboxError("passwords do NOT match") } return first, nil } // Stdin will retrieve stdin data. func Stdin(one bool) (string, error) { - b, err := stock.Stdin(one) + b, err := getStdin(one) if err != nil { return "", err } @@ -169,3 +172,54 @@ func IsInputFromPipe() bool { fileInfo, _ := os.Stdin.Stat() return fileInfo.Mode()&os.ModeCharDevice == 0 } + +// NewLockboxError creates a non-category error. +func NewLockboxError(message string) error { + return &LockboxError{message} +} + +// Error gets the error message for a basic error. +func (err *LockboxError) Error() string { + return err.message +} + +// 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 +} + +func getStdin(one bool) ([]byte, error) { + scanner := bufio.NewScanner(os.Stdin) + var b bytes.Buffer + for scanner.Scan() { + b.WriteString(scanner.Text()) + b.WriteString("\n") + if one { + break + } + } + if err := scanner.Err(); err != nil { + return nil, err + } + return b.Bytes(), nil +}