Net-Base Magazyn

30.05.2026

Szyfrowanie AES w Delphi: solidny fragment kodu źródłowego z IV, solą, nagłówkiem i strumieniowaniem

Praktyczny fragment źródła Delphi do szyfrowania AES z losową solą i IV (wektorem inicjalizacyjnym), przejrzystą strukturą nagłówka pliku, wyprowadzeniem klucza PBKDF2 oraz strumieniowym przetwarzaniem — wraz z typowymi pułapkami formatów legacy, kwestiami integralności i eksploatacji.

30.05.2026

Od tematu magazynowego do praktyki projektowej

Pasujące strony usługowe i techniczne do artykułu

Przy szyfrowaniu AES Delphi w praktyce rzadko chodzi o „sam AES”, lecz o warunki brzegowe: dane trzeba przetwarzać jako strumień (pliki, BLOBy, kopie zapasowe), stare formaty muszą pozostać czytelne, a w eksploatacji potrzebna jest możliwość debugowania (nagłówek, wersjonowanie) oraz bezpieczne wartości domyślne (Salt/IV losowe, brak ponownego użycia). Ten fragment kodu źródłowego pokazuje więc nie tylko „szyfrowanie/odszyfrowywanie”, lecz mały, odporny format z nagłówkiem, wersją, Saltem i IV – plus PBKDF2 do wyprowadzania klucza i miejsce, w którym sensownie można dodać integralność.

Dlaczego „zaszyfrowanie łańcucha przy użyciu AES” prawie nigdy nie wystarcza

W oprogramowaniu firmowym na zamówienie szyfrowanie zwykle pojawia się w trzech miejscach: (1) konfiguracja/sekrety (np. dane dostępowe), (2) pliki wymiany/eksportu i (3) dane nieaktywne (np. archiwa, kontenery dokumentów). Naiwny schemat „hasło → klucz AES → wrzucić/wyciągnąć ciąg” szybko się załamuje:

  • Powtórne użycie IV: W trybach takich jak CBC czy GCM wektor inicjalizacyjny (IV) musi być unikatowy dla każdego szyfrowania. Stały IV to wyciek, nawet jeśli hasło jest silne.
  • Klucz z hasła bez KDF: Użycie hasła bezpośrednio jako klucza (lub jednorazowe jego zhashowanie) umożliwia ataki offline. Funkcja KDF (Key Derivation Function) taka jak PBKDF2 spowalnia atakujących celowo.
  • Brak wersji formatu: Bez nagłówka/wersji trudno później zmienić liczbę iteracji, algorytm lub parametry, nie pozostawiając starych danych „bez opieki”.
  • Brak integralności: AES-CBC szyfruje, ale nie chroni przed manipulacją. Bez uwierzytelnienia (np. HMAC lub AEAD jak GCM) pojawią się problemy z bitflippingiem/paddingiem i trudno rozpoznawalne błędy.

Sedno tego artykułu: mały format kontenera, który obsługuje strumieniowanie, jest wersjonowalny i unika standardowych błędów.

Szyfrowanie AES Delphi z nagłówkiem, Saltem, IV i PBKDF2

Definiujemy prosty format kontenera, który można używać także w BLOBach bazy danych lub w payloadach wiadomości:

  • Magic: 4 bajty, np. NBAE (szybkie sprawdzenie „czy to nasz format?”)
  • Wersja: 1 bajt (umożliwia migrację)
  • Parametry KDF: liczba iteracji (4 bajty)
  • Salt: 16 bajtów (losowe dla każdego pliku)
  • IV: 16 bajtów (losowe dla każdego pliku dla AES-CBC)
  • Szyfrogram: zaszyfrowane dane użytkowe (obsługujące strumieniowanie)

Ważne: Salt i IV nie są tajne. Muszą być tylko nowe dla każdego szyfrowania. Hasło pozostaje tajne; wyprowadzony z niego klucz nie jest przechowywany.

Szyfrowanie AES Delphi w strumieniu: zapisywanie/odczytywanie kontenera

Ten kod jest świadomie napisany jako „schemat”: wyraźnie rozdzielone funkcje, możliwe do sprawdzenia nagłówki, brak ukrytych globalnych zmiennych. Do AES i PBKDF2 wiele zespołów używa sprawdzonej biblioteki kryptograficznej (np. DEC). Fragment pokazuje format i wzorzec strumieniowania; wywołania AES-/PBKDF2 są tak zapakowane, że można je wymienić zależnie od biblioteki.

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;

