diff --git a/CLAUDE.md b/CLAUDE.md index 0c217e1..6bee2d9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 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`) 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 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 ``. + **Tombstone purge on (re)create** : `POST /entries` and `POST /entries/bulk-import` both DELETE any `entry_tombstones` row 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 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** 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 diff --git a/android-client/README.md b/android-client/README.md index 3d8bcda..c56ab09 100644 --- a/android-client/README.md +++ b/android-client/README.md @@ -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 **à diff --git a/android-client/web/app.html b/android-client/web/app.html index c5a65f7..73f5639 100644 --- a/android-client/web/app.html +++ b/android-client/web/app.html @@ -592,9 +592,10 @@
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 - device that shares it; the token only opens the repository. It is not the - password that opens a vault created on this phone - that one is asked for - on the home screen.
+ device that shares it; the token only opens the repository. It is asked + for once: afterwards the vault opens with whatever you pick under + Unlock with below - the phone screen lock, an app code, or this + password.