Files
Password-Manager/android-client
r-zakarya 9a4170c44f feat(android): folder filter, icons, sort, template, updated_at, FLAG_SECURE
Works through the plan's §2 and §3 items that need no hardware and no
platform decision.

- allowBackup="false". The private directory holds vault.bin; a cloud
  backup has no business in a password manager's path, even though what
  would leave is encrypted by a key that stays in the chip.
- FLAG_SECURE, Release builds only - in debug it also blocks development
  screenshots. Complementary to hiding on WillBecomeInactive rather than
  redundant: that one covers the recents preview, the flag also covers
  manual captures, screen recording and casting to an external display.
- List sorted by name (snapshot order is DB order, arbitrary to the eye),
  entry icons decoded once per vault rather than on every keystroke, and a
  folder dropdown whose selection survives a background refresh. No tag
  filter: text search already matches tags.
- template and updated_at read and shown in the detail. updated_at is also
  step 1 of the write path - nothing is arbitrable without it.

Android code is now type-checked without deploying: dccaarm64 reaches the
link step and fails only on the NDK's ldandroid.exe. A JNI declaration that
does not compile no longer waits for an APK to say so.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 11:23:29 +01:00
..

Client Android — état et décisions

Ce qui reste à faire est dans PLAN.md.

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.

Afficher d'abord, synchroniser ensuite. Au lancement, le coffre local s'ouvre tout seul (biométrie si la clé l'exige) et la liste apparaît sans réseau ni dérivation ; le GET part derrière, en silence. Un 304 ne se voit pas, un échec réseau non plus — hors ligne est un état normal, et une alerte à chaque lancement apprendrait surtout à ignorer les alertes. Seul un instantané nouveau se manifeste, en rafraîchissant la liste.

C'est ce qui justifie de ranger le mot de passe de sync dans le fichier chiffré, à côté du coffre : sans lui, un instantané nouveau ne peut pas être ouvert, et chaque modification faite sur le poste ferait apparaître une invite au lancement. Le fichier contient déjà tous les mots de passe en clair ; celui-ci n'y ajoute presque rien en exposition. Ce qui est tapé dans le champ l'emporte sur ce qui est rangé — c'est ainsi qu'on corrige un mot de passe qui ne convient plus.

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.

Le détail est construit par entrée, pas d'après un gabarit fixe : site, identifiant, mot de passe et champs personnalisés se ramènent tous à un intitulé, une valeur, et le fait qu'elle soit secrète. Les traiter séparément aurait dupliqué révélation et copie autant de fois qu'il y a de sortes de champ. Deux exceptions assumées : le corps d'une note, qui est stocké dans le champ mot de passe et s'affiche donc sous un autre intitulé, sans masque ; et le code 2FA, seule ligne vivante, rafraîchie chaque seconde et placée en tête — c'est ce qu'on vient chercher sur un téléphone.

La liste est triée par nom — l'ordre de l'instantané est celui de la base, arbitraire à l'œil — et porte l'icône de l'entrée quand elle en a une, décodée une fois par coffre et non à chaque frappe. Une liste déroulante filtre par dossier ; il n'y a pas de filtre d'étiquette, la recherche texte les couvrant déjà.

Le TOTP (PMA.Totp.pas) accepte le base32 nu comme l'URI otpauth://, les deux circulant dans les instantanés selon la façon dont l'entrée a été créée. Vérifié contre les six vecteurs de la RFC 6238.

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.

"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 :

"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 :

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.Totp.pas codes TOTP (RFC 6238), base32 ou otpauth://
PMA.ScreenGuard.pas FLAG_SECURE (Release seulement)
selftest.dpr vérification console : conteneur + TOTP
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.