lockbox

password manager
Log | Files | Refs | README | LICENSE

commit 4542849384ce3eca17c6777f660519f3e9c3fc1d
parent f27884b36a12c7fad1f5c536085dd48674458c44
Author: Sean Enck <sean@ttypty.com>
Date:   Sun, 28 May 2023 09:09:05 -0400

purge pgl

Diffstat:
Mcmd/main.go | 6+++---
Mgo.mod | 1-
Mgo.sum | 2--
Minternal/app/core.go | 18++++++++++++------
Minternal/app/rekey.go | 6+++---
Minternal/backend/actions.go | 6+++---
Minternal/backend/core.go | 6+++---
Minternal/backend/hooks.go | 7+++----
Minternal/backend/query.go | 10+++++-----
Minternal/inputs/env.go | 26+++++++++++++-------------
Minternal/inputs/json.go | 4++--
Dinternal/inputs/stdin.go | 116-------------------------------------------------------------------------------
Minternal/inputs/totp.go | 4++--
Minternal/platform/clipboard.go | 4++--
Ainternal/system/env.go | 53+++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/system/env_test.go | 54++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/system/paths.go | 15+++++++++++++++
Ainternal/system/paths_test.go | 21+++++++++++++++++++++
Ainternal/system/stdin.go | 166+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Minternal/totp/core.go | 6+++---
20 files changed, 363 insertions(+), 168 deletions(-)

