lockbox

password manager
Log | Files | Refs | README | LICENSE

commit d41d026e5afdbb0c0b48c86608c40ce55b228583
parent f9a6bcadefecd6f0376a3e689407b207d51b141a
Author: Sean Enck <sean@ttypty.com>
Date:   Thu, 27 Jul 2023 19:12:08 -0400

renaming inputs -> config

Diffstat:
Mcmd/main.go | 4++--
Minternal/app/completions.go | 8++++----
Minternal/app/core.go | 4++--
Minternal/app/info.go | 4++--
Minternal/app/rekey.go | 8++++----
Minternal/app/totp.go | 20++++++++++----------
Minternal/backend/actions.go | 12++++++------
Minternal/backend/core.go | 8++++----
Minternal/backend/hooks.go | 4++--
Minternal/backend/query.go | 14+++++++-------
Ainternal/config/core.go | 271+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/config/core_test.go | 84+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/config/vars.go | 214+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/config/vars_test.go | 244+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dinternal/inputs/core.go | 271-------------------------------------------------------------------------------
Dinternal/inputs/core_test.go | 84-------------------------------------------------------------------------------
Dinternal/inputs/vars.go | 214-------------------------------------------------------------------------------
Dinternal/inputs/vars_test.go | 244-------------------------------------------------------------------------------
Minternal/platform/clipboard.go | 22+++++++++++-----------
Minternal/platform/clipboard_test.go | 8++++----
Minternal/platform/terminal.go | 6+++---
21 files changed, 874 insertions(+), 874 deletions(-)

