diff --git a/oss/app/api/handlers.go b/oss/app/api/handlers.go index edf14fe..ee844a9 100644 --- a/oss/app/api/handlers.go +++ b/oss/app/api/handlers.go @@ -426,6 +426,21 @@ func (h *Handlers) AuthLoginComplete(w http.ResponseWriter, r *http.Request) { // ListEntries returns all entries (tree structure). func (h *Handlers) ListEntries(w http.ResponseWriter, r *http.Request) { + // Metadata-only mode: returns entry_id, type, title — no field data, no decryption. + // Used by web UI list view. Full data fetched per entry on click. + if r.URL.Query().Get("meta") == "1" { + entries, err := lib.EntryListMeta(h.db(r)) + if err != nil { + ErrorResponse(w, http.StatusInternalServerError, "list_failed", "Failed to list entries") + return + } + if entries == nil { + entries = []lib.Entry{} + } + JSONResponse(w, http.StatusOK, entries) + return + } + actor := ActorFromContext(r.Context()) var parent *int64 if pidStr := r.URL.Query().Get("parent_id"); pidStr != "" { diff --git a/oss/app/lib/dbcore.go b/oss/app/lib/dbcore.go index fd4b907..ef070b4 100644 --- a/oss/app/lib/dbcore.go +++ b/oss/app/lib/dbcore.go @@ -354,6 +354,29 @@ func EntryList(db *DB, vaultKey []byte, parentID *int64) ([]Entry, error) { return entries, rows.Err() } +// EntryListMeta returns entry metadata only — no decryption, no field data. +// Used for list views. Individual entries fetched on demand via EntryGet. +func EntryListMeta(db *DB) ([]Entry, error) { + rows, err := db.Conn.Query( + `SELECT entry_id, parent_id, type, title, data_level, created_at, updated_at, version + FROM entries WHERE deleted_at IS NULL ORDER BY type, title`, + ) + if err != nil { + return nil, err + } + defer rows.Close() + + var entries []Entry + for rows.Next() { + var e Entry + if err := rows.Scan(&e.EntryID, &e.ParentID, &e.Type, &e.Title, &e.DataLevel, &e.CreatedAt, &e.UpdatedAt, &e.Version); err != nil { + return nil, err + } + entries = append(entries, e) + } + return entries, rows.Err() +} + // EntrySearch searches entries by title (blind index lookup). func EntrySearch(db *DB, vaultKey []byte, query string) ([]Entry, error) { hmacKey, err := DeriveHMACKey(vaultKey) diff --git a/oss/app/vault1984-rXJEfw b/oss/app/vault1984-rXJEfw new file mode 100644 index 0000000..762d1b4 Binary files /dev/null and b/oss/app/vault1984-rXJEfw differ diff --git a/oss/crypto/test_crypto.js b/oss/crypto/test_crypto.js index a6075a2..e534c14 100644 --- a/oss/crypto/test_crypto.js +++ b/oss/crypto/test_crypto.js @@ -216,6 +216,63 @@ }); }); + /* --- Test 13: L3 TOTP seed cannot be read with L2 key --- */ + tests.push(function(done) { + var name = 'L3 TOTP seed inaccessible to agent (L2 key)'; + var seed = 'JBSWY3DPEHPK3PXP'; // a TOTP seed + resolve(vault1984.crypto.encrypt_field(K32, 'totp', seed), function(ct, err) { + if (err) { fail(name, 'encrypt failed'); done(); return; } + // Agent has K16 (L2), tries to decrypt L3 TOTP seed + safe(function() { return vault1984.crypto.decrypt_field(K16, 'totp', ct); }, function(pt, err2) { + if (err2) pass(name); + else fail(name, 'L2 key decrypted L3 TOTP seed to "' + pt + '"'); + done(); + }); + }); + }); + + /* --- Test 14: L2 TOTP seed IS accessible with L2 key --- */ + tests.push(function(done) { + var name = 'L2 TOTP seed accessible to agent (L2 key)'; + var seed = 'JBSWY3DPEHPK3PXP'; + resolve(vault1984.crypto.encrypt_field(K16, 'totp', seed), function(ct, err) { + if (err) { fail(name, 'encrypt failed'); done(); return; } + resolve(vault1984.crypto.decrypt_field(K16, 'totp', ct), function(pt, err2) { + if (err2) fail(name, err2.message); + else if (pt === seed) pass(name); + else fail(name, 'got "' + pt + '"'); + done(); + }); + }); + }); + + /* --- Test 15: L3 card number cannot be read with L2 key --- */ + tests.push(function(done) { + var name = 'L3 card number inaccessible to agent (L2 key)'; + resolve(vault1984.crypto.encrypt_field(K32, 'Number', '5452120017212208'), function(ct, err) { + if (err) { fail(name, 'encrypt failed'); done(); return; } + safe(function() { return vault1984.crypto.decrypt_field(K16, 'Number', ct); }, function(pt, err2) { + if (err2) pass(name); + else fail(name, 'L2 key decrypted L3 card number'); + done(); + }); + }); + }); + + /* --- Test 16: Truncation model — L2 key is prefix of L3 but cannot derive L3 --- */ + tests.push(function(done) { + var name = 'truncation: L2 prefix of L3, still cannot decrypt L3'; + // K16 = K32[0..16] by design. Encrypt with full K32, try with K16. + resolve(vault1984.crypto.encrypt_field(K32, 'secret', 'classified'), function(ct, err) { + if (err) { fail(name, 'encrypt failed'); done(); return; } + safe(function() { return vault1984.crypto.decrypt_field(K16, 'secret', ct); }, function(pt, err2) { + if (err2) pass(name); + else fail(name, 'L2 (prefix of L3) decrypted L3 data'); + done(); + }); + }); + }); + /* --- Runner --- */ function run(idx) { if (idx >= tests.length) {