The harness stored the ETag on download, so a first attempt with no password left a valid tag behind: the next call answered 304 and there was nothing to decrypt, precisely when the right password had just been typed. Sending If-None-Match without holding the matching plaintext trades a download for having no data - the real client needs the same rule, the tag belonging to the cached vault. Also drops an em dash from an exception message: Delphi reads a BOM-less source as ANSI and it reached the device as mojibake.
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.
Fichiers
PMA.Crypto.pas |
PBKDF2 (Pascal + natif JNI), AES-GCM, ouverture du conteneur |
PMA.TestVector.pas |
le vecteur embarqué, pour le banc d'essai |
make-testvector.js |
régénère le vecteur avec WebCrypto |
testvector.md |
valeurs attendues, étage par étage |
Le banc d'essai 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.