lockbox

password manager
Log | Files | Refs | README | LICENSE

commit 0f84fcdb8e79754763060b12e42cb60aab824e45
parent 89014e8581ecabbd6cd7ff338f39c4cefbd572d4
Author: Sean Enck <sean@ttypty.com>
Date:   Thu,  5 Jun 2025 09:05:11 -0400

allow conv of attributes

Diffstat:
Mcmd/lb/tests/expected.log | 3+++
Minternal/backend/core.go | 6+++++-
Minternal/backend/query.go | 50++++++++++++++++++++++++++++++++++----------------
Minternal/backend/query_test.go | 91+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 133 insertions(+), 17 deletions(-)

diff --git a/cmd/lb/tests/expected.log b/cmd/lb/tests/expected.log @@ -56,6 +56,9 @@ XXXXXX "data": "132ab0244293c495a027cec12d0050598616daca888449920fc652719be0987830827d069ef78cc613e348de37c9b592d3406e2fb8d99a6961bf0c58da8a334f" } "test/k/totp": { + "attributes": { + "otp": "b6c44d5d8a75071d8e8a39df231b0b98584d1d42982b5cf230e44f94d9c48e2983e78955a54b70c0acb0428d6db7205101e332f950ffb6b6d643aa37287c6aa5" + }, "modtime": "XXXX-XX-XX", "data": "7ef183065ba70aaa417b87ea0a96b7e550a938a52440c640a07537f7794d8a89e50078eca6a7cbcfacabd97a2db06d11e82ddf7556ca909c4df9fc0d006013b1" } diff --git a/internal/backend/core.go b/internal/backend/core.go @@ -138,9 +138,13 @@ func encode(f *os.File, db *gokeepasslib.Database) error { return gokeepasslib.NewEncoder(f).Encode(db) } +func isRestrictedField(key string) bool { + return key == notesKey || key == passKey || key == titleKey +} + func isTOTP(title string) (bool, error) { t := config.EnvTOTPEntry.Get() - if t == notesKey || t == passKey || t == titleKey { + if isRestrictedField(t) { return false, errors.New("invalid totp field, uses restricted name") } return NewSuffix(title) == NewSuffix(t), nil diff --git a/internal/backend/query.go b/internal/backend/query.go @@ -25,8 +25,9 @@ type ( } // JSON is an entry as a JSON string JSON struct { - ModTime string `json:"modtime"` - Data string `json:"data,omitempty"` + Attributes map[string]string `json:"attributes,omitempty"` + ModTime string `json:"modtime"` + Data string `json:"data,omitempty"` } // QueryMode indicates HOW an entity will be found QueryMode int @@ -199,14 +200,28 @@ func (t *Transaction) QueryCallback(args QueryOptions) (QuerySeq2, error) { } jsonMode = m } - var hashLength int64 - if jsonMode == output.JSONModes.Hash { - hashLength, err = config.EnvJSONHashLength.Get() + jsonHasher := func(string) string { + return "" + } + switch jsonMode { + case output.JSONModes.Raw: + jsonHasher = func(val string) string { + return val + } + case output.JSONModes.Hash: + 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))) + if hashLength > 0 && len(data) > l { + data = data[0:hashLength] + } + return data + } } - l := int(hashLength) return func(yield func(Entity, error) bool) { for _, item := range entities { entity := Entity{Path: item.path} @@ -218,18 +233,21 @@ func (t *Transaction) QueryCallback(args QueryOptions) (QuerySeq2, error) { } switch args.Values { case JSONValue: - data := "" - switch jsonMode { - case output.JSONModes.Raw: - data = val - case output.JSONModes.Hash: - data = fmt.Sprintf("%x", sha512.Sum512([]byte(val))) - if hashLength > 0 && len(data) > l { - data = data[0:hashLength] + data := jsonHasher(val) + t := getValue(item.backing, modTimeKey) + var attrs map[string]string + for _, v := range item.backing.Values { + if attrs == nil { + attrs = make(map[string]string) + } + key := v.Key + if isRestrictedField(key) || key == modTimeKey { + continue } + + attrs[key] = jsonHasher(v.Value.Content) } - t := getValue(item.backing, modTimeKey) - s := JSON{ModTime: t, Data: data} + s := JSON{ModTime: t, Data: data, Attributes: attrs} m, jErr := json.Marshal(s) if jErr == nil { entity.Value = string(m) diff --git a/internal/backend/query_test.go b/internal/backend/query_test.go @@ -262,3 +262,94 @@ func TestSetModTime(t *testing.T) { t.Errorf("invalid error: %v", err) } } + +func TestAttributeModes(t *testing.T) { + store.Clear() + setupInserts(t) + fullSetup(t, true).Insert("test/test/totp", "atest") + q, err := fullSetup(t, true).Get("test/test/totp", backend.BlankValue) + if err != nil { + t.Errorf("no error: %v", err) + } + if q.Value != "" { + t.Errorf("invalid result value: %s", q.Value) + } + q, err = fullSetup(t, true).Get("test/test/totp", backend.JSONValue) + if err != nil { + t.Errorf("no error: %v", err) + } + m := backend.JSON{} + if err := json.Unmarshal([]byte(q.Value), &m); err != nil { + t.Errorf("no error: %v", err) + } + if m.Data != "d39e1526f86e69f9ff1f443f5a575b5ce516b66746773997fa11bd4aefb5facd0a6fa79d0b970cad3af342ff5a63f4df1a05ef110573631d84f174b7b1d17c63" { + t.Errorf("invalid result value: %s", q.Value) + } + if m.Attributes == nil || len(m.Attributes) != 1 { + t.Errorf("invalid result value: %v", m.Attributes) + } + val, ok := m.Attributes["otp"] + if !ok || val != "7f8fd0e1a714f63da75206748d0ea1dd601fc8f92498bc87c9579b403c3004a0eefdd7ead976f7dbd6e5143c9aa7a569e24322d870ec7745a4605a154557458e" { + t.Errorf("invalid result value: %v", m.Attributes) + } + if len(m.ModTime) < 20 { + t.Errorf("invalid date/time") + } + store.SetInt64("LOCKBOX_JSON_HASH_LENGTH", 10) + q, err = fullSetup(t, true).Get("test/test/totp", backend.JSONValue) + if err != nil { + t.Errorf("no error: %v", err) + } + m = backend.JSON{} + if err := json.Unmarshal([]byte(q.Value), &m); err != nil { + t.Errorf("no error: %v", err) + } + if m.Data != "d39e1526f8" { + t.Errorf("invalid result value: %s", q.Value) + } + if m.Attributes == nil || len(m.Attributes) != 1 { + t.Errorf("invalid result value: %v", m.Attributes) + } + val, ok = m.Attributes["otp"] + if !ok || val != "7f8fd0e1a7" { + t.Errorf("invalid result value: %v", m.Attributes) + } + store.SetString("LOCKBOX_JSON_MODE", "PlAINtExt") + q, err = fullSetup(t, true).Get("test/test/totp", backend.JSONValue) + if err != nil { + t.Errorf("no error: %v", err) + } + m = backend.JSON{} + if err := json.Unmarshal([]byte(q.Value), &m); err != nil { + t.Errorf("no error: %v", err) + } + if len(m.ModTime) < 20 || m.Data != "atest" { + t.Errorf("invalid json: %v", m) + } + if m.Attributes == nil || len(m.Attributes) != 1 { + t.Errorf("invalid result value: %v", m.Attributes) + } + val, ok = m.Attributes["otp"] + if !ok || !strings.Contains(val, "otpauth://") { + t.Errorf("invalid result value: %v", m.Attributes) + } + store.SetString("LOCKBOX_JSON_MODE", "emPty") + q, err = fullSetup(t, true).Get("test/test/totp", backend.JSONValue) + if err != nil { + t.Errorf("no error: %v", err) + } + m = backend.JSON{} + if err := json.Unmarshal([]byte(q.Value), &m); err != nil { + t.Errorf("no error: %v", err) + } + if len(m.ModTime) < 20 || m.Data != "" { + t.Errorf("invalid json: %v", m) + } + if m.Attributes == nil || len(m.Attributes) != 1 { + t.Errorf("invalid result value: %v", m.Attributes) + } + val, ok = m.Attributes["otp"] + if !ok || val != "" { + t.Errorf("invalid result value: %v", m.Attributes) + } +}