commit d41d026e5afdbb0c0b48c86608c40ce55b228583
parent f9a6bcadefecd6f0376a3e689407b207d51b141a
Author: Sean Enck <sean@ttypty.com>
Date: Thu, 27 Jul 2023 19:12:08 -0400
renaming inputs -> config
Diffstat:
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
}