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;
|
function DecryptContainerEx(const AContainerJson, APassword: string;
|
||||||
out AKey, ASalt: TBytes; out AIterations: Integer): 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 :
|
/// 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 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
|
/// 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);
|
Result := DecryptContainerEx(AContainerJson, APassword, LKey, LSalt, LIter);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function DecryptContainerEx(const AContainerJson, APassword: string;
|
/// L'enveloppe du conteneur, lue UNE SEULE FOIS pour les trois entrees
|
||||||
out AKey, ASalt: TBytes; out AIterations: Integer): string;
|
/// 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
|
var
|
||||||
LObj: TJSONObject;
|
LObj: TJSONObject;
|
||||||
LFormat, LKdf: string;
|
LFormat, LKdf: string;
|
||||||
LIterations: Integer;
|
|
||||||
LSalt, LIv, LCt, LKey, LPlain: TBytes;
|
|
||||||
begin
|
begin
|
||||||
LObj := TJSONObject.ParseJSONValue(AContainerJson) as TJSONObject;
|
LObj := TJSONObject.ParseJSONValue(AContainerJson) as TJSONObject;
|
||||||
if LObj = nil then
|
if LObj = nil then
|
||||||
@@ -747,32 +764,66 @@ begin
|
|||||||
if LKdf <> 'pbkdf2-sha256' then
|
if LKdf <> 'pbkdf2-sha256' then
|
||||||
raise EPMACrypto.CreateFmt('Unsupported KDF: "%s"', [LKdf]);
|
raise EPMACrypto.CreateFmt('Unsupported KDF: "%s"', [LKdf]);
|
||||||
|
|
||||||
LIterations := LObj.GetValue<Integer>('kdf_iterations', 600000);
|
AIterations := LObj.GetValue<Integer>('kdf_iterations', 600000);
|
||||||
LSalt := TNetEncoding.Base64.DecodeStringToBytes(LObj.GetValue<string>('kdf_salt', ''));
|
ASalt := TNetEncoding.Base64.DecodeStringToBytes(LObj.GetValue<string>('kdf_salt', ''));
|
||||||
LIv := TNetEncoding.Base64.DecodeStringToBytes(LObj.GetValue<string>('iv', ''));
|
AIv := TNetEncoding.Base64.DecodeStringToBytes(LObj.GetValue<string>('iv', ''));
|
||||||
LCt := TNetEncoding.Base64.DecodeStringToBytes(LObj.GetValue<string>('ciphertext', ''));
|
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');
|
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
|
finally
|
||||||
LObj.Free;
|
LObj.Free;
|
||||||
end;
|
end;
|
||||||
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.
|
end.
|
||||||
|
|||||||
+51
-6
@@ -116,6 +116,20 @@ type
|
|||||||
FUrl, FToken, FPwd: string;
|
FUrl, FToken, FPwd: string;
|
||||||
/// Le compte de cet appareil. Vide tant qu'aucun coffre local n'existe.
|
/// Le compte de cet appareil. Vide tant qu'aucun coffre local n'existe.
|
||||||
FUser: string;
|
FUser: string;
|
||||||
|
/// LA CLE DEJA DERIVEE, et de quoi savoir a quel conteneur elle convient.
|
||||||
|
///
|
||||||
|
/// Chaque enregistrement rederivait le mot de passe : 600 000 iterations,
|
||||||
|
/// quatre a six secondes, pour retrouver exactement la cle qui venait de
|
||||||
|
/// servir. C'est ce qui faisait durer un ajout d'entree cinq a sept
|
||||||
|
/// secondes. Elle ne se reutilise QUE si le conteneur porte le meme sel et
|
||||||
|
/// le meme nombre d'iterations - le poste peut avoir rechiffre le sien
|
||||||
|
/// entre-temps, avec un sel neuf, et une cle qui ne convient pas echouerait
|
||||||
|
/// sur la marque de GCM en accusant le mot de passe, qui serait bon.
|
||||||
|
///
|
||||||
|
/// Rien de nouveau n'est expose : le coffre EN CLAIR est deja en memoire.
|
||||||
|
/// Elle part au verrouillage, avec lui.
|
||||||
|
FKey, FKeySalt: TBytes;
|
||||||
|
FKeyIters: Integer;
|
||||||
/// Code d'ouverture du cache local, choisi dans l'application. JAMAIS
|
/// Code d'ouverture du cache local, choisi dans l'application. JAMAIS
|
||||||
/// ENREGISTRE : il est vide a chaque lancement et doit etre retape. C'est
|
/// ENREGISTRE : il est vide a chaque lancement et doit etre retape. C'est
|
||||||
/// ce qui le distingue du jeton, dont la persistance est un confort sans
|
/// ce qui le distingue du jeton, dont la persistance est un confort sans
|
||||||
@@ -204,6 +218,9 @@ type
|
|||||||
procedure PatchSelfCheck;
|
procedure PatchSelfCheck;
|
||||||
{$ENDIF}
|
{$ENDIF}
|
||||||
procedure OpenEntry(AVaultIndex: Integer);
|
procedure OpenEntry(AVaultIndex: Integer);
|
||||||
|
/// Ouvre un conteneur en evitant la derivation quand la cle en main
|
||||||
|
/// convient. Retient au passage celle qui a servi.
|
||||||
|
function OpenContainer(const AContainer, APwd: string): string;
|
||||||
procedure RefuseChange(const AMessage: string);
|
procedure RefuseChange(const AMessage: string);
|
||||||
procedure CommitChange(const ALabel, AComment, AReopenUuid: string;
|
procedure CommitChange(const ALabel, AComment, AReopenUuid: string;
|
||||||
const APatch: TFunc<string, string>);
|
const APatch: TFunc<string, string>);
|
||||||
@@ -612,6 +629,26 @@ end;
|
|||||||
/// desactive son bouton en envoyant, donc un refus qui se contente de parler
|
/// desactive son bouton en envoyant, donc un refus qui se contente de parler
|
||||||
/// laisse un formulaire mort avec la saisie derriere. La regle s'est deja fait
|
/// laisse un formulaire mort avec la saisie derriere. La regle s'est deja fait
|
||||||
/// oublier trois fois - a l'editeur, puis au formulaire de creation.
|
/// oublier trois fois - a l'editeur, puis au formulaire de creation.
|
||||||
|
function TForm8.OpenContainer(const AContainer, APwd: string): string;
|
||||||
|
var
|
||||||
|
LSalt: TBytes;
|
||||||
|
LIters: Integer;
|
||||||
|
LSw: TStopwatch;
|
||||||
|
begin
|
||||||
|
LSw := TStopwatch.StartNew;
|
||||||
|
if (FKey <> nil) and ContainerKdfParams(AContainer, LSalt, LIters)
|
||||||
|
and (LIters = FKeyIters) and (Length(LSalt) = Length(FKeySalt))
|
||||||
|
and CompareMem(Pointer(LSalt), Pointer(FKeySalt), Length(LSalt)) then
|
||||||
|
begin
|
||||||
|
Result := DecryptContainerWithKey(AContainer, FKey);
|
||||||
|
Log(Format(' opened with the key already in hand, in %d ms',
|
||||||
|
[LSw.ElapsedMilliseconds]));
|
||||||
|
Exit;
|
||||||
|
end;
|
||||||
|
Result := DecryptContainerEx(AContainer, APwd, FKey, FKeySalt, FKeyIters);
|
||||||
|
Log(Format(' key derived in %d ms', [LSw.ElapsedMilliseconds]));
|
||||||
|
end;
|
||||||
|
|
||||||
procedure TForm8.RefuseChange(const AMessage: string);
|
procedure TForm8.RefuseChange(const AMessage: string);
|
||||||
begin
|
begin
|
||||||
WebCall('PM.toast(' + JsArg(AMessage) + ')');
|
WebCall('PM.toast(' + JsArg(AMessage) + ')');
|
||||||
@@ -671,8 +708,8 @@ begin
|
|||||||
// cache ensuite : l'inverse laisserait un cache en avance sur
|
// cache ensuite : l'inverse laisserait un cache en avance sur
|
||||||
// l'autorite si l'ecriture echouait entre les deux.
|
// l'autorite si l'ecriture echouait entre les deux.
|
||||||
LSw := TStopwatch.StartNew;
|
LSw := TStopwatch.StartNew;
|
||||||
LPlain := DecryptContainerEx(LoadLocalContainer, FSyncPwd,
|
LPlain := OpenContainer(LoadLocalContainer, FSyncPwd);
|
||||||
LKey, LSalt, LIter);
|
LKey := FKey; LSalt := FKeySalt; LIter := FKeyIters;
|
||||||
LNew := APatch(LPlain);
|
LNew := APatch(LPlain);
|
||||||
Log(Format('save: decrypt+patch in %d ms', [LSw.ElapsedMilliseconds]));
|
Log(Format('save: decrypt+patch in %d ms', [LSw.ElapsedMilliseconds]));
|
||||||
LSw := TStopwatch.StartNew;
|
LSw := TStopwatch.StartNew;
|
||||||
@@ -707,8 +744,8 @@ begin
|
|||||||
// reutilise. Deriver deux fois le meme mot de passe coutait une
|
// reutilise. Deriver deux fois le meme mot de passe coutait une
|
||||||
// seconde fois quatre a six secondes, pour une cle equivalente.
|
// seconde fois quatre a six secondes, pour une cle equivalente.
|
||||||
LSw := TStopwatch.StartNew;
|
LSw := TStopwatch.StartNew;
|
||||||
LPlain := DecryptContainerEx(LGet.Container, FSyncPwd,
|
LPlain := OpenContainer(LGet.Container, FSyncPwd);
|
||||||
LKey, LSalt, LIter);
|
LKey := FKey; LSalt := FKeySalt; LIter := FKeyIters;
|
||||||
LNew := APatch(LPlain);
|
LNew := APatch(LPlain);
|
||||||
Log(Format('save: decrypt+patch in %d ms', [LSw.ElapsedMilliseconds]));
|
Log(Format('save: decrypt+patch in %d ms', [LSw.ElapsedMilliseconds]));
|
||||||
except
|
except
|
||||||
@@ -1034,6 +1071,14 @@ begin
|
|||||||
// ecrase ce que l'utilisateur vient de taper - l'auto-verrouillage le faisait
|
// ecrase ce que l'utilisateur vient de taper - l'auto-verrouillage le faisait
|
||||||
// donc sans le moindre geste de sa part, et le champ jeton semblait ne rien
|
// donc sans le moindre geste de sa part, et le champ jeton semblait ne rien
|
||||||
// retenir. Verrouiller efface le clair ; la configuration n'a rien a y voir.
|
// retenir. Verrouiller efface le clair ; la configuration n'a rien a y voir.
|
||||||
|
//
|
||||||
|
// LA CLE DERIVEE PART AUSSI. Elle ouvre le conteneur a elle seule, sans le
|
||||||
|
// mot de passe : la garder apres un verrouillage reviendrait a ne pas
|
||||||
|
// verrouiller. Le sel la suit, sinon la prochaine ouverture croirait tenir
|
||||||
|
// une cle valable et dechiffrerait avec du vide.
|
||||||
|
FKey := nil;
|
||||||
|
FKeySalt := nil;
|
||||||
|
FKeyIters := 0;
|
||||||
ShowUnlock;
|
ShowUnlock;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
@@ -1156,7 +1201,7 @@ begin
|
|||||||
LDoc: TJSONObject;
|
LDoc: TJSONObject;
|
||||||
begin
|
begin
|
||||||
try
|
try
|
||||||
LPlain := DecryptContainerEx(LoadLocalContainer, APwd, LKey, LSalt, LIters);
|
LPlain := OpenContainer(LoadLocalContainer, APwd);
|
||||||
LOwner := '';
|
LOwner := '';
|
||||||
LDoc := TJSONObject.ParseJSONValue(LPlain) as TJSONObject;
|
LDoc := TJSONObject.ParseJSONValue(LPlain) as TJSONObject;
|
||||||
if LDoc <> nil then
|
if LDoc <> nil then
|
||||||
@@ -1595,7 +1640,7 @@ begin
|
|||||||
LSw := TStopwatch.StartNew;
|
LSw := TStopwatch.StartNew;
|
||||||
// Volontairement AVANT de retenir l'ETag : un echec ici doit laisser
|
// Volontairement AVANT de retenir l'ETag : un echec ici doit laisser
|
||||||
// FETag vide pour que le prochain essai retelecharge.
|
// FETag vide pour que le prochain essai retelecharge.
|
||||||
LPlain := DecryptContainer(LRes.Container, APwd);
|
LPlain := OpenContainer(LRes.Container, APwd);
|
||||||
LSw.Stop;
|
LSw.Stop;
|
||||||
FETag := LRes.ETag;
|
FETag := LRes.ETag;
|
||||||
Log(' decrypted in ' + LSw.ElapsedMilliseconds.ToString + ' ms');
|
Log(' decrypted in ' + LSw.ElapsedMilliseconds.ToString + ' ms');
|
||||||
|
|||||||
Reference in New Issue
Block a user