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:
| M | cmd/lb/main.go | | | 71 | ++++++++--------------------------------------------------------------- |
| M | internal/encdec.go | | | 37 | +++++++++++++++++++++++++++++++++++++ |
| A | internal/socket.go | | | 168 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| M | internal/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
+}