feat(crypto): wire AES-GCM and PBKDF2 to Windows CNG
The viewer compiled for Win64 but died on first use: AesGcmDecrypt existed only as JNI, so the desktop build downloaded 27 KB and then refused it. Windows has the same primitive in bcrypt.dll - no DLL to ship, no OpenSSL, and BCryptDeriveKeyPBKDF2 takes bytes, so none of the char[] encoding doubt that keeps the Pascal loop around on Android. This exists so the client can be exercised on the desktop; deploying an APK to watch a list redraw is a slow way to build a UI. selftest.dpr replaces the crypto-vector button the viewer dropped: same known-answer vector, console, exit code = failures. Measured here - Pascal 1670 ms, CNG 528 ms, identical keys on the non-ASCII password. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -17,6 +17,10 @@ unit PMA.Crypto;
|
||||
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
|
||||
@@ -41,8 +45,10 @@ 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.
|
||||
/// 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
|
||||
@@ -75,6 +81,54 @@ uses
|
||||
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 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 = '';
|
||||
|
||||
@@ -306,9 +360,40 @@ begin
|
||||
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 outside Android
|
||||
Result := nil; // no platform provider here
|
||||
end;
|
||||
{$ENDIF}
|
||||
|
||||
@@ -337,11 +422,65 @@ begin
|
||||
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
|
||||
// Windows/macOS build of the same units: no platform AES-GCM available here.
|
||||
// The desktop app already has its own crypto; this unit targets Android.
|
||||
raise EPMACrypto.Create('AES-GCM is only wired for Android in this unit');
|
||||
raise EPMACrypto.Create('AES-GCM is not wired for this platform');
|
||||
end;
|
||||
{$ENDIF}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user