Files
Password-Manager/android-client/README.md
T
r-zakarya 0ddfe5c5fd feat(crypto): wire AES-GCM and PBKDF2 to Windows CNG
The viewer compiled for Win64 but died on first use: AesGcmDecrypt existed
only as JNI, so the desktop build downloaded 27 KB and then refused it.
Windows has the same primitive in bcrypt.dll - no DLL to ship, no OpenSSL,
and BCryptDeriveKeyPBKDF2 takes bytes, so none of the char[] encoding doubt
that keeps the Pascal loop around on Android.

This exists so the client can be exercised on the desktop; deploying an APK
to watch a list redraw is a slow way to build a UI.

selftest.dpr replaces the crypto-vector button the viewer dropped: same
known-answer vector, console, exit code = failures. Measured here - Pascal
1670 ms, CNG 528 ms, identical keys on the non-ASCII password.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 05:59:36 +01:00

146 lines
6.8 KiB
Markdown

# 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.
## 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é sur `EnteredBackground`.
- **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.Crypto` a une
branche Windows adossée à **CNG** (`bcrypt.dll`) — même primitive, aucune DLL à
livrer, et la clé dérivée est identique à celle d'Android (vérifié sur le
vecteur, mot de passe non-ASCII compris). `PMA.Store` répond « pas de coffre »
hors Android, donc chaque lancement retélécharge et redérive.
```bash
"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
```
`selftest.dpr` vérifie la chaîne crypto contre le vecteur en console — c'est ce
qui a remplacé le bouton *Test crypto* du banc d'essai. Code de sortie = nombre
d'échecs :
```bash
"C:\Program Files (x86)\Embarcadero\Studio\23.0\bin\dcc64.exe" -B "-UC:\Program Files (x86)\Embarcadero\Studio\23.0\lib\Win64\release" android-client\selftest.dpr
```
Mesures sur ce poste : PBKDF2 Pascal 1670 ms, CNG **528 ms**, clés identiques.
## Diagnostiquer un échec de déchiffrement
`try-open.js` ouvre un conteneur avec Node, donc avec les primitives du
navigateur :
```bash
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.bin` le 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.