3bb6f6f903
The local vault now opens by itself at launch and the GET runs behind it, silently. A 304 shows nothing; a network failure shows nothing either - offline is a normal state, and an alert on every launch only teaches people to dismiss alerts. Only a new snapshot manifests, by refreshing the list. That silence is what puts the sync password inside the Keystore-encrypted file next to the vault: without it a new snapshot cannot be opened, so every edit made on the desktop would raise a password prompt at launch. The file already holds every password in plaintext; this one adds almost nothing to its exposure. A typed password still wins over the stored one, which is how you fix one that no longer works. Adopting a snapshot while the app is already open rebuilds the list in place instead of navigating - mandatory even from the detail screen, since list rows carry indices into the old vault and a tap would open the wrong entry. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
179 lines
8.7 KiB
Markdown
179 lines
8.7 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"`** dans
|
|
[AndroidManifest.template.xml](../AndroidManifest.template.xml). Une ligne.
|
|
Aujourd'hui absente : la sauvegarde Google peut donc emporter `vault.bin`.
|
|
Le fichier est chiffré par une clé qui ne quitte pas la puce, donc ce qui
|
|
sort est inexploitable — mais il sort, et le README prescrit déjà l'inverse.
|
|
2. **Retirer `usesCleartextTraffic="true"`** du même fichier, et servir
|
|
l'endpoint en HTTPS avec un vrai certificat. L'attribut est marqué
|
|
TEST-ONLY dans le manifeste ; le jeton Bearer circule à chaque appel.
|
|
3. **`FLAG_SECURE`** sur la fenêtre. On masque déjà l'écran avant la capture
|
|
des applications récentes, mais rien n'empêche une capture d'écran
|
|
manuelle. `getWindow().setFlags` par JNI, quelques lignes.
|
|
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 via le sélecteur système.
|
|
Attention à la mémoire : `content_b64` est déjà décodé en RAM avec tout le
|
|
reste de l'instantané, un coffre avec des PDF pèse lourd.
|
|
2. **Filtres dossier et étiquette.** `TVault.Search` ne prend qu'une requête
|
|
texte ; ajouter deux paramètres facultatifs et une rangée de puces
|
|
au-dessus de la liste. Les dossiers viennent du tableau `folders`.
|
|
3. **Icônes** (`icon_b64`) en tête de ligne, à la place des initiales.
|
|
Purement cosmétique, mais c'est ce qui rend une liste balayable.
|
|
4. **`template`** — afficher le sous-type (carte, clé SSH…) comme le fait le
|
|
poste Windows, et adapter les intitulés d'une carte bancaire.
|
|
5. **Tri.** L'ordre actuel est celui de l'instantané. Par nom, au minimum.
|
|
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`.** Rien n'est arbitrable sans lui. À faire
|
|
même si l'écriture est repoussée : la lecture peut déjà afficher « modifié
|
|
le… ».
|
|
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.
|
|
2. §2 items 1 et 2 : deux lignes de manifeste, et le risque le plus concret.
|
|
3. §3 items 1 à 3 : pièces jointes, filtres, icônes. Le client devient
|
|
réellement utilisable au quotidien.
|
|
4. §4 étape 1 seule (`updated_at`), puis décider si l'écriture vaut son coût.
|