Quick search (Ctrl+Shift+Q fill mode) - Enter / left-click → full autofill (username + Tab + password), like Ctrl+Shift+L. - Shift+Enter / right-click → username only (new Delphi username-only SendInput path via field=user; ExecuteAutofill AUsernameOnly param). - Ctrl+Enter / Ctrl+click → password only. - Copy mode (tray / palette) unchanged: Enter/left = password, Shift+Enter/right = username. - Clipboard fix: copy-then-minimise no longer wipes the just-copied password — MinimizeToTray takes an AClearClipboard flag (False on the quick-search copy path, driven by app/minimize?keepclip=1). The 30s auto-clear still guards it. - Right-click on a result row suppresses the native/custom context menu (preventDefault + stopPropagation). Editable custom-field combobox - Option-backed custom fields (card brand, expiry year/month, etc.) now render a custom editable combobox instead of a locked <select>: an arrow drops a menu of ALL options (a native <datalist> filtered to the typed text, which confused users), while the input stays freely typeable for values not in the list. Storage shape unchanged. - Outside-click closes the menu via the existing slideover mousedown handler; item mousedown + preventDefault so blur doesn't race the pick. Build safety - BuildAssets.ps1 runs `node --check` on every embedded .js before generating assets.res. A syntax error now aborts the asset build (exit 1, file + line logged) instead of shipping a dead bundle that only surfaces after a full Delphi rebuild. Node is optional: absent → warn and continue. Docs - CODE_AUDIT.md: full static-analysis report (security, latent bugs, maintainability, future features, prioritized action plan). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
13 KiB
Audit de code — PMServer (Password Manager)
Analyse statique + revue architecture au 2026-07-03. Portée : crypto, auth, HTTP lockdown, sync, frontend, robustesse. Basé sur la lecture du code, pas sur un pentest dynamique.
Légende sévérité : 🔴 critique · 🟠 important · 🟡 moyen · 🔵 mineur / cosmétique
0. Résumé exécutif
Le produit est solide pour un gestionnaire perso offline : chiffrement client-side AES-GCM, zero-knowledge login (le serveur ne voit jamais le master pw), défense en profondeur sur le serveur loopback (token + PID check + CSP stricte), DPAPI pour la persistance device-bound. La majorité des "bugs" trouvés cette session ont été corrigés.
Les points qui méritent attention avant de considérer le produit "durci" :
- 🔴 Le verifier envoyé au serveur EST la clé de chiffrement (en hex).
- 🟠 Sync WebDAV sans concurrence atomique → lost update possible.
- 🟠 Arbitrage tombstone sensible au décalage d'horloge entre devices.
- 🟠 PBKDF2-SHA256 (GPU-friendly) au lieu d'Argon2id.
- 🟡
app.jsmonofichier ~14k lignes + aucun test automatisé.
1. Sécurité
1.1 🔴 Verifier = matériel de clé (couplage clé/authentifiant)
deriveKeyAndVerifier() (js/app.js) dérive un seul output
PBKDF2 et l'utilise pour DEUX rôles :
cryptoKey= les 32 octets bruts importés en AES-GCM (chiffre les entries)verifier= ces mêmes 32 octets en hex, envoyés au serveur à/login
Le serveur stocke SHA-256(verifier), donc une fuite de la DB ne donne
pas la clé (préimage SHA-256). MAIS :
- Le
verifiertransite en clair vers/login(loopback HTTP). Toute fuite du corps de requête (log, proxy de debug, extension, futur bug XSS contournant la CSP) expose directement la clé du vault. - Conceptuellement, l'authentifiant et le secret de chiffrement ne devraient jamais être le même matériel.
Recommandation : dériver le verifier dans un domaine séparé —
p.ex. verifier = HKDF(keyBytes, info="auth") ou une 2ᵉ dérivation
PBKDF2 avec un info/salt distinct. Ainsi le verifier transmis n'est
pas la clé. Migration possible sans re-chiffrer les entries (seul le
verifier stocké côté serveur change).
1.2 🟠 PBKDF2-SHA256 vs Argon2id
600k itérations SHA-256 pour les nouveaux comptes (correct, conforme OWASP 2023 ≥ 600k). Mais PBKDF2-SHA256 reste GPU/ASIC-friendly. Un master pw faible tombe vite sur du matériel dédié si la DB fuit.
Recommandation : migrer vers Argon2id (memory-hard). Coût : WebAssembly côté JS (argon2-browser) + implémentation Delphi, avec migration progressive (comme le passage 100k→600k déjà en place).
1.3 🟡 Métadonnées en clair
Documenté mais à rappeler pour un futur modèle de menace :
entry_attachments:filename,mime,size_bytesnon chiffrésusers.avatar_b64: image non chiffrée (cosmétique, assumé)vault_entries:site,title,username,folder,tags,kind,templatenon chiffrés (nécessaire pour recherche/tri sans déchiffrer)
Un attaquant avec accès disque voit la liste des sites et usernames. Pour un vault perso c'est un compromis acceptable (recherche instantanée), mais à documenter clairement pour l'utilisateur.
1.4 🟡 Snapshot de sync = tout le vault en clair sous le sync password
buildSyncSnapshot() déchiffre chaque entry puis re-chiffre le tout sous
syncEncPwd. Si l'utilisateur choisit un sync password faible, le fichier
WebDAV distant devient le maillon faible (le master pw fort ne protège
plus rien sur le remote). Recommandation : imposer une politique de
force minimale sur le sync password (déjà ≥ 6 chars — trop peu ; viser
≥ 12 + zxcvbn).
1.5 🔵 Points positifs (à conserver)
- ✅ AES-GCM 256 (AEAD, intégrité incluse) — pas de mode ECB/CBC nu
- ✅ IV aléatoire par blob (
crypto.getRandomValues(12)) — pas de réutilisation - ✅ Zero-knowledge : le master pw plaintext ne quitte jamais le client
- ✅ CSP stricte (
default-src 'self'; img-src 'self' data:) — bloque l'exfiltration même en cas d'injection - ✅ Serveur loopback + token + PID-on-socket check (defense in depth)
- ✅ DevTools / menu contextuel Edge désactivés
- ✅ Rendu via
textContent/el()(pas d'innerHTMLsur données user) → surface XSS minimale - ✅ Clipboard sécurisé (exclusion clipboard history + auto-clear 30s)
- ✅ Recovery code à usage limité (5), consommé, supprimé après rotation
2. Bugs / risques latents (non encore observés)
2.1 🟠 Sync : lost update (pas de concurrence atomique)
runSyncNow() fait GET → merge → PUT. Deux devices qui syncent
en même temps sur le même fichier WebDAV : le 2ᵉ PUT écrase le 1ᵉʳ
sans détecter le conflit → perte de la fenêtre de merge de l'un des deux.
Recommandation : concurrence optimiste via ETag / If-Match WebDAV.
Récupérer l'ETag au GET, l'envoyer en If-Match au PUT ; si 412
Precondition Failed → re-pull + re-merge + re-push. La plupart des serveurs
WebDAV (Nextcloud, Apache mod_dav) supportent les ETags.
2.2 🟠 Arbitrage tombstone sensible à l'horloge
applyRemoteSnapshot() compare entry.updated_at > tombstone.deleted_at
pour décider résurrection vs suppression. Ces timestamps viennent de
FormatDateTime('yyyy-mm-dd hh:nn:ss', Now) — heure locale du device
qui a écrit. Entre deux machines avec des horloges décalées (ou fuseaux
différents), l'arbitrage last-write-wins peut se tromper :
- Device A (horloge en avance) supprime → deleted_at "futur"
- Device B édite (horloge correcte) → updated_at "passé" vs deleted_at A
- B croit que la suppression est plus récente → tue l'édition de B
Recommandation : stocker les timestamps en UTC ISO 8601 partout (serveur ET snapshot), et idéalement un compteur logique (Lamport) en complément pour les cas d'égalité. À minima, documenter que les horloges des devices doivent être synchronisées (NTP).
2.3 🟡 catch (_) {} silencieux en cascade
Nombreux catch (_) {} dans applyRemoteSnapshot, buildSyncSnapshot,
restauration d'attachments, création de folders. Une erreur réseau/serveur
transitoire est avalée → l'entry/folder/attachment est silencieusement
sauté. Le garde-fou "failed count → abort push" atténue la perte côté
entries, mais pas côté attachments ni folders.
Recommandation : logger chaque catch (au moins console.warn +
compteur), et étendre le garde-fou "n échecs → warn utilisateur" aux
attachments.
2.4 🟡 Pas de pagination serveur sur GET /entries
Tout le vault est chargé + déchiffré à chaque unlock. Sur un gros vault
(5k-10k entries), buildSyncSnapshot (déchiffre chaque entry + fetch
attachments un par un) et computeHealthCache deviennent lents (O(n)
séquentiel, await en boucle). Acceptable pour usage perso (< 500
entries), problématique au-delà.
Recommandation : batcher les déchiffrements (Promise.all par lots), et si besoin paginer côté serveur pour la vue grille.
2.5 🟡 500 au lieu de 401 sur session expirée
Authenticate écrit 401 puis raise ESessionRejected → le try/except
global de PM.HTTPServer réécrit un 500. Pré-existant, tous les
handlers concernés. Cosmétique (le client voit un non-2xx) mais brouille
le debug.
Recommandation : attraper ESessionRejected spécifiquement dans le
dispatcher et ne pas réécrire la réponse déjà envoyée.
2.6 🔵 CSV re-import crée des doublons
Documenté/assumé (les lignes CSV sans uuid mint un nouvel uuid à chaque import). Le dedup uuid ne couvre que le JSON. Acceptable si documenté à l'utilisateur.
2.7 🔵 navigator.clipboard fallback échoue silencieusement si caché
Quand le Bridge Delphi est absent, le fallback navigator.clipboard. writeText échoue si le document n'a pas le focus (WebView caché). Cas
rare (le Bridge est quasi toujours actif en prod) mais le catch (_) {}
masque l'échec → l'utilisateur croit avoir copié.
3. Qualité de code / maintenabilité
3.1 🟡 app.js monofichier ~14 000 lignes
Tout le frontend en un seul fichier. Difficile à naviguer, à tester, et un edit malencontreux casse tout le parse (déjà arrivé cette session : une déclaration de fonction supprimée → app entièrement morte, découvert seulement au runtime).
Recommandations :
- Découper en modules ES (
crypto.js,sync.js,slideover.js,quicksearch.js,settings.js…) + un bundle simple (esbuild) dansBuildAssets.cmd. - Ajouter
node --check(ou eslint) en pré-étape deBuildAssets.cmd→ aurait attrapé le syntax error avant le rebuild. Gain immédiat, coût quasi nul.
3.2 🟡 Aucun test automatisé
Toute la validation est manuelle (TEST_PLAN.md, TEST_REGRESSION.md). Les zones à haut risque de régression (crypto round-trip, merge de sync, arbitrage tombstone, dirty-check) sont exactement celles qui bénéficieraient de tests unitaires.
Recommandations :
- Tests unitaires JS (Vitest/Jest) sur :
encryptPwd/decryptPwdround-trip,deriveKeyAndVerifier(vecteurs connus),applyRemoteSnapshot(merge + résurrection),isSoDirty,parseEntriesFromCSV/JSON. - Tests Delphi (DUnitX) sur les handlers critiques (bulk-import + tombstone purge, rotation master pw).
3.3 🔵 Duplication de constantes cross-langage
600000, formats de timestamp, noms de colonnes, shapes JSON sont
dupliqués entre JS et Delphi. Un changement d'un côté sans l'autre =
bug silencieux (déjà vu avec les champs oubliés dans la rotation/duplicate,
cf. la checklist "Entry payload" de CLAUDE.md).
4. Améliorations UX (rapides)
- 🔵 Force du sync password : afficher un indicateur zxcvbn au réglage, refuser < 12 chars.
- 🔵 Indicateur de force du master pw à l'enregistrement (zxcvbn).
- 🔵 Feedback d'échec sync partiel : "3 attachments non synchronisés" au lieu d'un silence.
- 🔵 Combobox custom fields : navigation clavier (flèches ↑↓ + Enter) dans le menu déroulant, pas seulement souris.
- 🔵 Auto-lock : afficher le temps restant avant lock dans un coin.
- 🔵 Avatar : l'inclure dans le sync snapshot + auto-backup (actuellement seulement dans l'export manuel) pour cohérence multi-device.
5. Fonctionnalités futures (par valeur / effort)
| Feature | Valeur | Effort | Note |
|---|---|---|---|
| Argon2id (KDF memory-hard) | 🔴 Haute | Moyen | argon2-browser + Delphi, migration progressive |
| ETag/If-Match sur sync | 🟠 Haute | Faible | Évite les lost updates multi-device |
| Extension navigateur | 🟠 Haute | Élevé | Autofill in-page, feature #1 demandée |
| Windows Hello (biométrie) | 🟠 Moyenne | Moyen | Windows.Security.Credentials — déverrouillage biométrique |
| Tests automatisés | 🟠 Haute | Moyen | Filet de sécurité contre les régressions |
| Import KeePass XML / 1PUX | 🟡 Moyenne | Moyen | Complète l'écosystème d'import |
| Password strength par entry | 🟡 Moyenne | Faible | zxcvbn dans le slideover + vault health |
| Verifier découplé de la clé | 🔴 Haute | Faible | Cf. §1.1 — fix de sécurité prioritaire |
| i18n (FR/EN propre) | 🔵 Basse | Moyen | Actuellement FR/EN mélangés dans l'UI |
| Timestamps UTC + Lamport | 🟠 Moyenne | Moyen | Fiabilise l'arbitrage sync (§2.2) |
| Emergency access / partage | 🔵 Basse | Élevé | Hors scope "perso offline" |
6. Plan d'action recommandé (ordre)
node --checkdansBuildAssets.cmd— 5 min, évite les JS cassés en prod.- Découpler verifier ↔ clé (§1.1) — fix sécu prioritaire, effort faible.
- ETag/If-Match sur sync (§2.1) — évite la perte de données multi-device.
- Timestamps UTC partout (§2.2) — fiabilise l'arbitrage tombstone.
- Tests unitaires crypto + merge (§3.2) — filet avant d'ajouter des features.
- Argon2id (§1.2) — durcissement KDF, migration progressive.
- Découpage
app.jsen modules (§3.1) — maintenabilité long terme.
7. Verdict
Pour un usage personnel, local, mono-utilisateur, le produit est sûr et fonctionnel aujourd'hui. Les défenses en profondeur (loopback, token, PID check, CSP, zero-knowledge, AES-GCM) sont bien pensées et au-dessus de la moyenne des projets perso.
Les axes de durcissement (verifier découplé, Argon2id, sync atomique, timestamps UTC) deviennent importants dès qu'on vise le multi-device sérieux ou un modèle de menace où la DB / le fichier de sync peut fuiter. Aucun de ces points n'est un trou béant immédiat en usage loopback solo, mais §1.1 (verifier=clé) est celui que je corrigerais en premier car il est peu coûteux et supprime un couplage dangereux.