perf(android): saving no longer pulls the vault it already holds

The crypto is down to about 135 ms, so the wait was never the crypto: the
log says HTTP 200 in 3244 ms for 266299 bytes. Every save fetched the whole
vault, decrypted it, patched it, and sent it back.

The fetch existed to be sure of modifying the latest published version. The
sha proves that better and for free: publish against the snapshot in hand,
and if another device published in between, Gitea refuses with a 409 - then,
and only then, pull for real. The retry loop was already written for exactly
that.

The snapshot kept is the one RECEIVED, never a re-serialisation of TVault:
that would drop everything the phone does not read - attachment bodies, the
avatar, tombstones - which is the invariant PMA.VaultPatch exists for. It
goes at lock, with the rest of the plaintext.

Note this makes the 409 path load-bearing, and it has still never run. It is
now what guarantees a save cannot clobber another device.

The PUT still carries the whole container; that is inherent to the format.
This commit is contained in:
r-zakarya
2026-08-21 15:00:46 +01:00
parent 527a953b2a
commit bb222e1903
+37
View File
@@ -130,6 +130,18 @@ type
/// Elle part au verrouillage, avec lui. /// Elle part au verrouillage, avec lui.
FKey, FKeySalt: TBytes; FKey, FKeySalt: TBytes;
FKeyIters: Integer; FKeyIters: Integer;
/// L'INSTANTANE BRUT, tel qu'il a ete recu. Pas une reserialisation de
/// TVault : celle-ci perdrait tout ce que le telephone ne lit pas -
/// le contenu des pieces jointes, l'avatar, les tombstones - c'est
/// l'invariant de PMA.VaultPatch.
///
/// Il permet d'enregistrer SANS RETIRER le coffre d'abord. Le tirage
/// coutait 3,2 s pour 266 Ko, a chaque sauvegarde, alors qu'on tient deja
/// ce qu'il rapporte. Ce qu'il apportait vraiment, c'etait la certitude de
/// modifier la derniere version publiee - et le sha la donne aussi : si un
/// autre appareil a publie entre-temps, Gitea refuse en 409 et on retire
/// pour de bon.
FPlain: string;
/// Code d'ouverture du cache local, choisi dans l'application. JAMAIS /// Code d'ouverture du cache local, choisi dans l'application. JAMAIS
/// ENREGISTRE : il est vide a chaque lancement et doit etre retape. C'est /// ENREGISTRE : il est vide a chaque lancement et doit etre retape. C'est
/// ce qui le distingue du jeton, dont la persistance est un confort sans /// ce qui le distingue du jeton, dont la persistance est un confort sans
@@ -729,6 +741,21 @@ begin
begin begin
LClient := TSyncClient.Create(FUrl, FToken); LClient := TSyncClient.Create(FUrl, FToken);
for LAttempt := 1 to 3 do for LAttempt := 1 to 3 do
begin
// PREMIER ESSAI SANS TIRER. On tient l'instantane et le sha qui va
// avec ; le tirage ne servait qu'a s'assurer de modifier la
// derniere version publiee, et le sha le prouve mieux : s'il n'est
// plus le bon, Gitea refuse. On ne retire donc que lorsqu'on a
// vraiment quelque chose a apprendre.
if (LAttempt = 1) and (FPlain <> '') and (FETag <> '') then
begin
Log('save: publishing against the snapshot in hand');
LGet.Status := ssOK;
LGet.Container := '';
LGet.ETag := FETag;
LPlain := FPlain;
end
else
begin begin
Log(Format('save: attempt %d - GET', [LAttempt])); Log(Format('save: attempt %d - GET', [LAttempt]));
// Sans empreinte connue : on VEUT le corps, pas un 304. // Sans empreinte connue : on VEUT le corps, pas un 304.
@@ -738,6 +765,8 @@ begin
LErr := Format('read failed (HTTP %d) %s', [LGet.HttpCode, LGet.Error]); LErr := Format('read failed (HTTP %d) %s', [LGet.HttpCode, LGet.Error]);
Break; Break;
end; end;
LPlain := '';
end;
try try
// UNE SEULE DERIVATION PAR ENREGISTREMENT : la variante Ex rend // UNE SEULE DERIVATION PAR ENREGISTREMENT : la variante Ex rend
@@ -745,6 +774,8 @@ begin
// reutilise. Deriver deux fois le meme mot de passe coutait une // reutilise. Deriver deux fois le meme mot de passe coutait une
// seconde fois quatre a six secondes, pour une cle equivalente. // seconde fois quatre a six secondes, pour une cle equivalente.
LSw := TStopwatch.StartNew; LSw := TStopwatch.StartNew;
// LPlain deja pose quand on publie contre l'instantane en main.
if LPlain = '' then
LPlain := OpenContainer(LGet.Container, FSyncPwd); LPlain := OpenContainer(LGet.Container, FSyncPwd);
LKey := FKey; LSalt := FKeySalt; LIter := FKeyIters; LKey := FKey; LSalt := FKeySalt; LIter := FKeyIters;
LNew := APatch(LPlain); LNew := APatch(LPlain);
@@ -760,7 +791,10 @@ begin
LSw := TStopwatch.StartNew; LSw := TStopwatch.StartNew;
LBody := EncryptContainerWithKey(LNew, LKey, LSalt, LIter); LBody := EncryptContainerWithKey(LNew, LKey, LSalt, LIter);
Log(Format('save: re-encrypt in %d ms', [LSw.ElapsedMilliseconds])); Log(Format('save: re-encrypt in %d ms', [LSw.ElapsedMilliseconds]));
LSw := TStopwatch.StartNew;
LPut := LClient.Put(LBody, LGet.ETag, AComment); LPut := LClient.Put(LBody, LGet.ETag, AComment);
Log(Format('save: PUT %d chars in %d ms',
[Length(LBody), LSw.ElapsedMilliseconds]));
if LPut.Status = ssOK then if LPut.Status = ssOK then
begin begin
Log('save: published, new sha ' + LPut.ETag); Log('save: published, new sha ' + LPut.ETag);
@@ -781,6 +815,7 @@ begin
if LPut.Status = ssConflict then if LPut.Status = ssConflict then
begin begin
Log('save: 409 - another device published, re-reading'); Log('save: 409 - another device published, re-reading');
LPlain := '';
Continue; Continue;
end; end;
@@ -1081,12 +1116,14 @@ begin
FKey := nil; FKey := nil;
FKeySalt := nil; FKeySalt := nil;
FKeyIters := 0; FKeyIters := 0;
FPlain := '';
ShowUnlock; ShowUnlock;
end; end;
procedure TForm8.AdoptVault(const APlainJson: string); procedure TForm8.AdoptVault(const APlainJson: string);
begin begin
FVault.LoadFromJson(APlainJson); FVault.LoadFromJson(APlainJson);
FPlain := APlainJson;
Log(' ' + FVault.LiveCount.ToString + ' entries, ' + Log(' ' + FVault.LiveCount.ToString + ' entries, ' +
FVault.TrashCount.ToString + ' in trash'); FVault.TrashCount.ToString + ' in trash');
FVaultUnreadable := False; FVaultUnreadable := False;