feat(crypto): encrypt username at rest (CODE_AUDIT §1.3)
username is no longer stored cleartext. New columns username_enc/username_iv (AES-GCM under the vault key, same as encrypted_password). Search/sort/render stay client-side, so the field is decrypted at loadEntries into e.username in memory — everything downstream is unchanged. Full-strength random-IV AES-GCM (no searchable/deterministic encryption) precisely because search is client-side. Server (PM.Handler.Entries / .Auth / PM.Database): - Schema: vault_entries.username_enc, username_iv. - GET returns them; POST/PUT/bulk-import read + persist them; master-pw rotation re-encrypts them under the new key (UPDATE + loop). - ?q= server search drops `username LIKE` (ciphertext won't match; frontend searches client-side anyway). Client (app.js / app.import.js): - loadEntries/loadTrash decrypt username_enc → e.username (fallback to cleartext for un-migrated rows). - withEncryptedUsername(obj): write choke point — encrypts obj.username into username_enc/username_iv and blanks the cleartext. Wraps every POST/PUT body: saveEntry, soSave, duplicateEntry, moveEntryToFolder, addTagToEntry, batchMove/AddTag, encryptImportEntry (import + sync-apply). - doChangeMasterPassword re-encrypts username under the new key. - migrateUsernamesAtRest(): one-time sweep at enterApp, PUT-re-ships rows that still carry cleartext username so the DB gets scrubbed (bumps updated_at once; plaintext unchanged so devices converge). site/title/tags stay cleartext (same pattern later — see memory note). +1 merge test (username encrypted on import). 65/65. NOT compiled/tested at runtime (Delphi) — large multi-handler change; rebuild BuildAssets + PMServer and test create/edit/rotate/import/sync + verify the DB shows no cleartext username. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -110,6 +110,12 @@ test('merge: remote-only entry is added locally, keeping its uuid', async () =>
|
||||
assert.equal(res.updated, 0);
|
||||
assert.equal(db.entries.length, 1);
|
||||
assert.equal(db.entries[0].uuid, 'uuid-new');
|
||||
// Metadata-at-rest: username is encrypted on the way in — the stored row
|
||||
// carries ciphertext + a blank cleartext field, never the plaintext.
|
||||
assert.equal(db.entries[0].username, '', 'cleartext username must be blanked');
|
||||
assert.ok(db.entries[0].username_enc, 'username_enc must be present');
|
||||
assert.ok(db.entries[0].username_iv, 'username_iv must be present');
|
||||
assert.notEqual(db.entries[0].username_enc, 'u', 'must not store plaintext');
|
||||
assert.equal(db.entries[0].site, 'https://new.example');
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user