// — Zależności, które należy zaimplementować w zależności od stosu kryptograficznego —

procedure FillRandomBytes(var B: TBytes);
begin
// Dla losowości kryptograficznej użyć OS-CSPRNG (Windows BCryptGenRandom,
// Linux getrandom/urandom). Tutaj celowo jako zastępnik.
raise ENbCryptoError.Create(‚FillRandomBytes: CSPRNG niepodłączony‘);
end;

function PBKDF2_HMAC_SHA256(const APassword: string; const ASalt: TBytes;
const AIterations, AKeyLen: Cardinal): TBytes;
begin
// Implementacja np. za pomocą DEC (PBKDF2) lub innej biblioteki.
// Wynik: AKeyLen bajtów.
raise ENbCryptoError.Create(‚PBKDF2_HMAC_SHA256: niezaimplementowane‘);
end;

procedure AES256_CBC_EncryptStream(const AKey, AIV: TBytes; const AIn, AOut: TStream);
begin
// Implementacja przy użyciu biblioteki:
// – KeyLen = 32 Bytes
// – IVLen = 16 Bytes
// – PKCS#7 Padding
// Ważne: przetwarzać w trybie strumieniowym, nie wszystko w pamięci.
raise ENbCryptoError.Create(‚AES256_CBC_EncryptStream: niezaimplementowane‘);
end;

procedure AES256_CBC_DecryptStream(const AKey, AIV: TBytes; const AIn, AOut: TStream);
begin
raise ENbCryptoError.Create(‚AES256_CBC_DecryptStream: niezaimplementowane‘);
end;

// — Funkcje pomocnicze —

procedure WriteHeaderV1(const AOut: TStream; const H: TNbHeaderV1);
begin
if AOut.Write(H, SizeOf(H)) <> SizeOf(H) then
raise ENbCryptoError.Create(‚Nie udało się zapisać nagłówka‘);
end;

function ReadHeaderV1(const AIn: TStream): TNbHeaderV1;
var
H: TNbHeaderV1;
begin
if AIn.Read(H, SizeOf(H)) <> SizeOf(H) then
raise ENbCryptoError.Create(‚Nagłówek niekompletny‘);

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(‚Nieprawidłowy kontener (Magic nie pasuje)‘);

if H.Version <> CVersion then
raise ENbCryptoError.CreateFmt(‚Nieznana wersja kontenera: %d‘, [H.Version]);

if (H.Iterations < 10000) or (H.Iterations > 5000000) then
raise ENbCryptoError.Create(‚Liczba iteracji poza rozsądnymi granicami‘);

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(‚Hasło nie może być puste‘);

// Generowanie Salt/IV
SetLength(Salt, CSaltLen);
SetLength(IV, CIvLen);
FillRandomBytes(Salt);
FillRandomBytes(IV);

// Wypełnianie nagłówka
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);

// Wyprowadzenie klucza (32 bajty dla AES-256)
Key := PBKDF2_HMAC_SHA256(APassword, Salt, AIterations, 32);

// Szyfrowanie danych użytkowych (ciphertext następuje bezpośrednio po nagłówku)
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(‚Hasło nie może być puste‘);

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);

// Odszyfrowanie od bieżącej pozycji strumienia (po nagłówku)
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.

Cel: Minimalny kontener nadający się do plików i BLOB‑ów, włącznie z wersjonowaniem i parametrami KDF. Warunki brzegowe: Muszą Państwo podłączyć prawdziwe CSPRNG (kryptograficznie bezpieczne źródło losowości z systemu operacyjnego) oraz solidną implementację AES/PBKDF2 pod spodem. Pułapki: Nie używać „jakiegokolwiek” Random (nie Random()), unikać stałych IV, oraz zaplanować jednoznaczne obsługi błędów przy deszyfracji (błędne hasło vs. uszkodzone dane). Warianty: zamiast CBC lepiej AEAD (patrz niżej), lub rozszerzyć nagłówek o ID algorytmu i HMAC.

Integralność: dlaczego AES‑CBC samodzielnie w eksploatacji jest zbyt ryzykowny

AES‑CBC występuje nadal w wielu kontekstach legacy i może działać, jeśli zastosują Państwo dodatkowo mechanizm zapewniający integralność. Bez integralności atakujący może modyfikować szyfrogram; nawet bez aktywnego atakującego błędy transmisji lub uszkodzone warstwy pamięci masowej powodują trudne do zdiagnozowania błędy „paddingu”.

