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:
r-zakarya
2026-07-08 22:04:48 +01:00
parent 2578ac0d06
commit 69fb2b10dd
8 changed files with 213 additions and 37 deletions
+17 -1
View File
@@ -429,11 +429,27 @@ 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,
username, folder, tags, kind, template`). `template` est le sous-type
folder, tags, 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]]).
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
sur certaines actions :