lockbox

password manager
Log | Files | Refs | README | LICENSE

commit 46934afcb75a339f3a8681128ad2b74e6dd9923b
parent 3f591cf1874d6e87360a9b67bf35465ca4d0eb11
Author: Sean Enck <sean@ttypty.com>
Date:   Sat,  7 Jun 2025 20:17:11 -0400

support URL field

Diffstat:
Mcmd/lb/main_test.go | 2+-
Mcmd/lb/tests/expected.log | 18+++++++++++++++++-
Minternal/kdbx/actions.go | 2+-
Minternal/kdbx/actions_test.go | 10++++++++--
Minternal/kdbx/core.go | 4+++-
5 files changed, 30 insertions(+), 6 deletions(-)

diff --git a/cmd/lb/main_test.go b/cmd/lb/main_test.go @@ -207,7 +207,7 @@ func test(profile string) error { for _, k := range []string{"insert test4/multiline/notes", "insert test5/multiline/notes", "insert test5/multiline/otp", "insert test5/multiline/password"} { r.run(`printf "testing3\ntesting4\n" |`, k) } - for _, k := range []string{"insert test6/multiline/password", "insert test6/multiline/notes", "insert test7/deeper/rooted/notes", "insert test7/deeper/rooted/otp", "insert test8/unset/password", "insert test8/unset/notes", "insert test9/key1/sub1/password", "insert test9/key1/sub2/password", "insert test9/key2/sub1/password"} { + for _, k := range []string{"insert test6/multiline/password", "insert test6/multiline/notes", "insert test7/deeper/rooted/notes", "insert test7/deeper/rooted/otp", "insert test7/deeper/root/url", "insert test8/unset/password", "insert test8/unset/notes", "insert test9/key1/sub1/password", "insert test9/key1/sub2/password", "insert test9/key2/sub1/password"} { r.run(`printf "testing5" |`, k) r.run("", fmt.Sprintf("show %s", strings.ReplaceAll(k, "insert ", ""))) } diff --git a/cmd/lb/tests/expected.log b/cmd/lb/tests/expected.log @@ -13,6 +13,7 @@ testing5 testing5 testing5 testing5 +testing5 test1/key1/password test2/key1/notes test2/key1/password @@ -20,6 +21,7 @@ test4/multiline/notes test5/multiline/notes test6/multiline/notes test6/multiline/password +test7/deeper/root/url test7/deeper/rooted/notes test7/deeper/rooted/otp test8/unset/notes @@ -32,6 +34,7 @@ test2/key1 test4/multiline test5/multiline test6/multiline +test7/deeper/root test7/deeper/rooted test8/unset test9/key1/sub1 @@ -43,6 +46,7 @@ test4/multiline/notes test5/multiline/notes test6/multiline/notes test6/multiline/password +test7/deeper/root/url test7/deeper/rooted/notes test7/deeper/rooted/otp test8/unset/notes @@ -72,6 +76,10 @@ test6/multiline/password "notes": "cbf67e6da88d43f048050e36d7010080536372397b6ed0a0446cd2640340bf47cb616ddbc5d35ec3500e295f875f806fc20120a2d5497b3e729e904091424633", "password": "cbf67e6da88d43f048050e36d7010080536372397b6ed0a0446cd2640340bf47cb616ddbc5d35ec3500e295f875f806fc20120a2d5497b3e729e904091424633" }, + "test7/deeper/root": { + "modtime": "XXXX-XX-XX", + "url": "cbf67e6da88d43f048050e36d7010080536372397b6ed0a0446cd2640340bf47cb616ddbc5d35ec3500e295f875f806fc20120a2d5497b3e729e904091424633" + }, "test7/deeper/rooted": { "modtime": "XXXX-XX-XX", "notes": "cbf67e6da88d43f048050e36d7010080536372397b6ed0a0446cd2640340bf47cb616ddbc5d35ec3500e295f875f806fc20120a2d5497b3e729e904091424633", @@ -141,6 +149,10 @@ period: 30 "otp": "b6c44d5d8a75071d8e8a39df231b0b98584d1d42982b5cf230e44f94d9c48e2983e78955a54b70c0acb0428d6db7205101e332f950ffb6b6d643aa37287c6aa5", "password": "cbf67e6da88d43f048050e36d7010080536372397b6ed0a0446cd2640340bf47cb616ddbc5d35ec3500e295f875f806fc20120a2d5497b3e729e904091424633" } +"test7/deeper/root": { + "modtime": "XXXX-XX-XX", + "url": "cbf67e6da88d43f048050e36d7010080536372397b6ed0a0446cd2640340bf47cb616ddbc5d35ec3500e295f875f806fc20120a2d5497b3e729e904091424633" +} "test7/deeper/rooted": { "modtime": "XXXX-XX-XX", "notes": "cbf67e6da88d43f048050e36d7010080536372397b6ed0a0446cd2640340bf47cb616ddbc5d35ec3500e295f875f806fc20120a2d5497b3e729e904091424633", @@ -167,7 +179,11 @@ no entities matching: test7/deeper no entities matching: test7/deeper/ro no entities matching: test1/key1/password delete entry? (y/N) -delete entry? (y/N) +selected entities: + test7/deeper/root + test7/deeper/rooted + +delete entries? (y/N) test4/multiline/notes test5/multiline/notes test6/multiline/notes diff --git a/internal/kdbx/actions.go b/internal/kdbx/actions.go @@ -225,7 +225,7 @@ func (t *Transaction) Move(src *Entity, dst string) error { for k, v := range values { val := v switch k { - case OTPField, PasswordField: + case OTPField, PasswordField, URLField: if strings.Contains(val, "\n") { return fmt.Errorf("%s can NOT be multi-line", strings.ToLower(k)) } diff --git a/internal/kdbx/actions_test.go b/internal/kdbx/actions_test.go @@ -129,7 +129,7 @@ func TestInserts(t *testing.T) { if err := setup(t).Insert("/tests", map[string]string{"password": "test"}); err.Error() != "path can NOT be rooted" { t.Errorf("wrong error: %v", err) } - if err := setup(t).Insert("test", map[string]string{"otp": "test"}); err.Error() != "input paths must contain at LEAST 2 components" { + if err := setup(t).Insert("test", map[string]string{"otp": "test", "url": "xyz"}); err.Error() != "input paths must contain at LEAST 2 components" { t.Errorf("wrong error: %v", err) } if err := setup(t).Insert("a", nil); err.Error() != "empty secrets not allowed" { @@ -144,7 +144,7 @@ func TestInserts(t *testing.T) { if err := fullSetup(t, true).Insert(kdbx.NewPath("test", "offset", "value"), map[string]string{"NoTes": "pass2"}); err != nil { t.Errorf("wrong error: %v", err) } - if err := fullSetup(t, true).Insert(kdbx.NewPath("test", "offset", "value2"), map[string]string{"NOTES": "pass\npass", "password": "xxx", "otP": "zzz"}); err != nil { + if err := fullSetup(t, true).Insert(kdbx.NewPath("test", "offset", "value2"), map[string]string{"NOTES": "pass\npass", "uRL": "123", "password": "xxx", "otP": "zzz"}); err != nil { t.Errorf("no error: %v", err) } q, err := fullSetup(t, true).Get(kdbx.NewPath("test", "offset", "value"), kdbx.SecretValue) @@ -167,12 +167,18 @@ func TestInserts(t *testing.T) { if val, ok := q.Value("otp"); !ok || val != "otpauth://totp/lbissuer:lbaccount?algorithm=SHA1&digits=6&issuer=lbissuer&period=30&secret=zzz" { t.Errorf("invalid retrieval: %s", val) } + if val, ok := q.Value("url"); !ok || val != "123" { + t.Errorf("invalid retrieval: %s", val) + } if err := fullSetup(t, true).Insert(kdbx.NewPath("test", "offset"), map[string]string{"otp": "5ae472sabqdekjqykoyxk7hvc2leklq5n"}); err != nil { t.Errorf("no error: %v", err) } if err := fullSetup(t, true).Insert(kdbx.NewPath("test", "offset"), map[string]string{"OTP": "ljaf\n5ae472abqdekjqykoyxk7hvc2leklq5n"}); err == nil || err.Error() != "otp can NOT be multi-line" { t.Errorf("wrong error: %v", err) } + if err := fullSetup(t, true).Insert(kdbx.NewPath("test", "offset"), map[string]string{"urL": "ljaf\n5ae472abqdekjqykoyxk7hvc2leklq5n"}); err == nil || err.Error() != "url can NOT be multi-line" { + t.Errorf("wrong error: %v", err) + } if err := fullSetup(t, true).Insert(kdbx.NewPath("test", "offset"), map[string]string{"password": "ljaf\n5ae472abqdekjqykoyxk7hvc2leklq5n"}); err == nil || err.Error() != "password can NOT be multi-line" { t.Errorf("wrong error: %v", err) } diff --git a/internal/kdbx/core.go b/internal/kdbx/core.go @@ -17,7 +17,7 @@ import ( var ( errPath = errors.New("input paths must contain at LEAST 2 components") // AllowedFields are the same of allowed names for storing in a kdbx entry - AllowedFields = []string{NotesField, OTPField, PasswordField} + AllowedFields = []string{NotesField, OTPField, PasswordField, URLField} ) const ( @@ -31,6 +31,8 @@ const ( NotesField = "Notes" // PasswordField is where the password is stored PasswordField = "Password" + // URLField is the URL field in the kdbx + URLField = "URL" ) type (