Net-Base Žurnalas

30.05.2026

AES šifravimas sistemoje Delphi: tvirtas šaltinio kodo pavyzdys su IV, Salt, Header ir srautiniu apdorojimu

Praktiškas Delphi kodo fragmentas AES šifravimui su atsitiktiniu salt ir IV, aiškia bylos antraštės struktūra, PBKDF2 rakto išvedimu ir srautinio apdorojimo palaikymu – įskaitant tipiškas spąstus, susijusius su legacy formatais, vientisumo užtikrinimu ir eksploatacija.

30.05.2026

Nuo žurnalo temos iki projekto įgyvendinimo

Tinkami puslapiai apie paslaugas ir techninę informaciją šiam įrašui

Praktikoje su AES šifravimu Delphi dažniau kyla ne problemų dėl paties AES, o dėl aplinkybių: duomenis reikia apdoroti kaip srautą (bylos, BLOBai, atsarginės kopijos), senos bylos formos turi likti skaitomos, o eksploatacijoje reikalinga derinamumas (antraštė, versijavimas) ir saugios numatytosios reikšmės (salt/IV atsitiktiniai, be pakartotinio naudojimo). Šis kodo fragmentas todėl rodo ne tik „užšifruoti/iššifruoti“, bet ir nedidelį, patikimą formatą su antrašte, versija, saltu ir IV – plius PBKDF2 raktų išvedimui ir vietą, kur prasmingai papildyti vientisumo apsaugą.

Kodėl „AES eilutės užšifravimas“ beveik niekada nepakanka

Individualioje įmonių programinėje įrangoje šifravimas dažniausiai pasirodo trijose vietose: (1) konfigūracija/paslaptis (pvz. prisijungimo duomenys), (2) mainų/eksporto bylos ir (3) nejudantys duomenys (pvz. archyvai, dokumentų konteineriai). Naivus požiūris „slaptažodis → AES raktas → eilutė į vidų/išorė“ greitai suklumpa:

  • IV pakartotinis naudojimas: Tokiuose režimuose kaip CBC arba GCM inicializacijos vektorius (IV) turi būti unikalus kiekvienam šifravimui. Pastovus IV yra saugumo skylė, net jei slaptažodis stiprus.
  • Raktas iš slaptažodžio be KDF: Naudoti slaptažodį tiesiogiai kaip raktą (ar vienkartinai jį hashuoti) leidžia vykdyti offline-ata kas. KDF (raktų išvedimo funkcija), pvz. PBKDF2, tikslingai sulėtina užpuoliką.
  • Nėra formato versijos: Be antraštės/versijos vėliau beveik neįmanoma pakeisti iteracijų skaičiaus, algoritmo ar parametrų taip, kad seni duomenys liktų perskaitomi.
  • Nėra vientisumo apsaugos: AES‑CBC šifruoja, tačiau neužkerta kelio duomenų klastojimui. Be autentifikacijos (pvz. HMAC arba AEAD, pvz. GCM) galite susidurti su bitų apvertimo ar padding problemomis ir sunkiai diagnozuojamais klaidų atvejais.

Šio įrašo esmė: nedidelis konteinerio formatas, palaikantis srautinį apdorojimą, turintis versijavimą ir išvengiantis įprastų klaidų.

AES šifravimas Delphi su antrašte, saltu, IV ir PBKDF2

Apibrėžiame paprastą konteinerio formatą, kurį galima naudoti ir duomenų bazės BLOBuose arba pranešimų payload’uose:

  • Magic: 4 baitai, pvz. NBAE (greitas „ar tai mūsų formatas?“ patikrinimas)
  • Version: 1 baitas (leidžia migraciją)
  • KDF-Parameter: iteracijų skaičius (4 baitai)
  • Salt: 16 baitų (atsitiktinis kiekvienai bylai)
  • IV: 16 baitų (atsitiktinis kiekvienai bylai AES-CBC)
  • Ciphertext: užšifruoti naudingieji duomenys (srautinio apdorojimo palaikymas)

Svarbu: Salt ir IV nėra slapti. Jie turi būti kiekvienam šifravimui nauji. Slaptažodis lieka slaptas; iš jo išvestas raktas nėra saugomas.

AES šifravimas Delphi sraute: konteinerio rašymas/ skaitymas

