lockbox

password manager
Log | Files | Refs | README | LICENSE

commit d7a7c7730603fb1efc323f500ff19da7c4df5f11
parent add014c76fc747b0b006a64e009d497fe77cb6a3
Author: Sean Enck <sean@ttypty.com>
Date:   Wed,  6 Oct 2021 18:08:56 -0400

adding a lockbox key management daemon

Diffstat:
Mcmd/lb/main.go | 71++++++++---------------------------------------------------------------
Minternal/encdec.go | 37+++++++++++++++++++++++++++++++++++++
Ainternal/socket.go | 168+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Minternal/utils.go | 79+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
4 files changed, 290 insertions(+), 65 deletions(-)

diff --git a/cmd/lb/main.go b/cmd/lb/main.go @@ -7,7 +7,6 @@ import ( "path/filepath" "sort" "strings" - "syscall" "time" "voidedtech.com/lockbox/internal" @@ -25,56 +24,6 @@ func getEntry(store string, args []string, idx int) string { return filepath.Join(store, args[idx]) + internal.Extension } -func termEcho(on bool) { - // Common settings and variables for both stty calls. - attrs := syscall.ProcAttr{ - Dir: "", - Env: []string{}, - Files: []uintptr{os.Stdin.Fd(), os.Stdout.Fd(), os.Stderr.Fd()}, - Sys: nil} - var ws syscall.WaitStatus - cmd := "echo" - if !on { - cmd = "-echo" - } - - // Enable/disable echoing. - pid, err := syscall.ForkExec( - "/bin/stty", - []string{"stty", cmd}, - &attrs) - if err != nil { - panic(err) - } - - // Wait for the stty process to complete. - _, err = syscall.Wait4(pid, &ws, 0, nil) - if err != nil { - panic(err) - } -} - -func readInput() (string, error) { - termEcho(false) - defer func() { - termEcho(true) - }() - fmt.Printf("please enter password: ") - first, err := stdin(true) - if err != nil { - return "", err - } - fmt.Printf("\nplease re-enter password: ") - second, err := stdin(true) - if err != nil { - return "", err - } - if first != second { - return "", internal.NewLockboxError("passwords do NOT match") - } - return first, nil -} - func main() { args := os.Args if len(args) < 2 { @@ -104,6 +53,10 @@ func main() { } fmt.Println(f) } + case "credential-server", "credential-client": + if err := internal.SocketHandler(command == "credential-server"); err != nil { + stock.Die("credential handler failed", err) + } case "version": fmt.Printf("version: %s\n", version) case "insert": @@ -140,13 +93,13 @@ func main() { } var password string if !multi && !isPipe { - input, err := readInput() + input, err := internal.ConfirmInput() if err != nil { stock.Die("password input failed", err) } password = input } else { - input, err := stdin(false) + input, err := internal.Stdin(false) if err != nil { stock.Die("failed to read stdin", err) } @@ -222,7 +175,7 @@ func main() { } case "clear": idx := 0 - val, err := stdin(false) + val, err := internal.Stdin(false) if err != nil { stock.Die("unable to read value to clear", err) } @@ -252,17 +205,9 @@ func main() { } } -func stdin(one bool) (string, error) { - b, err := stock.Stdin(one) - if err != nil { - return "", err - } - return strings.TrimSpace(string(b)), nil -} - func confirm(prompt string) bool { fmt.Printf("%s? (y/N) ", prompt) - resp, err := stdin(true) + resp, err := internal.Stdin(true) if err != nil { stock.Die("failed to get response", err) } diff --git a/internal/encdec.go b/internal/encdec.go @@ -21,6 +21,8 @@ const ( MacOSKeyMode = "macos" // PlainKeyMode is plaintext based key resolution. PlainKeyMode = "plaintext" + // LockboxKeyMode is a lockbox-based daemon key resolution. + LockboxKeyMode = "lockbox" ) type ( @@ -70,6 +72,41 @@ func getKey(keyMode, name string) ([]byte, error) { return nil, err } data = b + case LockboxKeyMode: + exe, err := os.Executable() + if err != nil { + return nil, err + } + cmd := exec.Command(exe, "credential-client") + stdin, err := cmd.StdinPipe() + if err != nil { + return nil, err + } + defer func() { + termEcho(true) + }() + + var stdinErr error + go func() { + defer stdin.Close() + termEcho(false) + input, err := readPassword() + if err != nil { + stdinErr = err + return + } + if _, err := io.WriteString(stdin, input); err != nil { + stdinErr = err + } + }() + b, err := cmd.Output() + if err != nil { + return nil, err + } + if stdinErr != nil { + return nil, stdinErr + } + data = b case PlainKeyMode: data = []byte(name) default: diff --git a/internal/socket.go b/internal/socket.go @@ -0,0 +1,168 @@ +package internal + +import ( + "bytes" + "fmt" + "net" + "os" + "path/filepath" + "strconv" + "strings" + "sync" + "time" + + "voidedtech.com/stock" +) + +const ( + getCommand = "get:" + setCommand = "set:" + respCommand = "res:" +) + +var ( + credential []byte + lock = &sync.Mutex{} + stored time.Time +) + +func readConn(conn net.Conn) (string, error) { + buf := make([]byte, 512) + if _, err := conn.Read(buf); err != nil { + return "", err + } + b := bytes.Trim(buf, "\x00") + return strings.TrimSpace(string(b)), nil +} + +func purge(duration time.Duration) { + for { + lock.Lock() + if credential != nil { + now := time.Now().Add(duration) + if stored.Before(now) { + credential = nil + stored = time.Now() + } + } + lock.Unlock() + time.Sleep(5 * time.Second) + } +} + +// SocketHandler handles the daemon socket for lockbox key resolution. +func SocketHandler(isHost bool) error { + path := os.Getenv("LOCKBOX_SOCKET") + if path == "" { + h := os.Getenv("HOME") + if h == "" { + return NewLockboxError("unable to get HOME") + } + path = filepath.Join(h, ".lb", "lockbox.sock") + } + if isHost { + caching := 1440 + if keep := os.Getenv("LOCKBOX_CCACHE"); keep != "" { + i, err := strconv.Atoi(keep) + if err != nil { + return err + } + caching = i + } + if caching != 0 { + if caching > 0 { + caching *= -1 + } + keepFor := time.Duration(caching) * time.Minute + go purge(keepFor) + } + dir := filepath.Dir(path) + if !stock.PathExists(dir) { + if err := os.MkdirAll(dir, 0700); err != nil { + return err + } + } + if stock.PathExists(path) { + if err := os.Remove(path); err != nil { + return err + } + } + l, err := net.Listen("unix", path) + if err != nil { + return err + } + defer l.Close() + for { + conn, err := l.Accept() + if err != nil { + stock.LogError("unable to accept connection", err) + continue + } + cmd, err := readConn(conn) + if err != nil { + stock.LogError("failed to read command", err) + conn.Close() + continue + } + lock.Lock() + if strings.HasPrefix(cmd, getCommand) { + write := []byte(respCommand) + if credential != nil { + write = append(write, credential...) + } + _, err := conn.Write(write) + if err != nil { + stock.LogError("failed to write credential to connection", err) + } + } else { + if strings.HasPrefix(cmd, setCommand) { + text := strings.Replace(cmd, setCommand, "", 1) + credential = []byte(text) + stored = time.Now() + if _, err := conn.Write([]byte(respCommand)); err != nil { + stock.LogError("failed to write empty set response", err) + } + } else { + stock.LogError("unknown command", nil) + } + } + lock.Unlock() + conn.Close() + } + } + + c, err := net.Dial("unix", path) + if err != nil { + return err + } + _, err = c.Write([]byte(getCommand)) + if err != nil { + c.Close() + return err + } + data, err := readConn(c) + c.Close() + if err != nil { + return err + } + if data == respCommand { + input, err := readPassword() + if err != nil { + return err + } + setting := []byte(setCommand) + setting = append(setting, input...) + c, err := net.Dial("unix", path) + if err != nil { + return err + } + if _, err := c.Write(setting); err != nil { + return err + } + data = input + } else { + data = strings.Replace(data, respCommand, "", 1) + } + fmt.Println(data) + return nil +} diff --git a/internal/utils.go b/internal/utils.go @@ -1,11 +1,13 @@ package internal import ( + "fmt" "io/fs" "os" "path/filepath" "sort" "strings" + "syscall" "voidedtech.com/stock" ) @@ -17,9 +19,9 @@ type ( const ( // Extension is the lockbox file extension. - Extension = ".lb" + Extension = ".lb" termBeginRed = "\033[1;31m" - termEndRed = "\033[0m" + termEndRed = "\033[0m" // ColorRed will get red terminal coloring. ColorRed = iota ) @@ -70,3 +72,76 @@ func Find(store string, display bool) ([]string, error) { } return results, nil } + +func termEcho(on bool) { + // Common settings and variables for both stty calls. + attrs := syscall.ProcAttr{ + Dir: "", + Env: []string{}, + Files: []uintptr{os.Stdin.Fd(), os.Stdout.Fd(), os.Stderr.Fd()}, + Sys: nil} + var ws syscall.WaitStatus + cmd := "echo" + if !on { + cmd = "-echo" + } + + // Enable/disable echoing. + pid, err := syscall.ForkExec( + "/bin/stty", + []string{"stty", cmd}, + &attrs) + if err != nil { + panic(err) + } + + // Wait for the stty process to complete. + _, err = syscall.Wait4(pid, &ws, 0, nil) + if err != nil { + panic(err) + } +} + +func readPassword() (string, error) { + return readInput(true) +} + +// ConfirmInput will get 2 inputs and confirm they are the same. +func ConfirmInput() (string, error) { + return readInput(false) +} + +func readInput(onlyOne bool) (string, error) { + if !onlyOne { + termEcho(false) + defer func() { + termEcho(true) + }() + fmt.Printf("please enter password: ") + } + first, err := Stdin(true) + if err != nil { + return "", err + } + if onlyOne { + return first, nil + } + fmt.Printf("\nplease re-enter password: ") + second, err := Stdin(true) + if err != nil { + return "", err + } + if first != second { + 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) + if err != nil { + return "", err + } + return strings.TrimSpace(string(b)), nil +}