diff --git a/cmd/main.go b/cmd/main.go @@ -10,7 +10,7 @@ import ( "time" "github.com/enckse/lockbox/internal/app" - "github.com/enckse/lockbox/internal/inputs" + "github.com/enckse/lockbox/internal/config" "github.com/enckse/lockbox/internal/platform" ) @@ -84,7 +84,7 @@ func run() error { case app.ConvCommand: return app.Conv(p) case app.TOTPCommand: - args, err := app.NewTOTPArguments(sub, inputs.EnvTOTPToken.Get()) + args, err := app.NewTOTPArguments(sub, config.EnvTOTPToken.Get()) if err != nil { return err } diff --git a/internal/app/completions.go b/internal/app/completions.go @@ -6,7 +6,7 @@ import ( "fmt" "text/template" - "github.com/enckse/lockbox/internal/inputs" + "github.com/enckse/lockbox/internal/config" ) type ( @@ -62,19 +62,19 @@ func GenerateCompletions(isBash, defaults bool) ([]string, error) { isClip := true isTOTP := true if !defaults { - ro, err := inputs.EnvReadOnly.Get() + ro, err := config.EnvReadOnly.Get() if err != nil { return nil, err } isReadOnly = ro - noClip, err := inputs.EnvNoClip.Get() + noClip, err := config.EnvNoClip.Get() if err != nil { return nil, err } if noClip { isClip = false } - noTOTP, err := inputs.EnvNoTOTP.Get() + noTOTP, err := config.EnvNoTOTP.Get() if err != nil { return nil, err } diff --git a/internal/app/core.go b/internal/app/core.go @@ -11,7 +11,7 @@ import ( "strings" "github.com/enckse/lockbox/internal/backend" - "github.com/enckse/lockbox/internal/inputs" + "github.com/enckse/lockbox/internal/config" "github.com/enckse/lockbox/internal/platform" ) @@ -211,7 +211,7 @@ func Usage(verbose bool) ([]string, error) { } results = append(results, strings.Split(strings.TrimSpace(doc), "\n")...) results = append(results, "") - results = append(results, inputs.ListEnvironmentVariables(false)...) + results = append(results, config.ListEnvironmentVariables(false)...) } return append(usage, results...), nil } diff --git a/internal/app/info.go b/internal/app/info.go @@ -7,7 +7,7 @@ import ( "io" "strings" - "github.com/enckse/lockbox/internal/inputs" + "github.com/enckse/lockbox/internal/config" ) // Info will report help/bash/env details @@ -53,7 +53,7 @@ func info(command string, args []string) ([]string, error) { return nil, err } if isEnv { - return inputs.ListEnvironmentVariables(!defaults), nil + return config.ListEnvironmentVariables(!defaults), nil } return GenerateCompletions(command == BashCommand, defaults) } diff --git a/internal/app/rekey.go b/internal/app/rekey.go @@ -9,7 +9,7 @@ import ( "strings" "github.com/enckse/lockbox/internal/backend" - "github.com/enckse/lockbox/internal/inputs" + "github.com/enckse/lockbox/internal/config" ) type ( @@ -72,14 +72,14 @@ func (r DefaultKeyer) Insert(entry ReKeyEntry) error { // ReKey handles entry rekeying func ReKey(cmd CommandOptions, r Keyer) error { args := cmd.Args() - vars, err := inputs.GetReKey(args) + vars, err := config.GetReKey(args) if err != nil { return err } if !cmd.Confirm("proceed with rekey") { return nil } - inputs.EnvJSONDataOutput.Set(string(inputs.JSONDataOutputRaw)) + config.EnvJSONDataOutput.Set(string(config.JSONDataOutputRaw)) entries, err := r.JSON() if err != nil { return err @@ -95,7 +95,7 @@ func ReKey(cmd CommandOptions, r Keyer) error { } var insertEnv []string insertEnv = append(insertEnv, vars...) - insertEnv = append(insertEnv, inputs.EnvModTime.KeyValue(modTime)) + insertEnv = append(insertEnv, config.EnvModTime.KeyValue(modTime)) if err := r.Insert(ReKeyEntry{Path: path, Env: insertEnv, Data: []byte(entry.Data)}); err != nil { return err } diff --git a/internal/app/totp.go b/internal/app/totp.go @@ -10,7 +10,7 @@ import ( "time" "github.com/enckse/lockbox/internal/backend" - "github.com/enckse/lockbox/internal/inputs" + "github.com/enckse/lockbox/internal/config" "github.com/enckse/lockbox/internal/platform" coreotp "github.com/pquerna/otp" otp "github.com/pquerna/otp/totp" @@ -67,8 +67,8 @@ func NewDefaultTOTPOptions(app CommandOptions) TOTPOptions { return TOTPOptions{ app: app, Clear: clear, - IsInteractive: inputs.EnvInteractive.Get, - IsNoTOTP: inputs.EnvNoTOTP.Get, + IsInteractive: config.EnvInteractive.Get, + IsNoTOTP: config.EnvNoTOTP.Get, } } @@ -80,12 +80,12 @@ func clear() { } } -func colorWhenRules() ([]inputs.ColorWindow, error) { - envTime := inputs.EnvTOTPColorBetween.Get() - if envTime == inputs.TOTPDefaultBetween { - return inputs.TOTPDefaultColorWindow, nil +func colorWhenRules() ([]config.ColorWindow, error) { + envTime := config.EnvTOTPColorBetween.Get() + if envTime == config.TOTPDefaultBetween { + return config.TOTPDefaultColorWindow, nil } - return inputs.ParseColorWindow(envTime) + return config.ParseColorWindow(envTime) } func (w totpWrapper) generateCode() (string, error) { @@ -117,7 +117,7 @@ func (args *TOTPArguments) display(opts TOTPOptions) error { return errors.New("object does not exist") } totpToken := string(entity.Value) - k, err := coreotp.NewKeyFromURL(inputs.EnvFormatTOTP.Get(totpToken)) + k, err := coreotp.NewKeyFromURL(config.EnvFormatTOTP.Get(totpToken)) if err != nil { return err } @@ -155,7 +155,7 @@ func (args *TOTPArguments) display(opts TOTPOptions) error { if err != nil { return err } - runFor, err := inputs.EnvMaxTOTP.Get() + runFor, err := config.EnvMaxTOTP.Get() if err != nil { return err } diff --git a/internal/backend/actions.go b/internal/backend/actions.go @@ -7,7 +7,7 @@ import ( "strings" "time" - "github.com/enckse/lockbox/internal/inputs" + "github.com/enckse/lockbox/internal/config" "github.com/tobischo/gokeepasslib/v3" ) @@ -30,12 +30,12 @@ func (t *Transaction) act(cb action) error { if !t.valid { return errors.New("invalid transaction") } - key, err := inputs.GetKey() + key, err := config.GetKey() if err != nil { return err } k := string(key) - file := inputs.EnvKeyFile.Get() + file := config.EnvKeyFile.Get() if !t.exists { if err := create(t.file, k, file); err != nil { return err @@ -173,10 +173,10 @@ func (t *Transaction) Move(src QueryEntity, dst string) error { if strings.TrimSpace(src.Value) == "" { return errors.New("empty secret not allowed") } - mod := inputs.EnvModTime.Get() + mod := config.EnvModTime.Get() modTime := time.Now() if mod != "" { - p, err := time.Parse(inputs.ModTimeFormat, mod) + p, err := time.Parse(config.ModTimeFormat, mod) if err != nil { return err } @@ -222,7 +222,7 @@ func (t *Transaction) Move(src QueryEntity, dst string) error { if multi { return errors.New("totp tokens can NOT be multi-line") } - otp := inputs.EnvFormatTOTP.Get(v) + otp := config.EnvFormatTOTP.Get(v) e.Values = append(e.Values, protectedValue("otp", otp)) } e.Values = append(e.Values, protectedValue(field, v)) diff --git a/internal/backend/core.go b/internal/backend/core.go @@ -7,7 +7,7 @@ import ( "os" "strings" - "github.com/enckse/lockbox/internal/inputs" + "github.com/enckse/lockbox/internal/config" "github.com/enckse/lockbox/internal/platform" "github.com/tobischo/gokeepasslib/v3" "github.com/tobischo/gokeepasslib/v3/wrappers" @@ -63,7 +63,7 @@ func loadFile(file string, must bool) (*Transaction, error) { return nil, errors.New("invalid file, does not exist") } } - ro, err := inputs.EnvReadOnly.Get() + ro, err := config.EnvReadOnly.Get() if err != nil { return nil, err } @@ -72,7 +72,7 @@ func loadFile(file string, must bool) (*Transaction, error) { // NewTransaction will use the underlying environment data store location func NewTransaction() (*Transaction, error) { - return loadFile(inputs.EnvStore.Get(), false) + return loadFile(config.EnvStore.Get(), false) } func splitComponents(path string) ([]string, string, error) { @@ -140,7 +140,7 @@ func encode(f *os.File, db *gokeepasslib.Database) error { } func isTOTP(title string) (bool, error) { - t := inputs.EnvTOTPToken.Get() + t := config.EnvTOTPToken.Get() if t == notesKey || t == passKey || t == titleKey { return false, errors.New("invalid totp field, uses restricted name") } diff --git a/internal/backend/hooks.go b/internal/backend/hooks.go @@ -8,7 +8,7 @@ import ( "path/filepath" "strings" - "github.com/enckse/lockbox/internal/inputs" + "github.com/enckse/lockbox/internal/config" "github.com/enckse/lockbox/internal/platform" ) @@ -36,7 +36,7 @@ func NewHook(path string, a ActionMode) (Hook, error) { if strings.TrimSpace(path) == "" { return Hook{}, errors.New("empty path is not allowed for hooks") } - dir := inputs.EnvHookDir.Get() + dir := config.EnvHookDir.Get() if dir == "" { return Hook{enabled: false}, nil } diff --git a/internal/backend/query.go b/internal/backend/query.go @@ -9,7 +9,7 @@ import ( "sort" "strings" - "github.com/enckse/lockbox/internal/inputs" + "github.com/enckse/lockbox/internal/config" "github.com/tobischo/gokeepasslib/v3" ) @@ -159,17 +159,17 @@ func (t *Transaction) QueryCallback(args QueryOptions) ([]QueryEntity, error) { if isSort { sort.Strings(keys) } - jsonMode := inputs.JSONDataOutputBlank + jsonMode := config.JSONDataOutputBlank if args.Values == JSONValue { - m, err := inputs.ParseJSONOutput() + m, err := config.ParseJSONOutput() if err != nil { return nil, err } jsonMode = m } var hashLength int - if jsonMode == inputs.JSONDataOutputHash { - hashLength, err = inputs.EnvHashLength.Get() + if jsonMode == config.JSONDataOutputHash { + hashLength, err = config.EnvHashLength.Get() if err != nil { return nil, err } @@ -190,9 +190,9 @@ func (t *Transaction) QueryCallback(args QueryOptions) ([]QueryEntity, error) { case JSONValue: data := "" switch jsonMode { - case inputs.JSONDataOutputRaw: + case config.JSONDataOutputRaw: data = val - case inputs.JSONDataOutputHash: + case config.JSONDataOutputHash: data = fmt.Sprintf("%x", sha512.Sum512([]byte(val))) if hashLength > 0 && len(data) > hashLength { data = data[0:hashLength] diff --git a/internal/config/core.go b/internal/config/core.go @@ -0,0 +1,271 @@ +// Package config handles user inputs/UI elements. +package config + +import ( + "errors" + "fmt" + "os" + "os/exec" + "strconv" + "strings" + + "mvdan.cc/sh/v3/shell" +) + +const ( + colorWindowDelimiter = "," + colorWindowSpan = ":" + yes = "yes" + no = "no" + // MacOSPlatform is the macos indicator for platform + MacOSPlatform = "macos" + // LinuxWaylandPlatform for linux+wayland + LinuxWaylandPlatform = "linux-wayland" + // LinuxXPlatform for linux+X + LinuxXPlatform = "linux-x" + // WindowsLinuxPlatform for WSL subsystems + WindowsLinuxPlatform = "wsl" + unknownPlatform = "" +) + +type ( + // JSONOutputMode is the output mode definition + JSONOutputMode string + environmentOutput struct { + showValues bool + } + // SystemPlatform represents the platform lockbox is running on. + SystemPlatform string + environmentBase struct { + key string + desc string + requirement string + } + // EnvironmentInt are environment settings that are integers + EnvironmentInt struct { + environmentBase + defaultValue int + allowZero bool + shortDesc string + } + // EnvironmentBool are environment settings that are booleans + EnvironmentBool struct { + environmentBase + defaultValue bool + } + // EnvironmentString are string-based settings + EnvironmentString struct { + environmentBase + canDefault bool + defaultValue string + allowed []string + } + // EnvironmentCommand are settings that are parsed as shell commands + EnvironmentCommand struct { + environmentBase + } + // EnvironmentFormatter allows for sending a string into a get request + EnvironmentFormatter struct { + environmentBase + allowed string + fxn func(string, string) string + } + printer interface { + values() (string, []string) + self() environmentBase + } + // ColorWindow for handling terminal colors based on timing + ColorWindow struct { + Start int + End int + } +) + +func shlex(in string) ([]string, error) { + return shell.Fields(in, os.Getenv) +} + +func environOrDefault(envKey, defaultValue string) string { + val := os.Getenv(envKey) + if strings.TrimSpace(val) == "" { + return defaultValue + } + return val +} + +// Get will get the boolean value for the setting +func (e EnvironmentBool) Get() (bool, error) { + read := strings.ToLower(strings.TrimSpace(os.Getenv(e.key))) + switch read { + case no: + return false, nil + case yes: + return true, nil + case "": + return e.defaultValue, nil + } + + return false, fmt.Errorf("invalid yes/no env value for %s", e.key) +} + +// Get will get the integer value for the setting +func (e EnvironmentInt) Get() (int, error) { + val := e.defaultValue + use := os.Getenv(e.key) + if use != "" { + i, err := strconv.Atoi(use) + if err != nil { + return -1, err + } + invalid := false + check := "" + if e.allowZero { + check = "=" + } + switch i { + case 0: + invalid = !e.allowZero + default: + invalid = i < 0 + } + if invalid { + return -1, fmt.Errorf("%s must be >%s 0", e.shortDesc, check) + } + val = i + } + return val, nil +} + +// Get will read the string from the environment +func (e EnvironmentString) Get() string { + if !e.canDefault { + return os.Getenv(e.key) + } + return environOrDefault(e.key, e.defaultValue) +} + +// Get will read (and shlex) the value if set +func (e EnvironmentCommand) Get() ([]string, error) { + value := environOrDefault(e.key, "") + if strings.TrimSpace(value) == "" { + return nil, nil + } + return shlex(value) +} + +// KeyValue will get the string representation of the key+value +func (e environmentBase) KeyValue(value string) string { + return fmt.Sprintf("%s=%s", e.key, value) +} + +// Set will do an environment set for the value to key +func (e environmentBase) Set(value string) { + os.Setenv(e.key, value) +} + +// Get will retrieve the value with the formatted input included +func (e EnvironmentFormatter) Get(value string) string { + return e.fxn(e.key, value) +} + +func (e EnvironmentString) values() (string, []string) { + return e.defaultValue, e.allowed +} + +func (e environmentBase) self() environmentBase { + return e +} + +func (e EnvironmentBool) values() (string, []string) { + val := no + if e.defaultValue { + val = yes + } + return val, []string{yes, no} +} + +func (e EnvironmentInt) values() (string, []string) { + return fmt.Sprintf("%d", e.defaultValue), []string{"integer"} +} + +func (e EnvironmentFormatter) values() (string, []string) { + return strings.ReplaceAll(strings.ReplaceAll(EnvFormatTOTP.Get("%s"), "%25s", "%s"), "&", " \\\n &"), []string{e.allowed} +} + +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") +} + +func toString(windows []ColorWindow) string { + var results []string + for _, w := range windows { + results = append(results, fmt.Sprintf("%d%s%d", w.Start, colorWindowSpan, w.End)) + } + return strings.Join(results, colorWindowDelimiter) +} + +// ParseColorWindow will handle parsing a window of colors for TOTP operations +func ParseColorWindow(windowString string) ([]ColorWindow, error) { + var rules []ColorWindow + for _, item := range strings.Split(windowString, colorWindowDelimiter) { + line := strings.TrimSpace(item) + if line == "" { + continue + } + parts := strings.Split(line, colorWindowSpan) + if len(parts) != 2 { + return nil, fmt.Errorf("invalid colorization rule found: %s", line) + } + s, err := strconv.Atoi(parts[0]) + if err != nil { + return nil, err + } + e, err := strconv.Atoi(parts[1]) + if err != nil { + return nil, err + } + if s < 0 || e < 0 || e < s || s > 59 || e > 59 { + return nil, fmt.Errorf("invalid time found for colorization rule: %s", line) + } + rules = append(rules, ColorWindow{Start: s, End: e}) + } + if len(rules) == 0 { + return nil, errors.New("invalid colorization rules for totp, none found") + } + return rules, nil +} diff --git a/internal/config/core_test.go b/internal/config/core_test.go @@ -0,0 +1,84 @@ +package config_test + +import ( + "os" + "testing" + + "github.com/enckse/lockbox/internal/config" +) + +func TestPlatformSet(t *testing.T) { + if len(config.Platforms) != 4 { + t.Error("invalid platform set") + } +} + +func TestSet(t *testing.T) { + os.Clearenv() + defer os.Clearenv() + config.EnvStore.Set("TEST") + if config.EnvStore.Get() != "TEST" { + t.Errorf("invalid set/get") + } +} + +func TestKeyValue(t *testing.T) { + val := config.EnvStore.KeyValue("TEST") + if val != "LOCKBOX_STORE=TEST" { + t.Errorf("invalid keyvalue") + } +} + +func TestNewPlatform(t *testing.T) { + for _, item := range config.Platforms { + os.Setenv("LOCKBOX_PLATFORM", item) + s, err := config.NewPlatform() + if err != nil { + t.Errorf("invalid clipboard: %v", err) + } + if s != config.SystemPlatform(item) { + t.Error("mismatch on input and resulting detection") + } + } +} + +func TestNewPlatformUnknown(t *testing.T) { + os.Setenv("LOCKBOX_PLATFORM", "afleaj") + _, err := config.NewPlatform() + if err == nil || err.Error() != "unknown platform mode" { + t.Errorf("error expected for platform: %v", err) + } +} + +func TestParseWindows(t *testing.T) { + if _, err := config.ParseColorWindow(""); err.Error() != "invalid colorization rules for totp, none found" { + t.Errorf("invalid error: %v", err) + } + if _, err := config.ParseColorWindow(",2"); err.Error() != "invalid colorization rule found: 2" { + t.Errorf("invalid error: %v", err) + } + if _, err := config.ParseColorWindow(",1:200"); err.Error() != "invalid time found for colorization rule: 1:200" { + t.Errorf("invalid error: %v", err) + } + if _, err := config.ParseColorWindow(",1:-1"); err.Error() != "invalid time found for colorization rule: 1:-1" { + t.Errorf("invalid error: %v", err) + } + if _, err := config.ParseColorWindow(",200:1"); err.Error() != "invalid time found for colorization rule: 200:1" { + t.Errorf("invalid error: %v", err) + } + if _, err := config.ParseColorWindow(",-1:1"); err.Error() != "invalid time found for colorization rule: -1:1" { + t.Errorf("invalid error: %v", err) + } + if _, err := config.ParseColorWindow(",2:1"); err.Error() != "invalid time found for colorization rule: 2:1" { + t.Errorf("invalid error: %v", err) + } + if _, err := config.ParseColorWindow(",xxx:1"); err.Error() != "strconv.Atoi: parsing \"xxx\": invalid syntax" { + t.Errorf("invalid error: %v", err) + } + if _, err := config.ParseColorWindow(",1:xxx"); err.Error() != "strconv.Atoi: parsing \"xxx\": invalid syntax" { + t.Errorf("invalid error: %v", err) + } + if _, err := config.ParseColorWindow(",1:2,11:22"); err != nil { + t.Errorf("invalid error: %v", err) + } +} diff --git a/internal/config/vars.go b/internal/config/vars.go @@ -0,0 +1,214 @@ +// Package config handles user inputs/UI elements. +package config + +import ( + "errors" + "flag" + "fmt" + "net/url" + "os" + "os/exec" + "sort" + "strings" + "time" +) + +const ( + prefixKey = "LOCKBOX_" + clipBaseEnv = prefixKey + "CLIP_" + plainKeyMode = "plaintext" + commandKeyMode = "command" + commandArgsExample = "[cmd args...]" + detectedValue = "(detected)" + requiredKeyOrKeyFile = "a key, a key file, or both must be set" + // ModTimeFormat is the expected modtime format + ModTimeFormat = time.RFC3339 + // JSONDataOutputHash means output data is hashed + JSONDataOutputHash JSONOutputMode = "hash" + // JSONDataOutputBlank means an empty entry is set + JSONDataOutputBlank JSONOutputMode = "empty" + // JSONDataOutputRaw means the RAW (unencrypted) value is displayed + JSONDataOutputRaw JSONOutputMode = "plaintext" +) + +var ( + // Platforms represent the platforms that lockbox understands to run on + Platforms = []string{MacOSPlatform, WindowsLinuxPlatform, LinuxXPlatform, LinuxWaylandPlatform} + // TOTPDefaultColorWindow is the default coloring rules for totp + TOTPDefaultColorWindow = []ColorWindow{{Start: 0, End: 5}, {Start: 30, End: 35}} + // TOTPDefaultBetween is the default color window as a string + TOTPDefaultBetween = toString(TOTPDefaultColorWindow) + // EnvClipMax gets the maximum clipboard time + EnvClipMax = EnvironmentInt{environmentBase: environmentBase{key: clipBaseEnv + "MAX", desc: "override the amount of time before totp clears the clipboard (e.g. 10),\nmust be an integer"}, shortDesc: "clipboard max time", allowZero: false, defaultValue: 45} + // EnvHashLength handles the hashing output length + EnvHashLength = EnvironmentInt{environmentBase: environmentBase{key: EnvJSONDataOutput.key + "_HASH_LENGTH", desc: fmt.Sprintf("maximum hash length the JSON output should contain\nwhen '%s' mode is set for JSON output", JSONDataOutputHash)}, shortDesc: "hash length", allowZero: true, defaultValue: 0} + // EnvClipOSC52 indicates if OSC52 clipboard mode is enabled + EnvClipOSC52 = EnvironmentBool{environmentBase: environmentBase{key: clipBaseEnv + "OSC52", desc: "enable OSC52 clipboard mode"}, defaultValue: false} + // EnvNoTOTP indicates if TOTP is disabled + EnvNoTOTP = EnvironmentBool{environmentBase: environmentBase{key: prefixKey + "NOTOTP", desc: "disable TOTP integrations"}, defaultValue: false} + // EnvReadOnly indicates if in read-only mode + EnvReadOnly = EnvironmentBool{environmentBase: environmentBase{key: prefixKey + "READONLY", desc: "operate in readonly mode"}, defaultValue: false} + // EnvNoClip indicates clipboard functionality is off + EnvNoClip = EnvironmentBool{environmentBase: environmentBase{key: prefixKey + "NOCLIP", desc: "disable clipboard operations"}, defaultValue: false} + // EnvNoColor indicates if color outputs are disabled + EnvNoColor = EnvironmentBool{environmentBase: environmentBase{key: prefixKey + "NOCOLOR", desc: "disable terminal colors"}, defaultValue: false} + // EnvInteractive indicates if operating in interactive mode + EnvInteractive = EnvironmentBool{environmentBase: environmentBase{key: prefixKey + "INTERACTIVE", desc: "enable interactive mode"}, defaultValue: true} + // EnvMaxTOTP is the max TOTP time to run (default) + EnvMaxTOTP = EnvironmentInt{environmentBase: environmentBase{key: EnvTOTPToken.key + "_MAX", desc: "time, in seconds, in which to show a TOTP token before automatically exiting"}, shortDesc: "max totp time", allowZero: false, defaultValue: 120} + // 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: Platforms, 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 + EnvHookDir = EnvironmentString{environmentBase: environmentBase{key: prefixKey + "HOOKDIR", desc: "the path to hooks to execute on actions against the database"}, allowed: []string{"directory"}, canDefault: true, defaultValue: ""} + // EnvClipCopy allows overriding the clipboard copy command + EnvClipCopy = EnvironmentCommand{environmentBase: environmentBase{key: clipBaseEnv + "COPY", desc: "override the detected platform copy command"}} + // EnvClipPaste allows overriding the clipboard paste command + EnvClipPaste = EnvironmentCommand{environmentBase: environmentBase{key: clipBaseEnv + "PASTE", desc: "override the detected platform paste command"}} + // EnvTOTPColorBetween handles terminal coloring for TOTP windows (seconds) + EnvTOTPColorBetween = EnvironmentString{environmentBase: environmentBase{key: EnvTOTPToken.key + "_BETWEEN", desc: "override when to set totp generated outputs to different colors, must be a\nlist of one (or more) rules where a semicolon delimits the start and end\nsecond (0-60 for each)"}, canDefault: true, defaultValue: TOTPDefaultBetween, allowed: []string{"start:end,start:end,start:end..."}} + // EnvKeyFile is an keyfile for the database + EnvKeyFile = EnvironmentString{environmentBase: environmentBase{key: prefixKey + "KEYFILE", requirement: requiredKeyOrKeyFile, desc: "keyfile to access/protect the database"}, allowed: []string{"keyfile"}, canDefault: true, defaultValue: ""} + // EnvModTime is modtime override ability for entries + EnvModTime = EnvironmentString{environmentBase: environmentBase{key: prefixKey + "SET_MODTIME", desc: fmt.Sprintf("input modification time to set for the entry\n(expected format: %s)", ModTimeFormat)}, canDefault: true, defaultValue: "", allowed: []string{"modtime"}} + // EnvJSONDataOutput controls how JSON is output in the 'data' field + EnvJSONDataOutput = EnvironmentString{environmentBase: environmentBase{key: prefixKey + "JSON_DATA_OUTPUT", desc: fmt.Sprintf("changes what the data field in JSON outputs will contain\nuse '%s' with CAUTION", JSONDataOutputRaw)}, canDefault: true, defaultValue: string(JSONDataOutputHash), allowed: []string{string(JSONDataOutputRaw), string(JSONDataOutputHash), string(JSONDataOutputBlank)}} + // EnvFormatTOTP supports formatting the TOTP tokens for generation of tokens + EnvFormatTOTP = EnvironmentFormatter{environmentBase: environmentBase{key: EnvTOTPToken.key + "_FORMAT", desc: "override the otpauth url used to store totp tokens. It must have ONE format\nstring ('%s') to insert the totp base code"}, fxn: formatterTOTP, allowed: "otpauth//url/%s/args..."} + envKeyMode = EnvironmentString{environmentBase: environmentBase{key: prefixKey + "KEYMODE", requirement: "must be set to a valid mode when using a key", desc: "how to retrieve the database store password"}, allowed: []string{commandKeyMode, plainKeyMode}, canDefault: true, defaultValue: commandKeyMode} + envKey = EnvironmentString{environmentBase: environmentBase{requirement: requiredKeyOrKeyFile, key: prefixKey + "KEY", desc: fmt.Sprintf("the database key ('%s' mode) or command to run ('%s' mode)\nto retrieve the database password", plainKeyMode, commandKeyMode)}, allowed: []string{commandArgsExample, "password"}, canDefault: false} +) + +// GetReKey will get the rekey environment settings +func GetReKey(args []string) ([]string, error) { + set := flag.NewFlagSet("rekey", flag.ExitOnError) + store := set.String("store", "", "new store") + key := set.String("key", "", "new key") + keyFile := set.String("keyfile", "", "new keyfile") + keyMode := set.String("keymode", "", "new keymode") + if err := set.Parse(args); err != nil { + return nil, err + } + type keyer struct { + env EnvironmentString + has bool + in string + } + check := func(in string, e EnvironmentString) keyer { + val := strings.TrimSpace(in) + return keyer{has: val != "", env: e, in: in} + } + inStore := check(*store, EnvStore) + inKey := check(*key, envKey) + inKeyFile := check(*keyFile, EnvKeyFile) + inKeyMode := check(*keyMode, envKeyMode) + var out []string + for _, k := range []keyer{inStore, inKey, inKeyFile, inKeyMode} { + out = append(out, k.env.KeyValue(k.in)) + } + sort.Strings(out) + if !inStore.has || (!inKey.has && !inKeyFile.has) { + return nil, fmt.Errorf("missing required arguments for rekey: %s", strings.Join(out, " ")) + } + return out, nil +} + +// GetKey will get the encryption key setup for lb +func GetKey() ([]byte, error) { + useKey := envKey.Get() + if useKey == "" { + return nil, nil + } + var data []byte + switch envKeyMode.Get() { + case commandKeyMode: + parts, err := shlex(useKey) + if err != nil { + return nil, err + } + cmd := exec.Command(parts[0], parts[1:]...) + b, err := cmd.Output() + if err != nil { + return nil, err + } + data = b + case plainKeyMode: + data = []byte(useKey) + default: + return nil, errors.New("unknown keymode") + } + b := []byte(strings.TrimSpace(string(data))) + if len(b) == 0 { + return nil, errors.New("key is empty") + } + return b, nil +} + +// ListEnvironmentVariables will print information about env variables and potential/set values +func ListEnvironmentVariables(showValues bool) []string { + out := environmentOutput{showValues: showValues} + var results []string + for _, item := range []printer{EnvStore, envKeyMode, envKey, EnvNoClip, EnvNoColor, EnvInteractive, EnvReadOnly, EnvTOTPToken, EnvFormatTOTP, EnvMaxTOTP, EnvTOTPColorBetween, EnvClipPaste, EnvClipCopy, EnvClipMax, EnvPlatform, EnvNoTOTP, EnvHookDir, EnvClipOSC52, EnvKeyFile, EnvModTime, EnvJSONDataOutput, EnvHashLength} { + env := item.self() + value, allow := item.values() + if out.showValues { + value = os.Getenv(env.key) + } + if len(value) == 0 { + value = "(unset)" + } + description := strings.ReplaceAll(env.desc, "\n", "\n ") + requirement := "optional/default" + r := strings.TrimSpace(env.requirement) + if r != "" { + requirement = r + } + text := fmt.Sprintf("\n%s\n %s\n\n requirement: %s\n value: %s\n options: %s\n", env.key, description, requirement, value, strings.Join(allow, "|")) + results = append(results, text) + } + return results +} + +func formatterTOTP(key, value string) string { + const ( + otpAuth = "otpauth" + otpIssuer = "lbissuer" + ) + if strings.HasPrefix(value, otpAuth) { + return value + } + override := environOrDefault(key, "") + if override != "" { + return fmt.Sprintf(override, value) + } + v := url.Values{} + v.Set("secret", value) + v.Set("issuer", otpIssuer) + v.Set("period", "30") + v.Set("algorithm", "SHA1") + v.Set("digits", "6") + u := url.URL{ + Scheme: otpAuth, + Host: "totp", + Path: "/" + otpIssuer + ":" + "lbaccount", + RawQuery: v.Encode(), + } + return u.String() +} + +// ParseJSONOutput handles detecting the JSON output mode +func ParseJSONOutput() (JSONOutputMode, error) { + val := strings.ToLower(strings.TrimSpace(EnvJSONDataOutput.Get())) + switch JSONOutputMode(val) { + case JSONDataOutputHash: + return JSONDataOutputHash, nil + case JSONDataOutputBlank: + return JSONDataOutputBlank, nil + case JSONDataOutputRaw: + return JSONDataOutputRaw, nil + } + return JSONDataOutputBlank, fmt.Errorf("invalid JSON output mode: %s", val) +} diff --git a/internal/config/vars_test.go b/internal/config/vars_test.go @@ -0,0 +1,244 @@ +package config_test + +import ( + "fmt" + "os" + "strings" + "testing" + + "github.com/enckse/lockbox/internal/config" +) + +func checkYesNo(key string, t *testing.T, obj config.EnvironmentBool, onEmpty bool) { + os.Setenv(key, "yes") + c, err := obj.Get() + if err != nil { + t.Errorf("invalid error: %v", err) + } + if !c { + t.Error("invalid setting") + } + os.Setenv(key, "") + c, err = obj.Get() + if err != nil { + t.Errorf("invalid error: %v", err) + } + if c != onEmpty { + t.Error("invalid setting") + } + os.Setenv(key, "no") + c, err = obj.Get() + if err != nil { + t.Errorf("invalid error: %v", err) + } + if c { + t.Error("invalid setting") + } + os.Setenv(key, "afoieae") + _, err = obj.Get() + if err == nil || err.Error() != fmt.Sprintf("invalid yes/no env value for %s", key) { + t.Errorf("unexpected error: %v", err) + } +} + +func TestColorSetting(t *testing.T) { + checkYesNo("LOCKBOX_NOCOLOR", t, config.EnvNoColor, false) +} + +func TestInteractiveSetting(t *testing.T) { + checkYesNo("LOCKBOX_INTERACTIVE", t, config.EnvInteractive, true) +} + +func TestIsReadOnly(t *testing.T) { + checkYesNo("LOCKBOX_READONLY", t, config.EnvReadOnly, false) +} + +func TestIsOSC52(t *testing.T) { + checkYesNo("LOCKBOX_CLIP_OSC52", t, config.EnvClipOSC52, false) +} + +func TestIsNoTOTP(t *testing.T) { + checkYesNo("LOCKBOX_NOTOTP", t, config.EnvNoTOTP, false) +} + +func TestIsNoClip(t *testing.T) { + checkYesNo("LOCKBOX_NOCLIP", t, config.EnvNoClip, false) +} + +func TestTOTP(t *testing.T) { + os.Setenv("LOCKBOX_TOTP", "abc") + if config.EnvTOTPToken.Get() != "abc" { + t.Error("invalid totp token field") + } + os.Setenv("LOCKBOX_TOTP", "") + if config.EnvTOTPToken.Get() != "totp" { + t.Error("invalid totp token field") + } +} + +func TestGetKey(t *testing.T) { + os.Setenv("LOCKBOX_KEY", "aaa") + os.Setenv("LOCKBOX_KEYMODE", "lak;jfea") + if _, err := config.GetKey(); err.Error() != "unknown keymode" { + t.Errorf("invalid error: %v", err) + } + os.Setenv("LOCKBOX_KEYMODE", "plaintext") + os.Setenv("LOCKBOX_KEY", "") + if _, err := config.GetKey(); err != nil { + t.Errorf("invalid error: %v", err) + } + os.Setenv("LOCKBOX_KEY", "key") + k, err := config.GetKey() + if err != nil || string(k) != "key" { + t.Error("invalid key retrieval") + } + os.Setenv("LOCKBOX_KEYMODE", "command") + os.Setenv("LOCKBOX_KEY", "invalid command text is long and invalid via shlex") + if _, err := config.GetKey(); err == nil { + t.Error("should have failed") + } +} + +func TestListVariables(t *testing.T) { + known := make(map[string]struct{}) + for _, v := range config.ListEnvironmentVariables(false) { + trim := strings.Split(strings.TrimSpace(v), " ")[0] + if !strings.HasPrefix(trim, "LOCKBOX_") { + t.Errorf("invalid env: %s", v) + } + if _, ok := known[trim]; ok { + t.Errorf("invalid re-used env: %s", trim) + } + known[trim] = struct{}{} + } + l := len(known) + if l != 22 { + t.Errorf("invalid env count, outdated? %d", l) + } +} + +func TestReKey(t *testing.T) { + _, err := config.GetReKey([]string{}) + if err == nil || err.Error() != "missing required arguments for rekey: LOCKBOX_KEY= LOCKBOX_KEYFILE= LOCKBOX_KEYMODE= LOCKBOX_STORE=" { + t.Errorf("failed: %v", err) + } + _, err = config.GetReKey([]string{"-store", "abc"}) + if err == nil || err.Error() != "missing required arguments for rekey: LOCKBOX_KEY= LOCKBOX_KEYFILE= LOCKBOX_KEYMODE= LOCKBOX_STORE=abc" { + t.Errorf("failed: %v", err) + } + out, err := config.GetReKey([]string{"-store", "abc", "-key", "aaa"}) + if err != nil { + t.Errorf("failed: %v", err) + } + if fmt.Sprintf("%v", out) != "[LOCKBOX_KEY=aaa LOCKBOX_KEYFILE= LOCKBOX_KEYMODE= LOCKBOX_STORE=abc]" { + t.Errorf("invalid env: %v", out) + } + out, err = config.GetReKey([]string{"-store", "abc", "-keyfile", "aaa"}) + if err != nil { + t.Errorf("failed: %v", err) + } + if fmt.Sprintf("%v", out) != "[LOCKBOX_KEY= LOCKBOX_KEYFILE=aaa LOCKBOX_KEYMODE= LOCKBOX_STORE=abc]" { + t.Errorf("invalid env: %v", out) + } + os.Setenv("LOCKBOX_KEY_NEW", "") + os.Setenv("LOCKBOX_STORE_NEW", "") + os.Setenv("LOCKBOX_KEY_NEW", "") + os.Setenv("LOCKBOX_KEYFILE_NEW", "") +} + +func TestFormatTOTP(t *testing.T) { + otp := config.EnvFormatTOTP.Get("otpauth://abc") + if otp != "otpauth://abc" { + t.Errorf("invalid totp token: %s", otp) + } + otp = config.EnvFormatTOTP.Get("abc") + if otp != "otpauth://totp/lbissuer:lbaccount?algorithm=SHA1&digits=6&issuer=lbissuer&period=30&secret=abc" { + t.Errorf("invalid totp token: %s", otp) + } + os.Setenv("LOCKBOX_TOTP_FORMAT", "test/%s") + otp = config.EnvFormatTOTP.Get("abc") + if otp != "test/abc" { + t.Errorf("invalid totp token: %s", otp) + } + os.Setenv("LOCKBOX_TOTP_FORMAT", "") + otp = config.EnvFormatTOTP.Get("abc") + if otp != "otpauth://totp/lbissuer:lbaccount?algorithm=SHA1&digits=6&issuer=lbissuer&period=30&secret=abc" { + t.Errorf("invalid totp token: %s", otp) + } +} + +func TestParseJSONMode(t *testing.T) { + defer os.Clearenv() + m, err := config.ParseJSONOutput() + if m != config.JSONDataOutputHash || err != nil { + t.Error("invalid mode read") + } + os.Setenv("LOCKBOX_JSON_DATA_OUTPUT", "hAsH ") + m, err = config.ParseJSONOutput() + if m != config.JSONDataOutputHash || err != nil { + t.Error("invalid mode read") + } + os.Setenv("LOCKBOX_JSON_DATA_OUTPUT", "EMPTY") + m, err = config.ParseJSONOutput() + if m != config.JSONDataOutputBlank || err != nil { + t.Error("invalid mode read") + } + os.Setenv("LOCKBOX_JSON_DATA_OUTPUT", " PLAINtext ") + m, err = config.ParseJSONOutput() + if m != config.JSONDataOutputRaw || err != nil { + t.Error("invalid mode read") + } + os.Setenv("LOCKBOX_JSON_DATA_OUTPUT", "a") + if _, err = config.ParseJSONOutput(); err == nil || err.Error() != "invalid JSON output mode: a" { + t.Errorf("invalid error: %v", err) + } +} + +func TestClipboardMax(t *testing.T) { + checkInt(config.EnvClipMax, "LOCKBOX_CLIP_MAX", "clipboard max time", 45, false, t) +} + +func TestHashLength(t *testing.T) { + checkInt(config.EnvHashLength, "LOCKBOX_JSON_DATA_OUTPUT_HASH_LENGTH", "hash length", 0, true, t) +} + +func TestMaxTOTP(t *testing.T) { + checkInt(config.EnvMaxTOTP, "LOCKBOX_TOTP_MAX", "max totp time", 120, false, t) +} + +func checkInt(e config.EnvironmentInt, key, text string, def int, allowZero bool, t *testing.T) { + os.Setenv(key, "") + defer os.Clearenv() + val, err := e.Get() + if err != nil || val != def { + t.Error("invalid read") + } + os.Setenv(key, "1") + val, err = e.Get() + if err != nil || val != 1 { + t.Error("invalid read") + } + os.Setenv(key, "-1") + zero := "" + if allowZero { + zero = "=" + } + if _, err := e.Get(); err == nil || err.Error() != fmt.Sprintf("%s must be >%s 0", text, zero) { + t.Errorf("invalid err: %v", err) + } + os.Setenv(key, "alk;ja") + if _, err := e.Get(); err == nil || err.Error() != "strconv.Atoi: parsing \"alk;ja\": invalid syntax" { + t.Errorf("invalid err: %v", err) + } + os.Setenv(key, "0") + if allowZero { + val, err = e.Get() + if err != nil || val != 0 { + t.Error("invalid read") + } + } else { + if _, err := e.Get(); err == nil || err.Error() != fmt.Sprintf("%s must be > 0", text) { + t.Errorf("invalid err: %v", err) + } + } +} diff --git a/internal/inputs/core.go b/internal/inputs/core.go @@ -1,271 +0,0 @@ -// Package inputs handles user inputs/UI elements. -package inputs - -import ( - "errors" - "fmt" - "os" - "os/exec" - "strconv" - "strings" - - "mvdan.cc/sh/v3/shell" -) - -const ( - colorWindowDelimiter = "," - colorWindowSpan = ":" - yes = "yes" - no = "no" - // MacOSPlatform is the macos indicator for platform - MacOSPlatform = "macos" - // LinuxWaylandPlatform for linux+wayland - LinuxWaylandPlatform = "linux-wayland" - // LinuxXPlatform for linux+X - LinuxXPlatform = "linux-x" - // WindowsLinuxPlatform for WSL subsystems - WindowsLinuxPlatform = "wsl" - unknownPlatform = "" -) - -type ( - // JSONOutputMode is the output mode definition - JSONOutputMode string - environmentOutput struct { - showValues bool - } - // SystemPlatform represents the platform lockbox is running on. - SystemPlatform string - environmentBase struct { - key string - desc string - requirement string - } - // EnvironmentInt are environment settings that are integers - EnvironmentInt struct { - environmentBase - defaultValue int - allowZero bool - shortDesc string - } - // EnvironmentBool are environment settings that are booleans - EnvironmentBool struct { - environmentBase - defaultValue bool - } - // EnvironmentString are string-based settings - EnvironmentString struct { - environmentBase - canDefault bool - defaultValue string - allowed []string - } - // EnvironmentCommand are settings that are parsed as shell commands - EnvironmentCommand struct { - environmentBase - } - // EnvironmentFormatter allows for sending a string into a get request - EnvironmentFormatter struct { - environmentBase - allowed string - fxn func(string, string) string - } - printer interface { - values() (string, []string) - self() environmentBase - } - // ColorWindow for handling terminal colors based on timing - ColorWindow struct { - Start int - End int - } -) - -func shlex(in string) ([]string, error) { - return shell.Fields(in, os.Getenv) -} - -func environOrDefault(envKey, defaultValue string) string { - val := os.Getenv(envKey) - if strings.TrimSpace(val) == "" { - return defaultValue - } - return val -} - -// Get will get the boolean value for the setting -func (e EnvironmentBool) Get() (bool, error) { - read := strings.ToLower(strings.TrimSpace(os.Getenv(e.key))) - switch read { - case no: - return false, nil - case yes: - return true, nil - case "": - return e.defaultValue, nil - } - - return false, fmt.Errorf("invalid yes/no env value for %s", e.key) -} - -// Get will get the integer value for the setting -func (e EnvironmentInt) Get() (int, error) { - val := e.defaultValue - use := os.Getenv(e.key) - if use != "" { - i, err := strconv.Atoi(use) - if err != nil { - return -1, err - } - invalid := false - check := "" - if e.allowZero { - check = "=" - } - switch i { - case 0: - invalid = !e.allowZero - default: - invalid = i < 0 - } - if invalid { - return -1, fmt.Errorf("%s must be >%s 0", e.shortDesc, check) - } - val = i - } - return val, nil -} - -// Get will read the string from the environment -func (e EnvironmentString) Get() string { - if !e.canDefault { - return os.Getenv(e.key) - } - return environOrDefault(e.key, e.defaultValue) -} - -// Get will read (and shlex) the value if set -func (e EnvironmentCommand) Get() ([]string, error) { - value := environOrDefault(e.key, "") - if strings.TrimSpace(value) == "" { - return nil, nil - } - return shlex(value) -} - -// KeyValue will get the string representation of the key+value -func (e environmentBase) KeyValue(value string) string { - return fmt.Sprintf("%s=%s", e.key, value) -} - -// Set will do an environment set for the value to key -func (e environmentBase) Set(value string) { - os.Setenv(e.key, value) -} - -// Get will retrieve the value with the formatted input included -func (e EnvironmentFormatter) Get(value string) string { - return e.fxn(e.key, value) -} - -func (e EnvironmentString) values() (string, []string) { - return e.defaultValue, e.allowed -} - -func (e environmentBase) self() environmentBase { - return e -} - -func (e EnvironmentBool) values() (string, []string) { - val := no - if e.defaultValue { - val = yes - } - return val, []string{yes, no} -} - -func (e EnvironmentInt) values() (string, []string) { - return fmt.Sprintf("%d", e.defaultValue), []string{"integer"} -} - -func (e EnvironmentFormatter) values() (string, []string) { - return strings.ReplaceAll(strings.ReplaceAll(EnvFormatTOTP.Get("%s"), "%25s", "%s"), "&", " \\\n &"), []string{e.allowed} -} - -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") -} - -func toString(windows []ColorWindow) string { - var results []string - for _, w := range windows { - results = append(results, fmt.Sprintf("%d%s%d", w.Start, colorWindowSpan, w.End)) - } - return strings.Join(results, colorWindowDelimiter) -} - -// ParseColorWindow will handle parsing a window of colors for TOTP operations -func ParseColorWindow(windowString string) ([]ColorWindow, error) { - var rules []ColorWindow - for _, item := range strings.Split(windowString, colorWindowDelimiter) { - line := strings.TrimSpace(item) - if line == "" { - continue - } - parts := strings.Split(line, colorWindowSpan) - if len(parts) != 2 { - return nil, fmt.Errorf("invalid colorization rule found: %s", line) - } - s, err := strconv.Atoi(parts[0]) - if err != nil { - return nil, err - } - e, err := strconv.Atoi(parts[1]) - if err != nil { - return nil, err - } - if s < 0 || e < 0 || e < s || s > 59 || e > 59 { - return nil, fmt.Errorf("invalid time found for colorization rule: %s", line) - } - rules = append(rules, ColorWindow{Start: s, End: e}) - } - if len(rules) == 0 { - return nil, errors.New("invalid colorization rules for totp, none found") - } - return rules, nil -} diff --git a/internal/inputs/core_test.go b/internal/inputs/core_test.go @@ -1,84 +0,0 @@ -package inputs_test - -import ( - "os" - "testing" - - "github.com/enckse/lockbox/internal/inputs" -) - -func TestPlatformSet(t *testing.T) { - if len(inputs.Platforms) != 4 { - t.Error("invalid platform set") - } -} - -func TestSet(t *testing.T) { - os.Clearenv() - defer os.Clearenv() - inputs.EnvStore.Set("TEST") - if inputs.EnvStore.Get() != "TEST" { - t.Errorf("invalid set/get") - } -} - -func TestKeyValue(t *testing.T) { - val := inputs.EnvStore.KeyValue("TEST") - if val != "LOCKBOX_STORE=TEST" { - t.Errorf("invalid keyvalue") - } -} - -func TestNewPlatform(t *testing.T) { - for _, item := range inputs.Platforms { - 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) - } -} - -func TestParseWindows(t *testing.T) { - if _, err := inputs.ParseColorWindow(""); err.Error() != "invalid colorization rules for totp, none found" { - t.Errorf("invalid error: %v", err) - } - if _, err := inputs.ParseColorWindow(",2"); err.Error() != "invalid colorization rule found: 2" { - t.Errorf("invalid error: %v", err) - } - if _, err := inputs.ParseColorWindow(",1:200"); err.Error() != "invalid time found for colorization rule: 1:200" { - t.Errorf("invalid error: %v", err) - } - if _, err := inputs.ParseColorWindow(",1:-1"); err.Error() != "invalid time found for colorization rule: 1:-1" { - t.Errorf("invalid error: %v", err) - } - if _, err := inputs.ParseColorWindow(",200:1"); err.Error() != "invalid time found for colorization rule: 200:1" { - t.Errorf("invalid error: %v", err) - } - if _, err := inputs.ParseColorWindow(",-1:1"); err.Error() != "invalid time found for colorization rule: -1:1" { - t.Errorf("invalid error: %v", err) - } - if _, err := inputs.ParseColorWindow(",2:1"); err.Error() != "invalid time found for colorization rule: 2:1" { - t.Errorf("invalid error: %v", err) - } - if _, err := inputs.ParseColorWindow(",xxx:1"); err.Error() != "strconv.Atoi: parsing \"xxx\": invalid syntax" { - t.Errorf("invalid error: %v", err) - } - if _, err := inputs.ParseColorWindow(",1:xxx"); err.Error() != "strconv.Atoi: parsing \"xxx\": invalid syntax" { - t.Errorf("invalid error: %v", err) - } - if _, err := inputs.ParseColorWindow(",1:2,11:22"); err != nil { - t.Errorf("invalid error: %v", err) - } -} diff --git a/internal/inputs/vars.go b/internal/inputs/vars.go @@ -1,214 +0,0 @@ -// Package inputs handles user inputs/UI elements. -package inputs - -import ( - "errors" - "flag" - "fmt" - "net/url" - "os" - "os/exec" - "sort" - "strings" - "time" -) - -const ( - prefixKey = "LOCKBOX_" - clipBaseEnv = prefixKey + "CLIP_" - plainKeyMode = "plaintext" - commandKeyMode = "command" - commandArgsExample = "[cmd args...]" - detectedValue = "(detected)" - requiredKeyOrKeyFile = "a key, a key file, or both must be set" - // ModTimeFormat is the expected modtime format - ModTimeFormat = time.RFC3339 - // JSONDataOutputHash means output data is hashed - JSONDataOutputHash JSONOutputMode = "hash" - // JSONDataOutputBlank means an empty entry is set - JSONDataOutputBlank JSONOutputMode = "empty" - // JSONDataOutputRaw means the RAW (unencrypted) value is displayed - JSONDataOutputRaw JSONOutputMode = "plaintext" -) - -var ( - // Platforms represent the platforms that lockbox understands to run on - Platforms = []string{MacOSPlatform, WindowsLinuxPlatform, LinuxXPlatform, LinuxWaylandPlatform} - // TOTPDefaultColorWindow is the default coloring rules for totp - TOTPDefaultColorWindow = []ColorWindow{{Start: 0, End: 5}, {Start: 30, End: 35}} - // TOTPDefaultBetween is the default color window as a string - TOTPDefaultBetween = toString(TOTPDefaultColorWindow) - // EnvClipMax gets the maximum clipboard time - EnvClipMax = EnvironmentInt{environmentBase: environmentBase{key: clipBaseEnv + "MAX", desc: "override the amount of time before totp clears the clipboard (e.g. 10),\nmust be an integer"}, shortDesc: "clipboard max time", allowZero: false, defaultValue: 45} - // EnvHashLength handles the hashing output length - EnvHashLength = EnvironmentInt{environmentBase: environmentBase{key: EnvJSONDataOutput.key + "_HASH_LENGTH", desc: fmt.Sprintf("maximum hash length the JSON output should contain\nwhen '%s' mode is set for JSON output", JSONDataOutputHash)}, shortDesc: "hash length", allowZero: true, defaultValue: 0} - // EnvClipOSC52 indicates if OSC52 clipboard mode is enabled - EnvClipOSC52 = EnvironmentBool{environmentBase: environmentBase{key: clipBaseEnv + "OSC52", desc: "enable OSC52 clipboard mode"}, defaultValue: false} - // EnvNoTOTP indicates if TOTP is disabled - EnvNoTOTP = EnvironmentBool{environmentBase: environmentBase{key: prefixKey + "NOTOTP", desc: "disable TOTP integrations"}, defaultValue: false} - // EnvReadOnly indicates if in read-only mode - EnvReadOnly = EnvironmentBool{environmentBase: environmentBase{key: prefixKey + "READONLY", desc: "operate in readonly mode"}, defaultValue: false} - // EnvNoClip indicates clipboard functionality is off - EnvNoClip = EnvironmentBool{environmentBase: environmentBase{key: prefixKey + "NOCLIP", desc: "disable clipboard operations"}, defaultValue: false} - // EnvNoColor indicates if color outputs are disabled - EnvNoColor = EnvironmentBool{environmentBase: environmentBase{key: prefixKey + "NOCOLOR", desc: "disable terminal colors"}, defaultValue: false} - // EnvInteractive indicates if operating in interactive mode - EnvInteractive = EnvironmentBool{environmentBase: environmentBase{key: prefixKey + "INTERACTIVE", desc: "enable interactive mode"}, defaultValue: true} - // EnvMaxTOTP is the max TOTP time to run (default) - EnvMaxTOTP = EnvironmentInt{environmentBase: environmentBase{key: EnvTOTPToken.key + "_MAX", desc: "time, in seconds, in which to show a TOTP token before automatically exiting"}, shortDesc: "max totp time", allowZero: false, defaultValue: 120} - // 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: Platforms, 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 - EnvHookDir = EnvironmentString{environmentBase: environmentBase{key: prefixKey + "HOOKDIR", desc: "the path to hooks to execute on actions against the database"}, allowed: []string{"directory"}, canDefault: true, defaultValue: ""} - // EnvClipCopy allows overriding the clipboard copy command - EnvClipCopy = EnvironmentCommand{environmentBase: environmentBase{key: clipBaseEnv + "COPY", desc: "override the detected platform copy command"}} - // EnvClipPaste allows overriding the clipboard paste command - EnvClipPaste = EnvironmentCommand{environmentBase: environmentBase{key: clipBaseEnv + "PASTE", desc: "override the detected platform paste command"}} - // EnvTOTPColorBetween handles terminal coloring for TOTP windows (seconds) - EnvTOTPColorBetween = EnvironmentString{environmentBase: environmentBase{key: EnvTOTPToken.key + "_BETWEEN", desc: "override when to set totp generated outputs to different colors, must be a\nlist of one (or more) rules where a semicolon delimits the start and end\nsecond (0-60 for each)"}, canDefault: true, defaultValue: TOTPDefaultBetween, allowed: []string{"start:end,start:end,start:end..."}} - // EnvKeyFile is an keyfile for the database - EnvKeyFile = EnvironmentString{environmentBase: environmentBase{key: prefixKey + "KEYFILE", requirement: requiredKeyOrKeyFile, desc: "keyfile to access/protect the database"}, allowed: []string{"keyfile"}, canDefault: true, defaultValue: ""} - // EnvModTime is modtime override ability for entries - EnvModTime = EnvironmentString{environmentBase: environmentBase{key: prefixKey + "SET_MODTIME", desc: fmt.Sprintf("input modification time to set for the entry\n(expected format: %s)", ModTimeFormat)}, canDefault: true, defaultValue: "", allowed: []string{"modtime"}} - // EnvJSONDataOutput controls how JSON is output in the 'data' field - EnvJSONDataOutput = EnvironmentString{environmentBase: environmentBase{key: prefixKey + "JSON_DATA_OUTPUT", desc: fmt.Sprintf("changes what the data field in JSON outputs will contain\nuse '%s' with CAUTION", JSONDataOutputRaw)}, canDefault: true, defaultValue: string(JSONDataOutputHash), allowed: []string{string(JSONDataOutputRaw), string(JSONDataOutputHash), string(JSONDataOutputBlank)}} - // EnvFormatTOTP supports formatting the TOTP tokens for generation of tokens - EnvFormatTOTP = EnvironmentFormatter{environmentBase: environmentBase{key: EnvTOTPToken.key + "_FORMAT", desc: "override the otpauth url used to store totp tokens. It must have ONE format\nstring ('%s') to insert the totp base code"}, fxn: formatterTOTP, allowed: "otpauth//url/%s/args..."} - envKeyMode = EnvironmentString{environmentBase: environmentBase{key: prefixKey + "KEYMODE", requirement: "must be set to a valid mode when using a key", desc: "how to retrieve the database store password"}, allowed: []string{commandKeyMode, plainKeyMode}, canDefault: true, defaultValue: commandKeyMode} - envKey = EnvironmentString{environmentBase: environmentBase{requirement: requiredKeyOrKeyFile, key: prefixKey + "KEY", desc: fmt.Sprintf("the database key ('%s' mode) or command to run ('%s' mode)\nto retrieve the database password", plainKeyMode, commandKeyMode)}, allowed: []string{commandArgsExample, "password"}, canDefault: false} -) - -// GetReKey will get the rekey environment settings -func GetReKey(args []string) ([]string, error) { - set := flag.NewFlagSet("rekey", flag.ExitOnError) - store := set.String("store", "", "new store") - key := set.String("key", "", "new key") - keyFile := set.String("keyfile", "", "new keyfile") - keyMode := set.String("keymode", "", "new keymode") - if err := set.Parse(args); err != nil { - return nil, err - } - type keyer struct { - env EnvironmentString - has bool - in string - } - check := func(in string, e EnvironmentString) keyer { - val := strings.TrimSpace(in) - return keyer{has: val != "", env: e, in: in} - } - inStore := check(*store, EnvStore) - inKey := check(*key, envKey) - inKeyFile := check(*keyFile, EnvKeyFile) - inKeyMode := check(*keyMode, envKeyMode) - var out []string - for _, k := range []keyer{inStore, inKey, inKeyFile, inKeyMode} { - out = append(out, k.env.KeyValue(k.in)) - } - sort.Strings(out) - if !inStore.has || (!inKey.has && !inKeyFile.has) { - return nil, fmt.Errorf("missing required arguments for rekey: %s", strings.Join(out, " ")) - } - return out, nil -} - -// GetKey will get the encryption key setup for lb -func GetKey() ([]byte, error) { - useKey := envKey.Get() - if useKey == "" { - return nil, nil - } - var data []byte - switch envKeyMode.Get() { - case commandKeyMode: - parts, err := shlex(useKey) - if err != nil { - return nil, err - } - cmd := exec.Command(parts[0], parts[1:]...) - b, err := cmd.Output() - if err != nil { - return nil, err - } - data = b - case plainKeyMode: - data = []byte(useKey) - default: - return nil, errors.New("unknown keymode") - } - b := []byte(strings.TrimSpace(string(data))) - if len(b) == 0 { - return nil, errors.New("key is empty") - } - return b, nil -} - -// ListEnvironmentVariables will print information about env variables and potential/set values -func ListEnvironmentVariables(showValues bool) []string { - out := environmentOutput{showValues: showValues} - var results []string - for _, item := range []printer{EnvStore, envKeyMode, envKey, EnvNoClip, EnvNoColor, EnvInteractive, EnvReadOnly, EnvTOTPToken, EnvFormatTOTP, EnvMaxTOTP, EnvTOTPColorBetween, EnvClipPaste, EnvClipCopy, EnvClipMax, EnvPlatform, EnvNoTOTP, EnvHookDir, EnvClipOSC52, EnvKeyFile, EnvModTime, EnvJSONDataOutput, EnvHashLength} { - env := item.self() - value, allow := item.values() - if out.showValues { - value = os.Getenv(env.key) - } - if len(value) == 0 { - value = "(unset)" - } - description := strings.ReplaceAll(env.desc, "\n", "\n ") - requirement := "optional/default" - r := strings.TrimSpace(env.requirement) - if r != "" { - requirement = r - } - text := fmt.Sprintf("\n%s\n %s\n\n requirement: %s\n value: %s\n options: %s\n", env.key, description, requirement, value, strings.Join(allow, "|")) - results = append(results, text) - } - return results -} - -func formatterTOTP(key, value string) string { - const ( - otpAuth = "otpauth" - otpIssuer = "lbissuer" - ) - if strings.HasPrefix(value, otpAuth) { - return value - } - override := environOrDefault(key, "") - if override != "" { - return fmt.Sprintf(override, value) - } - v := url.Values{} - v.Set("secret", value) - v.Set("issuer", otpIssuer) - v.Set("period", "30") - v.Set("algorithm", "SHA1") - v.Set("digits", "6") - u := url.URL{ - Scheme: otpAuth, - Host: "totp", - Path: "/" + otpIssuer + ":" + "lbaccount", - RawQuery: v.Encode(), - } - return u.String() -} - -// ParseJSONOutput handles detecting the JSON output mode -func ParseJSONOutput() (JSONOutputMode, error) { - val := strings.ToLower(strings.TrimSpace(EnvJSONDataOutput.Get())) - switch JSONOutputMode(val) { - case JSONDataOutputHash: - return JSONDataOutputHash, nil - case JSONDataOutputBlank: - return JSONDataOutputBlank, nil - case JSONDataOutputRaw: - return JSONDataOutputRaw, nil - } - return JSONDataOutputBlank, fmt.Errorf("invalid JSON output mode: %s", val) -} diff --git a/internal/inputs/vars_test.go b/internal/inputs/vars_test.go @@ -1,244 +0,0 @@ -package inputs_test - -import ( - "fmt" - "os" - "strings" - "testing" - - "github.com/enckse/lockbox/internal/inputs" -) - -func checkYesNo(key string, t *testing.T, obj inputs.EnvironmentBool, onEmpty bool) { - os.Setenv(key, "yes") - c, err := obj.Get() - if err != nil { - t.Errorf("invalid error: %v", err) - } - if !c { - t.Error("invalid setting") - } - os.Setenv(key, "") - c, err = obj.Get() - if err != nil { - t.Errorf("invalid error: %v", err) - } - if c != onEmpty { - t.Error("invalid setting") - } - os.Setenv(key, "no") - c, err = obj.Get() - if err != nil { - t.Errorf("invalid error: %v", err) - } - if c { - t.Error("invalid setting") - } - os.Setenv(key, "afoieae") - _, err = obj.Get() - if err == nil || err.Error() != fmt.Sprintf("invalid yes/no env value for %s", key) { - t.Errorf("unexpected error: %v", err) - } -} - -func TestColorSetting(t *testing.T) { - checkYesNo("LOCKBOX_NOCOLOR", t, inputs.EnvNoColor, false) -} - -func TestInteractiveSetting(t *testing.T) { - checkYesNo("LOCKBOX_INTERACTIVE", t, inputs.EnvInteractive, true) -} - -func TestIsReadOnly(t *testing.T) { - checkYesNo("LOCKBOX_READONLY", t, inputs.EnvReadOnly, false) -} - -func TestIsOSC52(t *testing.T) { - checkYesNo("LOCKBOX_CLIP_OSC52", t, inputs.EnvClipOSC52, false) -} - -func TestIsNoTOTP(t *testing.T) { - checkYesNo("LOCKBOX_NOTOTP", t, inputs.EnvNoTOTP, false) -} - -func TestIsNoClip(t *testing.T) { - checkYesNo("LOCKBOX_NOCLIP", t, inputs.EnvNoClip, false) -} - -func TestTOTP(t *testing.T) { - os.Setenv("LOCKBOX_TOTP", "abc") - if inputs.EnvTOTPToken.Get() != "abc" { - t.Error("invalid totp token field") - } - os.Setenv("LOCKBOX_TOTP", "") - if inputs.EnvTOTPToken.Get() != "totp" { - t.Error("invalid totp token field") - } -} - -func TestGetKey(t *testing.T) { - os.Setenv("LOCKBOX_KEY", "aaa") - os.Setenv("LOCKBOX_KEYMODE", "lak;jfea") - if _, err := inputs.GetKey(); err.Error() != "unknown keymode" { - t.Errorf("invalid error: %v", err) - } - os.Setenv("LOCKBOX_KEYMODE", "plaintext") - os.Setenv("LOCKBOX_KEY", "") - if _, err := inputs.GetKey(); err != nil { - t.Errorf("invalid error: %v", err) - } - os.Setenv("LOCKBOX_KEY", "key") - k, err := inputs.GetKey() - if err != nil || string(k) != "key" { - t.Error("invalid key retrieval") - } - os.Setenv("LOCKBOX_KEYMODE", "command") - os.Setenv("LOCKBOX_KEY", "invalid command text is long and invalid via shlex") - if _, err := inputs.GetKey(); err == nil { - t.Error("should have failed") - } -} - -func TestListVariables(t *testing.T) { - known := make(map[string]struct{}) - for _, v := range inputs.ListEnvironmentVariables(false) { - trim := strings.Split(strings.TrimSpace(v), " ")[0] - if !strings.HasPrefix(trim, "LOCKBOX_") { - t.Errorf("invalid env: %s", v) - } - if _, ok := known[trim]; ok { - t.Errorf("invalid re-used env: %s", trim) - } - known[trim] = struct{}{} - } - l := len(known) - if l != 22 { - t.Errorf("invalid env count, outdated? %d", l) - } -} - -func TestReKey(t *testing.T) { - _, err := inputs.GetReKey([]string{}) - if err == nil || err.Error() != "missing required arguments for rekey: LOCKBOX_KEY= LOCKBOX_KEYFILE= LOCKBOX_KEYMODE= LOCKBOX_STORE=" { - t.Errorf("failed: %v", err) - } - _, err = inputs.GetReKey([]string{"-store", "abc"}) - if err == nil || err.Error() != "missing required arguments for rekey: LOCKBOX_KEY= LOCKBOX_KEYFILE= LOCKBOX_KEYMODE= LOCKBOX_STORE=abc" { - t.Errorf("failed: %v", err) - } - out, err := inputs.GetReKey([]string{"-store", "abc", "-key", "aaa"}) - if err != nil { - t.Errorf("failed: %v", err) - } - if fmt.Sprintf("%v", out) != "[LOCKBOX_KEY=aaa LOCKBOX_KEYFILE= LOCKBOX_KEYMODE= LOCKBOX_STORE=abc]" { - t.Errorf("invalid env: %v", out) - } - out, err = inputs.GetReKey([]string{"-store", "abc", "-keyfile", "aaa"}) - if err != nil { - t.Errorf("failed: %v", err) - } - if fmt.Sprintf("%v", out) != "[LOCKBOX_KEY= LOCKBOX_KEYFILE=aaa LOCKBOX_KEYMODE= LOCKBOX_STORE=abc]" { - t.Errorf("invalid env: %v", out) - } - os.Setenv("LOCKBOX_KEY_NEW", "") - os.Setenv("LOCKBOX_STORE_NEW", "") - os.Setenv("LOCKBOX_KEY_NEW", "") - os.Setenv("LOCKBOX_KEYFILE_NEW", "") -} - -func TestFormatTOTP(t *testing.T) { - otp := inputs.EnvFormatTOTP.Get("otpauth://abc") - if otp != "otpauth://abc" { - t.Errorf("invalid totp token: %s", otp) - } - otp = inputs.EnvFormatTOTP.Get("abc") - if otp != "otpauth://totp/lbissuer:lbaccount?algorithm=SHA1&digits=6&issuer=lbissuer&period=30&secret=abc" { - t.Errorf("invalid totp token: %s", otp) - } - os.Setenv("LOCKBOX_TOTP_FORMAT", "test/%s") - otp = inputs.EnvFormatTOTP.Get("abc") - if otp != "test/abc" { - t.Errorf("invalid totp token: %s", otp) - } - os.Setenv("LOCKBOX_TOTP_FORMAT", "") - otp = inputs.EnvFormatTOTP.Get("abc") - if otp != "otpauth://totp/lbissuer:lbaccount?algorithm=SHA1&digits=6&issuer=lbissuer&period=30&secret=abc" { - t.Errorf("invalid totp token: %s", otp) - } -} - -func TestParseJSONMode(t *testing.T) { - defer os.Clearenv() - m, err := inputs.ParseJSONOutput() - if m != inputs.JSONDataOutputHash || err != nil { - t.Error("invalid mode read") - } - os.Setenv("LOCKBOX_JSON_DATA_OUTPUT", "hAsH ") - m, err = inputs.ParseJSONOutput() - if m != inputs.JSONDataOutputHash || err != nil { - t.Error("invalid mode read") - } - os.Setenv("LOCKBOX_JSON_DATA_OUTPUT", "EMPTY") - m, err = inputs.ParseJSONOutput() - if m != inputs.JSONDataOutputBlank || err != nil { - t.Error("invalid mode read") - } - os.Setenv("LOCKBOX_JSON_DATA_OUTPUT", " PLAINtext ") - m, err = inputs.ParseJSONOutput() - if m != inputs.JSONDataOutputRaw || err != nil { - t.Error("invalid mode read") - } - os.Setenv("LOCKBOX_JSON_DATA_OUTPUT", "a") - if _, err = inputs.ParseJSONOutput(); err == nil || err.Error() != "invalid JSON output mode: a" { - t.Errorf("invalid error: %v", err) - } -} - -func TestClipboardMax(t *testing.T) { - checkInt(inputs.EnvClipMax, "LOCKBOX_CLIP_MAX", "clipboard max time", 45, false, t) -} - -func TestHashLength(t *testing.T) { - checkInt(inputs.EnvHashLength, "LOCKBOX_JSON_DATA_OUTPUT_HASH_LENGTH", "hash length", 0, true, t) -} - -func TestMaxTOTP(t *testing.T) { - checkInt(inputs.EnvMaxTOTP, "LOCKBOX_TOTP_MAX", "max totp time", 120, false, t) -} - -func checkInt(e inputs.EnvironmentInt, key, text string, def int, allowZero bool, t *testing.T) { - os.Setenv(key, "") - defer os.Clearenv() - val, err := e.Get() - if err != nil || val != def { - t.Error("invalid read") - } - os.Setenv(key, "1") - val, err = e.Get() - if err != nil || val != 1 { - t.Error("invalid read") - } - os.Setenv(key, "-1") - zero := "" - if allowZero { - zero = "=" - } - if _, err := e.Get(); err == nil || err.Error() != fmt.Sprintf("%s must be >%s 0", text, zero) { - t.Errorf("invalid err: %v", err) - } - os.Setenv(key, "alk;ja") - if _, err := e.Get(); err == nil || err.Error() != "strconv.Atoi: parsing \"alk;ja\": invalid syntax" { - t.Errorf("invalid err: %v", err) - } - os.Setenv(key, "0") - if allowZero { - val, err = e.Get() - if err != nil || val != 0 { - t.Error("invalid read") - } - } else { - if _, err := e.Get(); err == nil || err.Error() != fmt.Sprintf("%s must be > 0", text) { - t.Errorf("invalid err: %v", err) - } - } -} diff --git a/internal/platform/clipboard.go b/internal/platform/clipboard.go @@ -8,7 +8,7 @@ import ( "os/exec" osc "github.com/aymanbagabas/go-osc52" - "github.com/enckse/lockbox/internal/inputs" + "github.com/enckse/lockbox/internal/config" ) type ( @@ -22,7 +22,7 @@ type ( ) func newClipboard(copying, pasting []string) (Clipboard, error) { - max, err := inputs.EnvClipMax.Get() + max, err := config.EnvClipMax.Get() if err != nil { return Clipboard{}, err } @@ -31,25 +31,25 @@ func newClipboard(copying, pasting []string) (Clipboard, error) { // NewClipboard will retrieve the commands to use for clipboard operations. func NewClipboard() (Clipboard, error) { - noClip, err := inputs.EnvNoClip.Get() + noClip, err := config.EnvNoClip.Get() if err != nil { return Clipboard{}, err } if noClip { return Clipboard{}, errors.New("clipboard is off") } - overridePaste, err := inputs.EnvClipPaste.Get() + overridePaste, err := config.EnvClipPaste.Get() if err != nil { return Clipboard{}, err } - overrideCopy, err := inputs.EnvClipCopy.Get() + overrideCopy, err := config.EnvClipCopy.Get() if err != nil { return Clipboard{}, err } if overrideCopy != nil && overridePaste != nil { return newClipboard(overrideCopy, overridePaste) } - isOSC, err := inputs.EnvClipOSC52.Get() + isOSC, err := config.EnvClipOSC52.Get() if err != nil { return Clipboard{}, err } @@ -57,7 +57,7 @@ func NewClipboard() (Clipboard, error) { c := Clipboard{isOSC52: true} return c, nil } - sys, err := inputs.NewPlatform() + sys, err := config.NewPlatform() if err != nil { return Clipboard{}, err } @@ -65,16 +65,16 @@ func NewClipboard() (Clipboard, error) { var copying []string var pasting []string switch sys { - case inputs.MacOSPlatform: + case config.MacOSPlatform: copying = []string{"pbcopy"} pasting = []string{"pbpaste"} - case inputs.LinuxXPlatform: + case config.LinuxXPlatform: copying = []string{"xclip"} pasting = []string{"xclip", "-o"} - case inputs.LinuxWaylandPlatform: + case config.LinuxWaylandPlatform: copying = []string{"wl-copy"} pasting = []string{"wl-paste"} - case inputs.WindowsLinuxPlatform: + case config.WindowsLinuxPlatform: copying = []string{"clip.exe"} pasting = []string{"powershell.exe", "-command", "Get-Clipboard"} default: diff --git a/internal/platform/clipboard_test.go b/internal/platform/clipboard_test.go @@ -5,7 +5,7 @@ import ( "os" "testing" - "github.com/enckse/lockbox/internal/inputs" + "github.com/enckse/lockbox/internal/config" "github.com/enckse/lockbox/internal/platform" ) @@ -22,7 +22,7 @@ func TestNoClipboard(t *testing.T) { func TestMaxTime(t *testing.T) { os.Setenv("LOCKBOX_NOCLIP", "no") os.Setenv("LOCKBOX_CLIP_OSC52", "no") - os.Setenv("LOCKBOX_PLATFORM", string(inputs.LinuxWaylandPlatform)) + os.Setenv("LOCKBOX_PLATFORM", string(config.LinuxWaylandPlatform)) os.Setenv("LOCKBOX_CLIP_MAX", "") c, err := platform.NewClipboard() if err != nil { @@ -55,7 +55,7 @@ func TestClipboardInstances(t *testing.T) { os.Setenv("LOCKBOX_NOCLIP", "no") os.Setenv("LOCKBOX_CLIP_MAX", "") os.Setenv("LOCKBOX_CLIP_OSC52", "no") - for _, item := range inputs.Platforms { + for _, item := range config.Platforms { os.Setenv("LOCKBOX_PLATFORM", item) _, err := platform.NewClipboard() if err != nil { @@ -80,7 +80,7 @@ func TestOSC52(t *testing.T) { func TestArgsOverride(t *testing.T) { os.Setenv("LOCKBOX_CLIP_PASTE", "abc xyz 111") os.Setenv("LOCKBOX_CLIP_OSC52", "no") - os.Setenv("LOCKBOX_PLATFORM", string(inputs.WindowsLinuxPlatform)) + os.Setenv("LOCKBOX_PLATFORM", string(config.WindowsLinuxPlatform)) c, _ := platform.NewClipboard() cmd, args, ok := c.Args(true) if cmd != "clip.exe" || len(args) != 0 || !ok { diff --git a/internal/platform/terminal.go b/internal/platform/terminal.go @@ -4,7 +4,7 @@ package platform import ( "errors" - "github.com/enckse/lockbox/internal/inputs" + "github.com/enckse/lockbox/internal/config" ) const ( @@ -35,13 +35,13 @@ func NewTerminal(color Color) (Terminal, error) { if color != Red { return Terminal{}, errors.New("bad color") } - interactive, err := inputs.EnvInteractive.Get() + interactive, err := config.EnvInteractive.Get() if err != nil { return Terminal{}, err } colors := interactive if colors { - isColored, err := inputs.EnvNoColor.Get() + isColored, err := config.EnvNoColor.Get() if err != nil { return Terminal{}, err }