lockbox

password manager
Log | Files | Refs | README | LICENSE

commit f02f8f7693010c0d20b19adf289a29c975540c1a
parent 531d4ddf150e494e0a42eb421b22214bdcf923fc
Author: Sean Enck <sean@ttypty.com>
Date:   Fri, 28 Jul 2023 19:17:12 -0400

support .env files as configs

Diffstat:
Mcmd/main.go | 24++++++++++++++++++++++++
Mgo.mod | 1+
Mgo.sum | 2++
Minternal/app/core_test.go | 2+-
Minternal/config/core.go | 27+++++++++++++++++++++++++++
Minternal/config/core_test.go | 19+++++++++++++++++++
Minternal/config/vars.go | 7+++++--
Minternal/config/vars_test.go | 2+-
Mtests/expected.log | 26++++++++++++++++++++++++++
Mtests/run.sh | 30+++++++++++++++++++++++++++---
10 files changed, 133 insertions(+), 7 deletions(-)

diff --git a/cmd/main.go b/cmd/main.go @@ -2,6 +2,7 @@ package main import ( + "bytes" "errors" "fmt" "os" @@ -12,6 +13,7 @@ import ( "github.com/enckse/lockbox/internal/app" "github.com/enckse/lockbox/internal/config" "github.com/enckse/lockbox/internal/platform" + env "github.com/hashicorp/go-envparse" ) var version string @@ -41,6 +43,28 @@ func handleEarly(command string, args []string) (bool, error) { } func run() error { + paths, err := config.NewEnvFiles() + if err != nil { + return err + } + for _, useEnv := range paths { + if !platform.PathExists(useEnv) { + continue + } + b, err := os.ReadFile(useEnv) + if err != nil { + return err + } + r := bytes.NewReader(b) + found, err := env.Parse(r) + if err != nil { + return err + } + for k, v := range found { + os.Setenv(k, v) + } + break + } args := os.Args if len(args) < 2 { return errors.New("requires subcommand") diff --git a/go.mod b/go.mod @@ -4,6 +4,7 @@ go 1.19 require ( github.com/aymanbagabas/go-osc52 v1.2.2 + github.com/hashicorp/go-envparse v0.1.0 github.com/pquerna/otp v1.4.0 github.com/tobischo/gokeepasslib/v3 v3.5.1 mvdan.cc/sh/v3 v3.7.0 diff --git a/go.sum b/go.sum @@ -11,6 +11,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/frankban/quicktest v1.14.5 h1:dfYrrRyLtiqT9GyKXgdh+k4inNeTvmGbuSgZ3lx3GhA= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/hashicorp/go-envparse v0.1.0 h1:bE++6bhIsNCPLvgDZkYqo3nA+/PFI51pkrHdmPSDFPY= +github.com/hashicorp/go-envparse v0.1.0/go.mod h1:OHheN1GoygLlAkTlXLXvAdnXdZxy8JUweQ1rAXx1xnc= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= diff --git a/internal/app/core_test.go b/internal/app/core_test.go @@ -13,7 +13,7 @@ func TestUsage(t *testing.T) { t.Errorf("invalid usage, out of date? %d", len(u)) } u, _ = app.Usage(true) - if len(u) != 95 { + if len(u) != 96 { t.Errorf("invalid verbose usage, out of date? %d", len(u)) } for _, usage := range u { diff --git a/internal/config/core.go b/internal/config/core.go @@ -6,6 +6,7 @@ import ( "fmt" "os" "os/exec" + "path/filepath" "strconv" "strings" @@ -17,6 +18,8 @@ const ( colorWindowSpan = ":" yes = "yes" no = "no" + detectEnvironment = "detect" + envFile = "lockbox.env" // MacOSPlatform is the macos indicator for platform MacOSPlatform = "macos" // LinuxWaylandPlatform for linux+wayland @@ -28,6 +31,8 @@ const ( unknownPlatform = "" ) +var detectEnvironmentPaths = []string{filepath.Join(".config", envFile), filepath.Join(".config", "lockbox", envFile)} + type ( // JSONOutputMode is the output mode definition JSONOutputMode string @@ -269,3 +274,25 @@ func ParseColorWindow(windowString string) ([]ColorWindow, error) { } return rules, nil } + +// NewEnvFiles will get the list of candidate environment files +// it will also set the environment to empty for the caller +func NewEnvFiles() ([]string, error) { + v := EnvConfig.Get() + if v == "" { + return []string{}, nil + } + EnvConfig.Set("") + if v != detectEnvironment { + return []string{v}, nil + } + h, err := os.UserHomeDir() + if err != nil { + return nil, err + } + var results []string + for _, p := range detectEnvironmentPaths { + results = append(results, filepath.Join(h, p)) + } + return results, nil +} diff --git a/internal/config/core_test.go b/internal/config/core_test.go @@ -82,3 +82,22 @@ func TestParseWindows(t *testing.T) { t.Errorf("invalid error: %v", err) } } + +func TestNewEnvFiles(t *testing.T) { + os.Setenv("LOCKBOX_ENV", "") + os.Setenv("HOME", "test") + f, err := config.NewEnvFiles() + if len(f) != 0 || err != nil { + t.Errorf("invalid files: %v %v", f, err) + } + os.Setenv("LOCKBOX_ENV", "test") + f, err = config.NewEnvFiles() + if len(f) != 1 || f[0] != "test" || err != nil { + t.Errorf("invalid files: %v %v", f, err) + } + os.Setenv("LOCKBOX_ENV", "detect") + f, err = config.NewEnvFiles() + if len(f) != 2 || err != nil { + t.Errorf("invalid files: %v %v", f, err) + } +} diff --git a/internal/config/vars.go b/internal/config/vars.go @@ -32,6 +32,7 @@ const ( ) var ( + fileExample = []string{"file"} // Platforms represent the platforms that lockbox understands to run on Platforms = []string{MacOSPlatform, WindowsLinuxPlatform, LinuxXPlatform, LinuxWaylandPlatform} // TOTPDefaultColorWindow is the default coloring rules for totp @@ -61,7 +62,7 @@ var ( // 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"}} + EnvStore = EnvironmentString{environmentBase: environmentBase{key: prefixKey + "STORE", desc: "directory to the database file", requirement: "must be set"}, canDefault: false, allowed: fileExample} // 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 @@ -80,6 +81,8 @@ var ( 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} + // EnvConfig is the location of the config file to read environment variables from + EnvConfig = EnvironmentString{environmentBase: environmentBase{key: prefixKey + "ENV", desc: fmt.Sprintf("allows setting a specific file of environment variables\nfor lockbox to read and use as configuration values (an '.env' file)\nthe keyword '%s' will search for a file in the following paths,\nmatching the first:\n(%v)", detectEnvironment, detectEnvironmentPaths)}, canDefault: false, allowed: fileExample} ) // GetReKey will get the rekey environment settings @@ -151,7 +154,7 @@ func GetKey() ([]byte, error) { 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} { + 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, EnvConfig} { env := item.self() value, allow := item.values() if out.showValues { diff --git a/internal/config/vars_test.go b/internal/config/vars_test.go @@ -112,7 +112,7 @@ func TestListVariables(t *testing.T) { known[trim] = struct{}{} } l := len(known) - if l != 22 { + if l != 23 { t.Errorf("invalid env count, outdated? %d", l) } } diff --git a/tests/expected.log b/tests/expected.log @@ -163,3 +163,29 @@ test2 } clipboard will clear in 5 seconds Wrong password? HMAC-SHA256 of header mismatching +no store set +keys/k/one2 +proceed with rekey? (y/N) rekeying: keys/k/one2 +Wrong password? HMAC-SHA256 of header mismatching +exit status 1 + +keys/k/one2 +test2 +{ + "keys/k/one2": { + "modtime": "XXXX-XX-XX", + "data": "6d2" + } +} +{ + "keys/k/one2": { + "modtime": "XXXX-XX-XX", + "data": "6d2" + } +} +{ + "keys/k/one2": { + "modtime": "XXXX-XX-XX", + "data": "6d2" + } +} diff --git a/tests/run.sh b/tests/run.sh @@ -1,6 +1,7 @@ #!/usr/bin/env bash LB_BINARY=../bin/lb DATA="bin/$1" +ENV="$DATA/env" CLIP_WAIT=1 CLIP_TRIES=3 CLIP_COPY="$DATA/clip.copy" @@ -81,13 +82,32 @@ _execute() { echo ${LB_BINARY} ls echo - _rekey + _rekey 1 _clipboard _invalid + _config + _rekey 2 +} + +_unset() { + local i + for i in $(env | grep '^LOCKBOX' | cut -d "=" -f 1); do + unset "$i" + done +} + +_config() { + env | grep '^LOCKBOX' > "$ENV" + _unset + ${LB_BINARY} ls + export LOCKBOX_ENV="$ENV" + ${LB_BINARY} ls } _invalid() { - local keyfile + local keyfile oldkey oldkeyfile + oldkey="$LOCKBOX_KEY" + oldkeyfile="$LOCKBOX_KEYFILE" if [ -n "$LOCKBOX_KEYFILE" ]; then export LOCKBOX_KEYFILE="" if [ -z "$LOCKBOX_KEY" ]; then @@ -99,6 +119,8 @@ _invalid() { export LOCKBOX_KEYFILE="$keyfile" fi ${LB_BINARY} ls + export LOCKBOX_KEYFILE="$oldkeyfile" + export LOCKBOX_KEY="$oldkey" } _rekey() { @@ -110,7 +132,7 @@ _rekey() { rekeyFile="$DATA/newkeyfile" echo "thisisanewkey" > "$rekeyFile" fi - echo y |${LB_BINARY} rekey -store="$rekey" -key="newkey" -keymode="plaintext" -keyfile="$rekeyFile" + echo y |${LB_BINARY} rekey -store="$rekey" -key="newkey$1" -keymode="plaintext" -keyfile="$rekeyFile" echo ${LB_BINARY} ls ${LB_BINARY} show keys/k/one2 @@ -166,6 +188,8 @@ if [ -z "$1" ]; then exit 1 fi +_unset +unset LOCKBOX_ENV mkdir -p "$DATA" find "$DATA" -type f -delete