perf(android): one key derivation per save, not two
Reported as "Save takes 6 to 8 seconds". A save decrypts the snapshot it just pulled and re-encrypts the patched one - with the same password. That was two full PBKDF2 runs at 600k iterations, four to six seconds each on this phone, for two keys that protect the same file. DecryptContainerEx now hands back the key, its salt and the iteration count, and EncryptContainerWithKey reuses them. The IV is still drawn fresh every time: reusing an IV with the same key breaks GCM entirely, whereas reusing the salt merely yields the same key - which is the whole point. A salt slows an attack on the password; it has no reason to differ between two versions of the same file. EncryptContainer keeps its old shape and now delegates, so there is a single place that composes a container. The log times both halves, so the next measurement says where the remaining seconds are rather than inviting another guess. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -91,6 +91,20 @@ function AesGcmEncrypt(const AKey, AIv, APlainText: TBytes): TBytes;
|
||||
function EncryptContainer(const APlainJson, APassword: string;
|
||||
AIterations: Integer = 600000): string;
|
||||
|
||||
/// Comme DecryptContainer, mais rend AUSSI la cle derivee, son sel et le
|
||||
/// nombre d'iterations. Sert au seul cas ou l'on rechiffre juste apres avoir
|
||||
/// dechiffre : deriver deux fois le meme mot de passe coute deux fois quatre a
|
||||
/// six secondes sur un telephone, pour un resultat identique.
|
||||
function DecryptContainerEx(const AContainerJson, APassword: string;
|
||||
out AKey, ASalt: TBytes; out AIterations: Integer): string;
|
||||
|
||||
/// Rechiffre avec une cle DEJA derivee. L'IV, lui, est retire a chaque appel :
|
||||
/// reutiliser un IV avec la meme cle casse GCM entierement, alors que
|
||||
/// reutiliser le sel ne fait que redonner la meme cle - ce qui est justement
|
||||
/// le but.
|
||||
function EncryptContainerWithKey(const APlainJson: string;
|
||||
const AKey, ASalt: TBytes; AIterations: Integer): string;
|
||||
|
||||
implementation
|
||||
|
||||
{$IF DEFINED(ANDROID)}
|
||||
@@ -658,21 +672,32 @@ end;
|
||||
function EncryptContainer(const APlainJson, APassword: string;
|
||||
AIterations: Integer): string;
|
||||
var
|
||||
LObj: TJSONObject;
|
||||
LSalt, LIv, LKey, LCt: TBytes;
|
||||
LSalt, LKey: TBytes;
|
||||
begin
|
||||
if APassword = '' then
|
||||
raise EPMACrypto.Create('Refusing to encrypt with an empty password');
|
||||
|
||||
// TIRES A CHAQUE APPEL, tous les deux. Reutiliser un IV avec la meme cle
|
||||
// casse GCM entierement - ce n'est pas une precaution de style.
|
||||
LSalt := SecureRandomBytes(32);
|
||||
LIv := SecureRandomBytes(12);
|
||||
|
||||
LKey := DeriveKeyPBKDF2Native(APassword, LSalt, AIterations, 32);
|
||||
if LKey = nil then
|
||||
LKey := DeriveKeyPBKDF2(APassword, LSalt, AIterations, 32);
|
||||
LCt := AesGcmEncrypt(LKey, LIv, TEncoding.UTF8.GetBytes(APlainJson));
|
||||
Result := EncryptContainerWithKey(APlainJson, LKey, LSalt, AIterations);
|
||||
end;
|
||||
|
||||
function EncryptContainerWithKey(const APlainJson: string;
|
||||
const AKey, ASalt: TBytes; AIterations: Integer): string;
|
||||
var
|
||||
LObj: TJSONObject;
|
||||
LIv, LCt: TBytes;
|
||||
begin
|
||||
if (Length(AKey) <> 32) or (Length(ASalt) = 0) then
|
||||
raise EPMACrypto.Create('Cle ou sel absent pour le rechiffrement');
|
||||
|
||||
// L'IV EST TIRE A CHAQUE APPEL, le sel non. Reutiliser un IV avec la meme
|
||||
// cle casse GCM entierement ; reutiliser le sel ne fait que redonner la meme
|
||||
// cle, ce qui est justement l'economie recherchee - le sel ralentit une
|
||||
// attaque sur le mot de passe, il n'a pas a differer d'une version a l'autre.
|
||||
LIv := SecureRandomBytes(12);
|
||||
LCt := AesGcmEncrypt(AKey, LIv, TEncoding.UTF8.GetBytes(APlainJson));
|
||||
|
||||
LObj := TJSONObject.Create;
|
||||
try
|
||||
@@ -681,7 +706,7 @@ begin
|
||||
LObj.AddPair('format', 'pm-encrypted-export-v1');
|
||||
LObj.AddPair('kdf', 'pbkdf2-sha256');
|
||||
LObj.AddPair('kdf_iterations', TJSONNumber.Create(AIterations));
|
||||
LObj.AddPair('kdf_salt', B64NoWrap(LSalt));
|
||||
LObj.AddPair('kdf_salt', B64NoWrap(ASalt));
|
||||
LObj.AddPair('iv', B64NoWrap(LIv));
|
||||
LObj.AddPair('ciphertext', B64NoWrap(LCt));
|
||||
LObj.AddPair('created_at', FormatDateTime('yyyy-mm-dd"T"hh:nn:ss"Z"',
|
||||
@@ -693,6 +718,15 @@ begin
|
||||
end;
|
||||
|
||||
function DecryptContainer(const AContainerJson, APassword: string): string;
|
||||
var
|
||||
LKey, LSalt: TBytes;
|
||||
LIter: Integer;
|
||||
begin
|
||||
Result := DecryptContainerEx(AContainerJson, APassword, LKey, LSalt, LIter);
|
||||
end;
|
||||
|
||||
function DecryptContainerEx(const AContainerJson, APassword: string;
|
||||
out AKey, ASalt: TBytes; out AIterations: Integer): string;
|
||||
var
|
||||
LObj: TJSONObject;
|
||||
LFormat, LKdf: string;
|
||||
@@ -731,6 +765,11 @@ begin
|
||||
LKey := DeriveKeyPBKDF2(APassword, LSalt, LIterations, 32);
|
||||
LPlain := AesGcmDecrypt(LKey, LIv, LCt);
|
||||
Result := TEncoding.UTF8.GetString(LPlain);
|
||||
// Rendus a l'appelant : ils lui evitent une seconde derivation s'il
|
||||
// rechiffre juste apres.
|
||||
AKey := LKey;
|
||||
ASalt := LSalt;
|
||||
AIterations := LIterations;
|
||||
finally
|
||||
LObj.Free;
|
||||
end;
|
||||
|
||||
Reference in New Issue
Block a user