docs(android): the cache chain is verified on device

Write with no prompt, read under the device credential, cache surviving a cold start with no derivation, and a silent 304. Records the three traps that hid behind it, none of which the compiler could see.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
r-zakarya
2026-08-15 14:28:58 +01:00
parent c4d2bdde73
commit f3a8631440
2 changed files with 80 additions and 1 deletions
+39 -1
View File
@@ -57,7 +57,43 @@ retéléchargé, pas rester illisible. C'est l'autre branche de la même garde.
---
## 1 bis. La décision qui bloque le cache (constatée le 2026-08-15)
## 1 bis. ~~La décision qui bloque le cache~~ — RÉSOLU le 2026-08-15
Mesuré sur appareil (schéma d'écran, aucune empreinte enrôlée) :
```
cache ecrit - protege par biometrie/code (aucune invite à l'écriture)
--- redémarrage à froid, saisie du schéma ---
4 entrees, 2 en corbeille (aucune ligne « dechiffre en … ms »)
HTTP 304 en 136 ms
304 : inchange, coffre deja ouvert
```
Les points §1.1, §1.2, §1.3 et §1.5 sont donc validés d'un coup : clé créée sous
authentification, ouverture par schéma, cache survivant à un démarrage à froid
sans dérivation, et rafraîchissement de fond silencieux.
**Trois pièges se cachaient derrière, aucun visible à la compilation** :
1. `USE_BIOMETRIC` absente de la liste par défaut de Delphi — et
`CanAuthenticate` lève au lieu de répondre `False`.
2. FMX n'affiche pas l'invite depuis notre activité : il lance
`BiometricFragmentActivity`, qui doit être déclarée au manifeste.
3. **Le digest du MGF1.** `setDigests()` n'autorise que celui du message ;
celui du MGF1 est une autorisation distincte dont le défaut est SHA-1.
Le chiffrement passait quand même — la clé publique reconstruite est servie
par le fournisseur logiciel, qui accepte n'importe quel MGF1 — et produisait
un chiffré valide que le matériel refusait de lire. Le symptôme
n'apparaissait qu'au lancement suivant.
Ce qui reste de §1 : l'expiration de la fenêtre de 30 s (le cache doit
survivre — il le fera, `LoadVault` n'efface plus jamais) et l'enrôlement d'une
nouvelle empreinte, qui invalide la clé pour de bon.
<details>
<summary>Le diagnostic d'origine, conservé pour la trace</summary>
## La décision qui bloquait le cache
Le cache ne s'écrit presque jamais. Journal sur appareil :
@@ -94,6 +130,8 @@ jamais.
Retenir la 1. Les deux autres échangent une propriété de sécurité contre du
confort, ce qui est le mauvais sens pour un gestionnaire de mots de passe.
</details>
## 2. Durcissement — petit, à faire avant tout usage réel
Par ordre de gravité, pas de taille.
+41
View File
@@ -222,6 +222,47 @@ adb reverse tcp:8080 tcp:80
> — `404` signifie que la chaîne est bonne (c'est la réponse voulue sans jeton),
> `000` que rien n'écoute.
## Le cache : écrire sans invite, lire sous authentification
Validé sur appareil le 2026-08-15, sur un téléphone protégé par un **schéma**
et aucune empreinte enrôlée :
```
cache ecrit - protege par biometrie/code (aucune invite à l'écriture)
--- redémarrage à froid, saisie du schéma ---
4 entrees, 2 en corbeille (pas de ligne « dechiffre en … ms »)
HTTP 304 en 136 ms · 304 : inchange, coffre deja ouvert
```
Une clé AES du Keystore ne pouvait pas convenir : `setUserAuthenticationRequired`
exige une authentification récente **pour chiffrer autant que pour déchiffrer**,
et l'écriture suit une synchronisation silencieuse. Le cache ne s'écrivait donc
jamais, sauf dans les secondes suivant un déverrouillage — un test qui passe,
puis ne passe plus.
C'est une **paire RSA** : la partie publique enveloppe une clé AES jetable sans
invite, la partie privée — protégée — la déballe. L'authentification n'est
exigée qu'à la lecture.
Trois pièges, aucun visible à la compilation :
| | |
|---|---|
| `USE_BIOMETRIC` absente de la liste Delphi | `CanAuthenticate` **lève** au lieu de répondre `False` |
| L'invite vit dans `BiometricFragmentActivity` | à déclarer au manifeste, sinon `ActivityNotFoundException` |
| `setDigests()` n'autorise que le digest **du message** | celui du MGF1 est distinct, défaut SHA-1 |
Le troisième est le plus retors : le chiffrement passe, parce que la clé
publique reconstruite est servie par le fournisseur logiciel qui accepte
n'importe quel MGF1. On produit un chiffré valide que le matériel refuse de
lire, et le symptôme n'apparaît qu'au lancement suivant.
> **Aucune routine de lecture n'efface le cache.** `LoadVault` triait
> « clé perdue » contre « pas authentifié » sur le texte de l'exception et
> supprimait le fichier quand il penchait pour la première. Un tri erroné
> détruisait le cache *en emportant la seule trace de sa cause*. Elle rapporte
> désormais via `LastStoreError` ; l'appelant décide.
## Le piège de la porte sans clé
`USE_BIOMETRIC` **ne fait pas partie de la liste de permissions par défaut de