feat(android): patch the snapshot in place, never rebuild it

Section 4 step 4. TVault reads a subset of the fields, so re-serialising the
vault from what it understood would drop attachment contents, created_at, the
account avatar, tombstones, and whatever the desktop adds next - and the
desktop would adopt that amputated version on its next pull, the merge being
last-write-wins on updated_at. Silent, and final.

So nothing is rebuilt: the received JSON is edited in place and the rest is
handed back untouched. What the phone does not understand travels intact by
construction rather than by vigilance.

A corollary worth naming, because it removes the step the plan feared most:
since the patch applies to the snapshot just pulled, there is no merge to
write. Anything untouched is already the server newest version. The only
possible conflict is another device publishing between our read and our write,
which Gitea refuses on the sha.

The self-check tests what is PRESERVED, not what changes: a field unknown to
TVault, an attachment content_b64, the untouched sibling entry, the
tombstones. It reports "patch keeps what it does not understand" on the device
at every debug launch.

Also confirmed by this log: the write token now survives a restart, so the 304
settings fix holds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
r-zakarya
2026-08-17 19:35:12 +01:00
parent 5324785ed9
commit 6bb056220e
3 changed files with 150 additions and 2 deletions
+48 -1
View File
@@ -187,6 +187,7 @@ type
procedure PushConfig;
{$IFDEF DEBUG}
procedure CryptoSelfCheck;
procedure PatchSelfCheck;
procedure WriteTest;
{$ENDIF}
procedure OpenEntry(AVaultIndex: Integer);
@@ -212,7 +213,7 @@ implementation
uses
PMA.Crypto, PMA.SyncClient, PMA.Store, PMA.Biometric, PMA.Totp,
PMA.ScreenGuard, PMA.Clipboard, PMA.Theme, PMA.WebUI, System.JSON;
PMA.ScreenGuard, PMA.Clipboard, PMA.Theme, PMA.WebUI, PMA.VaultPatch, System.JSON;
{$R *.fmx}
@@ -468,12 +469,58 @@ begin
System.IOUtils.TPath.GetHomePath, 'selfcheck.json');
TFile.WriteAllText(LPath, LContainer, TEncoding.UTF8);
Log('selfcheck: container written for cross-check');
PatchSelfCheck;
except
on E: Exception do
Log('selfcheck FAILED: ' + E.ClassName + ' - ' + E.Message);
end;
end;
/// Verifie ce que le patch PRESERVE, pas seulement ce qu'il change.
///
/// C'est l'invariant de §4 : un champ que le telephone ne comprend pas doit
/// ressortir intact, sinon une modification faite ici efface silencieusement
/// des donnees chez tous les autres appareils. On le controle sur un champ
/// inconnu de TVault et sur une piece jointe, qui sont precisement les deux
/// que la reserialisation naive perdrait.
procedure TForm8.PatchSelfCheck;
const
SNAP =
'{"version":1,"username":"test","folders":[{"name":"f"}],' +
'"entries":[' +
'{"uuid":"u1","title":"a","password":"old","updated_at":"2020-01-01T00:00:00Z",' +
'"attachments":[{"filename":"x.pdf","content_b64":"KEEPME"}],' +
'"champ_inconnu":"KEEPTOO"},' +
'{"uuid":"u2","title":"b","password":"other"}],' +
'"tombstones":[{"uuid":"u9"}]}';
var
LOut: string;
LRoot: TJSONObject;
LEntry: TJSONObject;
LOk: Boolean;
begin
try
LOut := PatchEntryField(SNAP, 'u1', 'password', 'NEW');
LRoot := TJSONObject.ParseJSONValue(LOut) as TJSONObject;
try
LEntry := (LRoot.GetValue<TJSONArray>('entries').Items[0]) as TJSONObject;
LOk := (LEntry.GetValue<string>('password') = 'NEW')
and (LEntry.GetValue<string>('updated_at') <> '2020-01-01T00:00:00Z')
and (LEntry.GetValue<string>('champ_inconnu') = 'KEEPTOO')
and (LOut.Contains('KEEPME')) // piece jointe intacte
and (LOut.Contains('"tombstones"')) // et le reste du document
and (LOut.Contains('"other"')); // l'autre entree non touchee
if LOk then Log('selfcheck: patch keeps what it does not understand')
else Log('selfcheck: PATCH LOSES DATA - do not enable writing');
finally
LRoot.Free;
end;
except
on E: Exception do
Log('selfcheck patch FAILED: ' + E.ClassName + ' - ' + E.Message);
end;
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