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;
|
||||
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
|
||||
/// produced by WebCrypto. Raises EPMACrypto when the tag does not verify —
|
||||
/// which is what a wrong password looks like.
|
||||
@@ -163,6 +175,42 @@ type
|
||||
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)
|
||||
['{E081D312-94A5-B6C7-02FD-1E8F90011223}']
|
||||
function _GetDECRYPT_MODE: Integer; cdecl;
|
||||
@@ -196,6 +244,42 @@ begin
|
||||
end;
|
||||
{$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;
|
||||
{$IF DEFINED(ANDROID)}
|
||||
var
|
||||
|
||||
+25
@@ -118,6 +118,31 @@ begin
|
||||
Exit; // inutile de tester AES avec une mauvaise cle
|
||||
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 ------------------------------
|
||||
Log('');
|
||||
Log('Dechiffrement du conteneur...');
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user