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.
This commit is contained in:
+66
-34
@@ -28,7 +28,7 @@ const
|
||||
//
|
||||
// Le port differe selon la plateforme : sur Windows on attaque Apache
|
||||
// directement (80), sur Android on passe par "adb reverse tcp:8080 tcp:80",
|
||||
// qui fait voir le PC comme 127.0.0.1 — ce qui satisfait au passage
|
||||
// qui fait voir le PC comme 127.0.0.1 - ce qui satisfait au passage
|
||||
// l'exemption HTTPS de l'endpoint, reservee a la boucle locale.
|
||||
{$IF DEFINED(ANDROID)}
|
||||
TEST_URL = 'http://127.0.0.1:8080/server-php/sync.php';
|
||||
@@ -36,7 +36,7 @@ const
|
||||
TEST_URL = 'http://127.0.0.1/server-php/sync.php';
|
||||
{$ENDIF}
|
||||
// Jeton d'un serveur de test local. Il n'authentifie qu'un appareil et ne
|
||||
// donne acces qu'a du chiffre — mais a remplacer avant toute mise en ligne.
|
||||
// donne acces qu'a du chiffre - mais a remplacer avant toute mise en ligne.
|
||||
TEST_TOKEN = '8765e5bf426039e9888f44bfbab27d489d12b7dbaaa54c8c695ae6af8b2a820e';
|
||||
|
||||
type
|
||||
@@ -53,6 +53,7 @@ type
|
||||
FETag: string;
|
||||
procedure RunSelfTest(Sender: TObject);
|
||||
procedure RunFetch(Sender: TObject);
|
||||
procedure CountEntries(const APlainJson: string);
|
||||
procedure Log(const AMsg: string);
|
||||
public
|
||||
constructor Create(AOwner: TComponent); override;
|
||||
@@ -64,22 +65,22 @@ var
|
||||
implementation
|
||||
|
||||
uses
|
||||
System.JSON, PMA.Crypto, PMA.TestVector, PMA.SyncClient;
|
||||
System.JSON, PMA.Crypto, PMA.TestVector, PMA.SyncClient, PMA.Store;
|
||||
|
||||
{$R *.fmx}
|
||||
|
||||
constructor TForm8.Create(AOwner: TComponent);
|
||||
begin
|
||||
inherited;
|
||||
Caption := 'PM — crypto self-test';
|
||||
Caption := 'PM - crypto self-test';
|
||||
|
||||
// ORDRE DE CREATION INVERSE : en FMX, des controles alignes en haut
|
||||
// s'empilent a rebours de leur creation — le dernier cree se retrouve tout
|
||||
// s'empilent a rebours de leur creation - le dernier cree se retrouve tout
|
||||
// en haut. On cree donc du bas vers le haut pour obtenir URL, jeton, mot de
|
||||
// passe, puis les deux boutons.
|
||||
//
|
||||
// Rien n'est persiste : c'est un banc d'essai, et le mot de passe de sync
|
||||
// ouvre le coffre entier — il n'a pas a trainer sur le disque tant que le
|
||||
// ouvre le coffre entier - il n'a pas a trainer sur le disque tant que le
|
||||
// stockage Keystore n'est pas ecrit.
|
||||
FBtn := TButton.Create(Self);
|
||||
FBtn.Parent := Self;
|
||||
@@ -137,6 +138,26 @@ begin
|
||||
end);
|
||||
end;
|
||||
|
||||
procedure TForm8.CountEntries(const APlainJson: string);
|
||||
var
|
||||
LJson: TJSONObject;
|
||||
LArr: TJSONArray;
|
||||
LLive, LTrash, I: Integer;
|
||||
begin
|
||||
LJson := TJSONObject.ParseJSONValue(APlainJson) as TJSONObject;
|
||||
if LJson = nil then Exit;
|
||||
try
|
||||
if not LJson.TryGetValue<TJSONArray>('entries', LArr) then Exit;
|
||||
LLive := 0; LTrash := 0;
|
||||
for I := 0 to LArr.Count - 1 do
|
||||
if (LArr.Items[I] as TJSONObject).GetValue<Integer>('deleted', 0) = 1
|
||||
then Inc(LTrash) else Inc(LLive);
|
||||
Log(' ' + LLive.ToString + ' entrees, ' + LTrash.ToString + ' en corbeille');
|
||||
finally
|
||||
LJson.Free;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TForm8.RunFetch(Sender: TObject);
|
||||
var
|
||||
LUrl, LTok, LPwd: string;
|
||||
@@ -154,12 +175,20 @@ begin
|
||||
var
|
||||
LClient: TSyncClient;
|
||||
LRes: TSyncResult;
|
||||
LPlain: string;
|
||||
LPlain, LCached, LCachedETag: string;
|
||||
LSw: TStopwatch;
|
||||
LJson: TJSONObject;
|
||||
LArr: TJSONArray;
|
||||
LLive, LTrash, I: Integer;
|
||||
begin
|
||||
// Le coffre local d'abord : s'il est la, on connait deja l'ETag et on
|
||||
// evite les 4 secondes de derivation quand rien n'a change.
|
||||
if LoadVault(LCached, LCachedETag) then
|
||||
begin
|
||||
FETag := LCachedETag;
|
||||
Log('Coffre local : ' + Length(LCached).ToString +
|
||||
' caracteres, ETag ' + Copy(LCachedETag, 1, 16) + '...');
|
||||
end
|
||||
else
|
||||
Log('Pas de coffre local - premier telechargement');
|
||||
|
||||
LClient := TSyncClient.Create(LUrl, LTok);
|
||||
// try/finally et NON un simple enchainement : les Exit des branches
|
||||
// d'erreur ci-dessous sautent la fin de la procedure, ce qui laissait le
|
||||
@@ -177,7 +206,17 @@ begin
|
||||
case LRes.Status of
|
||||
ssNotModified:
|
||||
begin
|
||||
Log(' ==> 304 : inchange, rien a re-dechiffrer');
|
||||
// C'est ICI que le cache paie : pas de telechargement, pas de
|
||||
// derivation, le coffre vient du fichier local.
|
||||
Log(' ==> 304 : inchange');
|
||||
if LCached <> '' then
|
||||
begin
|
||||
LPlain := LCached;
|
||||
Log(' coffre servi depuis le cache local, 0 ms de derivation');
|
||||
CountEntries(LPlain);
|
||||
end
|
||||
else
|
||||
Log(' mais aucun coffre local - incoherence, effacer le cache');
|
||||
Exit;
|
||||
end;
|
||||
ssNoSnapshot:
|
||||
@@ -204,26 +243,19 @@ begin
|
||||
LSw.Stop;
|
||||
FETag := LRes.ETag;
|
||||
if FETag <> '' then Log(' ETag retenu : ' + FETag);
|
||||
|
||||
// Ecrit APRES un dechiffrement reussi seulement, et avec son ETag :
|
||||
// les deux doivent rester coherents.
|
||||
try
|
||||
SaveVault(LPlain, FETag);
|
||||
Log(' coffre enregistre localement (chiffre par le Keystore)');
|
||||
except
|
||||
on E: Exception do
|
||||
Log(' cache non ecrit : ' + E.Message);
|
||||
end;
|
||||
Log(' ' + LSw.ElapsedMilliseconds.ToString + ' ms pour ' +
|
||||
Length(LPlain).ToString + ' caracteres');
|
||||
|
||||
// Compte vivant / corbeille : c'est ce que le vrai client devra
|
||||
// afficher separement.
|
||||
LJson := TJSONObject.ParseJSONValue(LPlain) as TJSONObject;
|
||||
if LJson <> nil then
|
||||
try
|
||||
if LJson.TryGetValue<TJSONArray>('entries', LArr) then
|
||||
begin
|
||||
LLive := 0; LTrash := 0;
|
||||
for I := 0 to LArr.Count - 1 do
|
||||
if (LArr.Items[I] as TJSONObject).GetValue<Integer>('deleted', 0) = 1
|
||||
then Inc(LTrash) else Inc(LLive);
|
||||
Log(' ' + LLive.ToString + ' entrees, ' +
|
||||
LTrash.ToString + ' en corbeille');
|
||||
end;
|
||||
finally
|
||||
LJson.Free;
|
||||
end;
|
||||
CountEntries(LPlain);
|
||||
except
|
||||
on E: Exception do
|
||||
Log('ERREUR ' + E.ClassName + ' : ' + E.Message);
|
||||
@@ -272,7 +304,7 @@ begin
|
||||
else
|
||||
begin
|
||||
Log(' attendu: ' + TESTVECTOR_KEY_HEX);
|
||||
Log(' ==> PBKDF2 FAUX — verifier l''encodage UTF-8 du mot de passe');
|
||||
Log(' ==> PBKDF2 FAUX - verifier l''encodage UTF-8 du mot de passe');
|
||||
Exit; // inutile de tester AES avec une mauvaise cle
|
||||
end;
|
||||
|
||||
@@ -293,11 +325,11 @@ begin
|
||||
LKeyHex := LowerCase(THash.DigestAsString(LKey));
|
||||
Log(' duree : ' + IntToStr(LSw.ElapsedMilliseconds) + ' ms');
|
||||
if LKeyHex = TESTVECTOR_KEY_HEX then
|
||||
Log(' ==> IDENTIQUE — adoptable, encodage UTF-8 confirme')
|
||||
Log(' ==> IDENTIQUE - adoptable, encodage UTF-8 confirme')
|
||||
else
|
||||
begin
|
||||
Log(' obtenu : ' + LKeyHex);
|
||||
Log(' ==> DIFFERENT — encodage du char[] incompatible, garder le Pascal');
|
||||
Log(' ==> DIFFERENT - encodage du char[] incompatible, garder le Pascal');
|
||||
end;
|
||||
end;
|
||||
|
||||
@@ -312,9 +344,9 @@ begin
|
||||
' octets (attendu ' + IntToStr(TESTVECTOR_PLAIN_LEN) + ')');
|
||||
Log(' sha256 : ' + LSha);
|
||||
if LSha = TESTVECTOR_PLAIN_SHA then
|
||||
Log(' ==> AES-GCM OK — la chaine complete est bonne')
|
||||
Log(' ==> AES-GCM OK - la chaine complete est bonne')
|
||||
else
|
||||
Log(' ==> SHA DIFFERENT — comparer avec testvector.md');
|
||||
Log(' ==> SHA DIFFERENT - comparer avec testvector.md');
|
||||
|
||||
Log('');
|
||||
Log('Debut du clair : ' + Copy(LPlain, 1, 60));
|
||||
|
||||
Reference in New Issue
Block a user