fix(android): declare KeySpec so the native PBKDF2 call resolves, and report why it fails

generateSecret takes a KeySpec; passing a JObject made the bridge emit a signature no method matches, and the call failed at runtime. Swallowed as 'unavailable', which hid my own mistake for a full test round - the failure now carries its exception message.

Measured on device: the Pascal PBKDF2 takes 6651 ms, unusable per unlock. AES-GCM through JNI works, plaintext and hash match the vector, so the container format is validated end to end.
This commit is contained in:
r-zakarya
2026-08-14 23:22:56 +01:00
parent c607e26c4a
commit 4cc79a5216
48 changed files with 1217 additions and 11 deletions
+37 -7
View File
@@ -53,6 +53,9 @@ function DeriveKeyPBKDF2(const APassword: string; const ASalt: TBytes;
function DeriveKeyPBKDF2Native(const APassword: string; const ASalt: TBytes;
AIterations, AKeyLenBytes: Integer): TBytes;
/// Why the last DeriveKeyPBKDF2Native returned nil. Empty when it succeeded.
function LastNativeError: string;
/// 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.
@@ -70,6 +73,14 @@ uses
Androidapi.JNIBridge, Androidapi.JNI.JavaTypes, Androidapi.Helpers;
{$ENDIF}
var
FLastNativeError: string = '';
function LastNativeError: string;
begin
Result := FLastNativeError;
end;
// ---------------------------------------------------------------------------
// PBKDF2-SHA256
// ---------------------------------------------------------------------------
@@ -175,17 +186,31 @@ type
end;
TJGCMParameterSpec = class(TJavaGenericImport<JGCMParameterSpecClass, JGCMParameterSpec>) end;
JKeySpec = interface;
JPBEKeySpec = interface;
JSecretKey = interface;
JSecretKeyFactory = interface;
JPBEKeySpecClass = interface(JObjectClass)
// generateSecret takes a KeySpec, not an Object. Declaring the real
// interface is what makes the bridge emit
// (Ljava/security/spec/KeySpec;)Ljavax/crypto/SecretKey; — with JObject it
// emitted a signature no method matches, and the call failed at runtime.
JKeySpecClass = interface(IJavaClass)
['{0A1B2C3D-4E5F-6071-8293-A4B5C6D7E8F9}']
end;
[JavaSignature('java/security/spec/KeySpec')]
JKeySpec = interface(IJavaInstance)
['{1B2C3D4E-5F60-7182-93A4-B5C6D7E8F901}']
end;
TJKeySpec = class(TJavaGenericImport<JKeySpecClass, JKeySpec>) end;
JPBEKeySpecClass = interface(JKeySpecClass)
['{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)
JPBEKeySpec = interface(JKeySpec)
['{2B3C4D5E-6F70-8192-A3B4-C5D6E7F80112}']
end;
TJPBEKeySpec = class(TJavaGenericImport<JPBEKeySpecClass, JPBEKeySpec>) end;
@@ -207,7 +232,7 @@ type
[JavaSignature('javax/crypto/SecretKeyFactory')]
JSecretKeyFactory = interface(JObject)
['{6F708192-A3B4-C5D6-E7F8-011223344556}']
function generateSecret(keySpec: JObject): JSecretKey; cdecl;
function generateSecret(keySpec: JKeySpec): JSecretKey; cdecl;
end;
TJSecretKeyFactory = class(TJavaGenericImport<JSecretKeyFactoryClass, JSecretKeyFactory>) end;
@@ -267,11 +292,16 @@ begin
AIterations, AKeyLenBytes * 8); // keyLength is in BITS
LFactory := TJSecretKeyFactory.JavaClass.getInstance(
StringToJString('PBKDF2WithHmacSHA256'));
Result := FromJavaBytes(LFactory.generateSecret(TJObject.Wrap((LSpec as ILocalObject).GetObjectID)).getEncoded);
Result := FromJavaBytes(LFactory.generateSecret(LSpec).getEncoded);
except
// Provider missing or algorithm unavailable: the caller falls back to the
// Pascal implementation, which always works.
Result := nil;
// Keep the reason: "unavailable" hid a signature mismatch of mine for a
// whole test round. A message here says whether Android refused, or
// whether the JNI declaration is wrong again.
on E: Exception do
begin
FLastNativeError := E.ClassName + ': ' + E.Message;
Result := nil;
end;
end;
end;
{$ELSE}