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:
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