fix(android): locking the vault no longer discards the typed settings

Reported as "the token field saves nothing", and it was exact: LockVault
called PushConfig, which pushes the STORED server settings back into the page.
Every lock therefore overwrote a freshly typed token with the old one -
including the three-minute auto-lock, so it happened with no gesture from the
user at all, and the field looked like it forgot on its own.

The user's own sequence isolated it: deleting one character gave a 401, which
proves the typed value does reach the wire; then open, lock, sync gave a 200
again, the lock having restored the old token in between.

PushConfig now runs at startup only. It was pointless in the other four places
anyway: PM.wipe clears the list and the detail, never the configuration fields.

Along the way, and part of the same confusion: the three settings fields had no
labels, only placeholders that vanish once filled - and since the token was
masked this morning it became indistinguishable from the sync password sitting
right below it. They are labelled now, and the log reports both fingerprints so
a value landing in the neighbouring field shows up immediately.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
r-zakarya
2026-08-17 18:52:45 +01:00
parent 90e8fabf58
commit 683655415f
2 changed files with 42 additions and 8 deletions
+10
View File
@@ -186,6 +186,8 @@
font: inherit; outline: none; font: inherit; outline: none;
} }
.field-in::placeholder { color: var(--muted); } .field-in::placeholder { color: var(--muted); }
.in-label { color: var(--muted); font-size: 12px; margin: 10px 0 -2px 4px; }
.in-label span { opacity: .7; }
#log { #log {
margin: 10px 0 0; padding: 10px 12px; max-height: 34vh; overflow: auto; margin: 10px 0 0; padding: 10px 12px; max-height: 34vh; overflow: auto;
background: rgba(0,0,0,.20); border-radius: var(--radius-s); background: rgba(0,0,0,.20); border-radius: var(--radius-s);
@@ -219,7 +221,13 @@
depot, jamais dans le coffre. --> depot, jamais dans le coffre. -->
<button class="btn wide" id="btnWriteTest" hidden>Write test (debug)</button> <button class="btn wide" id="btnWriteTest" hidden>Write test (debug)</button>
</div> </div>
<!-- DES LIBELLES, PAS SEULEMENT DES INDICATIONS D'AIDE. Celles-ci
disparaissent des que le champ est rempli, et depuis que le jeton est
masque comme le mot de passe, deux champs en points noirs se
succedent sans rien qui les distingue - on colle alors dans le mauvais
sans que rien ne le signale. -->
<div id="cfg" hidden> <div id="cfg" hidden>
<div class="in-label">Endpoint URL</div>
<input class="field-in" id="url" placeholder="endpoint URL" <input class="field-in" id="url" placeholder="endpoint URL"
autocomplete="off" autocorrect="off" autocapitalize="none" autocomplete="off" autocorrect="off" autocapitalize="none"
spellcheck="false" inputmode="url"> spellcheck="false" inputmode="url">
@@ -227,8 +235,10 @@
clair alors qu'il ouvre le depot : le moins grave des trois secrets clair alors qu'il ouvre le depot : le moins grave des trois secrets
de cet ecran, mais le seul qu'un regard par-dessus l'epaule de cet ecran, mais le seul qu'un regard par-dessus l'epaule
attrapait. On le saisit une fois ; le relire n'a aucun usage. --> attrapait. On le saisit une fois ; le relire n'a aucun usage. -->
<div class="in-label">Gitea token <span>&#183; needs read AND write</span></div>
<input class="field-in" id="tok" placeholder="bearer token" <input class="field-in" id="tok" placeholder="bearer token"
type="password" autocomplete="off"> type="password" autocomplete="off">
<div class="in-label">Sync password <span>&#183; decrypts the vault</span></div>
<input class="field-in" id="pwd" placeholder="sync password" <input class="field-in" id="pwd" placeholder="sync password"
type="password" autocomplete="off"> type="password" autocomplete="off">
</div> </div>
+32 -8
View File
@@ -32,7 +32,7 @@ interface
uses uses
System.SysUtils, System.Types, System.UITypes, System.Classes, System.SysUtils, System.Types, System.UITypes, System.Classes,
System.Diagnostics, System.Rtti, System.NetEncoding, System.DateUtils, System.Diagnostics, System.Rtti, System.NetEncoding, System.DateUtils,
System.Generics.Collections, System.IOUtils, System.Generics.Collections, System.IOUtils, System.Hash,
FMX.Types, FMX.Controls, FMX.Forms, FMX.Graphics, FMX.Dialogs, FMX.Platform, FMX.Types, FMX.Controls, FMX.Forms, FMX.Graphics, FMX.Dialogs, FMX.Platform,
FMX.Controls.Presentation, FMX.StdCtrls, FMX.Memo, FMX.Memo.Types, FMX.Controls.Presentation, FMX.StdCtrls, FMX.Memo, FMX.Memo.Types,
FMX.ScrollBox, FMX.Layouts, FMX.Edit, FMX.ListBox, FMX.Objects, FMX.ScrollBox, FMX.Layouts, FMX.Edit, FMX.ListBox, FMX.Objects,
@@ -418,6 +418,14 @@ begin
SetLength(FWebPending, 0); SetLength(FWebPending, 0);
end; end;
/// Empreinte courte d'un secret, pour le journal. On veut savoir LEQUEL des
/// jetons est en place, jamais sa valeur : huit caracteres de son SHA-256
/// suffisent a les distinguer et n'en revelent rien.
function Fingerprint(const ASecret: string): string;
begin
if ASecret = '' then Exit('(vide)');
Result := Copy(THashSHA2.GetHashString(ASecret), 1, 8);
end;
/// Chiffre un petit conteneur, le relit, et le laisse sur le disque prive pour /// Chiffre un petit conteneur, le relit, et le laisse sur le disque prive pour
/// qu'une autre implementation le verifie. Iterations volontairement basses : /// qu'une autre implementation le verifie. Iterations volontairement basses :
/// ce qu'on eprouve ici est le CABLAGE - cle, IV, place du tag - pas le cout /// ce qu'on eprouve ici est le CABLAGE - cle, IV, place du tag - pas le cout
@@ -496,8 +504,8 @@ begin
TTimeZone.Local.ToUniversalTime(Now))]), TTimeZone.Local.ToUniversalTime(Now))]),
FSyncPwd, 1000); FSyncPwd, 1000);
Log('PUT ' + LUrl); Log('PUT ' + LUrl);
Log(' token ' + IntToStr(Length(FToken)) + ' chars, body ' + Log(Format(' token %s (%d chars), body %d chars',
IntToStr(Length(LBody)) + ' chars'); [Fingerprint(FToken), Length(FToken), Length(LBody)]));
// Sha vide : on cree le fichier, ou on echoue en 409 s'il existe deja - // Sha vide : on cree le fichier, ou on echoue en 409 s'il existe deja -
// ce qui prouverait le controle de concurrence aussi surement. // ce qui prouverait le controle de concurrence aussi surement.
LRes := LClient.Put(LBody, '', 'write path probe from phone'); LRes := LClient.Put(LBody, '', 'write path probe from phone');
@@ -528,6 +536,8 @@ begin
FUrl := LUrl; FUrl := LUrl;
if LToken <> '' then FToken := LToken; if LToken <> '' then FToken := LToken;
end; end;
Log(Format('config loaded: token %s (%d chars), url %s',
[Fingerprint(FToken), Length(FToken), FUrl]));
WebCall(Format('PM.setConfig(%s, %s, %s, %s)', WebCall(Format('PM.setConfig(%s, %s, %s, %s)',
[JsArg(FUrl), JsArg(FToken), JsArg(FPwd), [JsArg(FUrl), JsArg(FToken), JsArg(FPwd),
// Replier des champs vides revient a cacher la seule chose a faire. // Replier des champs vides revient a cacher la seule chose a faire.
@@ -560,7 +570,16 @@ end;
procedure TForm8.OnWebNavigate(ASender: TObject; const AUrl: string); procedure TForm8.OnWebNavigate(ASender: TObject; const AUrl: string);
begin begin
// Le moteur navigue aussi pour charger la page elle-meme. // Le moteur navigue aussi pour charger la page elle-meme.
if AUrl.StartsWith('cmd://') then HandleWebCommand(AUrl.Substring(6)); // L'ACTION SEULE, jamais la requete : elle porte le jeton et le mot de passe.
// Sans cette trace on ne peut pas distinguer « la commande n'arrive pas » de
// « elle arrive et ne fait rien », et les deux se ressemblent de l'exterieur.
if AUrl.StartsWith('cmd://') then
begin
var LDbg := AUrl.Substring(6);
if LDbg.Contains('?') then LDbg := LDbg.Substring(0, LDbg.IndexOf('?')) + '?...';
Log('cmd ' + LDbg);
HandleWebCommand(AUrl.Substring(6));
end;
end; end;
procedure TForm8.HandleWebCommand(const AUrl: string); procedure TForm8.HandleWebCommand(const AUrl: string);
@@ -613,6 +632,11 @@ begin
FUrl := TNetEncoding.URL.Decode(Param('u')).Trim; FUrl := TNetEncoding.URL.Decode(Param('u')).Trim;
FToken := TNetEncoding.URL.Decode(Param('t')).Trim; FToken := TNetEncoding.URL.Decode(Param('t')).Trim;
FPwd := TNetEncoding.URL.Decode(Param('p')); FPwd := TNetEncoding.URL.Decode(Param('p'));
// L'empreinte du mot de passe AUSSI : si une saisie destinee au jeton
// atterrit dans le champ voisin - les deux sont masques - c'est celle-ci
// qui bouge, et le jeton reste inchange sans que rien ne l'explique.
Log(Format('config from page: token %s, pwd %s',
[Fingerprint(FToken), Fingerprint(FPwd)]));
end end
else if LAction = 'openvault' then else if LAction = 'openvault' then
@@ -678,7 +702,10 @@ begin
// Les lignes du detail portent des valeurs EN CLAIR (FRows) : les detruire // Les lignes du detail portent des valeurs EN CLAIR (FRows) : les detruire
// fait partie du verrouillage, pas de l'affichage. // fait partie du verrouillage, pas de l'affichage.
FCurrent := Default(TVaultEntry); FCurrent := Default(TVaultEntry);
PushConfig; // PAS DE PushConfig HORS DEMARRAGE. Redescendre les coordonnees du serveur
// ecrase ce que l'utilisateur vient de taper - l'auto-verrouillage le faisait
// donc sans le moindre geste de sa part, et le champ jeton semblait ne rien
// retenir. Verrouiller efface le clair ; la configuration n'a rien a y voir.
ShowUnlock; ShowUnlock;
end; end;
@@ -775,7 +802,6 @@ begin
// Un coffre local illisible se remplace, il ne se repare pas. // Un coffre local illisible se remplace, il ne se repare pas.
Log('Local vault corrupt: ' + E.Message); Log('Local vault corrupt: ' + E.Message);
ClearVault; ClearVault;
PushConfig;
ShowUnlock; ShowUnlock;
end; end;
end; end;
@@ -785,7 +811,6 @@ begin
// ShowUnlock AVANT le message : il repositionne le statut selon ce que // ShowUnlock AVANT le message : il repositionne le statut selon ce que
// l'appareil detient, et ecraserait donc l'explication. // l'appareil detient, et ecraserait donc l'explication.
FVaultUnreadable := True; FVaultUnreadable := True;
PushConfig;
ShowUnlock; ShowUnlock;
Log('Local vault unreadable: ' + LastStoreError); Log('Local vault unreadable: ' + LastStoreError);
Status('Local vault unreadable - see log'); Status('Local vault unreadable - see log');
@@ -841,7 +866,6 @@ begin
FVaultUnreadable := True; FVaultUnreadable := True;
Log('No usable protection on this device - vault kept but unreadable'); Log('No usable protection on this device - vault kept but unreadable');
Log(' ' + LastBiometricError); Log(' ' + LastBiometricError);
PushConfig;
ShowUnlock; ShowUnlock;
Status('Screen lock missing - download again to replace the cache'); Status('Screen lock missing - download again to replace the cache');
end; end;