unit PMA.Crypto; { Opens a pm-encrypted-export-v1 container (the vault snapshot) on Android. Format, produced by js/app.import.js encryptExportPayload(): kdf 'pbkdf2-sha256' kdf_iterations 600000 kdf_salt base64, 32 bytes iv base64, 12 bytes ciphertext base64, AES-GCM output = ciphertext || 16-byte tag (payload) UTF-8 JSON TWO DIFFERENT ROUTES, on purpose: - AES-GCM goes through JNI to javax.crypto. One call, no loop, and the platform implementation is hardware-accelerated. Java's Cipher expects the tag appended to the ciphertext, which is exactly what WebCrypto produces - the buffers are byte-identical, nothing to reshape. On Windows the same primitive comes from bcrypt.dll (CNG) - no DLL to ship, no OpenSSL. It exists so the client can be exercised on the desktop: deploying an APK to see a list redraw is a slow way to build a UI. - PBKDF2 is implemented HERE rather than via SecretKeyFactory, because javax.crypto.spec.PBEKeySpec takes a char[] and the provider decides how those chars become bytes: UTF-8 on some, low-byte on others. For a non-ASCII sync password the derived key would differ from the browser's, and the failure is silent - "wrong password" on a correct password. Doing it here fixes the encoding at UTF-8 and makes the match exact by construction. The cost is 600k HMAC-SHA256 rounds in Pascal; measure it on a real device (see DeriveKeyPBKDF2's note) before considering the JNI route again. } interface uses System.SysUtils, System.Classes, System.JSON, System.NetEncoding, System.Hash, // System.DateUtils : TTimeZone, pour horodater le conteneur en UTC. System.DateUtils; type EPMACrypto = class(Exception); /// Derive a key the same way WebCrypto's deriveExportKey does: PBKDF2-SHA256 /// over the UTF-8 bytes of the password. AKeyLenBytes = 32 for AES-256. function DeriveKeyPBKDF2(const APassword: string; const ASalt: TBytes; AIterations, AKeyLenBytes: Integer): TBytes; /// Same derivation, delegated to the platform - natively implemented, so /// faster than the Pascal loop. Android: SecretKeyFactory. Windows: CNG's /// BCryptDeriveKeyPBKDF2, which takes BYTES and so carries none of the doubt /// below. /// /// 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; /// 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. function AesGcmDecrypt(const AKey, AIv, ACipherText: TBytes): TBytes; /// Full container -> plaintext JSON payload. Validates the format marker so a /// truncated download or an HTML error page fails with a clear message rather /// than as "wrong password". function DecryptContainer(const AContainerJson, APassword: string): string; /// N octets d'un generateur CRYPTOGRAPHIQUE - SecureRandom sur Android, /// BCryptGenRandom sur Windows. JAMAIS Random : un sel ou un IV previsible /// ruine le chiffrement qui les emploie, et rien dans le resultat ne le /// montrerait. Leve plutot que de rendre des octets faibles. function SecureRandomBytes(ACount: Integer): TBytes; /// AES-256-GCM chiffrement. Le tag de 16 octets est colle A LA FIN du /// resultat, comme le fait WebCrypto - c'est ce que DecryptContainer et le /// poste Windows attendent tous les deux. function AesGcmEncrypt(const AKey, AIv, APlainText: TBytes): TBytes; /// Texte JSON -> conteneur complet, dans le format exact que le poste ecrit /// (`pm-encrypted-export-v1`). Sel et IV tires a chaque appel : deux /// chiffrements du meme coffre ne se ressemblent pas, et c'est voulu. function EncryptContainer(const APlainJson, APassword: string; AIterations: Integer = 600000): string; implementation {$IF DEFINED(ANDROID)} uses // Androidapi.Jni : sans elle, TJavaArray.Length reste un appel non inline // et le compilateur emet un avertissement a chaque usage. Androidapi.Jni, Androidapi.JNIBridge, Androidapi.JNI.JavaTypes, Androidapi.Helpers; {$ENDIF} {$IF DEFINED(MSWINDOWS)} // CNG, declare ici : Delphi ne livre pas d'unite Winapi.Bcrypt, et six // fonctions ne justifient pas d'en importer une d'ailleurs. const BCRYPT_DLL = 'bcrypt.dll'; BCRYPT_ALG_HANDLE_HMAC_FLAG = $00000008; AUTH_MODE_INFO_VERSION = 1; type // Disposition identique au C : champs dans l'ordre, alignement par defaut // ({$A8}) - les pointeurs et le UInt64 tombent sur 8, comme chez MSVC. TBCryptAuthCipherModeInfo = record cbSize: Cardinal; dwInfoVersion: Cardinal; pbNonce: PByte; cbNonce: Cardinal; pbAuthData: PByte; cbAuthData: Cardinal; pbTag: PByte; cbTag: Cardinal; pbMacContext: PByte; cbMacContext: Cardinal; cbAAD: Cardinal; cbData: UInt64; dwFlags: Cardinal; end; function BCryptOpenAlgorithmProvider(var phAlgorithm: Pointer; pszAlgId, pszImplementation: PWideChar; dwFlags: Cardinal): Integer; stdcall; external BCRYPT_DLL; function BCryptCloseAlgorithmProvider(hAlgorithm: Pointer; dwFlags: Cardinal): Integer; stdcall; external BCRYPT_DLL; function BCryptSetProperty(hObject: Pointer; pszProperty: PWideChar; pbInput: PByte; cbInput, dwFlags: Cardinal): Integer; stdcall; external BCRYPT_DLL; function BCryptGenerateSymmetricKey(hAlgorithm: Pointer; var phKey: Pointer; pbKeyObject: PByte; cbKeyObject: Cardinal; pbSecret: PByte; cbSecret, dwFlags: Cardinal): Integer; stdcall; external BCRYPT_DLL; function BCryptDestroyKey(hKey: Pointer): Integer; stdcall; external BCRYPT_DLL; function BCryptEncrypt(hKey: Pointer; pbInput: PByte; cbInput: Cardinal; pPaddingInfo: Pointer; pbIV: PByte; cbIV: Cardinal; pbOutput: PByte; cbOutput: Cardinal; var pcbResult: Cardinal; dwFlags: Cardinal): Integer; stdcall; external BCRYPT_DLL; function BCryptGenRandom(hAlgorithm: Pointer; pbBuffer: PByte; cbBuffer: Cardinal; dwFlags: Cardinal): Integer; stdcall; external BCRYPT_DLL; function BCryptDecrypt(hKey: Pointer; pbInput: PByte; cbInput: Cardinal; pPaddingInfo: Pointer; pbIV: PByte; cbIV: Cardinal; pbOutput: PByte; cbOutput: Cardinal; var pcbResult: Cardinal; dwFlags: Cardinal): Integer; stdcall; external BCRYPT_DLL; function BCryptDeriveKeyPBKDF2(hPrf: Pointer; pbPassword: PByte; cbPassword: Cardinal; pbSalt: PByte; cbSalt: Cardinal; cIterations: UInt64; pbDerivedKey: PByte; cbDerivedKey, dwFlags: Cardinal): Integer; stdcall; external BCRYPT_DLL; {$ENDIF} var FLastNativeError: string = ''; function LastNativeError: string; begin Result := FLastNativeError; end; // --------------------------------------------------------------------------- // PBKDF2-SHA256 // --------------------------------------------------------------------------- function XorBytes(const A, B: TBytes): TBytes; var I: Integer; begin SetLength(Result, Length(A)); for I := 0 to High(A) do Result[I] := A[I] xor B[I]; end; function DeriveKeyPBKDF2(const APassword: string; const ASalt: TBytes; AIterations, AKeyLenBytes: Integer): TBytes; // Straight RFC 2898. One block is enough for a 32-byte key with SHA-256 // (hLen = 32), so there is no block loop to get wrong. // // PERFORMANCE: 600k HMAC rounds. Budget a few seconds on a mid-range phone and // run it OFF the UI thread. If it proves unacceptable, the answer is to cache // the derived key in the Android Keystore behind biometrics - NOT to lower the // iteration count, which is shared with the desktop and defines the format. var LPwd, LBlock, LU, LT: TBytes; I: Integer; begin if AKeyLenBytes > 32 then raise EPMACrypto.Create('KeyLen > one SHA-256 block is not implemented'); LPwd := TEncoding.UTF8.GetBytes(APassword); // U1 = HMAC(pwd, salt || INT32BE(1)) SetLength(LBlock, Length(ASalt) + 4); if Length(ASalt) > 0 then Move(ASalt[0], LBlock[0], Length(ASalt)); LBlock[Length(ASalt)] := 0; LBlock[Length(ASalt) + 1] := 0; LBlock[Length(ASalt) + 2] := 0; LBlock[Length(ASalt) + 3] := 1; LU := THashSHA2.GetHMACAsBytes(LBlock, LPwd, THashSHA2.TSHA2Version.SHA256); LT := Copy(LU, 0, Length(LU)); for I := 2 to AIterations do begin LU := THashSHA2.GetHMACAsBytes(LU, LPwd, THashSHA2.TSHA2Version.SHA256); LT := XorBytes(LT, LU); end; Result := Copy(LT, 0, AKeyLenBytes); end; // --------------------------------------------------------------------------- // AES-GCM through javax.crypto // --------------------------------------------------------------------------- {$IF DEFINED(ANDROID)} type // Minimal JNI imports. Delphi ships no headers for javax.crypto, and the // bridge builds its method signatures from these declarations - hence the // real Java types (Key, AlgorithmParameterSpec) rather than JObject, which // would generate a signature no method matches. JKey = interface; JAlgorithmParameterSpec = interface; JSecretKeySpec = interface; JGCMParameterSpec = interface; JCipher = interface; JKeyClass = interface(IJavaClass) ['{6E0F5B9A-1C2D-4E3F-8A7B-9C0D1E2F3A4B}'] end; [JavaSignature('java/security/Key')] JKey = interface(IJavaInstance) ['{7F1A6CAB-2D3E-4F50-9B8C-AD1E2F3A4B5C}'] end; TJKey = class(TJavaGenericImport) end; JAlgorithmParameterSpecClass = interface(IJavaClass) ['{8A2B7DBC-3E4F-5061-AC9D-BE2F3A4B5C6D}'] end; [JavaSignature('java/security/spec/AlgorithmParameterSpec')] JAlgorithmParameterSpec = interface(IJavaInstance) ['{9B3C8ECD-4F50-6172-BDAE-CF3A4B5C6D7E}'] end; TJAlgorithmParameterSpec = class(TJavaGenericImport) end; JSecretKeySpecClass = interface(JKeyClass) ['{AC4D9FDE-5061-7283-CEBF-DA4B5C6D7E8F}'] function init(key: TJavaArray; algorithm: JString): JSecretKeySpec; cdecl; end; [JavaSignature('javax/crypto/spec/SecretKeySpec')] JSecretKeySpec = interface(JKey) ['{BD5EA0EF-6172-8394-DFCA-EB5C6D7E8F90}'] end; TJSecretKeySpec = class(TJavaGenericImport) end; JGCMParameterSpecClass = interface(JAlgorithmParameterSpecClass) ['{CE6FB1F0-7283-94A5-E0DB-FC6D7E8F9001}'] function init(tLen: Integer; src: TJavaArray): JGCMParameterSpec; cdecl; end; [JavaSignature('javax/crypto/spec/GCMParameterSpec')] JGCMParameterSpec = interface(JAlgorithmParameterSpec) ['{DF70C201-8394-A5B6-F1EC-0D7E8F900112}'] end; TJGCMParameterSpec = class(TJavaGenericImport) end; JKeySpec = interface; JPBEKeySpec = interface; JSecretKey = interface; JSecretKeyFactory = interface; // 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) end; JPBEKeySpecClass = interface(JKeySpecClass) ['{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(JKeySpec) ['{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: JKeySpec): JSecretKey; cdecl; end; TJSecretKeyFactory = class(TJavaGenericImport) end; JCipherClass = interface(JObjectClass) ['{E081D312-94A5-B6C7-02FD-1E8F90011223}'] function _GetDECRYPT_MODE: Integer; cdecl; function _GetENCRYPT_MODE: Integer; cdecl; function getInstance(transformation: JString): JCipher; cdecl; property DECRYPT_MODE: Integer read _GetDECRYPT_MODE; property ENCRYPT_MODE: Integer read _GetENCRYPT_MODE; end; [JavaSignature('javax/crypto/Cipher')] JCipher = interface(JObject) ['{F192E423-A5B6-C7D8-130E-2F9001122334}'] procedure init(opmode: Integer; key: JKey; params: JAlgorithmParameterSpec); cdecl; function doFinal(input: TJavaArray): TJavaArray; cdecl; end; TJCipher = class(TJavaGenericImport) end; // Declare ICI plutot qu'en important Androidapi.JNI.Java.Security : cette // unite redeclarerait JKey et consorts, et Delphi resout par le dernier uses // - de quoi relier silencieusement le code existant a d'autres interfaces. JSecureRandom = interface; JSecureRandomClass = interface(JObjectClass) ['{9C1D2E3F-4A5B-6C7D-8E9F-0A1B2C3D4E5F}'] function init: JSecureRandom; cdecl; end; [JavaSignature('java/security/SecureRandom')] JSecureRandom = interface(JObject) ['{AD2E3F40-5B6C-7D8E-9F0A-1B2C3D4E5F60}'] procedure nextBytes(bytes: TJavaArray); cdecl; end; TJSecureRandom = class(TJavaGenericImport) end; function ToJavaBytes(const B: TBytes): TJavaArray; var I: Integer; begin Result := TJavaArray.Create(Length(B)); for I := 0 to High(B) do Result.Items[I] := B[I]; end; function FromJavaBytes(const A: TJavaArray): TBytes; var I: Integer; begin SetLength(Result, A.Length); for I := 0 to A.Length - 1 do Result[I] := A.Items[I]; 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(LSpec).getEncoded); except // 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; {$ELSEIF DEFINED(MSWINDOWS)} var LPrf: Pointer; LPwd: TBytes; begin Result := nil; LPwd := TEncoding.UTF8.GetBytes(APassword); LPrf := nil; // HMAC flag : sans lui le handle sert au hachage simple, et la derivation // echoue au lieu de silencieusement produire autre chose. if BCryptOpenAlgorithmProvider(LPrf, 'SHA256', nil, BCRYPT_ALG_HANDLE_HMAC_FLAG) <> 0 then begin FLastNativeError := 'BCryptOpenAlgorithmProvider(SHA256) failed'; Exit; end; try SetLength(Result, AKeyLenBytes); // Le mot de passe part en OCTETS UTF-8 : contrairement au char[] de Java, // aucun choix d'encodage n'est laisse au fournisseur. if BCryptDeriveKeyPBKDF2(LPrf, PByte(LPwd), Length(LPwd), PByte(ASalt), Length(ASalt), AIterations, PByte(Result), AKeyLenBytes, 0) <> 0 then begin FLastNativeError := 'BCryptDeriveKeyPBKDF2 failed'; Result := nil; end; finally BCryptCloseAlgorithmProvider(LPrf, 0); end; end; {$ELSE} begin Result := nil; // no platform provider here end; {$ENDIF} function AesGcmDecrypt(const AKey, AIv, ACipherText: TBytes): TBytes; {$IF DEFINED(ANDROID)} var LCipher: JCipher; LKeySpec: JSecretKeySpec; LGcm: JGCMParameterSpec; begin try LKeySpec := TJSecretKeySpec.JavaClass.init(ToJavaBytes(AKey), StringToJString('AES')); // 128 = tag length in BITS. WebCrypto's default is the same 16 bytes; a // mismatch here fails authentication on otherwise valid data. LGcm := TJGCMParameterSpec.JavaClass.init(128, ToJavaBytes(AIv)); LCipher := TJCipher.JavaClass.getInstance(StringToJString('AES/GCM/NoPadding')); LCipher.init(TJCipher.JavaClass.DECRYPT_MODE, LKeySpec, LGcm); Result := FromJavaBytes(LCipher.doFinal(ToJavaBytes(ACipherText))); except // AEADBadTagException surfaces here: wrong password, or altered bytes. // Both are indistinguishable by design - that is what authentication means. on E: Exception do // Message en pur ASCII : un tiret cadratin dans une source lue en ANSI // par Delphi ressort en mojibake sur l'appareil. raise EPMACrypto.Create('Decryption failed - wrong password or corrupt file'); end; end; {$ELSEIF DEFINED(MSWINDOWS)} var LAlg, LKey: Pointer; LInfo: TBCryptAuthCipherModeInfo; LTag, LCt: TBytes; LOutLen: Cardinal; LMode: string; begin // GCM porte son tag a la fin, colle par WebCrypto. CNG le veut a part, dans // la structure - c'est la seule difference avec le chemin Java, qui l'accepte // concatene. if Length(ACipherText) < 16 then raise EPMACrypto.Create('Ciphertext shorter than its GCM tag'); LAlg := nil; LKey := nil; if BCryptOpenAlgorithmProvider(LAlg, 'AES', nil, 0) <> 0 then raise EPMACrypto.Create('BCryptOpenAlgorithmProvider(AES) failed'); try LMode := 'ChainingModeGCM'; // Longueur AVEC le zero terminal : CNG compare la chaine complete. if BCryptSetProperty(LAlg, 'ChainingMode', PByte(PWideChar(LMode)), (Length(LMode) + 1) * SizeOf(WideChar), 0) <> 0 then raise EPMACrypto.Create('BCryptSetProperty(GCM) failed'); if BCryptGenerateSymmetricKey(LAlg, LKey, nil, 0, PByte(AKey), Length(AKey), 0) <> 0 then raise EPMACrypto.Create('BCryptGenerateSymmetricKey failed'); LTag := Copy(ACipherText, Length(ACipherText) - 16, 16); LCt := Copy(ACipherText, 0, Length(ACipherText) - 16); FillChar(LInfo, SizeOf(LInfo), 0); LInfo.cbSize := SizeOf(LInfo); LInfo.dwInfoVersion := AUTH_MODE_INFO_VERSION; LInfo.pbNonce := PByte(AIv); LInfo.cbNonce := Length(AIv); LInfo.pbTag := PByte(LTag); LInfo.cbTag := Length(LTag); SetLength(Result, Length(LCt)); LOutLen := 0; // pbIV a nil : en mode authentifie le nonce passe par la structure, et // remplir les deux fait echouer l'appel. if BCryptDecrypt(LKey, PByte(LCt), Length(LCt), @LInfo, nil, 0, PByte(Result), Length(Result), LOutLen, 0) <> 0 then // STATUS_AUTH_TAG_MISMATCH arrive ici : mot de passe faux, ou octets // alteres. Indiscernables par construction - c'est ce qu'authentifier // veut dire. Meme message que la branche Android. raise EPMACrypto.Create('Decryption failed - wrong password or corrupt file'); SetLength(Result, LOutLen); finally if LKey <> nil then BCryptDestroyKey(LKey); BCryptCloseAlgorithmProvider(LAlg, 0); end; end; {$ELSE} begin raise EPMACrypto.Create('AES-GCM is not wired for this platform'); end; {$ENDIF} // --------------------------------------------------------------------------- // Alea et chiffrement // --------------------------------------------------------------------------- function SecureRandomBytes(ACount: Integer): TBytes; {$IF DEFINED(ANDROID)} var LRnd: JSecureRandom; LArr: TJavaArray; begin if ACount <= 0 then Exit(nil); // Le constructeur sans argument s'ensemence tout seul depuis le systeme ; // fournir une graine soi-meme ne ferait que l'affaiblir. LRnd := TJSecureRandom.JavaClass.init; LArr := TJavaArray.Create(ACount); try LRnd.nextBytes(LArr); Result := FromJavaBytes(LArr); finally LArr.Free; end; if Length(Result) <> ACount then raise EPMACrypto.Create('SecureRandom returned the wrong length'); end; {$ELSEIF DEFINED(MSWINDOWS)} const BCRYPT_USE_SYSTEM_PREFERRED_RNG = $00000002; begin if ACount <= 0 then Exit(nil); SetLength(Result, ACount); // Handle nul + drapeau systeme : on prend le generateur par defaut de // Windows sans avoir a ouvrir puis fermer un fournisseur. if BCryptGenRandom(nil, PByte(Result), ACount, BCRYPT_USE_SYSTEM_PREFERRED_RNG) <> 0 then raise EPMACrypto.Create('BCryptGenRandom failed'); end; {$ELSE} begin raise EPMACrypto.Create('No secure random source on this platform'); end; {$ENDIF} function AesGcmEncrypt(const AKey, AIv, APlainText: TBytes): TBytes; {$IF DEFINED(ANDROID)} var LCipher: JCipher; LKeySpec: JSecretKeySpec; LGcm: JGCMParameterSpec; begin LKeySpec := TJSecretKeySpec.JavaClass.init(ToJavaBytes(AKey), StringToJString('AES')); // 128 bits de tag, comme au dechiffrement et comme WebCrypto. LGcm := TJGCMParameterSpec.JavaClass.init(128, ToJavaBytes(AIv)); LCipher := TJCipher.JavaClass.getInstance(StringToJString('AES/GCM/NoPadding')); LCipher.init(TJCipher.JavaClass.ENCRYPT_MODE, LKeySpec, LGcm); // Java rend deja le tag colle a la fin - meme disposition que WebCrypto. Result := FromJavaBytes(LCipher.doFinal(ToJavaBytes(APlainText))); end; {$ELSEIF DEFINED(MSWINDOWS)} var LAlg, LKey: Pointer; LInfo: TBCryptAuthCipherModeInfo; LTag, LCt: TBytes; LOutLen: Cardinal; LMode: string; begin LAlg := nil; LKey := nil; if BCryptOpenAlgorithmProvider(LAlg, 'AES', nil, 0) <> 0 then raise EPMACrypto.Create('BCryptOpenAlgorithmProvider(AES) failed'); try LMode := 'ChainingModeGCM'; if BCryptSetProperty(LAlg, 'ChainingMode', PByte(PWideChar(LMode)), (Length(LMode) + 1) * SizeOf(WideChar), 0) <> 0 then raise EPMACrypto.Create('BCryptSetProperty(GCM) failed'); if BCryptGenerateSymmetricKey(LAlg, LKey, nil, 0, PByte(AKey), Length(AKey), 0) <> 0 then raise EPMACrypto.Create('BCryptGenerateSymmetricKey failed'); // CNG veut le tag DANS la structure, pas dans le flux : on l'y recueille, // puis on le colle a la fin pour rendre la meme forme que WebCrypto. SetLength(LTag, 16); SetLength(LCt, Length(APlainText)); FillChar(LInfo, SizeOf(LInfo), 0); LInfo.cbSize := SizeOf(LInfo); LInfo.dwInfoVersion := AUTH_MODE_INFO_VERSION; LInfo.pbNonce := PByte(AIv); LInfo.cbNonce := Length(AIv); LInfo.pbTag := PByte(LTag); LInfo.cbTag := Length(LTag); LOutLen := 0; if BCryptEncrypt(LKey, PByte(APlainText), Length(APlainText), @LInfo, nil, 0, PByte(LCt), Length(LCt), LOutLen, 0) <> 0 then raise EPMACrypto.Create('BCryptEncrypt failed'); SetLength(LCt, LOutLen); Result := Concat(LCt, LTag); finally if LKey <> nil then BCryptDestroyKey(LKey); BCryptCloseAlgorithmProvider(LAlg, 0); end; end; {$ELSE} begin raise EPMACrypto.Create('AES-GCM is not wired for this platform'); end; {$ENDIF} // --------------------------------------------------------------------------- // Container // --------------------------------------------------------------------------- /// Base64 SANS retour a la ligne. TNetEncoding.Base64 coupe a 76 caracteres, /// comme le courrier electronique ; WebCrypto n'en met aucun. Les decodeurs /// tolerants ignorent ces retours - `atob` retire les blancs, Node aussi - mais /// produire exactement la meme forme que l'autre implementation retire une /// dependance a leur tolerance. function B64NoWrap(const ABytes: TBytes): string; var LEnc: TBase64Encoding; begin LEnc := TBase64Encoding.Create(0); // 0 = jamais de coupure try Result := LEnc.EncodeBytesToString(ABytes); finally LEnc.Free; end; end; function EncryptContainer(const APlainJson, APassword: string; AIterations: Integer): string; var LObj: TJSONObject; LSalt, LIv, LKey, LCt: TBytes; begin if APassword = '' then raise EPMACrypto.Create('Refusing to encrypt with an empty password'); // TIRES A CHAQUE APPEL, tous les deux. Reutiliser un IV avec la meme cle // casse GCM entierement - ce n'est pas une precaution de style. LSalt := SecureRandomBytes(32); LIv := SecureRandomBytes(12); LKey := DeriveKeyPBKDF2Native(APassword, LSalt, AIterations, 32); if LKey = nil then LKey := DeriveKeyPBKDF2(APassword, LSalt, AIterations, 32); LCt := AesGcmEncrypt(LKey, LIv, TEncoding.UTF8.GetBytes(APlainJson)); LObj := TJSONObject.Create; try // Memes cles, memes valeurs que encryptExportPayload cote poste : c'est ce // conteneur-la que les deux clients savent relire. LObj.AddPair('format', 'pm-encrypted-export-v1'); LObj.AddPair('kdf', 'pbkdf2-sha256'); LObj.AddPair('kdf_iterations', TJSONNumber.Create(AIterations)); LObj.AddPair('kdf_salt', B64NoWrap(LSalt)); LObj.AddPair('iv', B64NoWrap(LIv)); LObj.AddPair('ciphertext', B64NoWrap(LCt)); LObj.AddPair('created_at', FormatDateTime('yyyy-mm-dd"T"hh:nn:ss"Z"', TTimeZone.Local.ToUniversalTime(Now))); Result := LObj.ToJSON; finally LObj.Free; end; end; function DecryptContainer(const AContainerJson, APassword: string): string; var LObj: TJSONObject; LFormat, LKdf: string; LIterations: Integer; LSalt, LIv, LCt, LKey, LPlain: TBytes; begin LObj := TJSONObject.ParseJSONValue(AContainerJson) as TJSONObject; if LObj = nil then raise EPMACrypto.Create('Not a JSON container (truncated download?)'); try // Checked before anything else: an HTML error page or a half-written file // would otherwise reach the cipher and be reported as a wrong password. LFormat := LObj.GetValue('format', ''); if LFormat <> 'pm-encrypted-export-v1' then raise EPMACrypto.CreateFmt('Unknown container format: "%s"', [LFormat]); LKdf := LObj.GetValue('kdf', ''); if LKdf <> 'pbkdf2-sha256' then raise EPMACrypto.CreateFmt('Unsupported KDF: "%s"', [LKdf]); LIterations := LObj.GetValue('kdf_iterations', 600000); LSalt := TNetEncoding.Base64.DecodeStringToBytes(LObj.GetValue('kdf_salt', '')); LIv := TNetEncoding.Base64.DecodeStringToBytes(LObj.GetValue('iv', '')); LCt := TNetEncoding.Base64.DecodeStringToBytes(LObj.GetValue('ciphertext', '')); if (Length(LSalt) = 0) or (Length(LIv) = 0) or (Length(LCt) = 0) then raise EPMACrypto.Create('Container is missing salt, iv or ciphertext'); // Android's provider first - measured identical to WebCrypto on a // non-ASCII password (so it does encode as UTF-8), and it is platform // code rather than ours. Barely faster though: 3869 ms against 4139 ms, // PBKDF2 gets no hardware acceleration. The Pascal path stays as the // fallback, and is the only one outside Android. LKey := DeriveKeyPBKDF2Native(APassword, LSalt, LIterations, 32); if LKey = nil then LKey := DeriveKeyPBKDF2(APassword, LSalt, LIterations, 32); LPlain := AesGcmDecrypt(LKey, LIv, LCt); Result := TEncoding.UTF8.GetString(LPlain); finally LObj.Free; end; end; end.