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:
r-zakarya
2026-08-14 18:33:56 +01:00
parent 386bf74a2a
commit c607e26c4a
3 changed files with 109 additions and 0 deletions
+84
View File
@@ -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
View File
@@ -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...');
BIN
View File
Binary file not shown.