docs: QR des reglages, trois facons d'ouvrir, scanner, AllowCreateLocal, debuggable

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
r-zakarya
2026-09-17 22:19:01 +01:00
parent 405f058a92
commit 034fa48994
3 changed files with 155 additions and 4 deletions
+112 -1
View File
@@ -176,7 +176,8 @@ Node ouvre le fichier et pas le client, le défaut est dans le client.
| `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) |
| `PMA.ScreenGuard.pas` | `FLAG_SECURE` (Release seulement) — bloque aussi `adb exec-out screencap` |
| `ThirdParty/ZXing/` | ZXing.Delphi (Apache 2.0), décodage du QR des réglages |
| `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 |
@@ -184,6 +185,100 @@ Node ouvre le fichier et pas le client, le défaut est dans le client.
L'interface est `mainApk.pas`, à la racine du dépôt.
## Trois façons d'ouvrir le coffre — 2026-09-17
`Settings → Unlock with` : le **verrouillage d'écran** (schéma, code,
empreinte, visage), un **code d'application**, ou le **mot de passe** qui ouvre
le coffre. Le bouton en place porte une coche, et la note dit lequel est actif.
Le coffre lui-même n'est jamais touché : seul le scellement de `vault.bin`
change (`VER_WRAP` ↔ `VER_PIN`).
- Le nom du mot de passe **suit l'origine du coffre** : « sync password » pour
un coffre venu du dépôt, « vault password » pour un coffre créé sur le
téléphone. `TForm8.PwdName` et `PushUnlockMode` le décident, et le libellé se
repose à chaque `AdoptVault` — sinon un téléchargement laissait l'ancien.
- Revenir au verrouillage d'écran demande de **retaper le code courant** :
retirer une protection exige de la connaître.
- Sans verrouillage d'écran sur l'appareil, le bouton est grisé et **dit
pourquoi** au lieu de rester muet.
### La copie de secours, pour survivre au schéma retiré
Retirer ou réinitialiser le verrouillage d'écran fait **détruire par Android**
la clé qui scelle `vault.bin` : le coffre devenait illisible et il fallait le
retélécharger. `SaveVault` écrit donc, **tant que le cache est en `VER_WRAP`**,
une seconde copie `vault-recover.bin` scellée sous le mot de passe (format
`VER_PIN` : clé d'appareil sans contrainte + PBKDF2), qu'Android n'invalide
jamais.
- Tout autre scellement l'efface : en mode code ou mot de passe, elle
n'apporterait rien.
- À l'ouverture, `OpenVault` bascule dessus quand il n'y a plus de
verrouillage utilisable, et `OpenLocalNow` quand le système vient de déclarer
la clé morte (`LastStoreKeyInvalidated`). L'accueil demande alors le mot de
passe au lieu d'annoncer un coffre illisible.
- **Rescellé immédiatement** après cette ouverture : sous le nouveau
verrouillage s'il existe (clé neuve, l'ancienne est morte), sinon sous le mot
de passe. Le coffre ne reste jamais sans protection.
- Un coffre **venu seulement de la synchro** avant cette version n'a pas de
copie de secours : lui, il se retélécharge.
## Scanner le QR des réglages — 2026-09-17
Le poste affiche un QR (`Settings → Sync → QR for phone`) qui porte
`pmserver://sync?u=&t=` — URL du dépôt et jeton, **jamais** le mot de passe de
synchro. Deux chemins le reçoivent :
1. **`Settings → Scan the QR shown on the PC`** : aperçu caméra plein écran,
décodage par **`ThirdParty/ZXing`** (ZXing.Delphi, Apache 2.0).
2. **Le lien lui-même**, si un scanner extérieur l'ouvre : `intent-filter`
`VIEW` sur le schéma `pmserver` dans le manifeste, intent de départ lu au
`FormCreate` et suivants par `TMessageReceivedNotification`.
Dans les deux cas **rien n'est enregistré** : les champs se remplissent, c'est
`Save` qui valide. Le lien peut venir de n'importe où, et l'URL est sous les
yeux de l'utilisateur au moment où il valide.
Quatre défauts rencontrés, tous invisibles à la compilation :
| Symptôme | Cause |
|---|---|
| L'appareil photo propose « Ouvrir avec » et des navigateurs | il envoie tout **texte** à une recherche web ; d'où le schéma `pmserver://`, que seule l'app ouvre |
| Caméra noire au **premier** scan après l'autorisation | la fenêtre d'autorisation fait passer par `BecameActive`, qui remettait la vue web **par-dessus** l'aperçu ; elle ne le fait plus pendant un scan |
| Écran noir, aucune image | la caméra est tenue par une autre application (l'appareil photo, même en arrière-plan). Un guet de 3 s sans image le **dit** maintenant |
| QR lu mais ignoré | le texte décodé arrive avec un **caractère invisible** en tête (104 lus pour 103 émis) : on cherche le lien dans la chaîne au lieu de comparer le début |
Le décodage est lent et n'aboutit que sur une image de temps en temps — c'est un
portage Delphi, pas le scanner natif de Google. Une image sur quatre part au
décodage, jamais deux à la fois, et l'indice **`TRY_HARDER`** est indispensable
pour un QR photographié sur un écran.
## Créer un coffre ici : essais seulement (`AllowCreateLocal`)
Un coffre né sur le téléphone n'existe nulle part ailleurs — le poste ne le
connaît pas, et personne ne peut le rendre à qui perd son mot de passe. Le
define **`AllowCreateLocal`**, posé par `{$IFDEF DEBUG}` et jamais en release,
garde ce chemin pour les essais.
En release l'accueil n'offre que **Download vault** (`PM.setAuthMode("download")`),
et `RegisterLocal` refuse aussi : l'interdit ne tient pas qu'à l'écran. Dépôt
vide (404) ou conteneur illisible, **rien n'est créé** — un coffre neuf
masquerait un réglage à corriger.
## Saisie : ce qui a résisté
- **`user-select: none` du `body`** était hérité par les champs : le curseur ne
suivait pas le doigt et le texte ne défilait jamais, donc la fin d'une URL
restait hors d'atteinte. Les champs ont leur exception, mais **ça n'a pas
suffi** : l'URL est désormais une `textarea` qui passe à la ligne et prend la
hauteur de son texte.
- La **croix** d'effacement couvrait toute la marge droite : toucher la fin du
texte effaçait le champ. Elle est plus étroite que la marge.
- **`Save` est un geste** : fermer les réglages n'enregistre plus rien. Une URL
effacée par erreur revient en revenant en arrière.
- L'URL s'affiche en **forme courte** (`https://hôte/Owner/Repo`) et
`normalizeRepoUrl` la rallonge en URL d'API à l'enregistrement.
## Fabriquer l'APK en ligne de commande
L'EDI y arrive seul ; MSBuild non, et l'erreur ne dit pas pourquoi.
@@ -229,6 +324,22 @@ signature* sur le dossier déjà prêt, sans le vider :
android-client\repack-apk.ps1
```
> **La sortie Android vit sur `E:`.** `Z:` est arrivé à 40 Ko libres le
> 2026-09-17 et `aapt` échouait sans dire pourquoi. `Z:\password-manager\Android64`
> est désormais une **jonction** vers `E:\password manager\Android64` : les
> scripts et le `.dproj` n'ont pas changé, et rien de machine-dépendant n'est
> entré dans le dépôt.
> **La release n'est plus `debuggable`.** Delphi écrit
> `android:debuggable="true"` dans le manifeste **même en Release**, et l'APK
> livré le portait. Sur un téléphone où le débogage USB est actif, `adb run-as`
> ouvrait alors le dossier privé de l'application — donc `vault.bin` et les
> réglages — sans accès root. `build-apk.ps1` force l'attribut à `false` dans
> le manifeste mis en scène, **et seulement pour une release**. Vérification :
> `adb shell dumpsys package com.embarcadero.pmserverApk` ne doit plus lister
> `DEBUGGABLE` dans `pkgFlags`. Conséquence assumée : plus de `run-as` ni de
> capture d'écran sur une release, c'est-à-dire plus d'inspection à distance.
> **Place disque.** Le `.so` de debug pèse **91 Mo** (symboles), et le volume
> `Z:` n'en fait que 2 Go. Le dépouiller avec le `strip` du NDK le ramène à
> 54 Mo, et l'APK final à 16 Mo — mais `strip` écrit sa copie temporaire **à