Pragmatyczne opcje:

  • Encrypt‑then‑HMAC: Do szyfrogramu dopisać HMAC (np. HMAC‑SHA‑256) licząc go po nagłówku+i szyfrogramie. Przy odczycie najpierw sprawdzić HMAC, potem odszyfrować. Najlepiej wyprowadzić dwa klucze z PBKDF2 (np. 64 bajty: 32 dla AES, 32 dla HMAC), zamiast używać tego samego klucza podwójnie.
  • AES‑GCM: Tryb AEAD (Authenticated Encryption with Associated Data). Dostarcza szyfrogram + tag uwierzytelniający. To często najczystszy wybór, jeśli Państwa Delphi‑biblioteka stabilnie wspiera GCM. Pola nagłówka można uwierzytelniać jako „AAD”, bez konieczności ich szyfrowania.

Jeżeli muszą Państwo pozostać przy CBC (np. ze względu na interoperacyjność), Encrypt‑then‑HMAC to solidne uzupełnienie. Dla nowych formatów warto rozważyć GCM, ponieważ zapewnia uwierzytelnienie „w zestawie” i czytelniejsze symptomy błędów.

Niezwykle ważne: „kryptograficzne źródło losowości” i dlaczego System.Hash nie wystarczy

Częsty reflex w projektach Delphi w starym kodzie: „zrobimy SHA256 z timestampu + czegoś i mamy Random”. To nie jest wiarygodna podstawa. Do salt i IV potrzebują Państwo CSPRNG (Cryptographically Secure Pseudo Random Number Generator) systemu operacyjnego. Pod Windows to typowo BCrypt‑API (CNG), pod Linux generator jądra jak getrandom() albo /dev/urandom. Różnica praktyczna: CSPRNG jest zaprojektowany tak, że obserwowane wartości nie pozwalają przewidywać kolejnych.

Architektoniczny trik: zamknijcie to w małej jednostce „RandomProvider”, którą będzie można zmockować w testach. W ten sposób rozwiążecie dwa przypadki: powtarzalne testy (z ustalonym seedem w mocku) i prawdziwe bezpieczeństwo w produkcji (z OS‑CSPRNG). To zapobiega sytuacjom, w których w szybkim hotfixie znów wchodzi Random(), bo „taka jest droga na skróty”.

Debugowanie i migracja z legacy: wersjonowanie to nie luksus

Nagłówek nie służy jedynie do „kryptograficznej estetyki”, lecz do utrzymania:

  • Dostrajanie iteracji: Liczby iteracji PBKDF2 zmieniają się z upływem lat. Pole w nagłówku pozwoli później je podnieść, bez uczynienia starych danych nieczytelnymi.
  • Zmiana formatu: Wersja 2 może na przykład przejść na AES‑GCM lub dodać HMAC.
  • Diagnostyka w terenie: Magic/Version umożliwiają szybkie kontrole w logach i narzędziach bez odszyfrowywania danych.

Wskazówka praktyczna: Zaimplementuj niewielki „Inspector”, który odczytuje tylko nagłówek (Magic/Version/Iterations) i zapisuje go do logu. Dzięki temu wyjaśnicie wiele przypadków wsparcia („Która wersja jest tutaj?”) bez obsługi haseł.

Czysta migracja: „Read old, write new“ zamiast Big Bang

Jeśli zastępujecie stary format (np. stałe IV, brak KDF, Blowfish/3DES lub własne XOR), w projektach Delphi sprawdził się następujący wzorzec: podczas odczytu rozpoznajecie wiele formatów (Magic/Version lub heurystyka awaryjna), podczas zapisu generujecie wyłącznie nowy format. Dodatkowo, po pomyślnym odszyfrowaniu możecie w tle ponownie zaszyfrować dane („lazy migration”), jeśli pasuje to do procesu. Dzięki temu zmniejszacie ryzyko wdrożenia i unikacie konieczności „jednorazowego ponownego zaszyfrowania wszystkiego” jako okna konserwacyjnego.

Wątkowanie i strumieniowanie: typowe krawędzie w Delphi

