lockbox

password manager
Log | Files | Refs | README | LICENSE

commit 29d9032e658254c625a5b8db838df2e072a23590
parent 9d30745d69593ffd1654b4ed6965e9a21b046a47
Author: Sean Enck <sean@ttypty.com>
Date:   Mon, 12 Jan 2026 21:13:37 -0500

add a checksum field, include path in hashes

Diffstat:
Mcmd/lb/tests/expected.log | 95++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------------
Minternal/kdbx/core.go | 7++++---
Minternal/kdbx/query.go | 30+++++++++++++++++++++++++-----
Minternal/kdbx/query_test.go | 19++++++++++++-------
4 files changed, 101 insertions(+), 50 deletions(-)

diff --git a/cmd/lb/tests/expected.log b/cmd/lb/tests/expected.log @@ -108,62 +108,75 @@ test6/multiline/password test7/deeper/root/url { "test1/key1": { + "checksum": "0005c", "modtime": "XXXX-XX-XX", - "password": "521b9ccefbcd14d179e7a1bb877752870a6d620938b28a66a107eac6e6805b9d0989f45b5730508041aa5e710847d439ea74cd312c9355f1f2dae08d40e41d50" + "password": "57c8df271f1dfecb52f1e56d419bcf57b9cbf649a29bba042ccc8e1301acb0d930b22bedeb1373d8489b9f8eb1ccb310a877f382f62e597c9f5dbdd4a3ee2323" }, "test4/multiline": { + "checksum": "000df", "modtime": "XXXX-XX-XX", - "notes": "fdb05182a34667e207ad36cf4688d046951a5877482da0b8dfaf1baa112369ede0fd29786e2cf771e0e00d968837d449ce7e373642a945d0675d888403178b77" + "notes": "08ecffb3ead6b577fbbacef5b1b87c240588208eee82904acc8e300fc10c504758878e9a2202a6e161d7d4925983117ee00293434bddb592656fcf01bca69ad3" }, "test5/multiline": { + "checksum": "000cf", "modtime": "XXXX-XX-XX", - "notes": "fdb05182a34667e207ad36cf4688d046951a5877482da0b8dfaf1baa112369ede0fd29786e2cf771e0e00d968837d449ce7e373642a945d0675d888403178b77" + "notes": "6be4f7e6c6761aa37ad0da252f7647e0d5401608dc193e0cf6c9cf16331600ca96df65cc5bdb2a085e79fb0818e98d1aabe8ae4e129ae369115f67f56ef9a859" }, "test6/multiline": { + "checksum": "002cc", "modtime": "XXXX-XX-XX", - "notes": "cbf67e6da88d43f048050e36d7010080536372397b6ed0a0446cd2640340bf47cb616ddbc5d35ec3500e295f875f806fc20120a2d5497b3e729e904091424633", - "password": "cbf67e6da88d43f048050e36d7010080536372397b6ed0a0446cd2640340bf47cb616ddbc5d35ec3500e295f875f806fc20120a2d5497b3e729e904091424633" + "notes": "5b88331608e33b74c5fd5d22e9053a65f5cace6a3c046d7fbc9aaea1340bd1de90b6198a99af93d02df3c4715febb1daac5babb596867137b2a65ff91fd799ad", + "password": "5b88331608e33b74c5fd5d22e9053a65f5cace6a3c046d7fbc9aaea1340bd1de90b6198a99af93d02df3c4715febb1daac5babb596867137b2a65ff91fd799ad" }, "test7/deeper/root": { + "checksum": "0007c", "modtime": "XXXX-XX-XX", - "url": "cbf67e6da88d43f048050e36d7010080536372397b6ed0a0446cd2640340bf47cb616ddbc5d35ec3500e295f875f806fc20120a2d5497b3e729e904091424633" + "url": "ea959087841400b15d443e852860b70d4350c7264b4a8a446021416868d193c992c7a864881da3c4cbaaed4f6c970a6aeeeee0a2bc39f8a1f6845d2d95e14786" }, "test7/deeper/rooted": { + "checksum": "0033c", "modtime": "XXXX-XX-XX", - "notes": "cbf67e6da88d43f048050e36d7010080536372397b6ed0a0446cd2640340bf47cb616ddbc5d35ec3500e295f875f806fc20120a2d5497b3e729e904091424633", - "otp": "3d126ef25692664fd102e72f4ebaebef79c0cf6a8ea442af099dd47d0a89fab9c9b2cb9b77e605b1d00d91c666c1f7d651a9bb2e3a06406fdd1f6155861eae55" + "notes": "1abfb8edd6c73ae6b1eb9842863b95a30e2308e7a0fe0070745ea3871a106813e9a3cf04fa43b0d322f79c03da6851b18e2139f827399fe019fc13eaaa993815", + "otp": "d7d94959b640a2a11bac8cfde21cb8780943625403592b77a520585cdaf0469d2d0c7a834b70c053676f7298cb3d6f9b0d0a6dba4599dc2d53ec8508aedea801" }, "test8/unset": { + "checksum": "002cc", "modtime": "XXXX-XX-XX", - "notes": "cbf67e6da88d43f048050e36d7010080536372397b6ed0a0446cd2640340bf47cb616ddbc5d35ec3500e295f875f806fc20120a2d5497b3e729e904091424633", - "password": "cbf67e6da88d43f048050e36d7010080536372397b6ed0a0446cd2640340bf47cb616ddbc5d35ec3500e295f875f806fc20120a2d5497b3e729e904091424633" + "notes": "0956b6450f907573260ad631985167421abbc38d2d69e690f87a370a0e4755a388b6e20e8ec15cfa8ce8d3c2a11117bbf345492ff9446f5781c10cfd54d68403", + "password": "0956b6450f907573260ad631985167421abbc38d2d69e690f87a370a0e4755a388b6e20e8ec15cfa8ce8d3c2a11117bbf345492ff9446f5781c10cfd54d68403" }, "test9/key1/sub1": { + "checksum": "0004c", "modtime": "XXXX-XX-XX", - "password": "cbf67e6da88d43f048050e36d7010080536372397b6ed0a0446cd2640340bf47cb616ddbc5d35ec3500e295f875f806fc20120a2d5497b3e729e904091424633" + "password": "e1fe8b29f0ed07e6af8a6a1100ed5268a58185eaf167726599be55ef09c363adf4df3257f45a84557977fd6b45f0713d6f791f6bc1e0ebce046568256859445b" }, "test9/key1/sub2": { + "checksum": "0007c", "modtime": "XXXX-XX-XX", - "password": "cbf67e6da88d43f048050e36d7010080536372397b6ed0a0446cd2640340bf47cb616ddbc5d35ec3500e295f875f806fc20120a2d5497b3e729e904091424633" + "password": "ef69a69c35f720f0f252d4ed94c65fa5217222689a1ced1b3c409eb0a47736ba9df2d8e78563a6b1b78fb89c36c8cdc74c0bb195e025131b2ef615dc45444784" }, "test9/key2/sub1": { + "checksum": "000cf", "modtime": "XXXX-XX-XX", - "password": "cbf67e6da88d43f048050e36d7010080536372397b6ed0a0446cd2640340bf47cb616ddbc5d35ec3500e295f875f806fc20120a2d5497b3e729e904091424633" + "password": "27141582d19bb3d4e6ef34333895f2e193655ab069bb71331755caf310dac9610ca7027ec66faeceb3ac98b20bac5a77d580e95e6d8bd404b5d1ffb816192b9e" } } { "test4/multiline": { + "checksum": "000df", "modtime": "XXXX-XX-XX", - "notes": "fdb05182a34667e207ad36cf4688d046951a5877482da0b8dfaf1baa112369ede0fd29786e2cf771e0e00d968837d449ce7e373642a945d0675d888403178b77" + "notes": "08ecffb3ead6b577fbbacef5b1b87c240588208eee82904acc8e300fc10c504758878e9a2202a6e161d7d4925983117ee00293434bddb592656fcf01bca69ad3" }, "test5/multiline": { + "checksum": "000cf", "modtime": "XXXX-XX-XX", - "notes": "fdb05182a34667e207ad36cf4688d046951a5877482da0b8dfaf1baa112369ede0fd29786e2cf771e0e00d968837d449ce7e373642a945d0675d888403178b77" + "notes": "6be4f7e6c6761aa37ad0da252f7647e0d5401608dc193e0cf6c9cf16331600ca96df65cc5bdb2a085e79fb0818e98d1aabe8ae4e129ae369115f67f56ef9a859" }, "test6/multiline": { + "checksum": "002cc", "modtime": "XXXX-XX-XX", - "notes": "cbf67e6da88d43f048050e36d7010080536372397b6ed0a0446cd2640340bf47cb616ddbc5d35ec3500e295f875f806fc20120a2d5497b3e729e904091424633", - "password": "cbf67e6da88d43f048050e36d7010080536372397b6ed0a0446cd2640340bf47cb616ddbc5d35ec3500e295f875f806fc20120a2d5497b3e729e904091424633" + "notes": "5b88331608e33b74c5fd5d22e9053a65f5cace6a3c046d7fbc9aaea1340bd1de90b6198a99af93d02df3c4715febb1daac5babb596867137b2a65ff91fd799ad", + "password": "5b88331608e33b74c5fd5d22e9053a65f5cace6a3c046d7fbc9aaea1340bd1de90b6198a99af93d02df3c4715febb1daac5babb596867137b2a65ff91fd799ad" } } totp @@ -189,52 +202,63 @@ algorithm: SHA1 period: 30 5ae472abqdekjqykoyxk7hvc2leklq5n "test1/key1": { + "checksum": "0005c", "modtime": "XXXX-XX-XX", - "password": "521b9ccefbcd14d179e7a1bb877752870a6d620938b28a66a107eac6e6805b9d0989f45b5730508041aa5e710847d439ea74cd312c9355f1f2dae08d40e41d50" + "password": "57c8df271f1dfecb52f1e56d419bcf57b9cbf649a29bba042ccc8e1301acb0d930b22bedeb1373d8489b9f8eb1ccb310a877f382f62e597c9f5dbdd4a3ee2323" } "test10/key1": { + "checksum": "000bc", "modtime": "XXXX-XX-XX", - "otp": "b6c44d5d8a75071d8e8a39df231b0b98584d1d42982b5cf230e44f94d9c48e2983e78955a54b70c0acb0428d6db7205101e332f950ffb6b6d643aa37287c6aa5" + "otp": "f79f64504a1841638238141f19ec05f237d1b692c935dc605a1efc0f42e7fcfdb838ece01c644fe8ee9586faf6ff5cc5df2c7b300165df7af33e9e2e87322687" } "test4/multiline": { + "checksum": "000df", "modtime": "XXXX-XX-XX", - "notes": "fdb05182a34667e207ad36cf4688d046951a5877482da0b8dfaf1baa112369ede0fd29786e2cf771e0e00d968837d449ce7e373642a945d0675d888403178b77" + "notes": "08ecffb3ead6b577fbbacef5b1b87c240588208eee82904acc8e300fc10c504758878e9a2202a6e161d7d4925983117ee00293434bddb592656fcf01bca69ad3" } "test5/multiline": { + "checksum": "000cf", "modtime": "XXXX-XX-XX", - "notes": "fdb05182a34667e207ad36cf4688d046951a5877482da0b8dfaf1baa112369ede0fd29786e2cf771e0e00d968837d449ce7e373642a945d0675d888403178b77" + "notes": "6be4f7e6c6761aa37ad0da252f7647e0d5401608dc193e0cf6c9cf16331600ca96df65cc5bdb2a085e79fb0818e98d1aabe8ae4e129ae369115f67f56ef9a859" } "test6/multiline": { + "checksum": "02bcc", "modtime": "XXXX-XX-XX", - "notes": "cbf67e6da88d43f048050e36d7010080536372397b6ed0a0446cd2640340bf47cb616ddbc5d35ec3500e295f875f806fc20120a2d5497b3e729e904091424633", - "otp": "b6c44d5d8a75071d8e8a39df231b0b98584d1d42982b5cf230e44f94d9c48e2983e78955a54b70c0acb0428d6db7205101e332f950ffb6b6d643aa37287c6aa5", - "password": "cbf67e6da88d43f048050e36d7010080536372397b6ed0a0446cd2640340bf47cb616ddbc5d35ec3500e295f875f806fc20120a2d5497b3e729e904091424633" + "notes": "5b88331608e33b74c5fd5d22e9053a65f5cace6a3c046d7fbc9aaea1340bd1de90b6198a99af93d02df3c4715febb1daac5babb596867137b2a65ff91fd799ad", + "otp": "6d76012d8097b71107085ba0427460d1cec5bf5f18d93ab7001ec43a65b73def59078680999fc9b9476a923cfe34850b8dbe74292aa7b862e214f5f1070cf720", + "password": "5b88331608e33b74c5fd5d22e9053a65f5cace6a3c046d7fbc9aaea1340bd1de90b6198a99af93d02df3c4715febb1daac5babb596867137b2a65ff91fd799ad" } "test7/deeper/root": { + "checksum": "0007c", "modtime": "XXXX-XX-XX", - "url": "cbf67e6da88d43f048050e36d7010080536372397b6ed0a0446cd2640340bf47cb616ddbc5d35ec3500e295f875f806fc20120a2d5497b3e729e904091424633" + "url": "ea959087841400b15d443e852860b70d4350c7264b4a8a446021416868d193c992c7a864881da3c4cbaaed4f6c970a6aeeeee0a2bc39f8a1f6845d2d95e14786" } "test7/deeper/rooted": { + "checksum": "0033c", "modtime": "XXXX-XX-XX", - "notes": "cbf67e6da88d43f048050e36d7010080536372397b6ed0a0446cd2640340bf47cb616ddbc5d35ec3500e295f875f806fc20120a2d5497b3e729e904091424633", - "otp": "3d126ef25692664fd102e72f4ebaebef79c0cf6a8ea442af099dd47d0a89fab9c9b2cb9b77e605b1d00d91c666c1f7d651a9bb2e3a06406fdd1f6155861eae55" + "notes": "1abfb8edd6c73ae6b1eb9842863b95a30e2308e7a0fe0070745ea3871a106813e9a3cf04fa43b0d322f79c03da6851b18e2139f827399fe019fc13eaaa993815", + "otp": "d7d94959b640a2a11bac8cfde21cb8780943625403592b77a520585cdaf0469d2d0c7a834b70c053676f7298cb3d6f9b0d0a6dba4599dc2d53ec8508aedea801" } "test8/unset": { + "checksum": "002cc", "modtime": "XXXX-XX-XX", - "notes": "cbf67e6da88d43f048050e36d7010080536372397b6ed0a0446cd2640340bf47cb616ddbc5d35ec3500e295f875f806fc20120a2d5497b3e729e904091424633", - "password": "cbf67e6da88d43f048050e36d7010080536372397b6ed0a0446cd2640340bf47cb616ddbc5d35ec3500e295f875f806fc20120a2d5497b3e729e904091424633" + "notes": "0956b6450f907573260ad631985167421abbc38d2d69e690f87a370a0e4755a388b6e20e8ec15cfa8ce8d3c2a11117bbf345492ff9446f5781c10cfd54d68403", + "password": "0956b6450f907573260ad631985167421abbc38d2d69e690f87a370a0e4755a388b6e20e8ec15cfa8ce8d3c2a11117bbf345492ff9446f5781c10cfd54d68403" } "test9/key1/sub1": { + "checksum": "0004c", "modtime": "XXXX-XX-XX", - "password": "cbf67e6da88d43f048050e36d7010080536372397b6ed0a0446cd2640340bf47cb616ddbc5d35ec3500e295f875f806fc20120a2d5497b3e729e904091424633" + "password": "e1fe8b29f0ed07e6af8a6a1100ed5268a58185eaf167726599be55ef09c363adf4df3257f45a84557977fd6b45f0713d6f791f6bc1e0ebce046568256859445b" } "test9/key1/sub2": { + "checksum": "0007c", "modtime": "XXXX-XX-XX", - "password": "cbf67e6da88d43f048050e36d7010080536372397b6ed0a0446cd2640340bf47cb616ddbc5d35ec3500e295f875f806fc20120a2d5497b3e729e904091424633" + "password": "ef69a69c35f720f0f252d4ed94c65fa5217222689a1ced1b3c409eb0a47736ba9df2d8e78563a6b1b78fb89c36c8cdc74c0bb195e025131b2ef615dc45444784" } "test9/key2/sub1": { + "checksum": "000cf", "modtime": "XXXX-XX-XX", - "password": "cbf67e6da88d43f048050e36d7010080536372397b6ed0a0446cd2640340bf47cb616ddbc5d35ec3500e295f875f806fc20120a2d5497b3e729e904091424633" + "password": "27141582d19bb3d4e6ef34333895f2e193655ab069bb71331755caf310dac9610ca7027ec66faeceb3ac98b20bac5a77d580e95e6d8bd404b5d1ffb816192b9e" } removing delete entry? (y/N) @@ -386,10 +410,11 @@ json } { "test6/multiline": { + "checksum": "02bcc", "modtime": "XXXX-XX-XX", - "notes": "cbf", - "otp": "b6c", - "password": "cbf" + "notes": "5b8", + "otp": "6d7", + "password": "5b8" } } clipboard diff --git a/internal/kdbx/core.go b/internal/kdbx/core.go @@ -21,9 +21,10 @@ var ( ) const ( - titleKey = "Title" - pathSep = "/" - modTimeKey = "ModTime" + checksumKey = "checksum" + titleKey = "Title" + pathSep = "/" + modTimeKey = "ModTime" // OTPField is the totp storage attribute OTPField = "otp" // NotesField is the multiline notes key diff --git a/internal/kdbx/query.go b/internal/kdbx/query.go @@ -173,22 +173,25 @@ func (t *Transaction) QueryCallback(args QueryOptions) (QuerySeq2, error) { } jsonMode = m } - jsonHasher := func(string) string { + jsonHasher := func(string, string) string { return "" } + isChecksum := false + formatString := "%" + fmt.Sprintf("%d", len(AllowedFields)+1) + "s" switch jsonMode { case output.JSONModes.Raw: - jsonHasher = func(val string) string { + jsonHasher = func(val, _ string) string { return val } case output.JSONModes.Hash: + isChecksum = args.Values == JSONValue hashLength, err := config.EnvJSONHashLength.Get() if err != nil { 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 { + data := fmt.Sprintf("%x", sha512.Sum512([]byte(val+path))) if hashLength > 0 && len(data) > l { data = data[0:hashLength] } @@ -200,24 +203,41 @@ func (t *Transaction) QueryCallback(args QueryOptions) (QuerySeq2, error) { entity := Entity{Path: item.path} var err error values := make(EntityValues) + var checksums []byte for _, v := range item.backing.Values { val := "" + raw := "" key := v.Key if args.Values != BlankValue { if args.Values == JSONValue { values["modtime"] = getValue(item.backing, modTimeKey) } val = v.Value.Content + raw = val switch args.Values { case JSONValue: - val = jsonHasher(val) + val = jsonHasher(val, entity.Path) } } if key == modTimeKey || key == titleKey { continue } + if isChecksum { + if r := jsonHasher(raw, ""); len(r) > 0 { + checksums = append(checksums, r[0]) + } + } values[strings.ToLower(key)] = val } + if isChecksum { + var check string + if len(checksums) > 0 { + checksums = append(checksums, jsonHasher(entity.Path, "")[0]) + slices.Sort(checksums) + check = strings.ReplaceAll(fmt.Sprintf(formatString, string(checksums)), " ", "0") + } + values[checksumKey] = check + } entity.Values = values if !yield(entity, err) { return diff --git a/internal/kdbx/query_test.go b/internal/kdbx/query_test.go @@ -155,8 +155,9 @@ func TestValueModes(t *testing.T) { if !compareEntity(q, kdbx.Entity{ Path: "test/test/abc", Values: map[string]string{ - "notes": "9057ff1aa9509b2a0af624d687461d2bbeb07e2f37d953b1ce4a9dc921a7f19c45dc35d7c5363b373792add57d0d7dc41596e1c585d6ef7844cdf8ae87af443f", - "password": "44276ba24db13df5568aa6db81e0190ab9d35d2168dce43dca61e628f5c666b1d8b091f1dda59c2359c86e7d393d59723a421d58496d279031e7f858c11d893e", + "checksum": "0049b", + "notes": "164f7d1c788400c54db852f5f1ef4629e4d0020a87e935dfd643dc4f765dfd201ce43b2b2ec23ff8f5b966ed15715f79d276d4ededf05691197096bb4247d665", + "password": "a3ea1c021135a8070c62a3a1080d9cd3385ebca45687636ba87c9abd1f5c2d68b17d68e72dc22461d0c8fc371573c568664e98fbfb832fcdda000318211b9538", }, }) { t.Errorf("invalid entity: %v", q) @@ -169,8 +170,9 @@ func TestValueModes(t *testing.T) { if !compareEntity(q, kdbx.Entity{ Path: "test/test/abc", Values: map[string]string{ - "notes": "9057ff1aa9", - "password": "44276ba24d", + "checksum": "0049b", + "notes": "164f7d1c78", + "password": "a3ea1c0211", }, }) { t.Errorf("invalid entity: %v", q) @@ -273,8 +275,9 @@ func TestSetModTime(t *testing.T) { if !compareEntity(q, kdbx.Entity{ Path: "test/xyz", Values: map[string]string{ - "password": "ee26b0dd4af7e749aa1a8ee3c10ae9923f618980772e473f8819a5d4940e0db27ac185f8a0e1d5f84f88bc887fd67b143732c304cc5fa9ad8e6f57f50028a8ff", + "password": "f4d691c1399b47b1a17d64da4e91f27ee739d8e49eee11d3ca5185940353325cfd5892cd375dd6a82f0b9f6e52d0365b4ddc2510106d134a1c3e9283becf72c9", "modtime": testDateTime, + "checksum": "000ef", }, }) { t.Errorf("invalid entity: %v", q) @@ -323,7 +326,8 @@ func TestAttributeModes(t *testing.T) { if !compareEntity(q, kdbx.Entity{ Path: "test/test/totp", Values: map[string]string{ - "otp": "7f8fd0e1a714f63da75206748d0ea1dd601fc8f92498bc87c9579b403c3004a0eefdd7ead976f7dbd6e5143c9aa7a569e24322d870ec7745a4605a154557458e", + "checksum": "0007e", + "otp": "cb9c99a3ba9f3370238a302adf9d3f4fa7cf4a2e01fe0225a7f69563b7c8160bd773471481d28d2f6654a6c88b41c54ca5c9930740554578b59832bd8ac2ee66", }, }) { t.Errorf("invalid entity: %v", q) @@ -336,7 +340,8 @@ func TestAttributeModes(t *testing.T) { if !compareEntity(q, kdbx.Entity{ Path: "test/test/totp", Values: map[string]string{ - "otp": "7f8fd0e1a7", + "checksum": "0007e", + "otp": "cb9c99a3ba", }, }) { t.Errorf("invalid entity: %v", q)