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

204 lines
10 KiB
Markdown

# 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](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](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](PMA.LocalConfig.template.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](../server-php/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` :
```bash
"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.