diff --git a/android-client/PMA.Crypto.pas b/android-client/PMA.Crypto.pas index b348375..35d842b 100644 --- a/android-client/PMA.Crypto.pas +++ b/android-client/PMA.Crypto.pas @@ -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; diff --git a/mainApk.pas b/mainApk.pas index b551137..911cf71 100644 --- a/mainApk.pas +++ b/mainApk.pas @@ -558,7 +558,9 @@ begin LClient: TSyncClient; LGet, LPut: TSyncResult; LPlain, LNew, LBody, LErr: string; - LAttempt: Integer; + LKey, LSalt: TBytes; + LIter, LAttempt: Integer; + LSw: TStopwatch; LDone: Boolean; begin LClient := TSyncClient.Create(FUrl, FToken); @@ -587,7 +589,9 @@ begin end; end; - LBody := EncryptContainer(LNew, FSyncPwd); + LSw := TStopwatch.StartNew; + LBody := EncryptContainerWithKey(LNew, LKey, LSalt, LIter); + Log(Format('save: re-encrypt in %d ms', [LSw.ElapsedMilliseconds])); LPut := LClient.Put(LBody, LGet.ETag, 'password change from phone'); if LPut.Status = ssOK then begin