Szyfrowanie często działa w worker-threadach (np. przy eksporcie, przy wysyłce do portal klienta, przy zapisie dużych archiwów). Dwa punkty, które w projektach Delphi pojawiają się regularnie:

  • Pozycje strumienia: Przed szyfrowaniem/odszyfrowaniem jasne kontrakty: strumień wejściowy jest czytany od bieżącej pozycji, strumień wyjściowy jest zapisywany od bieżącej pozycji. Przy ponownym użyciu strumieni koniecznie świadomie ustawcie Position := 0.
  • Szczyty pamięci: Unikajcie „wszystko w TBytes”. Podejście oparte na strumieniach jest istotne zwłaszcza dla dużych plików. Jeśli wasza biblioteka kryptograficzna akceptuje tylko tablice bajtów, warto podjąć dodatkowy wysiłek i przejść na implementację obsługującą strumienie lub zbudować buforowany adapter.

Jeżeli szyfrujecie w usługach (Windows- lub Linux-Services), zwróćcie także uwagę na rzetelne logowanie wyjątków: „nieprawidłowe hasło”, „uszkodzony nagłówek”, „Tag/HMAC nieprawidłowy” to różne przypadki operacyjne i powinny być rozróżnialne. Ważne: komunikaty błędów nie mogą być na zewnątrz zbyt szczegółowe (nie „Padding nieprawidłowy w bloku 7” jako błąd API), natomiast wewnętrznie w logu mogą być szczegółowe.

Kiedy podejście ma sens — i gdzie może zawieść

Ma sens, gdy: (a) przechowujecie zaszyfrowane dane eksportu/importu przez długi czas, (b) uruchamiacie równolegle różne wersje programu, (c) przetwarzacie dane strumieniowo lub (d) potrzebujecie czystego interfejsu kryptograficznego dla kilku modułów (Client/Server/Tooling).

Zawieść może, gdy będziecie próbowali rozwiązać nim „wszystko”: za transport odpowiada TLS, nie własnoręcznie zrobiony AES-wrapper. Dla sekretów (hasła, tokeny) często bardziej odpowiedni jest natywny OS-Secret-Store lub Vault. A jeśli potrzebujecie interoperacyjności z innymi językami, musicie dokładnie udokumentować nagłówek, kolejność bajtów (endianness) i kodowanie (lub użyć ustalonego formatu).

Podsumowanie: AES w Delphi to mniej algorytm, więcej inżynierii

Prawdziwą korzyścią tego fragmentu nie jest „AES działa”, lecz format nadający się do eksploatacji: losowy salt i IV, wersjonowany nagłówek, parametry PBKDF2 w payloadzie oraz obsługa strumieniowa. Dla nowych formatów uzupełnijcie to o integralność (AES-GCM lub Encrypt-then-HMAC). W ten sposób z „coś tam szyfrujemy” powstaje komponent, który w rozwiązaniach cyfrowych dla przedsiębiorstw pozostaje konserwowalny i możliwy do migracji nawet po latach.

Jeżeli mają Państwo zamiar zintegrować taki kontener z istniejącym Delphi-środowiskiem lub prawidłowo zmigrować go z formatu legacy, warto przeprowadzić krótki przegląd architektury (zarządzanie kluczami, wersje formatu, eksploatacja/logowanie). Szczegóły omówimy na życzenie podczas rozmowy:

W obszarze merytorycznym Delphi Aes i Pbkdf2 Delphi odgrywają istotną rolę, gdy integracje, przepływy danych i dalszy rozwój muszą ze sobą ściśle współgrać.

Omówić projekt lub przedsięwzięcie modernizacyjne z Net-Base.

Następny krok

Gdy temat stanie się rzeczywistym projektem, architekturę, stan istniejący i eksploatację należy wcześnie rozpatrywać wspólnie.

Wspieramy nie tylko w pojedynczych zagadnieniach, lecz także wtedy, gdy z fragmentów kodu źródłowego, kwestii związanych z systemami legacy lub koncepcji portalu ma powstać solidny projekt dla przedsiębiorstwa.

  • Stan istniejący, obraz docelowy i ryzyka techniczne są oceniane łącznie.
  • REST, dostęp do danych, portale i Rollout nie są odkładane na później.
  • Wcześnie widzą Państwo, która droga jest ekonomicznie opłacalna i operacyjnie wykonalna.

Udostępnij wpis

Udostępnij ten wpis bezpośrednio

LinkedIn, X, XING, Facebook, WhatsApp i e‑mail są natychmiast dostępne. Dla Instagrama przygotowujemy od razu link i krótki tekst.

E-mail

Instagram otwiera się w nowej karcie. Link i krótki tekst są wcześniej kopiowane do schowka.