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
+39
View File
@@ -459,6 +459,14 @@ en dix minutes serait le mauvais echange. Le but est de tenir le mobile attache
a un poste paye, pas de resister a une attaque — l'APK ne peut pas resister, et a un poste paye, pas de resister a une attaque — l'APK ne peut pas resister, et
faire semblant couterait plus que de l'admettre. faire semblant couterait plus que de l'admettre.
**Le reste du travail Android est dans
[android-client/README.md](android-client/README.md)**, tenu a jour le
2026-09-17 : les trois facons d'ouvrir le coffre (verrouillage d'ecran, code
d'application, mot de passe), la copie de secours `vault-recover.bin` qui
survit au schema retire, le scanner de QR (`ThirdParty/ZXing`), le define
`AllowCreateLocal` — creer un coffre sur le telephone est reserve au debogage —
et le correctif du manifeste, dont l'APK **release n'est plus `debuggable`**.
### Deuxieme couche : l'API refuse sans licence (`PM.LicenceGate`) ### Deuxieme couche : l'API refuse sans licence (`PM.LicenceGate`)
La premiere couche (l'ecran d'activation) ne consultait qu'un branchement, La premiere couche (l'ecran d'activation) ne consultait qu'un branchement,
@@ -1047,6 +1055,34 @@ device, never transmitted.
Sensitive actions (export, change master pw, recovery code…) still Sensitive actions (export, change master pw, recovery code…) still
require master pw via `askReauth` — sync never substitutes. require master pw via `askReauth` — sync never substitutes.
### QR des reglages pour le telephone (`sync/qr`)
Recopier une URL d'API et quarante caracteres de jeton sur un telephone est le
vrai obstacle a l'installation. **Settings -> Sync -> QR for phone** affiche un
QR que l'application Android scanne elle-meme.
- **C'est l'EXE qui fabrique le QR**, pas la page : il lit `syncUrl` et
`syncPwd` dans ses prefs DPAPI et n'envoie a la page que la **matrice**
noir/blanc (`Bridge.onSyncQr(taille, bits)`). Le jeton ne traverse donc pas le
moteur de rendu en clair - meme regle que `prefs/has`.
- **Le mot de passe de synchro n'y est JAMAIS** : un QR se photographie
par-dessus l'epaule, et avec lui il ouvrirait le coffre du depot. Il se tape
une fois sur le telephone. Le QR se masque seul au bout de 60 s.
- **Le contenu est un lien `pmserver://sync?u=&t=`, pas du texte.** L'appareil
photo du telephone envoie tout TEXTE a une recherche web : le jeton serait
parti chez Google a un appui pres. Un schema propre n'a qu'une application
pour l'ouvrir.
- **Le plus court possible** : l'adresse du depot (`https://hote/Owner/Repo`) et
non l'URL d'API - le telephone la rallonge a l'enregistrement -, et seuls
`% & # + espace` sont echappes. `TNetEncoding.URL.Encode` changeait chaque
`:` et `/` en trois caracteres, et le QR devenait trop dense pour etre lu.
- Generateur : **`Source/DelphiZXingQRCode.pas`** (portage ZXing, Apache 2.0,
licence a cote). `BCryptVerifySignature` n'a rien a voir ici : le QR n'est pas
signe, il ne porte que ce que l'utilisateur voit deja a l'ecran.
- **Piege CSS rencontre** : `css/style.css` impose `svg { width: 16px }` a
toutes les icones, ce qui reduisait le QR a la taille d'une icone. La taille
est donc posee **en style direct** sur le `<svg>`.
**Tombstone purge on (re)create** : `POST /entries` and **Tombstone purge on (re)create** : `POST /entries` and
`POST /entries/bulk-import` both DELETE any `entry_tombstones` row `POST /entries/bulk-import` both DELETE any `entry_tombstones` row
matching the inserted uuid (same transaction). Without this, restoring matching the inserted uuid (same transaction). Without this, restoring
@@ -1448,6 +1484,9 @@ le vide ET vide `FPendingURL` → écran noir permanent sur cold-start lent.
``` ```
Surcharge MSBuild, pas dans le `.dproj` : un chemin propre a une machine n'a Surcharge MSBuild, pas dans le `.dproj` : un chemin propre a une machine n'a
pas sa place dans un fichier versionne. **F9 dans l'IDE sort toujours sur Z:.** pas sa place dans un fichier versionne. **F9 dans l'IDE sort toujours sur Z:.**
**La sortie Android suit le meme chemin** : `Z:\password-manager\Android64` est
une **jonction** vers `E:\password manager\Android64`, posee le 2026-09-17 quand
`Z:` est tombe a 40 Ko libres et que `aapt` echouait sans dire pourquoi.
**Des `.dcu` separes par plateforme sont ce qui empeche la pollution TMS** **Des `.dcu` separes par plateforme sont ce qui empeche la pollution TMS**
ci-dessus : verifie au premier build 32+64, aucun `.dcu` depose a cote des ci-dessus : verifie au premier build 32+64, aucun `.dcu` depose a cote des
sources TMS. L'exe de `E:\password manager\` cree **son propre `vault.db`** a sources TMS. L'exe de `E:\password manager\` cree **son propre `vault.db`** a
+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.Biometric.pas` | authentification via `FMX.BiometricAuth` |
| `PMA.Vault.pas` | l'instantané en mémoire : lecture et recherche | | `PMA.Vault.pas` | l'instantané en mémoire : lecture et recherche |
| `PMA.Totp.pas` | codes TOTP (RFC 6238), base32 ou `otpauth://` | | `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 | | `selftest.dpr` | vérification console : conteneur + TOTP |
| `PMA.TestVector.pas` | le vecteur embarqué, plus référencé par l'interface | | `PMA.TestVector.pas` | le vecteur embarqué, plus référencé par l'interface |
| `make-testvector.js` | régénère le vecteur avec WebCrypto | | `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. 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 ## Fabriquer l'APK en ligne de commande
L'EDI y arrive seul ; MSBuild non, et l'erreur ne dit pas pourquoi. 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 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 > **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 à > `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 **à > 54 Mo, et l'APK final à 16 Mo — mais `strip` écrit sa copie temporaire **à
+4 -3
View File
@@ -592,9 +592,10 @@
<button class="btn wide main" id="sSave">Save</button> <button class="btn wide main" id="sSave">Save</button>
<div class="note">The sync password never leaves this device. It is what <div class="note">The sync password never leaves this device. It is what
encrypts the copy kept in the repository, so it must be the same on every encrypts the copy kept in the repository, so it must be the same on every
device that shares it; the token only opens the repository. It is not the device that shares it; the token only opens the repository. It is asked
password that opens a vault created on this phone - that one is asked for for once: afterwards the vault opens with whatever you pick under
on the home screen.</div> <b>Unlock with</b> below - the phone screen lock, an app code, or this
password.</div>
<!-- COFFRE FERME, SECTION CACHEE. Poser comme retirer un code exigent le <!-- COFFRE FERME, SECTION CACHEE. Poser comme retirer un code exigent le
clair en main : les deux ne pourraient que refuser, et proposer une clair en main : les deux ne pourraient que refuser, et proposer une