diff --git a/cmd/main.go b/cmd/main.go @@ -14,8 +14,8 @@ import ( "github.com/enckse/lockbox/internal/cli" "github.com/enckse/lockbox/internal/inputs" "github.com/enckse/lockbox/internal/platform" + "github.com/enckse/lockbox/internal/system" "github.com/enckse/lockbox/internal/totp" - "github.com/enckse/pgl/os/exit" ) //go:embed "vers.txt" @@ -23,7 +23,7 @@ var version string func main() { if err := run(); err != nil { - exit.Die(err) + app.Die(err.Error()) } } @@ -105,7 +105,7 @@ func run() error { func clearClipboard() error { idx := 0 - val, err := inputs.Stdin(false) + val, err := system.Stdin(false) if err != nil { return err } diff --git a/go.mod b/go.mod @@ -4,7 +4,6 @@ go 1.19 require ( github.com/aymanbagabas/go-osc52 v1.2.2 - github.com/enckse/pgl v1.0.11 github.com/pquerna/otp v1.4.0 github.com/tobischo/gokeepasslib/v3 v3.5.1 mvdan.cc/sh/v3 v3.6.0 diff --git a/go.sum b/go.sum @@ -9,8 +9,6 @@ github.com/boombuler/barcode v1.0.1 h1:NDBbPmhS+EqABEs5Kg3n/5ZNjy73Pz7SIV+KCeqyX github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/enckse/pgl v1.0.11 h1:2JGsmc3eZ+3P62yZNRrHejbZ+g95giGhh16sULNRQnI= -github.com/enckse/pgl v1.0.11/go.mod h1:r5bqGzwqnJIeY6UbGT5u38keJ5+ZySlsWeaYYzdBhMg= github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= diff --git a/internal/app/core.go b/internal/app/core.go @@ -2,12 +2,12 @@ package app import ( + "fmt" "io" "os" "github.com/enckse/lockbox/internal/backend" - "github.com/enckse/lockbox/internal/inputs" - "github.com/enckse/pgl/os/exit" + "github.com/enckse/lockbox/internal/system" ) type ( @@ -52,13 +52,19 @@ func (a *DefaultCommand) Transaction() *backend.Transaction { // Confirm will confirm with the user (dying if something abnormal happens) func (a *DefaultCommand) Confirm(prompt string) bool { - yesNo, err := inputs.ConfirmYesNoPrompt(prompt) + yesNo, err := system.ConfirmYesNoPrompt(prompt) if err != nil { - exit.Dief("failed to read stdin for confirmation: %v", err) + Die(fmt.Sprintf("failed to read stdin for confirmation: %v", err)) } return yesNo } +// Die will print a message and exit (non-zero) +func Die(msg string) { + fmt.Fprintf(os.Stderr, "%s\n", msg) + os.Exit(1) +} + // SetArgs allow updating the command args func (a *DefaultCommand) SetArgs(args ...string) { a.args = args @@ -66,10 +72,10 @@ func (a *DefaultCommand) SetArgs(args ...string) { // IsPipe will indicate if we're receiving pipe input func (a *DefaultCommand) IsPipe() bool { - return inputs.IsInputFromPipe() + return system.IsInputFromPipe() } // Input will read user input func (a *DefaultCommand) Input(pipe, multi bool) ([]byte, error) { - return inputs.GetUserInputPassword(pipe, multi) + return system.GetUserInputPassword(pipe, multi) } diff --git a/internal/app/rekey.go b/internal/app/rekey.go @@ -6,11 +6,11 @@ import ( "fmt" "os" "os/exec" + "strings" "github.com/enckse/lockbox/internal/backend" "github.com/enckse/lockbox/internal/cli" "github.com/enckse/lockbox/internal/inputs" - "github.com/enckse/pgl/types/values" ) type ( @@ -91,8 +91,8 @@ func ReKey(cmd CommandOptions, r Keyer) error { if _, err := fmt.Fprintf(writer, "rekeying: %s\n", path); err != nil { return err } - modTime, empty := values.EmptyStringTrimmed(entry.ModTime) - if empty { + modTime := strings.TrimSpace(entry.ModTime) + if modTime == "" { return errors.New("did not read modtime") } var insertEnv []string diff --git a/internal/backend/actions.go b/internal/backend/actions.go @@ -8,7 +8,7 @@ import ( "time" "github.com/enckse/lockbox/internal/inputs" - "github.com/enckse/pgl/os/env" + "github.com/enckse/lockbox/internal/system" "github.com/tobischo/gokeepasslib/v3" "github.com/tobischo/gokeepasslib/v3/wrappers" ) @@ -22,7 +22,7 @@ func (t *Transaction) act(cb action) error { return err } k := string(key) - file := env.GetOrDefault(inputs.KeyFileEnv, "") + file := system.EnvironOrDefault(inputs.KeyFileEnv, "") if !t.exists { if err := create(t.file, k, file); err != nil { return err @@ -164,7 +164,7 @@ func (t *Transaction) Move(src QueryEntity, dst string) error { if strings.TrimSpace(src.Value) == "" { return errors.New("empty secret not allowed") } - mod := env.GetOrDefault(inputs.ModTimeEnv, "") + mod := system.EnvironOrDefault(inputs.ModTimeEnv, "") modTime := time.Now() if mod != "" { p, err := time.Parse(inputs.ModTimeFormat, mod) diff --git a/internal/backend/core.go b/internal/backend/core.go @@ -7,7 +7,7 @@ import ( "strings" "github.com/enckse/lockbox/internal/inputs" - "github.com/enckse/pgl/os/paths" + "github.com/enckse/lockbox/internal/system" "github.com/tobischo/gokeepasslib/v3" ) @@ -23,7 +23,7 @@ func loadFile(file string, must bool) (*Transaction, error) { if !strings.HasSuffix(file, ".kdbx") { return nil, errors.New("should use a .kdbx extension") } - exists := paths.Exist(file) + exists := system.PathExists(file) if must { if !exists { return nil, errors.New("invalid file, does not exist") @@ -61,7 +61,7 @@ func splitComponents(path string) ([]string, string, error) { func getCredentials(key, keyFile string) (*gokeepasslib.DBCredentials, error) { if len(keyFile) > 0 { - if !paths.Exist(keyFile) { + if !system.PathExists(keyFile) { return nil, errors.New("no keyfile found on disk") } return gokeepasslib.NewPasswordAndKeyCredentials(key, keyFile) diff --git a/internal/backend/hooks.go b/internal/backend/hooks.go @@ -9,8 +9,7 @@ import ( "strings" "github.com/enckse/lockbox/internal/inputs" - "github.com/enckse/pgl/os/env" - "github.com/enckse/pgl/os/paths" + "github.com/enckse/lockbox/internal/system" ) // NewHook will create a new hook type @@ -18,11 +17,11 @@ func NewHook(path string, a ActionMode) (Hook, error) { if strings.TrimSpace(path) == "" { return Hook{}, errors.New("empty path is not allowed for hooks") } - dir := env.GetOrDefault(inputs.HookDirEnv, "") + dir := system.EnvironOrDefault(inputs.HookDirEnv, "") if dir == "" { return Hook{enabled: false}, nil } - if !paths.Exist(dir) { + if !system.PathExists(dir) { return Hook{}, errors.New("hook directory does NOT exist") } entries, err := os.ReadDir(dir) diff --git a/internal/backend/query.go b/internal/backend/query.go @@ -10,7 +10,6 @@ import ( "strings" "github.com/enckse/lockbox/internal/inputs" - "github.com/enckse/pgl/types/collections" "github.com/tobischo/gokeepasslib/v3" ) @@ -73,7 +72,8 @@ func (t *Transaction) QueryCallback(args QueryOptions) ([]QueryEntity, error) { if args.Mode == noneMode { return nil, errors.New("no query mode specified") } - entities := collections.Map[string, QueryEntity]{} + entities := make(map[string]QueryEntity) + var keys []string isSort := args.Mode != ExactMode decrypt := args.Values != BlankValue err := t.act(func(ctx Context) error { @@ -104,7 +104,8 @@ func (t *Transaction) QueryCallback(args QueryOptions) ([]QueryEntity, error) { } } } - entities.Set(path, QueryEntity{backing: entry}) + entities[path] = QueryEntity{backing: entry} + keys = append(keys, path) }) if decrypt { return ctx.db.UnlockProtectedEntries() @@ -114,7 +115,6 @@ func (t *Transaction) QueryCallback(args QueryOptions) ([]QueryEntity, error) { if err != nil { return nil, err } - keys := entities.Keys() if isSort { sort.Strings(keys) } @@ -130,7 +130,7 @@ func (t *Transaction) QueryCallback(args QueryOptions) ([]QueryEntity, error) { for _, k := range keys { entity := QueryEntity{Path: k} if args.Values != BlankValue { - e, ok := entities.Get(k) + e, ok := entities[k] if !ok { return nil, errors.New("failed to read entity back from map") } diff --git a/internal/inputs/env.go b/internal/inputs/env.go @@ -12,7 +12,7 @@ import ( "strings" "time" - "github.com/enckse/pgl/os/env" + "github.com/enckse/lockbox/internal/system" "mvdan.cc/sh/v3/shell" ) @@ -70,7 +70,7 @@ const ( JSONDataOutputEnv = prefixKey + "JSON_DATA_OUTPUT" ) -var isYesNoArgs = []string{env.Yes, env.No} +var isYesNoArgs = []string{system.Yes, system.No} type ( environmentOutput struct { @@ -185,13 +185,13 @@ func getKey(keyMode, name string) ([]byte, error) { } func isYesNoEnv(defaultValue bool, envKey string) (bool, error) { - read := env.GetValue(envKey) + read := system.EnvironValue(envKey) switch read { - case env.NoValue: + case system.NoValue: return false, nil - case env.YesValue: + case system.YesValue: return true, nil - case env.EmptyValue: + case system.EmptyValue: return defaultValue, nil } @@ -230,7 +230,7 @@ func IsInteractive() (bool, error) { // TOTPToken gets the name of the totp special case tokens func TOTPToken() string { - return env.GetOrDefault(fieldTOTPEnv, defaultTOTPField) + return system.EnvironOrDefault(fieldTOTPEnv, defaultTOTPField) } func (o environmentOutput) formatEnvironmentVariable(required bool, name, val, desc string, allowed []string) string { @@ -262,10 +262,10 @@ func ListEnvironmentVariables(showValues bool) []string { results = append(results, e.formatEnvironmentVariable(true, StoreEnv, "", "directory to the database file", []string{"file"})) results = append(results, e.formatEnvironmentVariable(true, keyModeEnv, commandKeyMode, "how to retrieve the database store password", []string{commandKeyMode, plainKeyMode})) results = append(results, e.formatEnvironmentVariable(true, keyEnv, "", fmt.Sprintf("the database key ('%s' mode) or command to run ('%s' mode)\nto retrieve the database password", plainKeyMode, commandKeyMode), []string{commandArgsExample, "password"})) - results = append(results, e.formatEnvironmentVariable(false, noClipEnv, env.No, "disable clipboard operations", isYesNoArgs)) - results = append(results, e.formatEnvironmentVariable(false, noColorEnv, env.No, "disable terminal colors", isYesNoArgs)) - results = append(results, e.formatEnvironmentVariable(false, interactiveEnv, env.Yes, "enable interactive mode", isYesNoArgs)) - results = append(results, e.formatEnvironmentVariable(false, readOnlyEnv, env.No, "operate in readonly mode", isYesNoArgs)) + results = append(results, e.formatEnvironmentVariable(false, noClipEnv, system.No, "disable clipboard operations", isYesNoArgs)) + results = append(results, e.formatEnvironmentVariable(false, noColorEnv, system.No, "disable terminal colors", isYesNoArgs)) + results = append(results, e.formatEnvironmentVariable(false, interactiveEnv, system.Yes, "enable interactive mode", isYesNoArgs)) + results = append(results, e.formatEnvironmentVariable(false, readOnlyEnv, system.No, "operate in readonly mode", isYesNoArgs)) results = append(results, e.formatEnvironmentVariable(false, fieldTOTPEnv, defaultTOTPField, "attribute name to store TOTP tokens within the database", []string{"string"})) results = append(results, e.formatEnvironmentVariable(false, formatTOTPEnv, strings.ReplaceAll(strings.ReplaceAll(FormatTOTP("%s"), "%25s", "%s"), "&", " \\\n &"), "override the otpauth url used to store totp tokens. It must have ONE format\nstring ('%s') to insert the totp base code", []string{"otpauth//url/%s/args..."})) results = append(results, e.formatEnvironmentVariable(false, MaxTOTPTime, MaxTOTPTimeDefault, "time, in seconds, in which to show a TOTP token before automatically exiting", []string{"integer"})) @@ -274,9 +274,9 @@ func ListEnvironmentVariables(showValues bool) []string { results = append(results, e.formatEnvironmentVariable(false, ClipCopyEnv, detectedValue, "override the detected platform copy command", []string{commandArgsExample})) results = append(results, e.formatEnvironmentVariable(false, clipMaxEnv, fmt.Sprintf("%d", defaultMaxClipboard), "override the amount of time before totp clears the clipboard (e.g. 10),\nmust be an integer", []string{"integer"})) results = append(results, e.formatEnvironmentVariable(false, PlatformEnv, detectedValue, "override the detected platform", PlatformSet())) - results = append(results, e.formatEnvironmentVariable(false, noTOTPEnv, env.No, "disable TOTP integrations", isYesNoArgs)) + results = append(results, e.formatEnvironmentVariable(false, noTOTPEnv, system.No, "disable TOTP integrations", isYesNoArgs)) results = append(results, e.formatEnvironmentVariable(false, HookDirEnv, "", "the path to hooks to execute on actions against the database", []string{"directory"})) - results = append(results, e.formatEnvironmentVariable(false, clipOSC52Env, env.No, "enable OSC52 clipboard mode", isYesNoArgs)) + results = append(results, e.formatEnvironmentVariable(false, clipOSC52Env, system.No, "enable OSC52 clipboard mode", isYesNoArgs)) results = append(results, e.formatEnvironmentVariable(false, KeyFileEnv, "", "additional keyfile to access/protect the database", []string{"keyfile"})) results = append(results, e.formatEnvironmentVariable(false, ModTimeEnv, ModTimeFormat, fmt.Sprintf("input modification time to set for the entry\n(expected format: %s)", ModTimeFormat), []string{"modtime"})) results = append(results, e.formatEnvironmentVariable(false, JSONDataOutputEnv, string(JSONDataOutputHash), fmt.Sprintf("changes what the data field in JSON outputs will contain\nuse '%s' with CAUTION", JSONDataOutputRaw), []string{string(JSONDataOutputRaw), string(JSONDataOutputHash), string(JSONDataOutputBlank)})) diff --git a/internal/inputs/json.go b/internal/inputs/json.go @@ -5,7 +5,7 @@ import ( "fmt" "strings" - "github.com/enckse/pgl/os/env" + "github.com/enckse/lockbox/internal/system" ) const ( @@ -24,7 +24,7 @@ type ( // ParseJSONOutput handles detecting the JSON output mode func ParseJSONOutput() (JSONOutputMode, error) { - val := strings.ToLower(strings.TrimSpace(env.GetOrDefault(JSONDataOutputEnv, string(JSONDataOutputHash)))) + val := strings.ToLower(strings.TrimSpace(system.EnvironOrDefault(JSONDataOutputEnv, string(JSONDataOutputHash)))) switch JSONOutputMode(val) { case JSONDataOutputHash: return JSONDataOutputHash, nil diff --git a/internal/inputs/stdin.go b/internal/inputs/stdin.go @@ -1,116 +0,0 @@ -// Package inputs handles stdin management/access. -package inputs - -import ( - "errors" - "fmt" - "os" - "strings" - "syscall" - - "github.com/enckse/pgl/os/stdin" -) - -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 = stdin.ReadLine() - } else { - b, err = stdin.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 -} diff --git a/internal/inputs/totp.go b/internal/inputs/totp.go @@ -6,7 +6,7 @@ import ( "net/url" "strings" - "github.com/enckse/pgl/os/env" + "github.com/enckse/lockbox/internal/system" ) const ( @@ -19,7 +19,7 @@ func FormatTOTP(value string) string { if strings.HasPrefix(value, otpAuth) { return value } - override := env.GetOrDefault(formatTOTPEnv, "") + override := system.EnvironOrDefault(formatTOTPEnv, "") if override != "" { return fmt.Sprintf(override, value) } diff --git a/internal/platform/clipboard.go b/internal/platform/clipboard.go @@ -10,7 +10,7 @@ import ( osc "github.com/aymanbagabas/go-osc52" "github.com/enckse/lockbox/internal/inputs" - "github.com/enckse/pgl/os/env" + "github.com/enckse/lockbox/internal/system" ) type ( @@ -32,7 +32,7 @@ func newClipboard(copying, pasting []string) (Clipboard, error) { } func overrideCommand(v string) ([]string, error) { - value := env.GetOrDefault(v, "") + value := system.EnvironOrDefault(v, "") if strings.TrimSpace(value) == "" { return nil, nil } diff --git a/internal/system/env.go b/internal/system/env.go @@ -0,0 +1,53 @@ +// Package system handles simple environment variable processing +package system + +import ( + "os" + "strings" +) + +const ( + // Yes is the string expected for yes values + Yes = "yes" + // No is the string expected for no values + No = "no" +) + +type ( + // ReadValue is the output of reading a known bool/yes/no + ReadValue uint +) + +const ( + // UnknownValue indicates an unknown value was read + UnknownValue ReadValue = iota + // YesValue means yes was set + YesValue + // NoValue means no was set + NoValue + // EmptyValue means that the value was not set (empty string) + EmptyValue +) + +// EnvironOrDefault will get the environment value OR default if env is not set. +func EnvironOrDefault(envKey, defaultValue string) string { + val := os.Getenv(envKey) + if strings.TrimSpace(val) == "" { + return defaultValue + } + return val +} + +// EnvironValue read a simple yes/no from an environment value +func EnvironValue(envKey string) ReadValue { + value := strings.ToLower(strings.TrimSpace(os.Getenv(envKey))) + switch value { + case No: + return NoValue + case Yes: + return YesValue + case "": + return EmptyValue + } + return UnknownValue +} diff --git a/internal/system/env_test.go b/internal/system/env_test.go @@ -0,0 +1,54 @@ +package system_test + +import ( + "os" + "testing" + + "github.com/enckse/lockbox/internal/system" +) + +func TestEnvDefault(t *testing.T) { + os.Clearenv() + val := system.EnvironOrDefault("TEST", "value") + if val != "value" { + t.Error("invalid read") + } + os.Setenv("TEST", " ") + val = system.EnvironOrDefault("TEST", "value") + if val != "value" { + t.Error("invalid read") + } + os.Setenv("TEST", " a") + val = system.EnvironOrDefault("TEST", "value") + if val != " a" { + t.Error("invalid read") + } +} + +func TestReadValue(t *testing.T) { + os.Clearenv() + val := system.EnvironValue("test") + if val != system.EmptyValue { + t.Error("bad read") + } + os.Setenv("TEST", "a") + val = system.EnvironValue("TEST") + if val != system.UnknownValue { + t.Error("bad read") + } + os.Setenv("TEST", " YeS ") + val = system.EnvironValue("TEST") + if val != system.YesValue { + t.Error("bad read") + } + os.Setenv("TEST", " NO ") + val = system.EnvironValue("TEST") + if val != system.NoValue { + t.Error("bad read") + } + os.Setenv("TEST", "FALSESSS") + val = system.EnvironValue("TEST") + if val != system.UnknownValue { + t.Error("bad read") + } +} diff --git a/internal/system/paths.go b/internal/system/paths.go @@ -0,0 +1,15 @@ +// Package system is responsible for pathing operations/commands +package system + +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/system/paths_test.go b/internal/system/paths_test.go @@ -0,0 +1,21 @@ +package system_test + +import ( + "os" + "path/filepath" + "testing" + + "github.com/enckse/lockbox/internal/system" +) + +func TestPathExist(t *testing.T) { + testDir := filepath.Join("testdata", "exists") + os.RemoveAll(testDir) + if system.PathExists(testDir) { + t.Error("test dir SHOULD NOT exist") + } + os.MkdirAll(testDir, 0o755) + if !system.PathExists(testDir) { + t.Error("test dir SHOULD exist") + } +} diff --git a/internal/system/stdin.go b/internal/system/stdin.go @@ -0,0 +1,166 @@ +// Package system handles stdin processing +package system + +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/totp/core.go b/internal/totp/core.go @@ -16,7 +16,7 @@ import ( "github.com/enckse/lockbox/internal/colors" "github.com/enckse/lockbox/internal/inputs" "github.com/enckse/lockbox/internal/platform" - "github.com/enckse/pgl/os/env" + "github.com/enckse/lockbox/internal/system" coreotp "github.com/pquerna/otp" otp "github.com/pquerna/otp/totp" ) @@ -86,7 +86,7 @@ func clear() { } func colorWhenRules() ([]inputs.ColorWindow, error) { - envTime := env.GetOrDefault(inputs.ColorBetweenEnv, inputs.TOTPDefaultBetween) + envTime := system.EnvironOrDefault(inputs.ColorBetweenEnv, inputs.TOTPDefaultBetween) if envTime == inputs.TOTPDefaultBetween { return inputs.TOTPDefaultColorWindow, nil } @@ -160,7 +160,7 @@ func (args *Arguments) display(opts Options) error { if err != nil { return err } - runString := env.GetOrDefault(inputs.MaxTOTPTime, inputs.MaxTOTPTimeDefault) + runString := system.EnvironOrDefault(inputs.MaxTOTPTime, inputs.MaxTOTPTimeDefault) runFor, err := strconv.Atoi(runString) if err != nil { return err