lockbox

password manager
Log | Files | Refs | README | LICENSE

commit bdcea6c4acd5a15bcd395e414aba965badd92eb7
parent 793fc32b2566210370b235368981f35a40d5a655
Author: Sean Enck <sean@ttypty.com>
Date:   Mon, 12 Jan 2026 17:45:16 -0500

enable including the path in the hash for uniqueness

Diffstat:
Mcmd/lb/main_test.go | 5+++++
Mcmd/lb/tests/expected.log | 9+++++++++
Minternal/config/vars.go | 8++++++++
Minternal/kdbx/query.go | 15++++++++++-----
Minternal/kdbx/query_test.go | 22+++++++++++++++++++---
5 files changed, 51 insertions(+), 8 deletions(-)

diff --git a/cmd/lb/main_test.go b/cmd/lb/main_test.go @@ -196,6 +196,7 @@ func test(profile string) error { } } + c["json.hash_path"] = "false" c["readonly"] = "true" r.writeConfig(c) r.run("echo testing |", "insert test1/key1/password") @@ -306,6 +307,10 @@ func test(profile string) error { c["json.hash_length"] = "3" r.writeConfig(c) r.run("", "json test6/*") + c["json.mode"] = c.quoteString("hash") + c["json.hash_path"] = "true" + r.writeConfig(c) + r.run("", "json test6/*") r.section("clipboard") copyFile := filepath.Join(r.testDir, "clip.copy") diff --git a/cmd/lb/tests/expected.log b/cmd/lb/tests/expected.log @@ -392,6 +392,14 @@ json "password": "cbf" } } +{ + "test6/multiline": { + "modtime": "XXXX-XX-XX", + "notes": "4df", + "otp": "630", + "password": "4df" + } +} clipboard invalids Wrong password? HMAC-SHA256 of header mismatching @@ -411,6 +419,7 @@ store ok env LOCKBOX_CLIP_COPY=[touch testdata/datadir/clip.copy] LOCKBOX_JSON_HASH_LENGTH=3 +LOCKBOX_JSON_HASH_PATH=true LOCKBOX_JSON_MODE=hash LOCKBOX_STORE=testdata/datadir/pass.kdbx LOCKBOX_TOTP_TIMEOUT=1 diff --git a/internal/config/vars.go b/internal/config/vars.go @@ -177,4 +177,12 @@ Set to '%s' to ignore the set key value`, noKeyMode, IgnoreKeyMode), flags: []stringsFlags{canExpandFlag}, }, }) + // EnvJSONHashPath will include path in hashing operations + EnvJSONHashPath = environmentRegister(EnvironmentBool{ + environmentDefault: newDefaultedEnvironment(true, + environmentBase{ + key: jsonCategory + "HASH_PATH", + description: "Enable including the entity path in hashing of entries.", + }), + }) ) diff --git a/internal/kdbx/query.go b/internal/kdbx/query.go @@ -173,12 +173,13 @@ func (t *Transaction) QueryCallback(args QueryOptions) (QuerySeq2, error) { } jsonMode = m } - jsonHasher := func(string) string { + hashPath := config.EnvJSONHashPath.Get() + jsonHasher := func(string, string) string { return "" } switch jsonMode { case output.JSONModes.Raw: - jsonHasher = func(val string) string { + jsonHasher = func(val, _ string) string { return val } case output.JSONModes.Hash: @@ -187,8 +188,12 @@ func (t *Transaction) QueryCallback(args QueryOptions) (QuerySeq2, error) { return nil, err } l := int(hashLength) - jsonHasher = func(val string) string { - data := fmt.Sprintf("%x", sha512.Sum512([]byte(val))) + jsonHasher = func(val, path string) string { + added := "" + if hashPath { + added = path + } + data := fmt.Sprintf("%x", sha512.Sum512([]byte(added+val))) if hashLength > 0 && len(data) > l { data = data[0:hashLength] } @@ -210,7 +215,7 @@ func (t *Transaction) QueryCallback(args QueryOptions) (QuerySeq2, error) { val = v.Value.Content switch args.Values { case JSONValue: - val = jsonHasher(val) + val = jsonHasher(val, entity.Path) } } if key == modTimeKey || key == titleKey { diff --git a/internal/kdbx/query_test.go b/internal/kdbx/query_test.go @@ -148,6 +148,7 @@ func TestValueModes(t *testing.T) { t.Errorf("invalid result value: %s", k) } } + store.SetBool("LOCKBOX_JSON_HASH_PATH", false) q, err = fullSetup(t, true).Get("test/test/abc", kdbx.JSONValue) if err != nil { t.Errorf("no error: %v", err) @@ -161,6 +162,20 @@ func TestValueModes(t *testing.T) { }) { t.Errorf("invalid entity: %v", q) } + store.SetBool("LOCKBOX_JSON_HASH_PATH", true) + q, err = fullSetup(t, true).Get("test/test/abc", kdbx.JSONValue) + if err != nil { + t.Errorf("no error: %v", err) + } + if !compareEntity(q, kdbx.Entity{ + Path: "test/test/abc", + Values: map[string]string{ + "notes": "aa9d44feade8efee5728a0a3ac61af09510649c2d2ef5ffa32187a48861c64c27ef5f204c7d4ee92bcbd1c18c53e76a4f2662987e1428ce4b51b7fe8f959074c", + "password": "9378107fdb4acbe3851bdd015017d35dcc921c18ce0c9ad24c730d3e1bf402ab1835ef547f8ccd191e21fd75d13fc86457dbbeae475e0ec817aae4c79ccf5884", + }, + }) { + t.Errorf("invalid entity: %v", q) + } store.SetInt64("LOCKBOX_JSON_HASH_LENGTH", 10) q, err = fullSetup(t, true).Get("test/test/abc", kdbx.JSONValue) if err != nil { @@ -169,8 +184,8 @@ func TestValueModes(t *testing.T) { if !compareEntity(q, kdbx.Entity{ Path: "test/test/abc", Values: map[string]string{ - "notes": "9057ff1aa9", - "password": "44276ba24d", + "notes": "aa9d44fead", + "password": "9378107fdb", }, }) { t.Errorf("invalid entity: %v", q) @@ -273,7 +288,7 @@ func TestSetModTime(t *testing.T) { if !compareEntity(q, kdbx.Entity{ Path: "test/xyz", Values: map[string]string{ - "password": "ee26b0dd4af7e749aa1a8ee3c10ae9923f618980772e473f8819a5d4940e0db27ac185f8a0e1d5f84f88bc887fd67b143732c304cc5fa9ad8e6f57f50028a8ff", + "password": "7117cd5faee8c269ced77e173fc88f3586bdb015099873deef6d97bcd41bb7494b17674ab98e3d78b7fffabbb331dd8c10be1d77bf5b9e485ec6f32f5dfca9e6", "modtime": testDateTime, }, }) { @@ -316,6 +331,7 @@ func TestAttributeModes(t *testing.T) { }) { t.Errorf("invalid entity: %v", q) } + store.SetBool("LOCKBOX_JSON_HASH_PATH", false) q, err = fullSetup(t, true).Get("test/test/totp", kdbx.JSONValue) if err != nil { t.Errorf("no error: %v", err)