Kodas sąmoningai parašytas kaip „planas“: aiškiai atskiros funkcijos, tikrinami antraštės laukai, jokių paslėptų globalių kintamųjų. Daug komandų AES ir PBKDF2 įgyvendinimui naudoja patikrintą kriptografijos biblioteką (pvz. DEC). Fragmentas demonstruoja formatą ir srautinio apdorojimo šabloną; AES/PBKDF2 kvietimai yra pakankamai kapsuliuoti, kad juos galėtumėte pakeisti pagal naudojamą biblioteką.

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

// --- Priklausomybės, kurias reikia įgyvendinti pagal kripto-įrankių rinkinį ---

procedure FillRandomBytes(var B: TBytes);
begin
  // Kriptografiniam atsitiktinumui: naudokite OS CSPRNG (Windows BCryptGenRandom,
  // Linux getrandom/urandom). Čia sąmoningai palikta kaip vietos užpildas.
  raise ENbCryptoError.Create('FillRandomBytes: CSPRNG neprijungtas');
end;

function PBKDF2_HMAC_SHA256(const APassword: string; const ASalt: TBytes;
  const AIterations, AKeyLen: Cardinal): TBytes;
begin
  // Įgyvendinimas, pvz., su DEC (PBKDF2) arba kita biblioteka.
  // Rezultatas: AKeyLen baitų.
  raise ENbCryptoError.Create('PBKDF2_HMAC_SHA256: neprijungta');
end;

procedure AES256_CBC_EncryptStream(const AKey, AIV: TBytes; const AIn, AOut: TStream);
begin
  // Įgyvendinti per biblioteką:
  // - KeyLen = 32 baitai
  // - IVLen  = 16 baitų
  // - PKCS#7 užpildymas
  // Svarbu: apdoroti srautais, ne viską krauti į atmintį.
  raise ENbCryptoError.Create('AES256_CBC_EncryptStream: neprijungta');
end;

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

// --- Pagalbinės procedūros ---

procedure WriteHeaderV1(const AOut: TStream; const H: TNbHeaderV1);
begin
  if AOut.Write(H, SizeOf(H)) <> SizeOf(H) then
    raise ENbCryptoError.Create('Antraštės įrašyti nepavyko');
end;

function ReadHeaderV1(const AIn: TStream): TNbHeaderV1;
var
  H: TNbHeaderV1;
begin
  if AIn.Read(H, SizeOf(H)) <> SizeOf(H) then
    raise ENbCryptoError.Create('Antraštė nevisiška');

  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('Neteisingas konteineris (Magic neatitinka)');

  if H.Version <> CVersion then
    raise ENbCryptoError.CreateFmt('Nežinoma konteinerio versija: %d', [H.Version]);

  if (H.Iterations < 10000) or (H.Iterations > 5000000) then
    raise ENbCryptoError.Create('Iteracijų skaičius už priimtinų ribų');

  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('Slaptažodis negali būti tuščias');

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

  // Užpildyti antraštę
  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);

  // Išvesti raktą (32 baitai AES-256)
  Key := PBKDF2_HMAC_SHA256(APassword, Salt, AIterations, 32);

  // Šifruoti naudotojo duomenis (ciphertext seka tiesiai po antrašte)
  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('Slaptažodis negali būti tuščias');

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

  // Iššifruoti nuo dabartinės srauto pozicijos (po antraštės)
  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.

Paskirtis: minimalus konteineris, tinkantis failams ir BLOB’ams, įskaitant versijavimą ir KDF parametrus. Apribojimai: reikia tikro CSPRNG prijungimo (kriptografiškai saugus atsitiktinių skaičių šaltinis iš operacinės sistemos) ir patikimos AES/PBKDF2 implementacijos po juo. Kliūtys: nenaudokite „bet kokio“ Random (ne Random()), nenaudokite fiksuotų IV, ir dešifravimo metu įdėkite aiškų klaidų apdorojimą (neteisingas slaptažodis vs. pažeisti duomenys). Variantai: vietoje CBC geriau AEAD (žr. žemiau), arba išplėsti antraštę algoritmo ID ir HMAC.

Vientisumas: kodėl AES-CBC vien tik eksploatacijoje yra per daug rizikingas

AES-CBC vis dar paplitęs daugelyje legacy kontekstų ir gali veikti, jei papildomai naudojate vientisumo apsaugą. Be vientisumo užtikrinimo užpuolėjas gali manipuliuoti šifruotu tekstu; net be aktyvaus užpuoliko perdavimo klaidos ar pažeistos saugyklos sluoksniai gali sukelti sunkiai diagnozuojamas „padding“ klaidas.

