feat: the last three passwords travel, and the phone can show them

The desktop keeps twenty previous versions per entry and never sent any. It
now sends three.

Three, not twenty, because the trade is not symmetrical. On the desktop they
sit encrypted column by column and never move. In the snapshot everything
travels in the clear inside the container, and comes down into a phone cache
sealed only by the device key - not by the master password. An old password
stays sensitive: it is often still valid somewhere else. The real use of the
list is short - "I changed it recently, what was it?" - and three cover it.
The other seventeen would be copies with no need facing them.

The phone reads them ON DEMAND from the raw snapshot, never into TVault:
loading them with the vault would put every entry's old passwords in the clear
in memory from the moment it opens, for something one looks at now and then.

Copying goes by RANK, not by value. The page displays the text but never sends
it back - that is the bridge's rule everywhere else, and an old password has
no reason to be the exception just because it happens to be on screen.

They leave with the plaintext at lock, on both sides.
This commit is contained in:
r-zakarya
2026-08-22 16:14:04 +01:00
parent 61c52e0edd
commit ba4ab18d0f
5 changed files with 225 additions and 5 deletions
+47
View File
@@ -99,6 +99,19 @@ function AttachmentContent(const APlainJson, AUuid: string;
function PatchEntryIcon(const APlainJson, AUuid, AUri: string;
ACustom: Boolean): string;
/// Les anciens mots de passe d'une entree, relus dans l'instantane BRUT.
///
/// A LA DEMANDE, ET JAMAIS DANS TVault. Les charger avec le coffre les
/// mettrait en clair en memoire pour TOUTES les entrees des l'ouverture, alors
/// qu'on en regarde une de temps en temps. Meme raison que pour les pieces
/// jointes, mais pour l'exposition plutot que pour le poids : un ancien mot de
/// passe est souvent encore valide ailleurs.
///
/// Rend une ligne par version, la plus recente d'abord, sous la forme
/// `date<TAB>mot de passe`. Vide s'il n'y en a pas - le poste ne l'envoie que
/// depuis peu, et un instantane plus ancien n'en porte aucun.
function PasswordHistory(const APlainJson, AUuid: string): TArray<string>;
/// Fond l'instantane LOCAL dans l'instantane DISTANT et rend le resultat.
///
/// LE DISTANT SERT DE BASE, jamais le local. Il porte ce que le telephone ne
@@ -309,6 +322,40 @@ begin
end;
end;
function PasswordHistory(const APlainJson, AUuid: string): TArray<string>;
var
LRoot, LEntry, LRow: TJSONObject;
LEntries, LHist: TJSONArray;
I, J: Integer;
begin
Result := nil;
if AUuid = '' then Exit;
LRoot := TJSONObject.ParseJSONValue(APlainJson) as TJSONObject;
if LRoot = nil then Exit;
try
if not LRoot.TryGetValue<TJSONArray>('entries', LEntries) then Exit;
for I := 0 to LEntries.Count - 1 do
begin
if not (LEntries.Items[I] is TJSONObject) then Continue;
LEntry := LEntries.Items[I] as TJSONObject;
if LEntry.GetValue<string>('uuid', '') <> AUuid then Continue;
if not LEntry.TryGetValue<TJSONArray>('password_history', LHist) then Exit;
// J, PAS I : la boucle exterieure cherche encore l'entree, et Delphi
// refuse - a juste titre - qu'on lui reecrive son compteur.
for J := 0 to LHist.Count - 1 do
begin
if not (LHist.Items[J] is TJSONObject) then Continue;
LRow := LHist.Items[J] as TJSONObject;
Result := Result + [LRow.GetValue<string>('changed_at', '') + #9 +
LRow.GetValue<string>('password', '')];
end;
Exit;
end;
finally
LRoot.Free;
end;
end;
function PatchEntryIcon(const APlainJson, AUuid, AUri: string;
ACustom: Boolean): string;
var