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