Files
Password-Manager/android-client/PLAN.md
T
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

10 KiB

Client Android — ce qu'il reste à faire

État au 2026-08-15. Le client lit l'instantané et l'affiche ; il n'écrit rien. Ce document liste le reste, dans l'ordre où je le ferais.

Voir README.md pour l'architecture et les décisions déjà prises.


0. Ce que l'instantané contient déjà

À vérifier avant de réclamer quoi que ce soit au poste Windows : la plupart des manques côté téléphone ne demandent aucun changement côté serveur.

Champ Dans l'instantané Lu par le client
uuid, title, site, username, password oui oui
folder, tags, kind oui oui
totp_secret oui oui
custom_fields (label/value/is_secret) oui oui
template (carte, clé SSH, serveur…) oui non
icon_b64, icon_custom oui non
attachments (avec content_b64) oui non
created_at, updated_at oui non
folders (liste, couleur, icône) oui non
avatar_b64, username (compte) oui non
tombstones oui non
favorite, pinned non — device-local par conception —
corbeille non — voyage dans les sauvegardes, pas la sync —

Autrement dit : dossiers, icônes, pièces jointes et horodatages sont déjà là, il ne manque que la lecture. Seules les favoris/épinglés et la corbeille demanderaient une décision côté Windows — et pour les deux, la décision actuelle est délibérée.


1. À vérifier sur téléphone — aucun code

L'émulateur n'a ni empreinte ni écran de verrouillage, donc PMA.Store est retombé sur la clé sans contrainte à chaque essai. Ces trois points n'ont jamais été exercés :

  1. Création de la clé sous authentification — SaveVault(…, True) doit écrire un en-tête à 2, et le journal doit dire « protégé par biométrie/code » et non « AUCUNE protection ».
  2. Ouverture — VaultNeedsAuth → invite système → déchiffrement dans les 30 s de validité de la clé. Au-delà, LoadVault échoue sans effacer le cache : la garde sur UserNotAuthenticated est écrite mais jamais éprouvée.
  3. Cache d'un lancement à l'autre — fermer, rouvrir, Ouvrir le coffre : ni réseau ni mot de passe, et 0 ms de dérivation.

Un quatrième cas mérite d'être provoqué exprès : enrôler une nouvelle empreinte invalide la clé définitivement (KeyPermanentlyInvalidatedException). Le cache doit alors être effacé et retéléchargé, pas rester illisible. C'est l'autre branche de la même garde.


2. Durcissement — petit, à faire avant tout usage réel

Par ordre de gravité, pas de taille.

  1. android:allowBackup="false" — fait. Le répertoire privé contient vault.bin ; la sauvegarde Google n'a rien à y faire, même si ce qui en sortirait serait chiffré par une clé restée dans la puce.
  2. Retirer usesCleartextTraffic="true" du manifeste, et servir l'endpoint en HTTPS avec un vrai certificat. L'attribut est marqué TEST-ONLY ; le jeton Bearer circule à chaque appel. Gardé pour l'instant : le retirer casserait l'essai local par adb reverse, qui passe en clair.
  3. FLAG_SECURE — fait, PMA.ScreenGuard.pas, en Release seulement : en debug le drapeau interdit aussi les captures d'écran de développement. Le vérifier demande donc une compilation Release. Complémentaire du masquage sur WillBecomeInactive, pas redondant : celui-ci traite l'aperçu des récentes, le drapeau traite en plus les captures manuelles, l'enregistrement d'écran et la duplication vers un écran externe.
  4. Marquer le presse-papiers sensible — depuis Android 13 le système affiche un aperçu de ce qui vient d'être copié, mot de passe compris. ClipDescription + extra android.content.extra.IS_SENSITIVE.
  5. Sortir l'URL et le jeton du code source. Ce sont encore des constantes. Le mot de passe de sync, lui, est déjà rangé dans le fichier chiffré par le Keystore ; reste à en faire autant pour les deux autres, et à supprimer le préremplissage depuis PMA.LocalConfig.inc.
  6. Verrouillage sur inactivité au premier plan. Aujourd'hui seul le passage en arrière-plan verrouille ; le poste Windows a en plus une minuterie.

3. Compléter la lecture — moyen

Rangé par ce que ça apporte réellement sur un téléphone.

  1. Pièces jointes — elles sont dans l'instantané, personne ne les lit. Lister nom/taille par entrée, et enregistrer. Décision à prendre avant d'écrire quoi que ce soit : où enregistrer sur Android 10+. Le répertoire privé de l'application ne sert à rien à l'utilisateur, et Téléchargements passe par MediaStore ou un Intent de partage. C'est cette question, pas le code, qui bloque. Attention aussi à la mémoire : content_b64 est déjà décodé en RAM avec le reste de l'instantané, un coffre avec des PDF pèse.
  2. Filtre dossier — fait, liste déroulante à côté de la recherche, alimentée par le tableau folders, et le choix survit à un rafraîchissement de fond. Pas de filtre d'étiquette : la recherche texte couvre déjà les étiquettes, en taper le nom suffit.
  3. Icônes (icon_b64) — fait, décodées une fois par coffre et non à chaque frappe (la liste est reconstruite à chaque caractère). Une icône SVG n'est pas chargeable par FMX : l'entrée retombe sur ses initiales.
  4. template — fait, affiché comme Type dans le détail. Adapter en plus les intitulés d'une carte bancaire reste à voir.
  5. Tri par nom — fait dans TVault.Search. L'ordre de l'instantané est celui de la base, donc arbitraire à l'œil.
  6. TListView à la place de TListBox le jour où la reconstruction à chaque frappe se voit. Marqué ponytail: dans le code, pas avant.

