From c684d9073fb4981d4981688e6ba7a478aa3b932a Mon Sep 17 00:00:00 2001 From: r-zakarya <82443831+r-zakarya@users.noreply.github.com> Date: Fri, 14 Aug 2026 23:30:15 +0100 Subject: [PATCH] feat(android): adopt the platform PBKDF2, record the timing and what follows from it Measured on device: Android derives the same key as WebCrypto on a non-ASCII password, so PBEKeySpec does encode as UTF-8 - the doubt that justified the Pascal implementation was unfounded. It is barely faster though, 3869 ms against 4139 ms: PBKDF2 gets no hardware acceleration. So 4 seconds per container is incompressible, and the iteration count belongs to the desktop format. The answer is not to speed up the derivation but to stop repeating it: derive once per snapshot, then keep the vault locally under an Android Keystore key. Written up in android-client/README.md. --- android-client/PMA.Crypto.pas | 9 ++++- android-client/README.md | 65 +++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 android-client/README.md diff --git a/android-client/PMA.Crypto.pas b/android-client/PMA.Crypto.pas index 06ef0a7..a952f6a 100644 --- a/android-client/PMA.Crypto.pas +++ b/android-client/PMA.Crypto.pas @@ -374,7 +374,14 @@ begin if (Length(LSalt) = 0) or (Length(LIv) = 0) or (Length(LCt) = 0) then raise EPMACrypto.Create('Container is missing salt, iv or ciphertext'); - LKey := DeriveKeyPBKDF2(APassword, LSalt, LIterations, 32); + // Android's provider first — measured identical to WebCrypto on a + // non-ASCII password (so it does encode as UTF-8), and it is platform + // code rather than ours. Barely faster though: 3869 ms against 4139 ms, + // PBKDF2 gets no hardware acceleration. The Pascal path stays as the + // fallback, and is the only one outside Android. + LKey := DeriveKeyPBKDF2Native(APassword, LSalt, LIterations, 32); + if LKey = nil then + LKey := DeriveKeyPBKDF2(APassword, LSalt, LIterations, 32); LPlain := AesGcmDecrypt(LKey, LIv, LCt); Result := TEncoding.UTF8.GetString(LPlain); finally diff --git a/android-client/README.md b/android-client/README.md new file mode 100644 index 0000000..441efb4 --- /dev/null +++ b/android-client/README.md @@ -0,0 +1,65 @@ +# 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 : + +1. **À la synchronisation** — télécharger, dériver (4 s, une fois), déchiffrer. +2. **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. +3. **À 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.