Files
Password-Manager/android-client/PMA.SyncClient.pas
T
r-zakarya 5fdb7f914e feat(android): keep the decrypted vault locally under an Android Keystore key
Opening a container costs ~4 s and the iteration count belongs to the desktop format, so the fix is to stop repeating it: derive once per snapshot, then read a local AES file which the hardware accelerates. The key is generated inside the Keystore and never leaves it - the file alone is useless, even rooted.

ETag and vault are encrypted together on purpose: an ETag outliving an unreadable vault would make the server answer 304 with nothing to show. A failed read wipes the cache rather than guessing.

Also normalises every Delphi source to pure ASCII: Delphi reads a BOM-less file as ANSI, so accented comments came back as mojibake, and each PowerShell rewrite added another layer.
2026-08-15 00:37:59 +01:00

144 lines
4.3 KiB
ObjectPascal

unit PMA.SyncClient;
{
Lecture de l'instantane chiffre depuis l'endpoint PHP (server-php/sync.php).
Ne dechiffre rien : il ramene des octets opaques et l'ETag qui les identifie.
Le dechiffrement est le travail de PMA.Crypto, et il coute 4 secondes - d'ou
l'importance de l'ETag, qui evite de le refaire quand rien n'a change.
Le serveur repond 404 aussi bien pour un jeton invalide que pour un
instantane absent : c'est voulu (un appelant non authentifie n'apprend pas
ce qui existe), et le client ne peut donc pas les distinguer. Le message
utilisateur doit couvrir les deux cas.
}
interface
uses
System.SysUtils, System.Classes, System.Net.HttpClient,
System.Net.URLClient, System.Net.HttpClientComponent;
type
TSyncStatus = (
ssOK, // 200 : nouveau contenu dans Container
ssNotModified, // 304 : l'ETag detenu est toujours le bon
ssNoSnapshot, // 404 : pas d'instantane, OU jeton invalide
ssRefused, // 403 : HTTPS exige par le serveur
ssHttpError, // autre code
ssNetworkError // rien n'a repondu
);
TSyncResult = record
Status: TSyncStatus;
Container: string; // le JSON chiffre, vide sauf sur ssOK
ETag: string; // a repasser au prochain appel
HttpCode: Integer;
Error: string; // message brut, pour le journal
end;
TSyncClient = class
private
FBaseUrl: string;
FToken: string;
FTimeoutMs: Integer;
public
constructor Create(const ABaseUrl, AToken: string);
/// GET de l'instantane. AKnownETag non vide -> If-None-Match, et un 304
/// evite de retelecharger ET de re-deriver la cle.
function Fetch(const AKnownETag: string = ''): TSyncResult;
property BaseUrl: string read FBaseUrl write FBaseUrl;
property Token: string read FToken write FToken;
/// Volontairement court : sur mobile, un reseau qui ne repond pas doit
/// rendre la main vite plutot que de figer l'ecran.
property TimeoutMs: Integer read FTimeoutMs write FTimeoutMs;
end;
implementation
constructor TSyncClient.Create(const ABaseUrl, AToken: string);
begin
inherited Create;
FBaseUrl := ABaseUrl.Trim;
FToken := AToken.Trim;
FTimeoutMs := 15000;
end;
function TSyncClient.Fetch(const AKnownETag: string): TSyncResult;
var
LHttp: THTTPClient;
LResp: IHTTPResponse;
LStream: TStringStream;
begin
Result := Default(TSyncResult);
Result.Status := ssNetworkError;
if (FBaseUrl = '') or (FToken = '') then
begin
Result.Error := 'URL ou jeton non configure';
Exit;
end;
LHttp := THTTPClient.Create;
try
LHttp.ConnectionTimeout := FTimeoutMs;
LHttp.ResponseTimeout := FTimeoutMs;
// Pas de redirection suivie : une redirection vers un autre hote nous
// ferait envoyer le jeton ailleurs.
LHttp.HandleRedirects := False;
LHttp.CustomHeaders['Authorization'] := 'Bearer ' + FToken;
if AKnownETag <> '' then
LHttp.CustomHeaders['If-None-Match'] := AKnownETag;
LStream := TStringStream.Create('', TEncoding.UTF8);
try
try
LResp := LHttp.Get(FBaseUrl, LStream);
except
on E: Exception do
begin
Result.Status := ssNetworkError;
Result.Error := E.ClassName + ': ' + E.Message;
Exit;
end;
end;
Result.HttpCode := LResp.StatusCode;
Result.ETag := LResp.HeaderValue['ETag'];
case LResp.StatusCode of
200:
begin
Result.Status := ssOK;
Result.Container := LStream.DataString;
// Un corps vide avec un 200 signale un proxy ou une erreur de
// configuration, pas un instantane : le signaler ici evite un
// "mot de passe incorrect" trompeur plus loin.
if Result.Container.Trim = '' then
begin
Result.Status := ssHttpError;
Result.Error := '200 avec un corps vide';
end;
end;
304: Result.Status := ssNotModified;
403:
begin
Result.Status := ssRefused;
Result.Error := 'Le serveur exige HTTPS';
end;
404: Result.Status := ssNoSnapshot;
else
Result.Status := ssHttpError;
Result.Error := 'HTTP ' + LResp.StatusCode.ToString + ' ' + LResp.StatusText;
end;
finally
LStream.Free;
end;
finally
LHttp.Free;
end;
end;
end.