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:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user