4. Le chemin d'écriture — le gros morceau

C'est ce qui ferait du téléphone un second appareil plutôt qu'une consultation. À ne pas commencer avant que le reste soit stable : une erreur ici perd des données, alors qu'une erreur en lecture n'affiche qu'une ligne de travers.

La contrainte qui gouverne tout le reste

Le client doit republier ce qu'il n'a pas compris, à l'identique.

TVault ne lit qu'un sous-ensemble des champs. S'il resérialise le coffre à partir de ce qu'il a lu, un envoi depuis le téléphone efface silencieusement icon_b64, template, attachments, created_at, l'avatar, les tombstones — et le poste Windows adopterait cette version au prochain tirage, puisque la fusion est un dernier-écrit-gagne sur updated_at.

Donc : garder le TJSONObject d'origine et ne modifier en place que ce que l'utilisateur a changé. C'est une contrainte de conception, pas une optimisation — elle décide de la forme de TVault avant la première ligne d'interface d'édition.

Dans l'ordre

  1. updated_at dans TVaultEntry — fait, et déjà affiché comme Modifié le dans le détail. Rien n'était arbitrable sans lui.
  2. AES-GCM en chiffrement. On n'a que le déchiffrement, des deux côtés (JNI et CNG). Il faut aussi un sel de 32 octets et un IV de 12 octets tirés d'un générateur cryptographique — SecureRandom côté Android, BCryptGenRandom côté Windows. Jamais Random.
  3. PUT dans sync.php, qui répond aujourd'hui 405. Même authentification, If-Match obligatoire, 412 si l'ETag ne correspond plus, écriture atomique (fichier temporaire puis renommage — sans quoi une coupure laisse un conteneur tronqué, donc un coffre perdu), et un plafond de taille.
  4. La fusion, calquée sur applyRemoteSnapshot côté JS : dernier-écrit- gagne par uuid sur updated_at, tombstones qui propagent les suppressions, et l'arbitrage de résurrection (une entrée modifiée après sa date de suppression survit à la tombstone).
  5. L'interface d'édition. Modifier un champ, marquer l'entrée sale, bumper updated_at en UTC (toISOString côté JS, NowUTC côté Delphi — mélanger heure locale et UTC a déjà décalé l'arbitrage une fois).
  6. Le conflit 412 — retirer, refusionner, renvoyer, avec un nombre d'essais borné. Le poste Windows fait exactement cela, en trois essais.

Ce qu'on peut s'épargner

La création et la suppression d'entrées peuvent attendre : modifier un mot de passe depuis le téléphone couvre l'essentiel du besoin, et n'exige ni tombstones ni génération d'uuid.


5. Empaquetage

  • Icône, nom d'application, numéro de version.
  • Signature avec un magasin de clés dédié, pas la clé de débogage.
  • Revue des permissions : INTERNET suffit.
  • Retirer le journal de diagnostic de l'écran de déverrouillage, ou le mettre derrière un appui long.
  • Le mot de passe préchargé depuis PMA.LocalConfig.inc doit disparaître.

Ordre proposé

  1. Vérifier §1 sur téléphone — c'est ce qui valide le travail déjà fait, et rien d'autre ne peut le remplacer.
  2. §2 item 4 (presse-papiers sensible) puis 6 (verrouillage sur inactivité).
  3. §3 item 1, une fois tranché où atterrissent les pièces jointes.
  4. §4 à partir de l'étape 2, si l'écriture vaut son coût.

Vérifier le code Android sans le déployer

Le compilateur croisé va jusqu'à l'édition de liens, qui échoue seulement parce que ldandroid.exe appartient au NDK et n'est pas dans le PATH :

"C:\Program Files (x86)\Embarcadero\Studio\23.0\bin\dccaarm64.exe" -B "-UC:\Program Files (x86)\Embarcadero\Studio\23.0\lib\android64\release" pmserverApk.dpr

F1026 File not found: 'ldandroid.exe' sans erreur au-dessus signifie que tout a compilé. C'est ce qui permet de valider des déclarations JNI sans attendre un APK — le pont construit ses signatures à partir des types déclarés, et une signature fausse ne se voyait jusqu'ici qu'à l'exécution. Cela ne prouve pas qu'une signature correspond à une méthode Java réelle, seulement qu'elle est cohérente : le déploiement reste le juge final.