Pragmatiški variantai:

  • Encrypt-then-HMAC: įrašykite po šifruoto teksto HMAC (pvz. HMAC-SHA-256) per antraštę+šifruotą tekstą. Skaitydami pirmiausia patikrinkite HMAC, tada dešifruokite. Tam idealu iš PBKDF2 išvesti du raktus (pvz. 64 baitai: 32 AES, 32 HMAC), užuot tą patį raktą naudodami dvigubai.
  • AES-GCM: AEAD režimas (Authenticated Encryption with Associated Data). Tiekia šifruotą tekstą + Auth-Tag. Tai dažnai yra švariausias pasirinkimas, jei jūsų Delphi biblioteka stabiliai palaiko GCM. Antraštės laukai gali būti autentifikuojami kaip „AAD“, be poreikio juos užšifruoti.

Jei privalote likti prie CBC (pvz., dėl tarpusavio suderinamumo), Encrypt-then-HMAC yra patikima priemonė. Naujiems formatams verta rinktis GCM, nes jis suteikia autentifikavimą „iš karto“ ir klaidų diagnozė tampa aiškesnė.

Neįprastai svarbu: „Kryptografinis atsitiktinumas“ ir kodėl System.Hash nepakanka

Dažna legacy reakcija Delphi projektuose: „Paimame paprastai SHA256 per laiko žymą + kažkas ir turime Random.“ Tai nėra patikima bazė. Saltui ir IV reikia OS CSPRNG (Cryptographically Secure Pseudo Random Number Generator). Po Windows tai paprastai yra BCrypt-API (CNG), o po Linux — kernelio generatorius, pvz. getrandom() arba /dev/urandom. Svarbus skirtumas: CSPRNG sukurtas taip, kad iš stebimų reikšmių negalima nuspėti kitų reikšmių.

Architektūrinis triukas: inkapsuliuokite tai į mažą „RandomProvider“ unitą, kurį galėsite mock’inti testuose. Tai sprendžia du atvejus vienu metu: reprodukuojamus testus (su fiksuotu seed’u mock’e) ir tikrą saugumą produkcijoje (su OS-CSPRNG). Taip užkertate kelią tam, kad skubus hotfix’as „tiesiog“ vėl įvestų Random(), nes tai atrodo greičiausia.

Derinimas ir migracija paveldėtose sistemose: Versijavimas nėra prabanga

Antraštė reikalinga ne tik „kriptografiniam grožiui“, bet ir priežiūrai:

  • Iteration Tuning: PBKDF2 iteracijų skaičius keičiasi per metus. Su antraštės lauku vėliau galite padidinti iteracijų skaičių, nepadarydami senų duomenų nebeprieinamų.
  • Formatwechsel: pvz., versija 2 gali pereiti prie AES-GCM arba pridėti HMAC.
  • Diagnose im Feld: Magic/Version leidžia greitai atlikti patikrinimus žurnaluose ir įrankiuose be duomenų dešifravimo.

Praktinis patarimas: įdiekite nedidelį „Inspector“, kuris tik perskaito antraštę (Magic/Version/Iterations) ir įrašo į žurnalą. Taip išspręsite daugelį palaikymo atvejų („Kokia čia versija?“) be slaptažodžių tvarkymo.

Tvarkingas migravimas: „Read old, write new“ vietoje Big Bang

Jei keičiate seną formatą (pvz. fiksuotas IV, be KDF, Blowfish/3DES arba savarankiškai sukurta XOR), Delphi projektuose pasiteisino modelis: skaitant atpažįstate kelis formatus (Magic/Version arba rezervinė heuristika), rašant generuojate tik naują formatą. Papildomai, sėkmingai iššifravus, fone galima atlikti re-encrypt („lazy migration“), jei tai dera su procesu. Taip sumažinate diegimo riziką ir išvengiate „viską iš karto peršifruoti“ kaip priežiūros lango.

Threading ir Streaming: tipinės problemos Delphi

