Replaces the crypto test bench with the three screens the client actually needs. PMA.Vault holds the decrypted snapshot and answers the search; mainApk builds unlock/list/detail in code rather than three .fmx files. Two safety rules live in the UI because nowhere else can hold them: the plaintext is dropped when the app backgrounds (screen hidden on WillBecomeInactive, which precedes the recents-preview capture), and the clipboard is cleared after 45 s only if it still holds what we put there. The search deliberately ignores the password field. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Client Android — état et décisions
Lecture seule de l'instantané chiffré du coffre. La crypto est validée sur appareil réel contre un vecteur produit par WebCrypto.
Ce qui est mesuré, pas supposé
| Question | Réponse | Mesure |
|---|---|---|
| Le conteneur s'ouvre-t-il en Delphi ? | oui | clair de 572 octets, empreinte identique |
| AES-GCM par JNI fonctionne-t-il ? | oui | javax.crypto, tag concaténé, rien à découper |
Android encode-t-il le char[] en UTF-8 ? |
oui | clé identique sur un mot de passe non-ASCII |
| Le PBKDF2 natif est-il plus rapide ? | à peine | 3869 ms contre 4139 ms |
Le natif est adopté — c'est du code de plateforme plutôt que le nôtre — avec le Pascal en repli, seul disponible hors Android.
La conséquence architecturale
4 secondes pour ouvrir un instantané, et c'est incompressible. PBKDF2 ne bénéficie d'aucune accélération matérielle, et on ne peut pas baisser les 600 000 itérations : elles sont inscrites dans le conteneur produit par le poste Windows.
Le piège serait de payer ces 4 secondes à chaque ouverture de l'application. Ce n'est pas nécessaire, parce que le coût est lié au conteneur, pas à la session — et chaque export génère un nouveau sel, donc une mise en cache par mot de passe ne servirait à rien.
L'architecture qui en découle :
- À la synchronisation — télécharger, dériver (4 s, une fois), déchiffrer.
- Puis stocker le coffre localement chiffré sous une clé du Android Keystore, adossée à la biométrie. AES est accéléré matériellement : la relecture est instantanée.
- À l'ouverture — biométrie, puis lecture locale. Aucune dérivation.
Les 4 secondes ne se paient donc qu'à l'arrivée d'un instantané nouveau, ce qui est acceptable et compréhensible pour l'utilisateur.
Le coffre déchiffré contient tous les mots de passe en clair. Il ne doit jamais toucher le disque autrement que chiffré sous la clé du Keystore, et la sauvegarde automatique Android doit être désactivée pour l'application.
Chaîne validée de bout en bout
Sur appareil : GET authentifié (80 ms) → déchiffrement (3893 ms) → lecture du
JSON, entrées vivantes et corbeille comptées séparément. Second appel : 304,
rien de retéléchargé, rien de re-déchiffré.
L'ETag n'est retenu que si le déchiffrement a réussi. Le conserver dès la réception paraît naturel et c'est un piège : après un échec, le prochain appel répond 304 et l'application se retrouve sans données tout en se croyant à jour. L'ETag appartient au clair détenu, pas au téléchargement.
L'interface
Trois écrans dans une seule forme, construits en code (mainApk.pas) :
déverrouillage, liste, détail. Une forme par écran aurait coûté trois .fmx
pour des dispositions triviales.
L'écran de déverrouillage s'adapte à ce que l'appareil détient : coffre local présent → Ouvrir (biométrie si la clé l'exige) ; absent → seul le téléchargement peut en produire un. La liste filtre à la frappe sur le nom, le site, l'identifiant et les étiquettes — pas sur le mot de passe : personne ne cherche par mot de passe, et le comparer allongerait son séjour en mémoire.
Deux règles tenues dans l'interface, pas ailleurs :
- Le clair est jeté quand l'application passe en arrière-plan. L'écran est
masqué sur
WillBecomeInactive, qui précède la capture de l'aperçu des applications récentes — sans quoi la liste des comptes y resterait visible — et le coffre est réellement effacé surEnteredBackground. - Le presse-papiers se vide après 45 s, et seulement si son contenu est toujours celui qu'on y a mis : l'utilisateur a pu copier autre chose entre-temps.
L'écriture n'existe pas. Le poste Windows reste la source ; modifier depuis le
téléphone demanderait le PUT côté serveur et une fusion côté client.
Tester sans téléphone
Tout sauf le Keystore et la biométrie tourne en Win64 — PMA.Store
retourne « pas de coffre » hors Android, donc chaque lancement retélécharge et
redérive. Suffisant pour éprouver la liste, la recherche et la copie :
"C:\Program Files (x86)\Embarcadero\Studio\23.0\bin\dcc64.exe" -B "-UC:\Program Files (x86)\Embarcadero\Studio\23.0\lib\Win64\release" pmserverApk.dpr
Diagnostiquer un échec de déchiffrement
try-open.js ouvre un conteneur avec Node, donc avec les primitives du
navigateur :
node try-open.js <fichier> <mot-de-passe>
Il sépare deux causes que l'application ne peut pas distinguer — un tag GCM qui ne vérifie pas signifie mot de passe faux OU fichier altéré, sans plus. Si Node ouvre le fichier et pas le client, le défaut est dans le client.
Le mot de passe de sync n'est pas le mot de passe maître. Il n'est affiché nulle part :
prefs.binle garde chiffré par DPAPI, et l'application sait seulement le définir. En cas d'oubli, le redéfinir puis relancer un sync pour rechiffrer l'instantané distant.
Fichiers
PMA.Crypto.pas |
PBKDF2 (Pascal + natif JNI), AES-GCM, ouverture du conteneur |
PMA.SyncClient.pas |
GET authentifié, ETag, statuts distingués |
PMA.Store.pas |
coffre local chiffré sous une clé du Keystore |
PMA.Biometric.pas |
authentification via FMX.BiometricAuth |
PMA.Vault.pas |
l'instantané en mémoire : lecture et recherche |
PMA.TestVector.pas |
le vecteur embarqué, plus référencé par l'interface |
make-testvector.js |
régénère le vecteur avec WebCrypto |
testvector.md |
valeurs attendues, étage par étage |
L'interface est mainApk.pas, à la racine du dépôt.
Piège de compilation rencontré
Cette installation de Delphi ne produisait aucun APK FireMonkey
fonctionnel : fmx.dex.jar avait été renommé sur disque et retiré de la
configuration de l'EDI. Symptôme : l'application s'installe puis se ferme
aussitôt, avec ClassNotFoundException: FMXNativeActivity dans adb logcat.
Le projet référence désormais explicitement fmx.dex.jar dans EnabledSysJars.
Après un tel changement, supprimer les classes*.dex intermédiaires : la
compilation les réutilise sans les régénérer, et le correctif semble sans effet.