commit 0f84fcdb8e79754763060b12e42cb60aab824e45
parent 89014e8581ecabbd6cd7ff338f39c4cefbd572d4
Author: Sean Enck <sean@ttypty.com>
Date: Thu, 5 Jun 2025 09:05:11 -0400
allow conv of attributes
Diffstat:
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)
+ }
+}