feat(android): show first, sync after
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>
This commit is contained in:
@@ -75,11 +75,10 @@ Par ordre de gravité, pas de taille.
|
||||
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 les paramètres du code source.** URL, jeton et mot de passe de
|
||||
sync sont des constantes, et le mot de passe vient d'un fichier hors dépôt
|
||||
([PMA.LocalConfig.inc](PMA.LocalConfig.template.inc)). Un vrai client les
|
||||
demande une fois et les range dans le même fichier chiffré par le Keystore
|
||||
que le coffre.
|
||||
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.
|
||||
|
||||
|
||||
@@ -13,17 +13,17 @@ unit PMA.Store;
|
||||
n'en sort pas : on envoie des donnees a chiffrer, on recoit le resultat. Un
|
||||
vol du fichier seul est donc inexploitable, meme root - il faut la puce.
|
||||
|
||||
CE QUE CE FICHIER CONTIENT : tous les mots de passe en clair, une fois
|
||||
dechiffre. D'ou trois regles tenues ici :
|
||||
CE QUE CE FICHIER CONTIENT : tous les mots de passe en clair une fois
|
||||
dechiffre, ET le mot de passe de sync (voir SaveVault pour l'arbitrage).
|
||||
D'ou trois regles tenues ici :
|
||||
- jamais hors du repertoire prive de l'application ;
|
||||
- jamais de copie en clair sur le disque, meme temporaire ;
|
||||
- la sauvegarde automatique Android doit etre desactivee pour l'app
|
||||
(android:allowBackup="false"), sinon Google en recupere une copie.
|
||||
|
||||
PAS ENCORE FAIT : exiger la biometrie a chaque lecture
|
||||
(setUserAuthenticationRequired). La cle est deja liee au materiel ; la
|
||||
biometrie ajouterait une barriere si le telephone est deverrouille et qu'un
|
||||
tiers ouvre l'application. A faire dans un second temps, avec BiometricPrompt.
|
||||
La biometrie est demandee a la lecture quand la cle a pu etre creee avec
|
||||
setUserAuthenticationRequired - deux alias existent pour cela, et l'en-tete
|
||||
du fichier dit lequel a servi.
|
||||
}
|
||||
|
||||
interface
|
||||
@@ -37,12 +37,22 @@ type
|
||||
/// Le coffre local existe-t-il ?
|
||||
function HasVault: Boolean;
|
||||
|
||||
/// Ecrit le coffre et l'ETag qui l'accompagne, chiffres ensemble.
|
||||
/// Ecrit le coffre, l'ETag qui l'accompagne et le mot de passe de sync,
|
||||
/// chiffres ensemble.
|
||||
///
|
||||
/// POURQUOI LE MOT DE PASSE EST LA : sans lui, un instantane nouveau ne peut
|
||||
/// pas etre ouvert sans intervention, et chaque modification faite sur le
|
||||
/// poste ferait apparaitre une demande de mot de passe au lancement - que
|
||||
/// l'utilisateur finirait par expedier sans la lire. Le fichier contient deja
|
||||
/// tous les mots de passe en clair ; celui-ci n'y ajoute presque rien en
|
||||
/// exposition, et rend la synchronisation silencieuse.
|
||||
///
|
||||
/// ARequireAuth demande une cle deverrouillee par biometrie ou code. Si
|
||||
/// l'appareil n'a aucune de ces protections, on retombe silencieusement sur
|
||||
/// une cle sans contrainte : mieux vaut un cache lie au materiel que pas de
|
||||
/// cache du tout. VaultNeedsAuth dira ensuite ce qui a reellement ete fait.
|
||||
procedure SaveVault(const APlainJson, AETag: string; ARequireAuth: Boolean = False);
|
||||
procedure SaveVault(const APlainJson, AETag, ASyncPwd: string;
|
||||
ARequireAuth: Boolean = False);
|
||||
|
||||
/// Le coffre local exige-t-il une authentification pour etre lu ? Lu dans
|
||||
/// l'en-tete du fichier, sans dechiffrer quoi que ce soit.
|
||||
@@ -50,7 +60,9 @@ function VaultNeedsAuth: Boolean;
|
||||
|
||||
/// Relit le coffre. False si absent ou illisible (cle perdue apres une
|
||||
/// reinstallation, fichier altere) - dans ce cas il faut retelecharger.
|
||||
function LoadVault(out APlainJson, AETag: string): Boolean;
|
||||
/// ASyncPwd est vide pour un fichier ecrit avant que le mot de passe y soit
|
||||
/// range : l'appelant redemande alors, et le prochain enregistrement le pose.
|
||||
function LoadVault(out APlainJson, AETag, ASyncPwd: string): Boolean;
|
||||
|
||||
/// Efface le coffre local. A appeler au moindre doute : il se retelecharge.
|
||||
procedure ClearVault;
|
||||
@@ -311,7 +323,8 @@ begin
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure SaveVault(const APlainJson, AETag: string; ARequireAuth: Boolean);
|
||||
procedure SaveVault(const APlainJson, AETag, ASyncPwd: string;
|
||||
ARequireAuth: Boolean);
|
||||
{$IF DEFINED(ANDROID)}
|
||||
var
|
||||
LWrap: TJSONObject;
|
||||
@@ -326,6 +339,7 @@ begin
|
||||
LWrap := TJSONObject.Create;
|
||||
try
|
||||
LWrap.AddPair('etag', AETag);
|
||||
LWrap.AddPair('pwd', ASyncPwd);
|
||||
LWrap.AddPair('vault', APlainJson);
|
||||
LPlain := TEncoding.UTF8.GetBytes(LWrap.ToJSON);
|
||||
finally
|
||||
@@ -367,7 +381,7 @@ begin
|
||||
end;
|
||||
{$ENDIF}
|
||||
|
||||
function LoadVault(out APlainJson, AETag: string): Boolean;
|
||||
function LoadVault(out APlainJson, AETag, ASyncPwd: string): Boolean;
|
||||
{$IF DEFINED(ANDROID)}
|
||||
var
|
||||
LRaw, LIv, LCt, LPlain: TBytes;
|
||||
@@ -377,6 +391,7 @@ begin
|
||||
Result := False;
|
||||
APlainJson := '';
|
||||
AETag := '';
|
||||
ASyncPwd := '';
|
||||
if not TFile.Exists(VaultPath) then Exit;
|
||||
|
||||
try
|
||||
@@ -402,6 +417,7 @@ begin
|
||||
if LWrap = nil then Exit;
|
||||
try
|
||||
AETag := LWrap.GetValue<string>('etag', '');
|
||||
ASyncPwd := LWrap.GetValue<string>('pwd', '');
|
||||
APlainJson := LWrap.GetValue<string>('vault', '');
|
||||
Result := APlainJson <> '';
|
||||
finally
|
||||
@@ -431,6 +447,7 @@ begin
|
||||
Result := False;
|
||||
APlainJson := '';
|
||||
AETag := '';
|
||||
ASyncPwd := '';
|
||||
end;
|
||||
{$ENDIF}
|
||||
|
||||
|
||||
@@ -61,6 +61,21 @@ Trois écrans dans une seule forme, construits en code (`mainApk.pas`) :
|
||||
déverrouillage, liste, détail. Une forme par écran aurait coûté trois `.fmx`
|
||||
pour des dispositions triviales.
|
||||
|
||||
**Afficher d'abord, synchroniser ensuite.** Au lancement, le coffre local
|
||||
s'ouvre tout seul (biométrie si la clé l'exige) et la liste apparaît sans
|
||||
réseau ni dérivation ; le `GET` part derrière, en silence. Un 304 ne se voit
|
||||
pas, un échec réseau non plus — hors ligne est un état normal, et une alerte à
|
||||
chaque lancement apprendrait surtout à ignorer les alertes. Seul un instantané
|
||||
**nouveau** se manifeste, en rafraîchissant la liste.
|
||||
|
||||
C'est ce qui justifie de ranger le **mot de passe de sync** dans le fichier
|
||||
chiffré, à côté du coffre : sans lui, un instantané nouveau ne peut pas être
|
||||
ouvert, et chaque modification faite sur le poste ferait apparaître une invite
|
||||
au lancement. Le fichier contient déjà tous les mots de passe en clair ;
|
||||
celui-ci n'y ajoute presque rien en exposition. Ce qui est tapé dans le champ
|
||||
l'emporte sur ce qui est rangé — c'est ainsi qu'on corrige un mot de passe qui
|
||||
ne convient plus.
|
||||
|
||||
L'écran de déverrouillage s'adapte à ce que l'appareil détient : coffre local
|
||||
présent → *Ouvrir* (biométrie si la clé l'exige) ; absent → seul le
|
||||
téléchargement peut en produire un. La liste filtre à la frappe sur le nom, le
|
||||
|
||||
Reference in New Issue
Block a user