commit 437f861fa25cbf1b389bfdfc4f48c6b6f277115d
parent 0c8a8a7be66adccfe1955eb9086d570ebf1b1762
Author: Sean Enck <sean@ttypty.com>
Date: Tue, 25 Jul 2023 20:13:50 -0400
split env to impl and definition
Diffstat:
4 files changed, 470 insertions(+), 442 deletions(-)
diff --git a/internal/inputs/env.go b/internal/inputs/env.go
@@ -2,52 +2,17 @@
package inputs
import (
- "errors"
- "flag"
"fmt"
"os"
- "os/exec"
- "sort"
"strconv"
"strings"
- "time"
"mvdan.cc/sh/v3/shell"
)
const (
- yes = "yes"
- no = "no"
- prefixKey = "LOCKBOX_"
- noClipEnv = prefixKey + "NOCLIP"
- noColorEnv = prefixKey + "NOCOLOR"
- interactiveEnv = prefixKey + "INTERACTIVE"
- readOnlyEnv = prefixKey + "READONLY"
- fieldTOTPEnv = prefixKey + "TOTP"
- clipBaseEnv = prefixKey + "CLIP_"
- formatTOTPEnv = fieldTOTPEnv + "_FORMAT"
- keyModeEnv = prefixKey + "KEYMODE"
- keyEnv = prefixKey + "KEY"
- // KeyFileEnv is an OPTIONAL keyfile for the database
- KeyFileEnv = prefixKey + "KEYFILE"
- plainKeyMode = "plaintext"
- commandKeyMode = "command"
- // PlatformEnv is the platform lb is running on.
- PlatformEnv = prefixKey + "PLATFORM"
- // StoreEnv is the location of the filesystem store that lb is operating on.
- StoreEnv = prefixKey + "STORE"
- clipMaxEnv = clipBaseEnv + "MAX"
- // ColorBetweenEnv is a comma-delimited list of times to color totp outputs (e.g. 0:5,30:35 which is the default).
- ColorBetweenEnv = fieldTOTPEnv + "_BETWEEN"
- // MaxTOTPTime indicate how long TOTP tokens will be shown
- MaxTOTPTime = fieldTOTPEnv + "_MAX"
- // ClipPasteEnv allows overriding the clipboard paste command
- ClipPasteEnv = clipBaseEnv + "PASTE"
- // ClipCopyEnv allows overriding the clipboard copy command
- ClipCopyEnv = clipBaseEnv + "COPY"
- clipOSC52Env = clipBaseEnv + "OSC52"
- defaultTOTPField = "totp"
- commandArgsExample = "[cmd args...]"
+ yes = "yes"
+ no = "no"
// MacOSPlatform is the macos indicator for platform
MacOSPlatform = "macos"
// LinuxWaylandPlatform for linux+wayland
@@ -56,204 +21,20 @@ const (
LinuxXPlatform = "linux-x"
// WindowsLinuxPlatform for WSL subsystems
WindowsLinuxPlatform = "wsl"
- defaultMaxClipboard = 45
- detectedValue = "(detected)"
- noTOTPEnv = prefixKey + "NOTOTP"
- // HookDirEnv represents a stored location for user hooks
- HookDirEnv = prefixKey + "HOOKDIR"
- // ModTimeEnv is modtime override ability for entries
- ModTimeEnv = prefixKey + "SET_MODTIME"
- // ModTimeFormat is the expected modtime format
- ModTimeFormat = time.RFC3339
- // MaxTOTPTimeDefault is the max TOTP time to run (default)
- MaxTOTPTimeDefault = "120"
- // JSONDataOutputEnv controls how JSON is output
- JSONDataOutputEnv = prefixKey + "JSON_DATA_OUTPUT"
- defaultHashLength = 0
- hashJSONLengthEnv = JSONDataOutputEnv + "_HASH_LENGTH"
)
var isYesNoArgs = []string{yes, no}
type (
- environmentOutput struct {
- showValues bool
- }
// SystemPlatform represents the platform lockbox is running on.
SystemPlatform string
)
-// 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
- }
- mapped := map[string]string{
- keyModeEnv: *keyMode,
- keyEnv: *key,
- KeyFileEnv: *keyFile,
- StoreEnv: *store,
- }
- hasStore := false
- hasKey := false
- hasKeyFile := false
- var out []string
- for k, val := range mapped {
- if val != "" {
- switch k {
- case StoreEnv:
- hasStore = true
- case keyEnv:
- hasKey = true
- case KeyFileEnv:
- hasKeyFile = true
- }
- }
- out = append(out, fmt.Sprintf("%s=%s", k, val))
- }
- sort.Strings(out)
- if !hasStore || (!hasKey && !hasKeyFile) {
- return nil, fmt.Errorf("missing required arguments for rekey: %s", strings.Join(out, " "))
- }
- return out, nil
-}
-
-// GetClipboardMax will get max time to keep an entry in the clipboard before clearing
-func GetClipboardMax() (int, error) {
- return getPositiveIntEnv(defaultMaxClipboard, clipMaxEnv, "clipboard max time", false)
-}
-
-// GetHashLength will get the maximum hash length allowed in JSON output hashing mode
-func GetHashLength() (int, error) {
- return getPositiveIntEnv(defaultHashLength, hashJSONLengthEnv, "hash length", true)
-}
-
-func getPositiveIntEnv(defaultVal int, key, desc string, canBeZero bool) (int, error) {
- val := defaultVal
- use := os.Getenv(key)
- if use != "" {
- i, err := strconv.Atoi(use)
- if err != nil {
- return -1, err
- }
- invalid := false
- check := ""
- if canBeZero {
- check = "="
- }
- switch i {
- case 0:
- invalid = !canBeZero
- default:
- invalid = i < 0
- }
- if invalid {
- return -1, fmt.Errorf("%s must be >%s 0", desc, check)
- }
- val = i
- }
- return val, nil
-}
-
-// GetKey will get the encryption key setup for lb
-func GetKey() ([]byte, error) {
- useKeyMode := os.Getenv(keyModeEnv)
- if useKeyMode == "" {
- useKeyMode = commandKeyMode
- }
- useKey := os.Getenv(keyEnv)
- if useKey == "" {
- return nil, errors.New("no key given")
- }
- b, err := getKey(useKeyMode, useKey)
- if err != nil {
- return nil, err
- }
- if len(b) == 0 {
- return nil, errors.New("key is empty")
- }
- return b, nil
-}
-
// Shlex will do simple shell command lex-ing
func Shlex(in string) ([]string, error) {
return shell.Fields(in, os.Getenv)
}
-func getKey(keyMode, name string) ([]byte, error) {
- var data []byte
- switch keyMode {
- case commandKeyMode:
- parts, err := Shlex(name)
- 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(name)
- default:
- return nil, errors.New("unknown keymode")
- }
- return []byte(strings.TrimSpace(string(data))), nil
-}
-
-// IsClipOSC52 indicates if OSC52 mode is enabled
-func IsClipOSC52() (bool, error) {
- return isYesNoEnv(false, clipOSC52Env)
-}
-
-// IsNoTOTP indicates if TOTP is disabled
-func IsNoTOTP() (bool, error) {
- return isYesNoEnv(false, noTOTPEnv)
-}
-
-// IsReadOnly indicates to operate in readonly, no writing to file allowed
-func IsReadOnly() (bool, error) {
- return isYesNoEnv(false, readOnlyEnv)
-}
-
-// IsNoClipEnabled indicates if clipboard mode is enabled.
-func IsNoClipEnabled() (bool, error) {
- return isYesNoEnv(false, noClipEnv)
-}
-
-// IsNoColorEnabled indicates if the flag is set to disable color.
-func IsNoColorEnabled() (bool, error) {
- return isYesNoEnv(false, noColorEnv)
-}
-
-// IsInteractive indicates if running as a user UI experience.
-func IsInteractive() (bool, error) {
- return isYesNoEnv(true, interactiveEnv)
-}
-
-// TOTPToken gets the name of the totp special case tokens
-func TOTPToken() string {
- return EnvironOrDefault(fieldTOTPEnv, defaultTOTPField)
-}
-
-func (o environmentOutput) formatEnvironmentVariable(required bool, name, val, desc string, allowed []string) string {
- value := val
- if o.showValues {
- value = os.Getenv(name)
- }
- if len(value) == 0 {
- value = "(unset)"
- }
- description := strings.ReplaceAll(desc, "\n", "\n ")
- return fmt.Sprintf("\n%s\n %s\n\n required: %t\n value: %s\n options: %s\n", name, description, required, value, strings.Join(allowed, "|"))
-}
-
// PlatformSet returns the list of possible platforms
func PlatformSet() []string {
return []string{
@@ -264,35 +45,6 @@ func PlatformSet() []string {
}
}
-// ListEnvironmentVariables will print information about env variables and potential/set values
-func ListEnvironmentVariables(showValues bool) []string {
- e := environmentOutput{showValues: showValues}
- var results []string
- results = append(results, e.formatEnvironmentVariable(true, StoreEnv, "", "directory to the database file", []string{"file"}))
- results = append(results, e.formatEnvironmentVariable(true, keyModeEnv, commandKeyMode, "how to retrieve the database store password", []string{commandKeyMode, plainKeyMode}))
- results = append(results, e.formatEnvironmentVariable(true, keyEnv, "", fmt.Sprintf("the database key ('%s' mode) or command to run ('%s' mode)\nto retrieve the database password", plainKeyMode, commandKeyMode), []string{commandArgsExample, "password"}))
- results = append(results, e.formatEnvironmentVariable(false, noClipEnv, no, "disable clipboard operations", isYesNoArgs))
- results = append(results, e.formatEnvironmentVariable(false, noColorEnv, no, "disable terminal colors", isYesNoArgs))
- results = append(results, e.formatEnvironmentVariable(false, interactiveEnv, yes, "enable interactive mode", isYesNoArgs))
- results = append(results, e.formatEnvironmentVariable(false, readOnlyEnv, no, "operate in readonly mode", isYesNoArgs))
- results = append(results, e.formatEnvironmentVariable(false, fieldTOTPEnv, defaultTOTPField, "attribute name to store TOTP tokens within the database", []string{"string"}))
- results = append(results, e.formatEnvironmentVariable(false, formatTOTPEnv, strings.ReplaceAll(strings.ReplaceAll(FormatTOTP("%s"), "%25s", "%s"), "&", " \\\n &"), "override the otpauth url used to store totp tokens. It must have ONE format\nstring ('%s') to insert the totp base code", []string{"otpauth//url/%s/args..."}))
- results = append(results, e.formatEnvironmentVariable(false, MaxTOTPTime, MaxTOTPTimeDefault, "time, in seconds, in which to show a TOTP token before automatically exiting", []string{"integer"}))
- results = append(results, e.formatEnvironmentVariable(false, ColorBetweenEnv, TOTPDefaultBetween, "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)", []string{"start:end,start:end,start:end..."}))
- results = append(results, e.formatEnvironmentVariable(false, ClipPasteEnv, detectedValue, "override the detected platform paste command", []string{commandArgsExample}))
- results = append(results, e.formatEnvironmentVariable(false, ClipCopyEnv, detectedValue, "override the detected platform copy command", []string{commandArgsExample}))
- results = append(results, e.formatEnvironmentVariable(false, clipMaxEnv, fmt.Sprintf("%d", defaultMaxClipboard), "override the amount of time before totp clears the clipboard (e.g. 10),\nmust be an integer", []string{"integer"}))
- results = append(results, e.formatEnvironmentVariable(false, PlatformEnv, detectedValue, "override the detected platform", PlatformSet()))
- results = append(results, e.formatEnvironmentVariable(false, noTOTPEnv, no, "disable TOTP integrations", isYesNoArgs))
- results = append(results, e.formatEnvironmentVariable(false, HookDirEnv, "", "the path to hooks to execute on actions against the database", []string{"directory"}))
- results = append(results, e.formatEnvironmentVariable(false, clipOSC52Env, no, "enable OSC52 clipboard mode", isYesNoArgs))
- results = append(results, e.formatEnvironmentVariable(false, KeyFileEnv, "", "additional keyfile to access/protect the database", []string{"keyfile"}))
- results = append(results, e.formatEnvironmentVariable(false, ModTimeEnv, ModTimeFormat, fmt.Sprintf("input modification time to set for the entry\n(expected format: %s)", ModTimeFormat), []string{"modtime"}))
- results = append(results, e.formatEnvironmentVariable(false, JSONDataOutputEnv, string(JSONDataOutputHash), fmt.Sprintf("changes what the data field in JSON outputs will contain\nuse '%s' with CAUTION", JSONDataOutputRaw), []string{string(JSONDataOutputRaw), string(JSONDataOutputHash), string(JSONDataOutputBlank)}))
- results = append(results, e.formatEnvironmentVariable(false, hashJSONLengthEnv, fmt.Sprintf("%d", defaultHashLength), fmt.Sprintf("maximum hash length the JSON output should contain\nwhen '%s' mode is set for JSON output", JSONDataOutputHash), []string{"integer"}))
- return results
-}
-
// EnvironOrDefault will get the environment value OR default if env is not set.
func EnvironOrDefault(envKey, defaultValue string) string {
val := os.Getenv(envKey)
@@ -315,3 +67,30 @@ func isYesNoEnv(defaultValue bool, envKey string) (bool, error) {
return false, fmt.Errorf("invalid yes/no env value for %s", envKey)
}
+
+func getPositiveIntEnv(defaultVal int, key, desc string, canBeZero bool) (int, error) {
+ val := defaultVal
+ use := os.Getenv(key)
+ if use != "" {
+ i, err := strconv.Atoi(use)
+ if err != nil {
+ return -1, err
+ }
+ invalid := false
+ check := ""
+ if canBeZero {
+ check = "="
+ }
+ switch i {
+ case 0:
+ invalid = !canBeZero
+ default:
+ invalid = i < 0
+ }
+ if invalid {
+ return -1, fmt.Errorf("%s must be >%s 0", desc, check)
+ }
+ val = i
+ }
+ return val, nil
+}
diff --git a/internal/inputs/env_test.go b/internal/inputs/env_test.go
@@ -1,204 +1,12 @@
package inputs_test
import (
- "fmt"
"os"
- "strings"
"testing"
"github.com/enckse/lockbox/internal/inputs"
)
-func checkYesNo(key string, t *testing.T, cb func() (bool, error), onEmpty bool) {
- os.Setenv(key, "yes")
- c, err := cb()
- if err != nil {
- t.Errorf("invalid error: %v", err)
- }
- if !c {
- t.Error("invalid setting")
- }
- os.Setenv(key, "")
- c, err = cb()
- if err != nil {
- t.Errorf("invalid error: %v", err)
- }
- if c != onEmpty {
- t.Error("invalid setting")
- }
- os.Setenv(key, "no")
- c, err = cb()
- if err != nil {
- t.Errorf("invalid error: %v", err)
- }
- if c {
- t.Error("invalid setting")
- }
- os.Setenv(key, "afoieae")
- _, err = cb()
- 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.IsNoColorEnabled, false)
-}
-
-func TestInteractiveSetting(t *testing.T) {
- checkYesNo("LOCKBOX_INTERACTIVE", t, inputs.IsInteractive, true)
-}
-
-func TestIsReadOnly(t *testing.T) {
- checkYesNo("LOCKBOX_READONLY", t, inputs.IsReadOnly, false)
-}
-
-func TestIsOSC52(t *testing.T) {
- checkYesNo("LOCKBOX_CLIP_OSC52", t, inputs.IsClipOSC52, false)
-}
-
-func TestIsNoTOTP(t *testing.T) {
- checkYesNo("LOCKBOX_NOTOTP", t, inputs.IsNoTOTP, false)
-}
-
-func TestIsNoClip(t *testing.T) {
- checkYesNo("LOCKBOX_NOCLIP", t, inputs.IsNoClipEnabled, false)
-}
-
-func TestTOTP(t *testing.T) {
- os.Setenv("LOCKBOX_TOTP", "abc")
- if inputs.TOTPToken() != "abc" {
- t.Error("invalid totp token field")
- }
- os.Setenv("LOCKBOX_TOTP", "")
- if inputs.TOTPToken() != "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.Error() != "no key given" {
- 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 TestGetClipboardMax(t *testing.T) {
- os.Setenv("LOCKBOX_CLIP_MAX", "")
- defer os.Clearenv()
- max, err := inputs.GetClipboardMax()
- if err != nil || max != 45 {
- t.Error("invalid clipboard read")
- }
- os.Setenv("LOCKBOX_CLIP_MAX", "1")
- max, err = inputs.GetClipboardMax()
- if err != nil || max != 1 {
- t.Error("invalid clipboard read")
- }
- os.Setenv("LOCKBOX_CLIP_MAX", "-1")
- if _, err := inputs.GetClipboardMax(); err == nil || err.Error() != "clipboard max time must be > 0" {
- t.Errorf("invalid err: %v", err)
- }
- os.Setenv("LOCKBOX_CLIP_MAX", "alk;ja")
- if _, err := inputs.GetClipboardMax(); err == nil || err.Error() != "strconv.Atoi: parsing \"alk;ja\": invalid syntax" {
- t.Errorf("invalid err: %v", err)
- }
- os.Setenv("LOCKBOX_CLIP_MAX", "0")
- if _, err := inputs.GetClipboardMax(); err == nil || err.Error() != "clipboard max time must be > 0" {
- t.Errorf("invalid err: %v", err)
- }
-}
-
-func TestGetHashLength(t *testing.T) {
- os.Setenv("LOCKBOX_JSON_DATA_OUTPUT_HASH_LENGTH", "")
- defer os.Clearenv()
- val, err := inputs.GetHashLength()
- if err != nil || val != 0 {
- t.Error("invalid hash read")
- }
- os.Setenv("LOCKBOX_JSON_DATA_OUTPUT_HASH_LENGTH", "1")
- val, err = inputs.GetHashLength()
- if err != nil || val != 1 {
- t.Error("invalid hash read")
- }
- os.Setenv("LOCKBOX_JSON_DATA_OUTPUT_HASH_LENGTH", "0")
- val, err = inputs.GetHashLength()
- if err != nil || val != 0 {
- t.Error("invalid hash read")
- }
- os.Setenv("LOCKBOX_JSON_DATA_OUTPUT_HASH_LENGTH", "-1")
- if _, err := inputs.GetHashLength(); err == nil || err.Error() != "hash length must be >= 0" {
- t.Errorf("invalid err: %v", err)
- }
- os.Setenv("LOCKBOX_JSON_DATA_OUTPUT_HASH_LENGTH", "-aoaofaij;p1")
- if _, err := inputs.GetHashLength(); err == nil || err.Error() != "strconv.Atoi: parsing \"-aoaofaij;p1\": invalid syntax" {
- t.Errorf("invalid err: %v", err)
- }
-}
-
func TestEnvDefault(t *testing.T) {
os.Clearenv()
val := inputs.EnvironOrDefault("TEST", "value")
@@ -216,3 +24,9 @@ func TestEnvDefault(t *testing.T) {
t.Error("invalid read")
}
}
+
+func TestPlatformSet(t *testing.T) {
+ if len(inputs.PlatformSet()) != 4 {
+ t.Error("invalid platform set")
+ }
+}
diff --git a/internal/inputs/vars.go b/internal/inputs/vars.go
@@ -0,0 +1,235 @@
+// Package inputs handles user inputs/UI elements.
+package inputs
+
+import (
+ "errors"
+ "flag"
+ "fmt"
+ "os"
+ "os/exec"
+ "sort"
+ "strings"
+ "time"
+)
+
+const (
+ prefixKey = "LOCKBOX_"
+ noClipEnv = prefixKey + "NOCLIP"
+ noColorEnv = prefixKey + "NOCOLOR"
+ interactiveEnv = prefixKey + "INTERACTIVE"
+ readOnlyEnv = prefixKey + "READONLY"
+ fieldTOTPEnv = prefixKey + "TOTP"
+ clipBaseEnv = prefixKey + "CLIP_"
+ formatTOTPEnv = fieldTOTPEnv + "_FORMAT"
+ keyModeEnv = prefixKey + "KEYMODE"
+ keyEnv = prefixKey + "KEY"
+ // KeyFileEnv is an OPTIONAL keyfile for the database
+ KeyFileEnv = prefixKey + "KEYFILE"
+ plainKeyMode = "plaintext"
+ commandKeyMode = "command"
+ // PlatformEnv is the platform lb is running on.
+ PlatformEnv = prefixKey + "PLATFORM"
+ // StoreEnv is the location of the filesystem store that lb is operating on.
+ StoreEnv = prefixKey + "STORE"
+ clipMaxEnv = clipBaseEnv + "MAX"
+ // ColorBetweenEnv is a comma-delimited list of times to color totp outputs (e.g. 0:5,30:35 which is the default).
+ ColorBetweenEnv = fieldTOTPEnv + "_BETWEEN"
+ // MaxTOTPTime indicate how long TOTP tokens will be shown
+ MaxTOTPTime = fieldTOTPEnv + "_MAX"
+ // ClipPasteEnv allows overriding the clipboard paste command
+ ClipPasteEnv = clipBaseEnv + "PASTE"
+ // ClipCopyEnv allows overriding the clipboard copy command
+ ClipCopyEnv = clipBaseEnv + "COPY"
+ clipOSC52Env = clipBaseEnv + "OSC52"
+ defaultTOTPField = "totp"
+ commandArgsExample = "[cmd args...]"
+ defaultMaxClipboard = 45
+ detectedValue = "(detected)"
+ noTOTPEnv = prefixKey + "NOTOTP"
+ // HookDirEnv represents a stored location for user hooks
+ HookDirEnv = prefixKey + "HOOKDIR"
+ // ModTimeEnv is modtime override ability for entries
+ ModTimeEnv = prefixKey + "SET_MODTIME"
+ // ModTimeFormat is the expected modtime format
+ ModTimeFormat = time.RFC3339
+ // MaxTOTPTimeDefault is the max TOTP time to run (default)
+ MaxTOTPTimeDefault = "120"
+ // JSONDataOutputEnv controls how JSON is output
+ JSONDataOutputEnv = prefixKey + "JSON_DATA_OUTPUT"
+ defaultHashLength = 0
+ hashJSONLengthEnv = JSONDataOutputEnv + "_HASH_LENGTH"
+)
+
+type (
+ environmentOutput struct {
+ showValues bool
+ }
+)
+
+// 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
+ }
+ mapped := map[string]string{
+ keyModeEnv: *keyMode,
+ keyEnv: *key,
+ KeyFileEnv: *keyFile,
+ StoreEnv: *store,
+ }
+ hasStore := false
+ hasKey := false
+ hasKeyFile := false
+ var out []string
+ for k, val := range mapped {
+ if val != "" {
+ switch k {
+ case StoreEnv:
+ hasStore = true
+ case keyEnv:
+ hasKey = true
+ case KeyFileEnv:
+ hasKeyFile = true
+ }
+ }
+ out = append(out, fmt.Sprintf("%s=%s", k, val))
+ }
+ sort.Strings(out)
+ if !hasStore || (!hasKey && !hasKeyFile) {
+ return nil, fmt.Errorf("missing required arguments for rekey: %s", strings.Join(out, " "))
+ }
+ return out, nil
+}
+
+// GetClipboardMax will get max time to keep an entry in the clipboard before clearing
+func GetClipboardMax() (int, error) {
+ return getPositiveIntEnv(defaultMaxClipboard, clipMaxEnv, "clipboard max time", false)
+}
+
+// GetHashLength will get the maximum hash length allowed in JSON output hashing mode
+func GetHashLength() (int, error) {
+ return getPositiveIntEnv(defaultHashLength, hashJSONLengthEnv, "hash length", true)
+}
+
+// GetKey will get the encryption key setup for lb
+func GetKey() ([]byte, error) {
+ useKeyMode := os.Getenv(keyModeEnv)
+ if useKeyMode == "" {
+ useKeyMode = commandKeyMode
+ }
+ useKey := os.Getenv(keyEnv)
+ if useKey == "" {
+ return nil, errors.New("no key given")
+ }
+ b, err := getKey(useKeyMode, useKey)
+ if err != nil {
+ return nil, err
+ }
+ if len(b) == 0 {
+ return nil, errors.New("key is empty")
+ }
+ return b, nil
+}
+
+func getKey(keyMode, name string) ([]byte, error) {
+ var data []byte
+ switch keyMode {
+ case commandKeyMode:
+ parts, err := Shlex(name)
+ 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(name)
+ default:
+ return nil, errors.New("unknown keymode")
+ }
+ return []byte(strings.TrimSpace(string(data))), nil
+}
+
+// IsClipOSC52 indicates if OSC52 mode is enabled
+func IsClipOSC52() (bool, error) {
+ return isYesNoEnv(false, clipOSC52Env)
+}
+
+// IsNoTOTP indicates if TOTP is disabled
+func IsNoTOTP() (bool, error) {
+ return isYesNoEnv(false, noTOTPEnv)
+}
+
+// IsReadOnly indicates to operate in readonly, no writing to file allowed
+func IsReadOnly() (bool, error) {
+ return isYesNoEnv(false, readOnlyEnv)
+}
+
+// IsNoClipEnabled indicates if clipboard mode is enabled.
+func IsNoClipEnabled() (bool, error) {
+ return isYesNoEnv(false, noClipEnv)
+}
+
+// IsNoColorEnabled indicates if the flag is set to disable color.
+func IsNoColorEnabled() (bool, error) {
+ return isYesNoEnv(false, noColorEnv)
+}
+
+// IsInteractive indicates if running as a user UI experience.
+func IsInteractive() (bool, error) {
+ return isYesNoEnv(true, interactiveEnv)
+}
+
+// TOTPToken gets the name of the totp special case tokens
+func TOTPToken() string {
+ return EnvironOrDefault(fieldTOTPEnv, defaultTOTPField)
+}
+
+func (o environmentOutput) formatEnvironmentVariable(required bool, name, val, desc string, allowed []string) string {
+ value := val
+ if o.showValues {
+ value = os.Getenv(name)
+ }
+ if len(value) == 0 {
+ value = "(unset)"
+ }
+ description := strings.ReplaceAll(desc, "\n", "\n ")
+ return fmt.Sprintf("\n%s\n %s\n\n required: %t\n value: %s\n options: %s\n", name, description, required, value, strings.Join(allowed, "|"))
+}
+
+// ListEnvironmentVariables will print information about env variables and potential/set values
+func ListEnvironmentVariables(showValues bool) []string {
+ e := environmentOutput{showValues: showValues}
+ var results []string
+ results = append(results, e.formatEnvironmentVariable(true, StoreEnv, "", "directory to the database file", []string{"file"}))
+ results = append(results, e.formatEnvironmentVariable(true, keyModeEnv, commandKeyMode, "how to retrieve the database store password", []string{commandKeyMode, plainKeyMode}))
+ results = append(results, e.formatEnvironmentVariable(true, keyEnv, "", fmt.Sprintf("the database key ('%s' mode) or command to run ('%s' mode)\nto retrieve the database password", plainKeyMode, commandKeyMode), []string{commandArgsExample, "password"}))
+ results = append(results, e.formatEnvironmentVariable(false, noClipEnv, no, "disable clipboard operations", isYesNoArgs))
+ results = append(results, e.formatEnvironmentVariable(false, noColorEnv, no, "disable terminal colors", isYesNoArgs))
+ results = append(results, e.formatEnvironmentVariable(false, interactiveEnv, yes, "enable interactive mode", isYesNoArgs))
+ results = append(results, e.formatEnvironmentVariable(false, readOnlyEnv, no, "operate in readonly mode", isYesNoArgs))
+ results = append(results, e.formatEnvironmentVariable(false, fieldTOTPEnv, defaultTOTPField, "attribute name to store TOTP tokens within the database", []string{"string"}))
+ results = append(results, e.formatEnvironmentVariable(false, formatTOTPEnv, strings.ReplaceAll(strings.ReplaceAll(FormatTOTP("%s"), "%25s", "%s"), "&", " \\\n &"), "override the otpauth url used to store totp tokens. It must have ONE format\nstring ('%s') to insert the totp base code", []string{"otpauth//url/%s/args..."}))
+ results = append(results, e.formatEnvironmentVariable(false, MaxTOTPTime, MaxTOTPTimeDefault, "time, in seconds, in which to show a TOTP token before automatically exiting", []string{"integer"}))
+ results = append(results, e.formatEnvironmentVariable(false, ColorBetweenEnv, TOTPDefaultBetween, "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)", []string{"start:end,start:end,start:end..."}))
+ results = append(results, e.formatEnvironmentVariable(false, ClipPasteEnv, detectedValue, "override the detected platform paste command", []string{commandArgsExample}))
+ results = append(results, e.formatEnvironmentVariable(false, ClipCopyEnv, detectedValue, "override the detected platform copy command", []string{commandArgsExample}))
+ results = append(results, e.formatEnvironmentVariable(false, clipMaxEnv, fmt.Sprintf("%d", defaultMaxClipboard), "override the amount of time before totp clears the clipboard (e.g. 10),\nmust be an integer", []string{"integer"}))
+ results = append(results, e.formatEnvironmentVariable(false, PlatformEnv, detectedValue, "override the detected platform", PlatformSet()))
+ results = append(results, e.formatEnvironmentVariable(false, noTOTPEnv, no, "disable TOTP integrations", isYesNoArgs))
+ results = append(results, e.formatEnvironmentVariable(false, HookDirEnv, "", "the path to hooks to execute on actions against the database", []string{"directory"}))
+ results = append(results, e.formatEnvironmentVariable(false, clipOSC52Env, no, "enable OSC52 clipboard mode", isYesNoArgs))
+ results = append(results, e.formatEnvironmentVariable(false, KeyFileEnv, "", "additional keyfile to access/protect the database", []string{"keyfile"}))
+ results = append(results, e.formatEnvironmentVariable(false, ModTimeEnv, ModTimeFormat, fmt.Sprintf("input modification time to set for the entry\n(expected format: %s)", ModTimeFormat), []string{"modtime"}))
+ results = append(results, e.formatEnvironmentVariable(false, JSONDataOutputEnv, string(JSONDataOutputHash), fmt.Sprintf("changes what the data field in JSON outputs will contain\nuse '%s' with CAUTION", JSONDataOutputRaw), []string{string(JSONDataOutputRaw), string(JSONDataOutputHash), string(JSONDataOutputBlank)}))
+ results = append(results, e.formatEnvironmentVariable(false, hashJSONLengthEnv, fmt.Sprintf("%d", defaultHashLength), fmt.Sprintf("maximum hash length the JSON output should contain\nwhen '%s' mode is set for JSON output", JSONDataOutputHash), []string{"integer"}))
+ return results
+}
diff --git a/internal/inputs/vars_test.go b/internal/inputs/vars_test.go
@@ -0,0 +1,200 @@
+package inputs_test
+
+import (
+ "fmt"
+ "os"
+ "strings"
+ "testing"
+
+ "github.com/enckse/lockbox/internal/inputs"
+)
+
+func checkYesNo(key string, t *testing.T, cb func() (bool, error), onEmpty bool) {
+ os.Setenv(key, "yes")
+ c, err := cb()
+ if err != nil {
+ t.Errorf("invalid error: %v", err)
+ }
+ if !c {
+ t.Error("invalid setting")
+ }
+ os.Setenv(key, "")
+ c, err = cb()
+ if err != nil {
+ t.Errorf("invalid error: %v", err)
+ }
+ if c != onEmpty {
+ t.Error("invalid setting")
+ }
+ os.Setenv(key, "no")
+ c, err = cb()
+ if err != nil {
+ t.Errorf("invalid error: %v", err)
+ }
+ if c {
+ t.Error("invalid setting")
+ }
+ os.Setenv(key, "afoieae")
+ _, err = cb()
+ 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.IsNoColorEnabled, false)
+}
+
+func TestInteractiveSetting(t *testing.T) {
+ checkYesNo("LOCKBOX_INTERACTIVE", t, inputs.IsInteractive, true)
+}
+
+func TestIsReadOnly(t *testing.T) {
+ checkYesNo("LOCKBOX_READONLY", t, inputs.IsReadOnly, false)
+}
+
+func TestIsOSC52(t *testing.T) {
+ checkYesNo("LOCKBOX_CLIP_OSC52", t, inputs.IsClipOSC52, false)
+}
+
+func TestIsNoTOTP(t *testing.T) {
+ checkYesNo("LOCKBOX_NOTOTP", t, inputs.IsNoTOTP, false)
+}
+
+func TestIsNoClip(t *testing.T) {
+ checkYesNo("LOCKBOX_NOCLIP", t, inputs.IsNoClipEnabled, false)
+}
+
+func TestTOTP(t *testing.T) {
+ os.Setenv("LOCKBOX_TOTP", "abc")
+ if inputs.TOTPToken() != "abc" {
+ t.Error("invalid totp token field")
+ }
+ os.Setenv("LOCKBOX_TOTP", "")
+ if inputs.TOTPToken() != "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.Error() != "no key given" {
+ 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 TestGetClipboardMax(t *testing.T) {
+ os.Setenv("LOCKBOX_CLIP_MAX", "")
+ defer os.Clearenv()
+ max, err := inputs.GetClipboardMax()
+ if err != nil || max != 45 {
+ t.Error("invalid clipboard read")
+ }
+ os.Setenv("LOCKBOX_CLIP_MAX", "1")
+ max, err = inputs.GetClipboardMax()
+ if err != nil || max != 1 {
+ t.Error("invalid clipboard read")
+ }
+ os.Setenv("LOCKBOX_CLIP_MAX", "-1")
+ if _, err := inputs.GetClipboardMax(); err == nil || err.Error() != "clipboard max time must be > 0" {
+ t.Errorf("invalid err: %v", err)
+ }
+ os.Setenv("LOCKBOX_CLIP_MAX", "alk;ja")
+ if _, err := inputs.GetClipboardMax(); err == nil || err.Error() != "strconv.Atoi: parsing \"alk;ja\": invalid syntax" {
+ t.Errorf("invalid err: %v", err)
+ }
+ os.Setenv("LOCKBOX_CLIP_MAX", "0")
+ if _, err := inputs.GetClipboardMax(); err == nil || err.Error() != "clipboard max time must be > 0" {
+ t.Errorf("invalid err: %v", err)
+ }
+}
+
+func TestGetHashLength(t *testing.T) {
+ os.Setenv("LOCKBOX_JSON_DATA_OUTPUT_HASH_LENGTH", "")
+ defer os.Clearenv()
+ val, err := inputs.GetHashLength()
+ if err != nil || val != 0 {
+ t.Error("invalid hash read")
+ }
+ os.Setenv("LOCKBOX_JSON_DATA_OUTPUT_HASH_LENGTH", "1")
+ val, err = inputs.GetHashLength()
+ if err != nil || val != 1 {
+ t.Error("invalid hash read")
+ }
+ os.Setenv("LOCKBOX_JSON_DATA_OUTPUT_HASH_LENGTH", "0")
+ val, err = inputs.GetHashLength()
+ if err != nil || val != 0 {
+ t.Error("invalid hash read")
+ }
+ os.Setenv("LOCKBOX_JSON_DATA_OUTPUT_HASH_LENGTH", "-1")
+ if _, err := inputs.GetHashLength(); err == nil || err.Error() != "hash length must be >= 0" {
+ t.Errorf("invalid err: %v", err)
+ }
+ os.Setenv("LOCKBOX_JSON_DATA_OUTPUT_HASH_LENGTH", "-aoaofaij;p1")
+ if _, err := inputs.GetHashLength(); err == nil || err.Error() != "strconv.Atoi: parsing \"-aoaofaij;p1\": invalid syntax" {
+ t.Errorf("invalid err: %v", err)
+ }
+}