diff --git a/android-client/PMA.Crypto.pas b/android-client/PMA.Crypto.pas index 96c381a..3c17721 100644 --- a/android-client/PMA.Crypto.pas +++ b/android-client/PMA.Crypto.pas @@ -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) end; + JPBEKeySpec = interface; + JSecretKey = interface; + JSecretKeyFactory = interface; + + JPBEKeySpecClass = interface(JObjectClass) + ['{1A2B3C4D-5E6F-7081-92A3-B4C5D6E7F801}'] + function init(password: TJavaArray; salt: TJavaArray; + iterationCount: Integer; keyLength: Integer): JPBEKeySpec; cdecl; + end; + [JavaSignature('javax/crypto/spec/PBEKeySpec')] + JPBEKeySpec = interface(JObject) + ['{2B3C4D5E-6F70-8192-A3B4-C5D6E7F80112}'] + end; + TJPBEKeySpec = class(TJavaGenericImport) 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; cdecl; + end; + TJSecretKey = class(TJavaGenericImport) 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) 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; + 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.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 diff --git a/mainApk.pas b/mainApk.pas index 0d6222a..ae05806 100644 --- a/mainApk.pas +++ b/mainApk.pas @@ -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...'); diff --git a/pmserverApk.res b/pmserverApk.res new file mode 100644 index 0000000..f9fd9e4 Binary files /dev/null and b/pmserverApk.res differ