lockbox

password manager
Log | Files | Refs | README | LICENSE

commit fc0c6c317501548ecb60f69796f626693d96c981
parent 98bbda36808e6abb99bdf309b7e5cd23b29d42a2
Author: Sean Enck <sean@ttypty.com>
Date:   Wed, 28 Dec 2022 18:46:27 -0500

support keyfile

Diffstat:
MMakefile | 7+++++--
Minternal/backend/actions.go | 9+++++++--
Minternal/backend/actions_test.go | 21+++++++++++++++++++++
Minternal/backend/core.go | 18++++++++++++++++--
Minternal/inputs/env.go | 3+++
Minternal/inputs/env_test.go | 2+-
Mscripts/check.go | 11+++++++++++
7 files changed, 64 insertions(+), 7 deletions(-)

diff --git a/Makefile b/Makefile @@ -7,6 +7,7 @@ MAN := $(BUILD)lb.man DOCTEXT := scripts/doc.sections ACTUAL := $(BUILD)actual.log DATE := $(shell date +%Y-%m-%d) +RUNS := -keyfile=true -keyfile=false .PHONY: $(TESTDIR) @@ -21,9 +22,11 @@ $(TARGET): cmd/main.go internal/**/*.go go.* internal/cli/completions* $(TESTDIR): cd $@ && go test -check: $(TARGET) $(TESTDIR) +check: $(TARGET) $(TESTDIR) $(RUNS) + +$(RUNS): rm -f $(BUILD)*.kdbx - LB_BUILD=$(TARGET) TEST_DATA=$(BUILD) SCRIPTS=$(PWD)/scripts/ go run scripts/check.go 2>&1 | sed "s#$(PWD)/$(DATA)##g" | sed 's/^[0-9][0-9][0-9][0-9][0-9][0-9]$$/XXXXXX/g' | sed 's/modtime: $(DATE).*/modtime: XXXX-XX-XX/g' > $(ACTUAL) + LB_BUILD=$(TARGET) TEST_DATA=$(BUILD) SCRIPTS=$(PWD)/scripts/ go run scripts/check.go $@ 2>&1 | sed "s#$(PWD)/$(DATA)##g" | sed 's/^[0-9][0-9][0-9][0-9][0-9][0-9]$$/XXXXXX/g' | sed 's/modtime: $(DATE).*/modtime: XXXX-XX-XX/g' > $(ACTUAL) diff -u $(ACTUAL) scripts/tests.expected.log clean: diff --git a/internal/backend/actions.go b/internal/backend/actions.go @@ -65,8 +65,9 @@ func (t *Transaction) act(cb action) error { return err } k := string(key) + file := inputs.EnvOrDefault(inputs.KeyFileEnv, "") if !t.exists { - if err := create(t.file, k); err != nil { + if err := create(t.file, k, file); err != nil { return err } } @@ -76,7 +77,11 @@ func (t *Transaction) act(cb action) error { } defer f.Close() db := gokeepasslib.NewDatabase() - db.Credentials = gokeepasslib.NewPasswordCredentials(k) + creds, err := getCredentials(k, file) + if err != nil { + return err + } + db.Credentials = creds if err := gokeepasslib.NewDecoder(f).Decode(db); err != nil { return err } diff --git a/internal/backend/actions_test.go b/internal/backend/actions_test.go @@ -17,6 +17,7 @@ func fullSetup(t *testing.T, keep bool) *backend.Transaction { os.Setenv("LOCKBOX_READONLY", "no") os.Setenv("LOCKBOX_STORE", "test.kdbx") os.Setenv("LOCKBOX_KEY", "test") + os.Setenv("LOCKBOX_KEYFILE", "") os.Setenv("LOCKBOX_KEYMODE", "plaintext") os.Setenv("LOCKBOX_TOTP", "totp") os.Setenv("LOCKBOX_HOOKDIR", "") @@ -27,6 +28,26 @@ func fullSetup(t *testing.T, keep bool) *backend.Transaction { return tr } +func TestKeyFile(t *testing.T) { + os.Remove("file.key") + os.Remove("keyfile_test.kdbx") + os.Setenv("LOCKBOX_READONLY", "no") + os.Setenv("LOCKBOX_STORE", "keyfile_test.kdbx") + os.Setenv("LOCKBOX_KEY", "test") + os.Setenv("LOCKBOX_KEYFILE", "file.key.kdbx") + os.Setenv("LOCKBOX_KEYMODE", "plaintext") + os.Setenv("LOCKBOX_TOTP", "totp") + os.Setenv("LOCKBOX_HOOKDIR", "") + os.WriteFile("file.key.kdbx", []byte("test"), 0644) + tr, err := backend.NewTransaction() + if err != nil { + t.Errorf("failed: %v", err) + } + if err := tr.Insert(backend.NewPath("a", "b"), "t"); err != nil { + t.Errorf("no error: %v", err) + } +} + func setup(t *testing.T) *backend.Transaction { return fullSetup(t, false) } diff --git a/internal/backend/core.go b/internal/backend/core.go @@ -37,11 +37,25 @@ func NewTransaction() (*Transaction, error) { return loadFile(os.Getenv(inputs.StoreEnv), false) } -func create(file, key string) error { +func getCredentials(key, keyFile string) (*gokeepasslib.DBCredentials, error) { + if len(keyFile) > 0 { + if !pathExists(keyFile) { + return nil, errors.New("no keyfile found on disk") + } + return gokeepasslib.NewPasswordAndKeyCredentials(key, keyFile) + } + return gokeepasslib.NewPasswordCredentials(key), nil +} + +func create(file, key, keyFile string) error { root := gokeepasslib.NewGroup() root.Name = "root" db := gokeepasslib.NewDatabase(gokeepasslib.WithDatabaseKDBXVersion4()) - db.Credentials = gokeepasslib.NewPasswordCredentials(key) + creds, err := getCredentials(key, keyFile) + if err != nil { + return err + } + db.Credentials = creds db.Content.Root = &gokeepasslib.RootData{ Groups: []gokeepasslib.Group{root}, diff --git a/internal/inputs/env.go b/internal/inputs/env.go @@ -26,6 +26,8 @@ const ( 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. @@ -292,5 +294,6 @@ func ListEnvironmentVariables(showValues bool) []string { results = append(results, e.formatEnvironmentVariable(false, noTOTPEnv, isNo, "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, isNo, "enable OSC52 clipboard mode", isYesNoArgs)) + results = append(results, e.formatEnvironmentVariable(false, KeyFileEnv, "", "additional keyfile to access/protect the database", []string{"keyfile"})) return results } diff --git a/internal/inputs/env_test.go b/internal/inputs/env_test.go @@ -166,7 +166,7 @@ func TestListVariables(t *testing.T) { known[trim] = struct{}{} } l := len(known) - if l != 17 { + if l != 18 { t.Errorf("invalid env count, outdated? %d", l) } } diff --git a/scripts/check.go b/scripts/check.go @@ -3,6 +3,7 @@ package main import ( "bytes" + "flag" "fmt" "os" "os/exec" @@ -56,7 +57,16 @@ func totpList() { } func main() { + keyFile := flag.Bool("keyfile", false, "enable keyfile") + flag.Parse() path := os.Getenv("TEST_DATA") + useKeyFile := "" + if *keyFile { + useKeyFile = filepath.Join(path, "test.key") + if err := os.WriteFile(useKeyFile, []byte("thisisatest"), 0644); err != nil { + die("unable to write keyfile", err) + } + } store := filepath.Join(path, fmt.Sprintf("%s.kdbx", time.Now().Format("20060102150405"))) os.Setenv("LOCKBOX_HOOKDIR", "") os.Setenv("LOCKBOX_STORE", store) @@ -65,6 +75,7 @@ func main() { os.Setenv("LOCKBOX_INTERACTIVE", "no") os.Setenv("LOCKBOX_READONLY", "no") os.Setenv("LOCKBOX_KEYMODE", "plaintext") + os.Setenv("LOCKBOX_KEYFILE", useKeyFile) insert("keys/k/one2", []string{"test2"}) for _, k := range []string{"keys/k/one", "key/a/one", "keys/k/one", "keys/k/one/", "/keys/k/one", "keys/aa/b//s///e"} { insert(k, []string{"test"})