feat(android): change a password from the phone, section 4 complete
Pull, patch, re-encrypt, publish - in that order, and against the SERVER's snapshot rather than the local cache. That is what replaces the merge the plan asked for: anything untouched is already everyone's newest version, so there is nothing to reconcile. The only conflict left is another device publishing between our read and our write. Gitea refuses it on the sha with a 409, and the whole cycle repeats, decryption included, since the content changed. Three attempts: past that the other device is publishing in a loop and insisting would not help. The editable field is marked in Delphi rather than recognised by its caption in the page. A caption is prose; the day it changes, matching on it would silently stop offering the edit. The editor is inline rather than window.prompt: a JavaScript prompt needs a WebChromeClient that FMX does not provide, and prompt() then returns null without a word - the button would appear to do nothing. The value shows in clear while typing, deliberately: you are entering a password you cannot read back anywhere else, and masking it would mean confirming a keystroke blind. This is the one command that carries a secret over the bridge, agreed beforehand. It stays in the process, the iframe carrying it is destroyed, and the handler logs the action only - never the query. Only the password is editable. Creating or deleting an entry would need tombstones and uuid generation, and changing a password covers the need without either. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+121
@@ -191,6 +191,7 @@ type
|
||||
procedure WriteTest;
|
||||
{$ENDIF}
|
||||
procedure OpenEntry(AVaultIndex: Integer);
|
||||
procedure SaveEntryPassword(const ANewValue: string);
|
||||
procedure OnClipTimer(Sender: TObject);
|
||||
procedure OnDeadline(Sender: TObject);
|
||||
procedure OnHardwareBack(Sender: TObject; var Key: Word;
|
||||
@@ -521,6 +522,120 @@ begin
|
||||
end;
|
||||
end;
|
||||
|
||||
/// Change le mot de passe de l'entree ouverte, et publie.
|
||||
///
|
||||
/// TIRER, MODIFIER, RECHIFFRER, PUBLIER - dans cet ordre, et sur l'instantane
|
||||
/// DU SERVEUR, pas sur le cache local. C'est ce qui remplace la fusion : ce
|
||||
/// qu'on ne touche pas est deja la version la plus recente de tout le monde.
|
||||
///
|
||||
/// Le seul conflit possible est qu'un autre appareil publie entre notre lecture
|
||||
/// et notre ecriture. Gitea le refuse alors sur le sha (409), et on recommence
|
||||
/// tout - y compris le dechiffrement, parce que le contenu a change. Trois
|
||||
/// essais : au-dela, c'est que l'autre appareil publie en boucle, et insister
|
||||
/// n'y changerait rien.
|
||||
procedure TForm8.SaveEntryPassword(const ANewValue: string);
|
||||
var
|
||||
LUuid, LTitle: string;
|
||||
begin
|
||||
if FBusy then Exit;
|
||||
if FCurrent.Uuid = '' then
|
||||
begin
|
||||
WebCall('PM.toast("This entry has no uuid - cannot be updated")');
|
||||
Exit;
|
||||
end;
|
||||
if FSyncPwd = '' then
|
||||
begin
|
||||
WebCall('PM.toast("Sync password missing - open the vault first")');
|
||||
Exit;
|
||||
end;
|
||||
LUuid := FCurrent.Uuid;
|
||||
LTitle := FCurrent.Display;
|
||||
SetBusy(True);
|
||||
|
||||
TThread.CreateAnonymousThread(
|
||||
procedure
|
||||
var
|
||||
LClient: TSyncClient;
|
||||
LGet, LPut: TSyncResult;
|
||||
LPlain, LNew, LBody, LErr: string;
|
||||
LAttempt: Integer;
|
||||
LDone: Boolean;
|
||||
begin
|
||||
LClient := TSyncClient.Create(FUrl, FToken);
|
||||
LDone := False;
|
||||
LErr := '';
|
||||
try
|
||||
for LAttempt := 1 to 3 do
|
||||
begin
|
||||
Log(Format('save: attempt %d - GET', [LAttempt]));
|
||||
// Sans empreinte connue : on VEUT le corps, pas un 304.
|
||||
LGet := LClient.Fetch;
|
||||
if LGet.Status <> ssOK then
|
||||
begin
|
||||
LErr := Format('read failed (HTTP %d) %s', [LGet.HttpCode, LGet.Error]);
|
||||
Break;
|
||||
end;
|
||||
|
||||
try
|
||||
LPlain := DecryptContainer(LGet.Container, FSyncPwd);
|
||||
LNew := PatchEntryField(LPlain, LUuid, 'password', ANewValue);
|
||||
except
|
||||
on E: Exception do
|
||||
begin
|
||||
LErr := E.Message;
|
||||
Break;
|
||||
end;
|
||||
end;
|
||||
|
||||
LBody := EncryptContainer(LNew, FSyncPwd);
|
||||
LPut := LClient.Put(LBody, LGet.ETag, 'password change from phone');
|
||||
if LPut.Status = ssOK then
|
||||
begin
|
||||
Log('save: published, new sha ' + LPut.ETag);
|
||||
// Le cache local suit le distant, avec SON empreinte : les laisser
|
||||
// diverger ferait retelecharger et redechiffrer a la prochaine
|
||||
// ouverture pour retrouver ce qu'on vient d'ecrire.
|
||||
FETag := LPut.ETag;
|
||||
try
|
||||
SaveVault(LNew, FETag, FSyncPwd, BiometricAvailable);
|
||||
except
|
||||
on E: Exception do Log(' cache not written: ' + E.Message);
|
||||
end;
|
||||
LDone := True;
|
||||
Break;
|
||||
end;
|
||||
|
||||
if LPut.Status = ssConflict then
|
||||
begin
|
||||
Log('save: 409 - another device published, re-reading');
|
||||
Continue;
|
||||
end;
|
||||
|
||||
LErr := Format('write failed (HTTP %d) %s', [LPut.HttpCode, LPut.Error]);
|
||||
Break;
|
||||
end;
|
||||
|
||||
if (not LDone) and (LErr = '') then
|
||||
LErr := 'the remote kept changing - try again';
|
||||
|
||||
TThread.Queue(nil,
|
||||
procedure
|
||||
begin
|
||||
if LDone then
|
||||
begin
|
||||
AdoptVault(LNew);
|
||||
WebCall('PM.toast(' + JsArg('Saved: ' + LTitle) + ')');
|
||||
end
|
||||
else
|
||||
WebCall('PM.toast(' + JsArg('Not saved - ' + LErr) + ')');
|
||||
end);
|
||||
finally
|
||||
LClient.Free;
|
||||
TThread.Queue(nil, procedure begin SetBusy(False); end);
|
||||
end;
|
||||
end).Start;
|
||||
end;
|
||||
|
||||
/// Essai du chemin d'ecriture, VERS UN AUTRE FICHIER que le coffre.
|
||||
///
|
||||
/// Le premier PUT est le moment ou une erreur de transport se paie en donnees
|
||||
@@ -702,6 +817,12 @@ begin
|
||||
|
||||
else if LAction = 'sync' then
|
||||
DoSync(nil)
|
||||
|
||||
// La NOUVELLE valeur remonte par le pont - c'est la seule commande qui
|
||||
// transporte un secret. Elle ne quitte pas le processus, l'iframe qui la
|
||||
// porte est detruite, et HandleWebCommand ne journalise que l'action.
|
||||
else if LAction = 'setpwd' then
|
||||
SaveEntryPassword(TNetEncoding.URL.Decode(Param('v')))
|
||||
{$IFDEF DEBUG}
|
||||
else if LAction = 'writetest' then
|
||||
WriteTest
|
||||
|
||||
Reference in New Issue
Block a user