От темата в списанието към проектната практика
Подходящи страници за услуги и технологии към публикацията
При AES шифроване Delphi на практика рядко проблемът е „самият AES“, а заобикалящите условия: данните трябва да се обработват като поток (файлове, BLOB-ове, бекъпи), старите формати трябва да останат четими, а в експлоатация са нужни възможности за отстраняване на грешки (заглавен блок, версиониране) и сигурни стойности по подразбиране (Salt/IV случайни, без повторна употреба). Този фрагмент от изходния код показва затова не само „криптиране/декриптиране“, а малък, надежден формат със заглавен блок, версия, Salt и IV – плюс PBKDF2 за извеждане на ключ и място, където целостта може да бъде добавена разумно.
Защо „шифроването на AES-стринг“ почти никога не е достатъчно
В индивидуален корпоративен софтуер шифроването обикновено се появява на три места: (1) конфигурация/секрети (напр. данни за достъп), (2) обменни/експортни файлове и (3) данни в покой (напр. архиви, контейнер за документи). Наивният подход „Парола → AES-ключ → String вътре/вън“ бързо се проваля:
- Повторна употреба на IV: При режими като CBC или GCM инициализационният вектор (IV) трябва да е уникален за всяко шифроване. Постоянен IV води до изтичане, дори ако паролата е силна.
- Ключ от парола без KDF: Използването на парола директно като ключ (или еднократното ѝ хеширане) позволява офлайн атаки. KDF (Key Derivation Function), като PBKDF2, забавя атакуващите целенасочено.
- Липса на версия на формата: Без заглавен блок/версия трудно ще можете да промените броя итерации, алгоритъма или параметрите по-късно, без старите данни да останат неподдържани.
- Липса на целостност: AES-CBC шифрира, но не предотвратява манипулации. Без автентикация (напр. HMAC или AEAD като GCM) ще получите битфлипинг/падинг проблеми и трудно диагностикирани грешки.
Ядрото на този материал: малък контейнерен формат, който поддържа стрийминг, позволява версиониране и избягва стандартните грешки.
AES шифроване Delphi със заглавен блок, Salt, IV и PBKDF2
Дефинираме прост контейнерен формат, който може да се използва и в BLOB-ове в база данни или в payload-и на съобщения:
- Magic: 4 Bytes, з. б.
NBAE(бърза проверка „Това ли е нашият формат?“) - Version: 1 Byte (позволява миграция)
- KDF-Parameter: брой итерации (4 Bytes)
- Salt: 16 Bytes (случайно за всеки файл)
- IV: 16 Bytes (случайно за всеки файл за AES-CBC)
- Ciphertext: криптирани полезни данни (поддържа стрийминг)
Важно: Salt и IV не са тайни. Те трябва само да бъдат нови за всяко шифроване. Паролата остава тайна; извлеченият от нея ключ не се съхранява.
AES шифроване Delphi в поток: записване/четене на контейнера
Кодът е умишлено написан като „чертеж“: ясно разделени функции, проверими заглавни блокове, никакви скрити глобални променливи. За AES и PBKDF2 много екипи използват утвърдена крипто-библиотека (з. б. DEC). Фрагментът показва формата и шаблона за стрийминг; извикванията на AES/PBKDF2 са капсулирани така, че да можете да ги замените в зависимост от библиотеката.
unit Nb.AesContainer;
interface
uses
System.SysUtils, System.Classes, System.NetEncoding;
type
ENbCryptoError = class(Exception);
TNbAesContainer = class
public
class procedure EncryptStreamToStream(const AIn: TStream; const AOut: TStream;
const APassword: string; const AIterations: Cardinal = 200000);
class procedure DecryptStreamToStream(const AIn: TStream; const AOut: TStream;
const APassword: string);
class function EncryptBytesToBase64(const APlain: TBytes; const APassword: string): string;
class function DecryptBase64ToBytes(const ACipherB64: string; const APassword: string): TBytes;
end;
implementation
const
CMagic: array[0..3] of AnsiChar = ('N','B','A','E');
CVersion: Byte = 1;
CSaltLen = 16;
CIvLen = 16;
type
TNbHeaderV1 = packed record
Magic: array[0..3] of AnsiChar;
Version: Byte;
Iterations: Cardinal; // little endian
Salt: array[0..CSaltLen-1] of Byte;
IV: array[0..CIvLen-1] of Byte;
end;
// --- Зависимости, които трябва да реализирате в зависимост от крипто-стека ---
procedure FillRandomBytes(var B: TBytes);
begin
// За криптографски случайни стойности: използвайте OS-CSPRNG (Windows BCryptGenRandom,
// Linux getrandom/urandom). Тук е умишлено като заместител.
raise ENbCryptoError.Create('FillRandomBytes: CSPRNG не е свързан');
end;
function PBKDF2_HMAC_SHA256(const APassword: string; const ASalt: TBytes;
const AIterations, AKeyLen: Cardinal): TBytes;
begin
// Реализация, напр. с DEC (PBKDF2) или друга библиотека.
// Резултат: AKeyLen байта.
raise ENbCryptoError.Create('PBKDF2_HMAC_SHA256: не е свързан');
end;
procedure AES256_CBC_EncryptStream(const AKey, AIV: TBytes; const AIn, AOut: TStream);
begin
// Реализация чрез библиотека:
// - KeyLen = 32 байта
// - IVLen = 16 байта
// - PKCS#7 попълване
// Важно: обработвайте на поток, не всичко в паметта.
raise ENbCryptoError.Create('AES256_CBC_EncryptStream: не е свързан');
end;
procedure AES256_CBC_DecryptStream(const AKey, AIV: TBytes; const AIn, AOut: TStream);
begin
raise ENbCryptoError.Create('AES256_CBC_DecryptStream: не е свързан');
end;
// --- Помощни функции ---
procedure WriteHeaderV1(const AOut: TStream; const H: TNbHeaderV1);
begin
if AOut.Write(H, SizeOf(H)) <> SizeOf(H) then
raise ENbCryptoError.Create('Хедърът не може да бъде записан');
end;
function ReadHeaderV1(const AIn: TStream): TNbHeaderV1;
var
H: TNbHeaderV1;
begin
if AIn.Read(H, SizeOf(H)) <> SizeOf(H) then
raise ENbCryptoError.Create('Хедърът е непълен');
if (H.Magic[0] <> CMagic[0]) or (H.Magic[1] <> CMagic[1]) or
(H.Magic[2] <> CMagic[2]) or (H.Magic[3] <> CMagic[3]) then
raise ENbCryptoError.Create('Невалиден контейнер (Magic не съвпада)');
if H.Version <> CVersion then
raise ENbCryptoError.CreateFmt('Неизвестна версия на контейнера: %d', [H.Version]);
if (H.Iterations < 10000) or (H.Iterations > 5000000) then
raise ENbCryptoError.Create('Стойността Iterations е извън разумни граници');
Result := H;
end;
class procedure TNbAesContainer.EncryptStreamToStream(const AIn, AOut: TStream;
const APassword: string; const AIterations: Cardinal);
var
H: TNbHeaderV1;
Salt, IV, Key: TBytes;
begin
if APassword = '' then
raise ENbCryptoError.Create('Паролата не може да бъде празна');
// Salt/IV erzeugen
SetLength(Salt, CSaltLen);
SetLength(IV, CIvLen);
FillRandomBytes(Salt);
FillRandomBytes(IV);
// Header befüllen
Move(CMagic[0], H.Magic[0], Length(CMagic));
H.Version := CVersion;
H.Iterations := AIterations;
Move(Salt[0], H.Salt[0], CSaltLen);
Move(IV[0], H.IV[0], CIvLen);
WriteHeaderV1(AOut, H);
// Key ableiten (32 Bytes für AES-256)
Key := PBKDF2_HMAC_SHA256(APassword, Salt, AIterations, 32);
// Nutzdaten verschlüsseln (Ciphertext folgt direkt nach Header)
AES256_CBC_EncryptStream(Key, IV, AIn, AOut);
end;
class procedure TNbAesContainer.DecryptStreamToStream(const AIn, AOut: TStream;
const APassword: string);
var
H: TNbHeaderV1;
Salt, IV, Key: TBytes;
begin
if APassword = '' then
raise ENbCryptoError.Create('Паролата не може да бъде празна');
H := ReadHeaderV1(AIn);
SetLength(Salt, CSaltLen);
SetLength(IV, CIvLen);
Move(H.Salt[0], Salt[0], CSaltLen);
Move(H.IV[0], IV[0], CIvLen);
Key := PBKDF2_HMAC_SHA256(APassword, Salt, H.Iterations, 32);
// Entschlüsseln ab aktueller Stream-Position (nach Header)
AES256_CBC_DecryptStream(Key, IV, AIn, AOut);
end;
class function TNbAesContainer.EncryptBytesToBase64(const APlain: TBytes;
const APassword: string): string;
var
InS, OutS: TBytesStream;
begin
InS := TBytesStream.Create(APlain);
try
OutS := TBytesStream.Create;
try
EncryptStreamToStream(InS, OutS, APassword);
Result := TNetEncoding.Base64.EncodeBytesToString(OutS.Bytes, 0, OutS.Size);
finally
OutS.Free;
end;
finally
InS.Free;
end;
end;
class function TNbAesContainer.DecryptBase64ToBytes(const ACipherB64,
APassword: string): TBytes;
var
Cipher: TBytes;
InS, OutS: TBytesStream;
begin
Cipher := TNetEncoding.Base64.DecodeStringToBytes(ACipherB64);
InS := TBytesStream.Create(Cipher);
try
OutS := TBytesStream.Create;
try
DecryptStreamToStream(InS, OutS, APassword);
Result := OutS.Bytes;
SetLength(Result, OutS.Size);
finally
OutS.Free;
end;
finally
InS.Free;
end;
end;
end.Цел: Минимален контейнер, подходящ за файлове и BLOB-обекти, включително версияция и параметри на KDF. Изисквания: Трябва да положите реална CSPRNG-връзка (криптографски сигурен произвол от операционната система) и надеждна AES/PBKDF2-имплементация под нея. Капани: Не използвайте „някакъв“ Random (не Random()), не ползвайте фиксирани IV, и предвидете ясна обработка на грешки при декриптиране (грешна парола срещу повредени данни). Варианти: вместо CBC предпочитайте AEAD (вж. по-долу), или разширете хедъра с ID на алгоритъма и HMAC.
Интегритет: защо AES-CBC сам по себе си е твърде рисков в експлоатация
AES-CBC все още присъства в много legacy контексти и може да функционира, ако използвате допълнително средство за гаранция на интегритета. Без интегритет атакуващ може да манипулира шифротекста; дори без активен атакуващ, грешки при предаване или дефектни слоеве за съхранение могат да породят трудно диагностицирани „Padding“-грешки.
Прагматични опции:
- Encrypt-then-HMAC: След шифротекста запишете HMAC (напр. HMAC-SHA-256) над header+ciphertext. При четене първо проверете HMAC, след това декриптирайте. За това идеално извеждайте два ключа от PBKDF2 (напр. 64 байта: 32 за AES, 32 за HMAC), вместо да използвате един и същ ключ два пъти.
- AES-GCM: AEAD режим (Authenticated Encryption with Associated Data). Дава ciphertext + auth-tag. Това често е най-чистият избор днес, ако вашата Delphi-библиотека поддържа GCM стабилно. Полета в хедъра могат да бъдат автентикирани като „AAD“, без да ги шифрирате.
Ако трябва да останете на CBC (напр. заради интероп), Encrypt-then-HMAC е здраво допълнение. За нови формати GCM си струва, защото осигурява автентикация „включена“ и прави картината на грешките по-ясна.
Изключително важно: „криптографски случайни числа“ и защо System.Hash не е достатъчен
Често срещан наследствен рефлекс в Delphi-проекти: „Просто вземаме SHA256 върху времеви печат + нещо и имаме Random.“ Това не е надеждна основа. За salt и IV ви трябва CSPRNG (криптографски сигурен псевдослучаен генератор) на операционната система. Под Windows това типично е BCrypt-API (CNG), под Linux — генератор в ядрото като getrandom() или /dev/urandom. Разликата е практическа: CSPRNG е проектиран така, че от наблюдавани стойности да не може да се предвидят следващи стойности.
Архитектурен трик: Капсулйте това в малка „RandomProvider“-Unit, която можете да мокнете в тестове. По този начин решавате два гранични случая наведнъж: възпроизводими тестове (с фиксиран seed в мока) и реална сигурност в продукция (с OS-CSPRNG). Така предотвратявате в един Hotfix „да се вкара“ отново Random(), само защото е бързо.
Отстраняване на грешки и миграция на наследен код: версияцията не е лукс
Хедърът не е само за „крипто-естетика“, а за поддръжка:
- Iteration Tuning: Числото на итерациите на PBKDF2 се променя през годините. С поле в хедъра можете по-късно да го увеличите, без да направите старите данни нечетаеми.
- Formatwechsel: Версия 2 например може да премине към AES-GCM или да добави HMAC.
- Diagnose im Feld: Magic/Version позволяват бързи проверки в логове и инструменти, без да е необходимо да дешифрирате данните.
Практически съвет: имплементирайте малък „Inspector“, който прочита само хедъра (Magic/Version/Iterations) и записва в лог. Така изяснявате много поддръжни случаи („Коя версия е тук?“) без работа с пароли.
Чиста миграция: „Read old, write new“ вместо Big Bang
Ако заменяте стар формат (например фиксиран IV, липса на KDF, Blowfish/3DES или собственоръчно направен XOR), в Delphi-проекти се е доказал следният модел: При четене разпознавате няколко формата (Magic/Version или fallback-хеуристика), при запис вече генерирате само новия формат. Допълнително можете при успешно дешифриране на заден фон да правите пре-шифриране („lazy migration“), ако това пасва на процеса. По този начин намалявате риска при разгръщане и избягвате „всичко да се презшифрира наведнъж“ като прозорец за поддръжка.
Threading und Streaming: typische Kanten in Delphi
Шифроването често се изпълнява в Worker-нишки (например при експорт, при upload в портал на клиента, при запис на големи архиви). Два аспекта, които в Delphi-проекти редовно изпъкват:
- Stream-Positionen: Преди шифроване/дешифриране ясни контракти: входният стрийм се чете от текущата позиция, изходният стрийм се записва от текущата позиция. При повторна употреба на стримове задължително задайте
Position := 0съзнателно. - Memory-Spitzen: Избягвайте „всичко в TBytes“. Подходът със стрийм е критичен за големи файлове. Ако вашата Crypto-Bibliothek приема само Byte-Arrays, струва си допълнителната работа да преминете към стрийм-способна имплементация или да изградите буфериран адаптер.
Ако шифровате в услуги (Windows- или Linux-Services), обърнете също внимание на чисто логване на изключения: „грешна парола“, „хедър повреден“, „Tag/HMAC невалиден“ са различни оперативни случаи и трябва да могат да се различават. Важно: съобщенията за грешка не бива да са твърде детайлни навън (никакво „Padding грешен в блок 7“ като API-грешка), но в логовете вътрешно могат да бъдат по-подробни.
Кога подходът си струва – и къде може да се провали
Струва си, ако вие: (a) съхранявате шифровани експорт-/импорт-данни дългосрочно, (b) оперирате паралелно различни версии на програмата, (c) обработвате данни като стримове или (d) имате нужда от чиста крипто-интерфейс за няколко модула (Client/Server/Tooling).
Може да се провали, ако се опитвате да решите „всичко“ с това: за транспорт е отговорен TLS, не собствен AES-обвивка. За секрети (пароли, токени) често е по-подходящ OS-Secret-Store или Vault. И ако ви трябва интероп с други езици, трябва да документирате хедъра, Endianness и Encoding точно (или да използвате утвърден формат).
Заключение: AES в Delphi е по-малко алгоритъм, повече инженеринг
Реалната полза от този фрагмент не е „AES работи“, а оперативно годен формат: случайна сол и IV, версиониран хедър, PBKDF2-параметри в payload-а и стрийм-ориентирана обработка. Добавете за новите формати, където е възможно, интегритет (AES-GCM или Encrypt-then-HMAC). Така от „ние шифрираме нещо“ се получава компонент, който в дигиталните корпоративни решения остава поддържим и мигруем и след години.
Ако трябва да интегрирате такъв контейнер в съществуваща Delphi среда или да мигрирате чисто от наследен формат, си струва кратка архитектурна проверка (управление на ключове, версии на формата, експлоатация/логиране). Детайлите можем при необходимост да изясним в разговор:
В експертната област Delphi Aes и Pbkdf2 Delphi също играят важна роля, когато интеграции, потоци от данни и по-нататъшно развитие трябва да работят коректно заедно.
Следваща стъпка
Когато темата прерасне в реален проект, архитектурата, съществуващото състояние и експлоатацията трябва да бъдат разгледани съвместно още в ранна фаза.
Подпомагаме не само при отделни въпроси, но и когато от фрагменти от изходен код, проблеми с наследени системи или идеи за портал трябва да бъде реализиран надежден корпоративен проект.
- Сегашното състояние, целевото състояние и техническите рискове се оценяват съвместно.
- REST, достъпът до данни, порталите и разгръщането не се отлагат като по-късни последици.
- Виждате рано кой път е икономически и експлоатационно жизнеспособен.