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:
@@ -429,26 +429,32 @@ Résout le cas "j'ai ajouté un mot de passe avec tri A-Z, où se loge-t-il ?"
|
||||
|
||||
Une `vault_entries` row porte **plusieurs blobs chiffrés indépendants** :
|
||||
`encrypted_password/iv`, `totp_secret/totp_iv`, `custom_fields/custom_fields_iv`,
|
||||
**`username_enc/username_iv`** (métadonnée-at-rest, cf. plus bas),
|
||||
plus le champ-icône `icon_b64` et les méta non chiffrées (`site, title,
|
||||
folder, tags, kind, template`). `template` est le sous-type
|
||||
**`username_enc/iv`, `site_enc/iv`, `title_enc/iv`, `tags_enc/iv`**
|
||||
(métadonnées-at-rest, cf. plus bas), plus le champ-icône `icon_b64` et les
|
||||
méta non chiffrées (`folder, kind, template`). `template` est le sous-type
|
||||
(ex: `credit-card`, `ssh-key`, `server`, `recovery-codes`) qui drive le
|
||||
label de card/table — vide pour login/note génériques.
|
||||
|
||||
**`username` chiffré (§1.3)** : la colonne `username` en clair est en voie
|
||||
d'extinction — les nouvelles écritures y mettent `''` et rangent le chiffré
|
||||
dans `username_enc/username_iv` (AES-GCM sous la clé du vault, comme
|
||||
`encrypted_password`). `loadEntries`/`loadTrash` déchiffrent → `e.username`
|
||||
en mémoire, donc **recherche/tri/render/autofill-match marchent inchangés**
|
||||
(tout est déjà côté client). Choke-point d'écriture : `withEncryptedUsername(obj)`
|
||||
(chiffre `obj.username`, blanchit le clair) — enveloppe **chaque** body
|
||||
POST/PUT `/entries` (saveEntry, soSave, duplicateEntry, moveEntryToFolder,
|
||||
addTagToEntry, batchMove/AddTag, encryptImportEntry, migration). Anciennes
|
||||
lignes migrées au unlock par `migrateUsernamesAtRest` (PUT re-ship, bump
|
||||
`updated_at` assumé une fois). Rotation master-pw re-chiffre `username_enc`
|
||||
sous la nouvelle clé (JS loop + UPDATE serveur). Le `?q=` serveur ne LIKE
|
||||
plus que `site`. `site`/`title`/`tags` restent en clair (à chiffrer plus
|
||||
tard, même patron — cf. [[encrypt-metadata-plan]]).
|
||||
**Métadonnées chiffrées (§1.3)** : `username`, `site`, `title`, `tags` sont
|
||||
chiffrés au repos (colonnes `<f>_enc/<f>_iv`, AES-GCM sous la clé du vault
|
||||
comme `encrypted_password`). Les colonnes en clair reçoivent `''` sur écriture.
|
||||
`loadEntries`/`loadTrash` déchiffrent → `e.<f>` en mémoire, donc
|
||||
**recherche/tri/render/autofill-match/favicon marchent inchangés** (tout est
|
||||
déjà côté client). Liste des champs : `ENCRYPTED_META_FIELDS =
|
||||
['username','site','title','tags']`. Choke-point d'écriture :
|
||||
`withEncryptedMeta(obj)` (chiffre chaque champ, blanchit le clair) — enveloppe
|
||||
**chaque** body POST/PUT `/entries` (saveEntry, soSave, duplicateEntry,
|
||||
moveEntryToFolder, addTagToEntry, batchMove/AddTag, encryptImportEntry,
|
||||
migration). Lecture : `decryptEntryMeta(list)`. Anciennes lignes migrées au
|
||||
unlock par `migrateMetadataAtRest` (PUT re-ship, y compris corbeille, bump
|
||||
`updated_at` assumé une fois). Rotation master-pw re-chiffre les 4 champs sous
|
||||
la nouvelle clé (JS loop `ENCRYPTED_META_FIELDS` + UPDATE serveur).
|
||||
**Le `?q=` serveur est neutralisé** (site+username chiffrés → LIKE inutile ;
|
||||
le front cherche côté client). **La validation « Site required » serveur est
|
||||
retirée** (site='' quand chiffré) — le client la fait. `folder` reste en clair
|
||||
(requête serveur de réassignation sur delete-folder). Reste en clair :
|
||||
`folder`, `kind`, `template`, métadonnées d'attachments, nombre de lignes,
|
||||
timestamps.
|
||||
|
||||
Quand tu ajoutes un nouveau champ (chiffré ou non), il faut **toujours**
|
||||
mettre à jour ces 6 endroits sous peine de perdre la donnée silencieusement
|
||||
|
||||
Reference in New Issue
Block a user