lockbox

password manager
Log | Files | Refs | README | LICENSE

commit e5d68c07d1c4f71e5a4f3039b4963c0d538d3606
parent 47c3363f5699ab3573a8efa2e94f91c239b14e68
Author: Sean Enck <sean@ttypty.com>
Date:   Thu, 27 Jul 2023 18:59:00 -0400

restructure platform

Diffstat:
Minternal/inputs/env.go | 38++++++++++++++++++++++++++++++++++++++
Minternal/inputs/env_test.go | 21+++++++++++++++++++++
Minternal/inputs/vars.go | 2+-
Minternal/platform/clipboard.go | 2+-
Dinternal/platform/colors.go | 54------------------------------------------------------
Dinternal/platform/core.go | 50--------------------------------------------------
Dinternal/platform/core_test.go | 30------------------------------
Ainternal/platform/os.go | 159+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rinternal/platform/paths_test.go -> internal/platform/os_test.go | 0
Dinternal/platform/paths.go | 15---------------
Dinternal/platform/stdin.go | 166-------------------------------------------------------------------------------
Ainternal/platform/terminal.go | 54++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rinternal/platform/colors_test.go -> internal/platform/terminal_test.go | 0
13 files changed, 274 insertions(+), 317 deletions(-)

diff --git a/internal/inputs/env.go b/internal/inputs/env.go @@ -2,8 +2,10 @@ package inputs import ( + "errors" "fmt" "os" + "os/exec" "strconv" "strings" @@ -21,6 +23,7 @@ const ( LinuxXPlatform = "linux-x" // WindowsLinuxPlatform for WSL subsystems WindowsLinuxPlatform = "wsl" + unknownPlatform = "" ) type ( @@ -195,3 +198,38 @@ func (e EnvironmentFormatter) values() (string, []string) { func (e EnvironmentCommand) values() (string, []string) { return detectedValue, []string{commandArgsExample} } + +// NewPlatform gets a new system platform. +func NewPlatform() (SystemPlatform, error) { + env := EnvPlatform.Get() + if env != "" { + for _, p := range EnvPlatform.allowed { + if p == env { + return SystemPlatform(p), nil + } + } + return unknownPlatform, errors.New("unknown platform mode") + } + b, err := exec.Command("uname", "-a").Output() + if err != nil { + return unknownPlatform, err + } + raw := strings.ToLower(strings.TrimSpace(string(b))) + parts := strings.Split(raw, " ") + switch parts[0] { + case "darwin": + return MacOSPlatform, nil + case "linux": + if strings.Contains(raw, "microsoft-standard-wsl") { + return WindowsLinuxPlatform, nil + } + if strings.TrimSpace(os.Getenv("WAYLAND_DISPLAY")) == "" { + if strings.TrimSpace(os.Getenv("DISPLAY")) == "" { + return unknownPlatform, errors.New("unable to detect linux clipboard mode") + } + return LinuxXPlatform, nil + } + return LinuxWaylandPlatform, nil + } + return unknownPlatform, errors.New("unable to detect clipboard mode") +} diff --git a/internal/inputs/env_test.go b/internal/inputs/env_test.go @@ -28,3 +28,24 @@ func TestKeyValue(t *testing.T) { t.Errorf("invalid keyvalue") } } + +func TestNewPlatform(t *testing.T) { + for _, item := range inputs.PlatformSet() { + os.Setenv("LOCKBOX_PLATFORM", item) + s, err := inputs.NewPlatform() + if err != nil { + t.Errorf("invalid clipboard: %v", err) + } + if s != inputs.SystemPlatform(item) { + t.Error("mismatch on input and resulting detection") + } + } +} + +func TestNewPlatformUnknown(t *testing.T) { + os.Setenv("LOCKBOX_PLATFORM", "afleaj") + _, err := inputs.NewPlatform() + if err == nil || err.Error() != "unknown platform mode" { + t.Errorf("error expected for platform: %v", err) + } +} diff --git a/internal/inputs/vars.go b/internal/inputs/vars.go @@ -53,7 +53,7 @@ var ( // EnvTOTPToken is the leaf token to use to store TOTP tokens EnvTOTPToken = EnvironmentString{environmentBase: environmentBase{key: prefixKey + "TOTP", desc: "attribute name to store TOTP tokens within the database"}, allowed: []string{"string"}, canDefault: true, defaultValue: "totp"} // EnvPlatform is the platform that the application is running on - EnvPlatform = EnvironmentString{environmentBase: environmentBase{key: prefixKey + "PLATFORM", desc: "override the detected platform"}, defaultValue: detectedValue, allowed: PlatformSet(), canDefault: false} + EnvPlatform = EnvironmentString{environmentBase: environmentBase{key: prefixKey + "PLATFORM", desc: "override the detected platform"}, defaultValue: detectedValue, allowed: []string{MacOSPlatform, WindowsLinuxPlatform, LinuxXPlatform, LinuxWaylandPlatform}, canDefault: false} // EnvStore is the location of the keepass file/store EnvStore = EnvironmentString{environmentBase: environmentBase{key: prefixKey + "STORE", desc: "directory to the database file", requirement: "must be set"}, canDefault: false, allowed: []string{"file"}} // EnvHookDir is the directory of hooks to execute diff --git a/internal/platform/clipboard.go b/internal/platform/clipboard.go @@ -57,7 +57,7 @@ func NewClipboard() (Clipboard, error) { c := Clipboard{isOSC52: true} return c, nil } - sys, err := NewPlatform() + sys, err := inputs.NewPlatform() if err != nil { return Clipboard{}, err } diff --git a/internal/platform/colors.go b/internal/platform/colors.go @@ -1,54 +0,0 @@ -// Package platform manages definitions within lockbox for colorization. -package platform - -import ( - "errors" - - "github.com/enckse/lockbox/internal/inputs" -) - -const ( - // Red will get red terminal coloring. - Red = iota -) - -type ( - // Color are terminal colors for dumb terminal coloring. - Color int -) - -const ( - termBeginRed = "\033[1;31m" - termEndRed = "\033[0m" -) - -type ( - // Terminal represents terminal coloring information. - Terminal struct { - Start string - End string - } -) - -// 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 := inputs.EnvInteractive.Get() - if err != nil { - return Terminal{}, err - } - colors := interactive - if colors { - isColored, err := inputs.EnvNoColor.Get() - if err != nil { - return Terminal{}, err - } - colors = !isColored - } - if colors { - return Terminal{Start: termBeginRed, End: termEndRed}, nil - } - return Terminal{}, nil -} diff --git a/internal/platform/core.go b/internal/platform/core.go @@ -1,50 +0,0 @@ -// Package platform handles platform-specific operations. -package platform - -import ( - "errors" - "os" - "os/exec" - "strings" - - "github.com/enckse/lockbox/internal/inputs" -) - -const ( - unknownPlatform = "" -) - -// NewPlatform gets a new system platform. -func NewPlatform() (inputs.SystemPlatform, error) { - env := inputs.EnvPlatform.Get() - if env != "" { - for _, p := range inputs.PlatformSet() { - if p == env { - return inputs.SystemPlatform(p), nil - } - } - return unknownPlatform, errors.New("unknown platform mode") - } - b, err := exec.Command("uname", "-a").Output() - if err != nil { - return unknownPlatform, err - } - raw := strings.ToLower(strings.TrimSpace(string(b))) - parts := strings.Split(raw, " ") - switch parts[0] { - case "darwin": - return inputs.MacOSPlatform, nil - case "linux": - if strings.Contains(raw, "microsoft-standard-wsl") { - return inputs.WindowsLinuxPlatform, nil - } - if strings.TrimSpace(os.Getenv("WAYLAND_DISPLAY")) == "" { - if strings.TrimSpace(os.Getenv("DISPLAY")) == "" { - return unknownPlatform, errors.New("unable to detect linux clipboard mode") - } - return inputs.LinuxXPlatform, nil - } - return inputs.LinuxWaylandPlatform, nil - } - return unknownPlatform, errors.New("unable to detect clipboard mode") -} diff --git a/internal/platform/core_test.go b/internal/platform/core_test.go @@ -1,30 +0,0 @@ -package platform_test - -import ( - "os" - "testing" - - "github.com/enckse/lockbox/internal/inputs" - "github.com/enckse/lockbox/internal/platform" -) - -func TestNewPlatform(t *testing.T) { - for _, item := range inputs.PlatformSet() { - os.Setenv("LOCKBOX_PLATFORM", item) - s, err := platform.NewPlatform() - if err != nil { - t.Errorf("invalid clipboard: %v", err) - } - if s != inputs.SystemPlatform(item) { - t.Error("mismatch on input and resulting detection") - } - } -} - -func TestNewPlatformUnknown(t *testing.T) { - os.Setenv("LOCKBOX_PLATFORM", "afleaj") - _, err := platform.NewPlatform() - if err == nil || err.Error() != "unknown platform mode" { - t.Errorf("error expected for platform: %v", err) - } -} diff --git a/internal/platform/os.go b/internal/platform/os.go @@ -0,0 +1,159 @@ +// Package platform handles stdin processing +package platform + +import ( + "bufio" + "bytes" + "errors" + "fmt" + "os" + "strings" + "syscall" +) + +type ( + stdinReaderFunc func(string) (bool, error) +) + +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) + } +} + +// GetUserInputPassword will read the user's input from stdin via multiple means. +func GetUserInputPassword(piping, multiLine bool) ([]byte, error) { + var password string + if !multiLine && !piping { + input, err := confirmInputsMatch() + if err != nil { + return nil, err + } + password = input + } else { + input, err := Stdin(false) + if err != nil { + return nil, err + } + password = input + } + if password == "" { + return nil, errors.New("password can NOT be empty") + } + return []byte(password), nil +} + +func confirmInputsMatch() (string, error) { + termEcho(false) + defer func() { + termEcho(true) + }() + fmt.Print("please enter password: ") + first, err := Stdin(true) + if err != nil { + return "", err + } + fmt.Print("\nplease re-enter password: ") + second, err := Stdin(true) + if err != nil { + return "", err + } + if first != second { + return "", errors.New("passwords do NOT match") + } + return first, nil +} + +// Stdin will get one (or more) lines of stdin as a string. +func Stdin(one bool) (string, error) { + b, err := read(one) + if err != nil { + return "", err + } + return strings.TrimSpace(string(b)), nil +} + +// IsInputFromPipe will indicate if connected to stdin pipe. +func IsInputFromPipe() bool { + fileInfo, _ := os.Stdin.Stat() + return fileInfo.Mode()&os.ModeCharDevice == 0 +} + +// ConfirmYesNoPrompt will ask a yes/no question. +func ConfirmYesNoPrompt(prompt string) (bool, error) { + fmt.Printf("%s? (y/N) ", prompt) + resp, err := Stdin(true) + if err != nil { + return false, err + } + return resp == "Y" || resp == "y", nil +} + +func readFunc(reader stdinReaderFunc) error { + if reader == nil { + return errors.New("invalid reader, nil") + } + scanner := bufio.NewScanner(os.Stdin) + for scanner.Scan() { + ok, err := reader(scanner.Text()) + if err != nil { + return err + } + if !ok { + break + } + } + return scanner.Err() +} + +func read(one bool) ([]byte, error) { + var b bytes.Buffer + err := readFunc(func(line string) (bool, error) { + if _, err := b.WriteString(line); err != nil { + return false, err + } + if _, err := b.WriteString("\n"); err != nil { + return false, err + } + if one { + return false, nil + } + return true, nil + }) + if err != nil { + return nil, err + } + return b.Bytes(), nil +} + +// PathExists indicates whether a path exists (true) or not (false) +func PathExists(file string) bool { + if _, err := os.Stat(file); errors.Is(err, os.ErrNotExist) { + return false + } + return true +} diff --git a/internal/platform/paths_test.go b/internal/platform/os_test.go diff --git a/internal/platform/paths.go b/internal/platform/paths.go @@ -1,15 +0,0 @@ -// Package platform is responsible for pathing operations/commands -package platform - -import ( - "errors" - "os" -) - -// PathExists indicates whether a path exists (true) or not (false) -func PathExists(file string) bool { - if _, err := os.Stat(file); errors.Is(err, os.ErrNotExist) { - return false - } - return true -} diff --git a/internal/platform/stdin.go b/internal/platform/stdin.go @@ -1,166 +0,0 @@ -// Package platform handles stdin processing -package platform - -import ( - "bufio" - "bytes" - "errors" - "fmt" - "os" - "strings" - "syscall" -) - -type ( - stdinReaderFunc func(string) (bool, error) -) - -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) - } -} - -// GetUserInputPassword will read the user's input from stdin via multiple means. -func GetUserInputPassword(piping, multiLine bool) ([]byte, error) { - var password string - if !multiLine && !piping { - input, err := confirmInputsMatch() - if err != nil { - return nil, err - } - password = input - } else { - input, err := Stdin(false) - if err != nil { - return nil, err - } - password = input - } - if password == "" { - return nil, errors.New("password can NOT be empty") - } - return []byte(password), nil -} - -func confirmInputsMatch() (string, error) { - termEcho(false) - defer func() { - termEcho(true) - }() - fmt.Print("please enter password: ") - first, err := Stdin(true) - if err != nil { - return "", err - } - fmt.Print("\nplease re-enter password: ") - second, err := Stdin(true) - if err != nil { - return "", err - } - if first != second { - return "", errors.New("passwords do NOT match") - } - return first, nil -} - -// Stdin will get one (or more) lines of stdin as string. -func Stdin(one bool) (string, error) { - var b []byte - var err error - if one { - b, err = readLine() - } else { - b, err = readAll() - } - if err != nil { - return "", err - } - return strings.TrimSpace(string(b)), nil -} - -// IsInputFromPipe will indicate if connected to stdin pipe. -func IsInputFromPipe() bool { - fileInfo, _ := os.Stdin.Stat() - return fileInfo.Mode()&os.ModeCharDevice == 0 -} - -// ConfirmYesNoPrompt will ask a yes/no question. -func ConfirmYesNoPrompt(prompt string) (bool, error) { - fmt.Printf("%s? (y/N) ", prompt) - resp, err := Stdin(true) - if err != nil { - return false, err - } - return resp == "Y" || resp == "y", nil -} - -func readAll() ([]byte, error) { - return read(false) -} - -func readLine() ([]byte, error) { - return read(true) -} - -// ReadFunc will read stdin and execute the given function -func ReadFunc(reader stdinReaderFunc) error { - if reader == nil { - return errors.New("invalid reader, nil") - } - scanner := bufio.NewScanner(os.Stdin) - for scanner.Scan() { - ok, err := reader(scanner.Text()) - if err != nil { - return err - } - if !ok { - break - } - } - return scanner.Err() -} - -func read(one bool) ([]byte, error) { - var b bytes.Buffer - err := ReadFunc(func(line string) (bool, error) { - if _, err := b.WriteString(line); err != nil { - return false, err - } - if _, err := b.WriteString("\n"); err != nil { - return false, err - } - if one { - return false, nil - } - return true, nil - }) - if err != nil { - return nil, err - } - return b.Bytes(), nil -} diff --git a/internal/platform/terminal.go b/internal/platform/terminal.go @@ -0,0 +1,54 @@ +// Package platform handles platform-specific operations. +package platform + +import ( + "errors" + + "github.com/enckse/lockbox/internal/inputs" +) + +const ( + termBeginRed = "\033[1;31m" + termEndRed = "\033[0m" +) + +const ( + // Red will get red terminal coloring. + Red = iota +) + +type ( + // Color are terminal colors for dumb terminal coloring. + Color int +) + +type ( + // Terminal represents terminal coloring information. + Terminal struct { + Start string + End string + } +) + +// 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 := inputs.EnvInteractive.Get() + if err != nil { + return Terminal{}, err + } + colors := interactive + if colors { + isColored, err := inputs.EnvNoColor.Get() + if err != nil { + return Terminal{}, err + } + colors = !isColored + } + if colors { + return Terminal{Start: termBeginRed, End: termEndRed}, nil + } + return Terminal{}, nil +} diff --git a/internal/platform/colors_test.go b/internal/platform/terminal_test.go