feat(crypto): encrypt site/title/tags at rest too (CODE_AUDIT §1.3)
Extends the username-at-rest scheme to site, title and tags — the last searchable metadata still stored cleartext. Same design: dedicated <f>_enc/<f>_iv columns (AES-GCM under the vault key), decrypted at load into e.<f>, so client-side search/sort/render/favicon/autofill-match are unchanged. Full-strength random-IV AES-GCM (no searchable encryption) because search is client-side. Generalized the helpers over ENCRYPTED_META_FIELDS = [username, site, title, tags]: - withEncryptedUsername → withEncryptedMeta (encrypts all four, blanks cleartext) — wraps every POST/PUT body. - decryptEntryUsernames → decryptEntryMeta (decrypts all four at load). - migrateUsernamesAtRest → migrateMetadataAtRest (sweeps any field still cleartext, live + trash). - doChangeMasterPassword re-encrypts all four under the new key. Server (Entries + Auth + Database): - Columns site_enc/iv, title_enc/iv, tags_enc/iv; GET emits them (new AddNullableField helper); POST/PUT/bulk read+persist (BindNullable helper); rotation UPDATE re-encrypts them. - Removed the server "Site required" validation (site='' when encrypted — the client enforces it) at POST/PUT/bulk. - ?q= server search neutralized (site+username ciphertext → LIKE useless; the frontend never sends ?search=). Tests: merge assertions updated to decrypt site (encrypted on import). 65/65. username was runtime-validated earlier; site/title/tags NOT yet compiled/ runtime-tested (Delphi) — large multi-handler change. Rebuild BuildAssets + PMServer, then create/edit/dup/move/tag/import/rotate and verify the DB shows no cleartext site/title/tags (and the app still renders/searches). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1041,6 +1041,9 @@ begin
|
||||
' totp_secret = :ts, totp_iv = :tiv, ' +
|
||||
' custom_fields = :cf, custom_fields_iv = :cfiv, ' +
|
||||
' username_enc = :uenc, username_iv = :uiv, ' +
|
||||
' site_enc = :senc, site_iv = :siv, ' +
|
||||
' title_enc = :tenc, title_iv = :tiv2, ' +
|
||||
' tags_enc = :genc, tags_iv = :giv, ' +
|
||||
' updated_at = CURRENT_TIMESTAMP ' +
|
||||
'WHERE id = :id AND user_id = :uid';
|
||||
|
||||
@@ -1056,6 +1059,12 @@ begin
|
||||
var LCfIv := LEntry.GetValue<string>('custom_fields_iv', '');
|
||||
var LUEnc := LEntry.GetValue<string>('username_enc', '');
|
||||
var LUIv := LEntry.GetValue<string>('username_iv', '');
|
||||
var LSEnc := LEntry.GetValue<string>('site_enc', '');
|
||||
var LSIv := LEntry.GetValue<string>('site_iv', '');
|
||||
var LTEnc := LEntry.GetValue<string>('title_enc', '');
|
||||
var LTIv := LEntry.GetValue<string>('title_iv', '');
|
||||
var LGEnc := LEntry.GetValue<string>('tags_enc', '');
|
||||
var LGIv := LEntry.GetValue<string>('tags_iv', '');
|
||||
if (LEntryId <= 0) or (LEncPwd = '') or (LIv = '') then
|
||||
raise Exception.CreateFmt('Invalid entry payload at index %d', [I]);
|
||||
|
||||
@@ -1079,10 +1088,28 @@ begin
|
||||
else LQ.ParamByName('cfiv').Value := LCfIv;
|
||||
LQ.ParamByName('uenc').DataType := ftMemo;
|
||||
LQ.ParamByName('uiv').DataType := ftMemo;
|
||||
LQ.ParamByName('senc').DataType := ftMemo;
|
||||
LQ.ParamByName('siv').DataType := ftMemo;
|
||||
LQ.ParamByName('tenc').DataType := ftMemo;
|
||||
LQ.ParamByName('tiv2').DataType := ftMemo;
|
||||
LQ.ParamByName('genc').DataType := ftMemo;
|
||||
LQ.ParamByName('giv').DataType := ftMemo;
|
||||
if LUEnc = '' then LQ.ParamByName('uenc').Clear
|
||||
else LQ.ParamByName('uenc').Value := LUEnc;
|
||||
if LUIv = '' then LQ.ParamByName('uiv').Clear
|
||||
else LQ.ParamByName('uiv').Value := LUIv;
|
||||
if LSEnc = '' then LQ.ParamByName('senc').Clear
|
||||
else LQ.ParamByName('senc').Value := LSEnc;
|
||||
if LSIv = '' then LQ.ParamByName('siv').Clear
|
||||
else LQ.ParamByName('siv').Value := LSIv;
|
||||
if LTEnc = '' then LQ.ParamByName('tenc').Clear
|
||||
else LQ.ParamByName('tenc').Value := LTEnc;
|
||||
if LTIv = '' then LQ.ParamByName('tiv2').Clear
|
||||
else LQ.ParamByName('tiv2').Value := LTIv;
|
||||
if LGEnc = '' then LQ.ParamByName('genc').Clear
|
||||
else LQ.ParamByName('genc').Value := LGEnc;
|
||||
if LGIv = '' then LQ.ParamByName('giv').Clear
|
||||
else LQ.ParamByName('giv').Value := LGIv;
|
||||
LQ.ExecSQL;
|
||||
end;
|
||||
// Password history is encrypted with the OLD vault key — we
|
||||
|
||||
Reference in New Issue
Block a user