Šifravimas dažnai vykdomas darbo gijose (pvz. eksportuojant, įkeliant į klientų portalą, rašant didelius archyvus). Du punktai, kurie reguliariai pastebimi Delphi projektuose:

  • Srauto pozicijos: prieš šifruojant/iššifruojant aiškūs sutartiniai susitarimai: įvesties srautas skaitomas nuo esamos pozicijos, išvesties srautas rašomas nuo esamos pozicijos. Pakartotinai naudojant srautus būtinai sąmoningai nustatykite Position := 0.
  • Atminties pikai: venkite „visko TBytes“. Srautinė paradigma ypač svarbi dideliems failams. Jei jūsų kriptografijos biblioteka priima tik baitų masyvus, verta įdėti papildomą darbą — pereiti prie srautui pritaikytos implementacijos arba sukurti buferizuotą adapterį.

Jei šifruojate servisuose (Windows- arba Linux-Services), taip pat atkreipkite dėmesį į tvarkingą išimčių žurnalo generavimą: „neteisingas slaptažodis“, „antraštė sugadinta“, „Tag/HMAC negalioja“ yra skirtingi eksploataciniai atvejai ir turėtų būti skiriami. Svarbu: klaidų pranešimai išoriniam vartotojui neturėtų būti per detalūs (ne „Padding klaida bloke 7“ kaip API klaida), tačiau vidiniuose žurnaluose detalumas leidžiamas.

Kada šis požiūris apsimoka – ir kur jis gali neveikti

Apsimoka, jei jūs: (a) saugote šifruotus eksportavimo/importavimo duomenis ilgalaikiai saugojimui, (b) vienu metu eksploatuojate skirtingas programos versijas, (c) apdorojate duomenis kaip srautus arba (d) reikalinga tvarkinga kripto sąsaja keliems moduliams (Client/Server/Tooling).

Neapsimoka, jei bandote su tuo išspręsti „viską“: transportui atsakingas TLS, o ne savarankiškai sukurtas AES-Wrapper. Slaptažodžiams ir sekretams (Passwörter, Tokens) dažnai tinkamesnis OS-Secret-Store arba Vault. Jei reikia interoperabilumo su kitomis kalbomis, būtina tiksliai dokumentuoti antraštę, endianness ir encoding (arba naudoti įtvirtintą formatą).

Išvada: AES in Delphi yra mažiau algoritmas, daugiau inžinerija

Pagrindinė šio fragmento nauda nėra „AES veikia“, o eksploatuojamas formatas: atsitiktinis Salt ir IV, versijuota antraštė, PBKDF2 parametrai payload’e ir srautui pritaikytas apdorojimas. Naujiems formatams papildomai užtikrinkite integralumą (AES-GCM arba Encrypt-then-HMAC). Taip iš „mes kažką šifruojame“ tampa komponentu, kuris skaitmeninėse įmonių sprendimuose išliks prižiūrimas ir migruojamas net po metų.

Jei tokį konteinerį reikia integruoti į susiformavusią Delphi aplinką arba tvarkingai migruoti iš legacy formato, verta atlikti trumpą architektūros patikrą (raktų valdymas, formato versijos, eksploatacija/logavimas). Dėl detalių mielai pasikalbėsime:

Profesiniame kontekste Delphi Aes ir Pbkdf2 Delphi taip pat atlieka svarbų vaidmenį, kai integracijos, duomenų srautai ir tolesnė plėtra turi sklandžiai veikti kartu.

Aptarti projektą arba modernizacijos užmojį su Net-Base.

Kitas žingsnis

Kai tema virsta realiu projektu, architektūra, esami sprendimai ir eksploatavimas turėtų būti nagrinėjami kartu nuo pat pradžių.

Wir unterstuetzen nicht nur bei Einzelfragen, sondern auch dann, wenn aus Source-Schnipseln, Legacy-Themen oder Portalideen ein belastbares Unternehmensprojekt werden soll.

  • Esama padėtis, tikslinis vaizdas ir techninės rizikos vertinami kartu.
  • REST, duomenų prieiga, portalai ir rollout nebus perkelti į vėlesnį etapą kaip vėlyvos pasekmės.
  • Jūs anksti matote, kuris kelias yra ekonomiškai ir operaciniškai tvarus.

Pasidalinti įrašu

Tiesiogiai pasidalinti šiuo įrašu

LinkedIn, X, XING, Facebook, WhatsApp ir el. paštas yra iš karto prieinami. Instagramui paruošiame nuorodą ir trumpą tekstą iš karto.

El. paštas

Instagram atidaromas naujame skirtuke. Nuoroda ir trumpas tekstas iš anksto nukopijuojami į iškarpinę.