perf(android): the key is derived once per session, not once per save
Adding an entry took five to seven seconds. The log now says where they went: key derived in 4395 ms. Every save re-derived the master password - 600000 iterations - to arrive at exactly the key that had just been used. The remedy was already half written: EncryptContainerWithKey exists for this reason, but only within a single save. The key now lives for the session, and is reused only when the container carries the same salt and the same iteration count - the desktop may have re-encrypted its own with a fresh salt, and a key that does not fit fails on GCM's tag while the message blames the password, which would be right. Nothing new is exposed: the plaintext vault is already in memory, and the cache already holds the password that produces this key. It goes with them, at lock - salt included, or the next open would think it holds a usable key and decrypt against nothing. The envelope is read in one place now rather than copied per entry point: at three copies, a drift between them would surface as a wrong-password error. Measured against the real repository: HTTP 200, decrypted, 8 entries - so the envelope refactor holds. The reuse line itself is not yet measured on the device.
This commit is contained in:
@@ -98,6 +98,21 @@ function EncryptContainer(const APlainJson, APassword: string;
|
||||
function DecryptContainerEx(const AContainerJson, APassword: string;
|
||||
out AKey, ASalt: TBytes; out AIterations: Integer): string;
|
||||
|
||||
/// Le sel et le nombre d'iterations du conteneur, SANS rien deriver.
|
||||
///
|
||||
/// Sert a repondre a une seule question : la cle qu'on tient deja convient-elle
|
||||
/// a ce conteneur ? Y repondre coute une lecture de JSON, alors que la deriver
|
||||
/// pour rien coute quatre a six secondes.
|
||||
function ContainerKdfParams(const AContainerJson: string;
|
||||
out ASalt: TBytes; out AIterations: Integer): Boolean;
|
||||
|
||||
/// Dechiffre avec une cle DEJA derivee - le pendant d'EncryptContainerWithKey.
|
||||
/// L'APPELANT DOIT AVOIR VERIFIE que le sel et les iterations du conteneur sont
|
||||
/// ceux de cette cle : sans quoi le dechiffrement echoue sur la marque de GCM,
|
||||
/// et le message parlera d'un mot de passe faux alors qu'il est bon.
|
||||
function DecryptContainerWithKey(const AContainerJson: string;
|
||||
const AKey: TBytes): 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
|
||||
@@ -725,13 +740,15 @@ begin
|
||||
Result := DecryptContainerEx(AContainerJson, APassword, LKey, LSalt, LIter);
|
||||
end;
|
||||
|
||||
function DecryptContainerEx(const AContainerJson, APassword: string;
|
||||
out AKey, ASalt: TBytes; out AIterations: Integer): string;
|
||||
/// L'enveloppe du conteneur, lue UNE SEULE FOIS pour les trois entrees
|
||||
/// publiques. Elle a ete recopiee tant qu'il n'y en avait qu'une ; a trois, une
|
||||
/// divergence entre les copies se paierait par un dechiffrement qui accuse le
|
||||
/// mot de passe.
|
||||
procedure ReadContainer(const AContainerJson: string;
|
||||
out ASalt, AIv, ACt: TBytes; out AIterations: Integer);
|
||||
var
|
||||
LObj: TJSONObject;
|
||||
LFormat, LKdf: string;
|
||||
LIterations: Integer;
|
||||
LSalt, LIv, LCt, LKey, LPlain: TBytes;
|
||||
begin
|
||||
LObj := TJSONObject.ParseJSONValue(AContainerJson) as TJSONObject;
|
||||
if LObj = nil then
|
||||
@@ -747,32 +764,66 @@ begin
|
||||
if LKdf <> 'pbkdf2-sha256' then
|
||||
raise EPMACrypto.CreateFmt('Unsupported KDF: "%s"', [LKdf]);
|
||||
|
||||
LIterations := LObj.GetValue<Integer>('kdf_iterations', 600000);
|
||||
LSalt := TNetEncoding.Base64.DecodeStringToBytes(LObj.GetValue<string>('kdf_salt', ''));
|
||||
LIv := TNetEncoding.Base64.DecodeStringToBytes(LObj.GetValue<string>('iv', ''));
|
||||
LCt := TNetEncoding.Base64.DecodeStringToBytes(LObj.GetValue<string>('ciphertext', ''));
|
||||
AIterations := LObj.GetValue<Integer>('kdf_iterations', 600000);
|
||||
ASalt := TNetEncoding.Base64.DecodeStringToBytes(LObj.GetValue<string>('kdf_salt', ''));
|
||||
AIv := TNetEncoding.Base64.DecodeStringToBytes(LObj.GetValue<string>('iv', ''));
|
||||
ACt := TNetEncoding.Base64.DecodeStringToBytes(LObj.GetValue<string>('ciphertext', ''));
|
||||
|
||||
if (Length(LSalt) = 0) or (Length(LIv) = 0) or (Length(LCt) = 0) then
|
||||
if (Length(ASalt) = 0) or (Length(AIv) = 0) or (Length(ACt) = 0) then
|
||||
raise EPMACrypto.Create('Container is missing salt, iv or ciphertext');
|
||||
|
||||
// Android's provider first - measured identical to WebCrypto on a
|
||||
// non-ASCII password (so it does encode as UTF-8), and it is platform
|
||||
// code rather than ours. Barely faster though: 3869 ms against 4139 ms,
|
||||
// PBKDF2 gets no hardware acceleration. The Pascal path stays as the
|
||||
// fallback, and is the only one outside Android.
|
||||
LKey := DeriveKeyPBKDF2Native(APassword, LSalt, LIterations, 32);
|
||||
if LKey = nil then
|
||||
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;
|
||||
end;
|
||||
|
||||
function ContainerKdfParams(const AContainerJson: string;
|
||||
out ASalt: TBytes; out AIterations: Integer): Boolean;
|
||||
var
|
||||
LIv, LCt: TBytes;
|
||||
begin
|
||||
Result := True;
|
||||
try
|
||||
ReadContainer(AContainerJson, ASalt, LIv, LCt, AIterations);
|
||||
except
|
||||
// Une enveloppe illisible n'est pas une erreur ICI : l'appelant va tenter
|
||||
// le dechiffrement, qui dira ce qui ne va pas avec son propre message.
|
||||
Result := False;
|
||||
ASalt := nil;
|
||||
AIterations := 0;
|
||||
end;
|
||||
end;
|
||||
|
||||
function DecryptContainerWithKey(const AContainerJson: string;
|
||||
const AKey: TBytes): string;
|
||||
var
|
||||
LSalt, LIv, LCt: TBytes;
|
||||
LIterations: Integer;
|
||||
begin
|
||||
ReadContainer(AContainerJson, LSalt, LIv, LCt, LIterations);
|
||||
Result := TEncoding.UTF8.GetString(AesGcmDecrypt(AKey, LIv, LCt));
|
||||
end;
|
||||
|
||||
function DecryptContainerEx(const AContainerJson, APassword: string;
|
||||
out AKey, ASalt: TBytes; out AIterations: Integer): string;
|
||||
var
|
||||
LIterations: Integer;
|
||||
LSalt, LIv, LCt, LKey: TBytes;
|
||||
begin
|
||||
ReadContainer(AContainerJson, LSalt, LIv, LCt, LIterations);
|
||||
// Android's provider first - measured identical to WebCrypto on a
|
||||
// non-ASCII password (so it does encode as UTF-8), and it is platform
|
||||
// code rather than ours. Barely faster though: 3869 ms against 4139 ms,
|
||||
// PBKDF2 gets no hardware acceleration. The Pascal path stays as the
|
||||
// fallback, and is the only one outside Android.
|
||||
LKey := DeriveKeyPBKDF2Native(APassword, LSalt, LIterations, 32);
|
||||
if LKey = nil then
|
||||
LKey := DeriveKeyPBKDF2(APassword, LSalt, LIterations, 32);
|
||||
Result := TEncoding.UTF8.GetString(AesGcmDecrypt(LKey, LIv, LCt));
|
||||
// Rendus a l'appelant : ils lui evitent une seconde derivation s'il
|
||||
// rechiffre juste apres, ou s'il rouvre le meme conteneur plus tard.
|
||||
AKey := LKey;
|
||||
ASalt := LSalt;
|
||||
AIterations := LIterations;
|
||||
end;
|
||||
|
||||
end.
|
||||
|
||||
Reference in New Issue
Block a user