feat(android): measure the native PBKDF2 against the vector instead of guessing
The Pascal loop takes 1736 ms on a desktop CPU, so several seconds on a phone - too slow for every unlock. SecretKeyFactory is native and roughly an order of magnitude faster, but PBEKeySpec takes a char[] and the provider picks the encoding, which was the reason for avoiding it. The non-ASCII vector settles it on a real device: same key means the provider uses UTF-8 and we adopt it; a different key means the Pascal implementation stays. DecryptContainer keeps using the Pascal one until that measurement says otherwise.
This commit is contained in:
@@ -41,6 +41,18 @@ type
|
|||||||
function DeriveKeyPBKDF2(const APassword: string; const ASalt: TBytes;
|
function DeriveKeyPBKDF2(const APassword: string; const ASalt: TBytes;
|
||||||
AIterations, AKeyLenBytes: Integer): TBytes;
|
AIterations, AKeyLenBytes: Integer): TBytes;
|
||||||
|
|
||||||
|
/// Same derivation, delegated to Android's SecretKeyFactory — natively
|
||||||
|
/// implemented, so roughly an order of magnitude faster than the Pascal loop.
|
||||||
|
///
|
||||||
|
/// NOT used by DecryptContainer, and deliberately so: PBEKeySpec takes a
|
||||||
|
/// char[] and the provider decides how those chars become bytes. If Android
|
||||||
|
/// encodes them as anything other than UTF-8, this returns a DIFFERENT key for
|
||||||
|
/// a non-ASCII password — silently, reported later as a wrong password.
|
||||||
|
/// The self-test compares it against the known vector on a real device; adopt
|
||||||
|
/// it only if they match there. Returns nil on any failure.
|
||||||
|
function DeriveKeyPBKDF2Native(const APassword: string; const ASalt: TBytes;
|
||||||
|
AIterations, AKeyLenBytes: Integer): TBytes;
|
||||||
|
|
||||||
/// AES-256-GCM decrypt. ACipherText carries the 16-byte tag at the end, as
|
/// AES-256-GCM decrypt. ACipherText carries the 16-byte tag at the end, as
|
||||||
/// produced by WebCrypto. Raises EPMACrypto when the tag does not verify —
|
/// produced by WebCrypto. Raises EPMACrypto when the tag does not verify —
|
||||||
/// which is what a wrong password looks like.
|
/// which is what a wrong password looks like.
|
||||||
@@ -163,6 +175,42 @@ type
|
|||||||
end;
|
end;
|
||||||
TJGCMParameterSpec = class(TJavaGenericImport<JGCMParameterSpecClass, JGCMParameterSpec>) end;
|
TJGCMParameterSpec = class(TJavaGenericImport<JGCMParameterSpecClass, JGCMParameterSpec>) end;
|
||||||
|
|
||||||
|
JPBEKeySpec = interface;
|
||||||
|
JSecretKey = interface;
|
||||||
|
JSecretKeyFactory = interface;
|
||||||
|
|
||||||
|
JPBEKeySpecClass = interface(JObjectClass)
|
||||||
|
['{1A2B3C4D-5E6F-7081-92A3-B4C5D6E7F801}']
|
||||||
|
function init(password: TJavaArray<Char>; salt: TJavaArray<Byte>;
|
||||||
|
iterationCount: Integer; keyLength: Integer): JPBEKeySpec; cdecl;
|
||||||
|
end;
|
||||||
|
[JavaSignature('javax/crypto/spec/PBEKeySpec')]
|
||||||
|
JPBEKeySpec = interface(JObject)
|
||||||
|
['{2B3C4D5E-6F70-8192-A3B4-C5D6E7F80112}']
|
||||||
|
end;
|
||||||
|
TJPBEKeySpec = class(TJavaGenericImport<JPBEKeySpecClass, JPBEKeySpec>) end;
|
||||||
|
|
||||||
|
JSecretKeyClass = interface(JKeyClass)
|
||||||
|
['{3C4D5E6F-7081-92A3-B4C5-D6E7F8011223}']
|
||||||
|
end;
|
||||||
|
[JavaSignature('javax/crypto/SecretKey')]
|
||||||
|
JSecretKey = interface(JKey)
|
||||||
|
['{4D5E6F70-8192-A3B4-C5D6-E7F801122334}']
|
||||||
|
function getEncoded: TJavaArray<Byte>; cdecl;
|
||||||
|
end;
|
||||||
|
TJSecretKey = class(TJavaGenericImport<JSecretKeyClass, JSecretKey>) end;
|
||||||
|
|
||||||
|
JSecretKeyFactoryClass = interface(JObjectClass)
|
||||||
|
['{5E6F7081-92A3-B4C5-D6E7-F80112233445}']
|
||||||
|
function getInstance(algorithm: JString): JSecretKeyFactory; cdecl;
|
||||||
|
end;
|
||||||
|
[JavaSignature('javax/crypto/SecretKeyFactory')]
|
||||||
|
JSecretKeyFactory = interface(JObject)
|
||||||
|
['{6F708192-A3B4-C5D6-E7F8-011223344556}']
|
||||||
|
function generateSecret(keySpec: JObject): JSecretKey; cdecl;
|
||||||
|
end;
|
||||||
|
TJSecretKeyFactory = class(TJavaGenericImport<JSecretKeyFactoryClass, JSecretKeyFactory>) end;
|
||||||
|
|
||||||
JCipherClass = interface(JObjectClass)
|
JCipherClass = interface(JObjectClass)
|
||||||
['{E081D312-94A5-B6C7-02FD-1E8F90011223}']
|
['{E081D312-94A5-B6C7-02FD-1E8F90011223}']
|
||||||
function _GetDECRYPT_MODE: Integer; cdecl;
|
function _GetDECRYPT_MODE: Integer; cdecl;
|
||||||
@@ -196,6 +244,42 @@ begin
|
|||||||
end;
|
end;
|
||||||
{$ENDIF}
|
{$ENDIF}
|
||||||
|
|
||||||
|
function DeriveKeyPBKDF2Native(const APassword: string; const ASalt: TBytes;
|
||||||
|
AIterations, AKeyLenBytes: Integer): TBytes;
|
||||||
|
{$IF DEFINED(ANDROID)}
|
||||||
|
var
|
||||||
|
LChars: TJavaArray<Char>;
|
||||||
|
LSpec: JPBEKeySpec;
|
||||||
|
LFactory: JSecretKeyFactory;
|
||||||
|
I: Integer;
|
||||||
|
begin
|
||||||
|
Result := nil;
|
||||||
|
try
|
||||||
|
// The password goes in as chars — the whole point of the doubt. Delphi
|
||||||
|
// strings are UTF-16, so this hands the provider the code units and lets
|
||||||
|
// it apply whatever encoding it wants. That is exactly what the test
|
||||||
|
// measures against the vector.
|
||||||
|
LChars := TJavaArray<Char>.Create(Length(APassword));
|
||||||
|
for I := 1 to Length(APassword) do
|
||||||
|
LChars.Items[I - 1] := APassword[I];
|
||||||
|
|
||||||
|
LSpec := TJPBEKeySpec.JavaClass.init(LChars, ToJavaBytes(ASalt),
|
||||||
|
AIterations, AKeyLenBytes * 8); // keyLength is in BITS
|
||||||
|
LFactory := TJSecretKeyFactory.JavaClass.getInstance(
|
||||||
|
StringToJString('PBKDF2WithHmacSHA256'));
|
||||||
|
Result := FromJavaBytes(LFactory.generateSecret(TJObject.Wrap((LSpec as ILocalObject).GetObjectID)).getEncoded);
|
||||||
|
except
|
||||||
|
// Provider missing or algorithm unavailable: the caller falls back to the
|
||||||
|
// Pascal implementation, which always works.
|
||||||
|
Result := nil;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
{$ELSE}
|
||||||
|
begin
|
||||||
|
Result := nil; // no platform provider outside Android
|
||||||
|
end;
|
||||||
|
{$ENDIF}
|
||||||
|
|
||||||
function AesGcmDecrypt(const AKey, AIv, ACipherText: TBytes): TBytes;
|
function AesGcmDecrypt(const AKey, AIv, ACipherText: TBytes): TBytes;
|
||||||
{$IF DEFINED(ANDROID)}
|
{$IF DEFINED(ANDROID)}
|
||||||
var
|
var
|
||||||
|
|||||||
+25
@@ -118,6 +118,31 @@ begin
|
|||||||
Exit; // inutile de tester AES avec une mauvaise cle
|
Exit; // inutile de tester AES avec une mauvaise cle
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
// ---- Etage 1bis : le PBKDF2 natif d'Android ----------------------
|
||||||
|
// Question ouverte, tranchee ici plutot que supposee : PBEKeySpec
|
||||||
|
// prend un char[] et le fournisseur choisit l'encodage. S'il donne la
|
||||||
|
// MEME cle sur ce mot de passe non-ASCII, on peut l'adopter et gagner
|
||||||
|
// un ordre de grandeur. Sinon, le Pascal reste la seule option juste.
|
||||||
|
Log('');
|
||||||
|
Log('PBKDF2 natif (Android)...');
|
||||||
|
LSw := TStopwatch.StartNew;
|
||||||
|
LKey := DeriveKeyPBKDF2Native(TESTVECTOR_PASSWORD, LSalt, 600000, 32);
|
||||||
|
LSw.Stop;
|
||||||
|
if LKey = nil then
|
||||||
|
Log(' indisponible sur cette plateforme')
|
||||||
|
else
|
||||||
|
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')
|
||||||
|
else
|
||||||
|
begin
|
||||||
|
Log(' obtenu : ' + LKeyHex);
|
||||||
|
Log(' ==> DIFFERENT — encodage du char[] incompatible, garder le Pascal');
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
// ---- Etage 2 : AES-GCM + conteneur ------------------------------
|
// ---- Etage 2 : AES-GCM + conteneur ------------------------------
|
||||||
Log('');
|
Log('');
|
||||||
Log('Dechiffrement du conteneur...');
|
Log('Dechiffrement du conteneur...');
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user