No žurnāla tēmas līdz projektu praksei
Atbilstošas pakalpojumu un tehniskās lapas rakstam
Praksē ar AES šifrēšanu Delphi reti vienīgi rodas problēmas ar „AES kā tādu“; parasti problēmas ir saistītas ar malas nosacījumiem: dati jāapstrādā kā plūsma (faili, BLOBi, dublējumi), vecajiem formātiem jāpaliek lasāmiem, un ekspluatācijā nepieciešama debug iespēja (galvene, versiju vadība) un droši noklusējumi (Salt/IV nejauši, bez atkārtotas izmantošanas). Šis avota koda fragments rāda ne tikai „Encrypt/Decrypt“, bet nelielu, robustu formātu ar galveni, versiju, Salt un IV — plus PBKDF2 atslēgas atvasināšanai un vietu, kur saprātīgi papildināt integritātes pārbaudi.
Kāpēc „AES virknes šifrēšana“ gandrīz nekad nepietiek
Individuālajā uzņēmuma programmatūrā šifrēšana parasti parādās trīs vietās: (1) konfigurācija/sekreti (piem., piekļuves dati), (2) apmaiņas/eksporta faili un (3) stāvošie dati (piem., arhīvi, dokumentu konteineri). Naivs piegājiens „parole → AES atslēga → virkne iekšā/ārā“ ātri sabrūk:
- IV atkārtota izmantošana: režīmos kā CBC vai GCM inicializācijas vektoram (IV) jābūt unikālam katrai šifrēšanas operācijai. Fiksēts IV ir informācijas noplūde, pat ja parole ir spēcīga.
- Atslēga no paroles bez KDF: paroles tieša izmantošana kā atslēga (vai tikai vienreizēja hash) atstāj iespēju offline uzbrukumiem. KDF (Key Derivation Function), piemēram PBKDF2, mērķtiecīgi palēnina uzbrucējus.
- Nav formāta versijas: bez galvenes/versijas vēlāk būs grūti mainīt iterāciju skaitu, algoritmu vai parametrus, nezaudējot piekļuvi veciem datiem.
- Nav integritātes: AES-CBC šifrē datus, bet nepasargā pret manipulācijām. Bez autentifikācijas (piem., HMAC vai AEAD kā GCM) rodas bitu apgriešanas/padding problēmas un grūti diagnosticējami kļūdu stāvokļi.
Šī ieraksta kodols: neliels konteinerformāts, kas atbalsta straumēšanu, ir versionējams un izvairās no tipiskajām kļūdām.
AES šifrēšana Delphi ar galveni, Salt, IV un PBKDF2
Definējam vienkāršu konteinerformātu, ko var izmantot arī datubāzes BLOBos vai ziņojumu payloados:
- Magic: 4 baiti, piemēram
NBAE(ātra pārbaude “vai tas ir mūsu formāts?”) - Versija: 1 baits (atvieglo migrācijas)
- KDF parametri: iterāciju skaits (4 baiti)
- Salt: 16 baiti (nejaušs katrai datnei)
- IV: 16 baiti (nejaušs katrai datnei AES-CBC gadījumā)
- Ciphertext: šifrēti lietotāja dati (piemēroti straumēšanai)
Svarīgi: Salt un IV nav noslēpums. Tie vienkārši jāģenerē jaunāki katrai šifrēšanai. Parole paliek slepena; no tās izrietošā atslēga netiek saglabāta.
AES šifrēšana Delphi plūsmā: konteineru rakstīšana/lasīšana
Kods ir apzināti rakstīts kā „būvplāns“: skaidri nodalītas funkcijas, pārbaudāmas galvenes, nav slēptu globālo mainīgo. AES un PBKDF2 daudzas komandas īsteno ar pārbaudītu kriptogrāfijas bibliotēku (piem., DEC). Fragments demonstrē formātu un straumēšanas modeli; AES/PBKDF2 izsaukumus var nokapsulēt tā, lai tos varētu aizstāt atkarībā no izmantotās bibliotēkas.
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;
// --- Abhängigkeiten, die Sie je nach Crypto-Stack implementieren ---
procedure FillRandomBytes(var B: TBytes);
begin
// Kriptogrāfiskai nejaušībai: izmantot OS-CSPRNG (Windows BCryptGenRandom,
// Linux getrandom/urandom). Šeit apzināti kā vietturis.
raise ENbCryptoError.Create('FillRandomBytes: CSPRNG nav piesaistīts');
end;
function PBKDF2_HMAC_SHA256(const APassword: string; const ASalt: TBytes;
const AIterations, AKeyLen: Cardinal): TBytes;
begin
// Implementācija, piem., ar DEC (PBKDF2) vai citu bibliotēku.
// Rezultāts: AKeyLen baiti.
raise ENbCryptoError.Create('PBKDF2_HMAC_SHA256: nav piesaistīts');
end;
procedure AES256_CBC_EncryptStream(const AKey, AIV: TBytes; const AIn, AOut: TStream);
begin
// Implementācija ar bibliotēku:
// - KeyLen = 32 Bytes
// - IVLen = 16 Bytes
// - PKCS#7 Padding
// Svarīgi: apstrādāt straumi orientēti, nevis visu ielādēt atmiņā.
raise ENbCryptoError.Create('AES256_CBC_EncryptStream: nav piesaistīts');
end;
procedure AES256_CBC_DecryptStream(const AKey, AIV: TBytes; const AIn, AOut: TStream);
begin
raise ENbCryptoError.Create('AES256_CBC_DecryptStream: nav piesaistīts');
end;
// --- Helper ---
procedure WriteHeaderV1(const AOut: TStream; const H: TNbHeaderV1);
begin
if AOut.Write(H, SizeOf(H)) <> SizeOf(H) then
raise ENbCryptoError.Create('Neizdevās ierakstīt Headeru');
end;
function ReadHeaderV1(const AIn: TStream): TNbHeaderV1;
var
H: TNbHeaderV1;
begin
if AIn.Read(H, SizeOf(H)) <> SizeOf(H) then
raise ENbCryptoError.Create('Header nav pilnīgs');
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('Nav derīgs konteiners (Magic nesakrīt)');
if H.Version <> CVersion then
raise ENbCryptoError.CreateFmt('Nezināma konteineru versija: %d', [H.Version]);
if (H.Iterations < 10000) or (H.Iterations > 5000000) then
raise ENbCryptoError.Create('Iterāciju skaits ārpus pieņemamām robežām');
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('Parole nedrīkst būt tukša');
// 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('Parole nedrīkst būt tukša');
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.Mērķis: Minimāls konteiners, piemērots failiem un BLOBs, ieskaitot versiju vadību un KDF parametrus. Nosacījumi: Jānodrošina reāla CSPRNG pieslēgums (kriptogrāfiski drošs nejaušais skaitlis no operētājsistēmas) un robusta AES/PBKDF2 implementācija zem tā. Klupšanas akmeņi: Neizmantojiet „jebkādu“ Random (ne Random()), nedrīkst būt fiksēti IV, un dekriptēšanas laikā paredziet skaidru kļūdu apstrādi (nepareiza parole pret bojātiem datiem). Varianti: CBC vietā labāk AEAD (skat. zemāk), vai paplašināt headeru ar algoritma ID un HMAC.
Integritāte: kāpēc AES-CBC vien pats darbībā ir pārāk riskants
AES-CBC joprojām sastopams daudzos mantojuma kontekstos un var darboties, ja jūs papildus izmantojat integritātes nodrošinājumu. Bez integritātes uzbrucējs var manipulēt ar šifrtekstu; pat bez aktīva uzbrucēja pārsūtīšanas kļūdas vai bojātas krātuves slāņi var radīt grūti diagnosticējamas „Padding” kļūdas.
Pragmatiski risinājumi:
- Encrypt-then-HMAC: pēc šifrteksta ierakstīt HMAC (piem., HMAC-SHA-256) pāri Header+Ciphertext. Lasīšanas laikā vispirms pārbaudīt HMAC, tikai pēc tam atšifrēt. Tam ideāli divas atslēgas atvasināt no PBKDF2 (piem., 64 baiti: 32 priekš AES, 32 priekš HMAC), nevis atkārtoti izmantot vienu un to pašu atslēgu.
- AES-GCM: AEAD režīms (Authenticated Encryption with Associated Data). Sniedz šifrtekstu + Auth-Tag. Tā mūsdienās bieži ir tīrākā izvēle, ja jūsu Delphi-bibliotēka stabilā veidā atbalsta GCM. Header lauki var tikt autentificēti kā „AAD” bez to šifrēšanas.
Ja ir jāpaliek pie CBC (piem., interoperabilitātes dēļ), Encrypt-then-HMAC ir robusts papildinājums. Jauniem formātiem GCM atmaksājas, jo tas nodrošina autentifikāciju un kļūdu diagnostika kļūst skaidrāka.
Pārsteidzoši svarīgi: „Kryptografischer Zufall“ un kāpēc System.Hash nav pietiekami
Bieži sastopams mantojuma reflekss Delphi projektos: „Mēs vienkārši ņemam SHA256 pār laika zīmogu + kaut ko un mums ir Random.“ Tas nav uzticams pamats. Salt un IV vajag operētājsistēmas CSPRNG (Cryptographically Secure Pseudo Random Number Generator). Zem Windows tas parasti ir BCrypt-API (CNG), zem Linux — kernela ģenerators kā getrandom() vai /dev/urandom. Praktiskais atšķirības punkts: CSPRNG ir projektēts tā, lai no novērotām vērtībām nebūtu iespējams paredzēt nākamās vērtības.
Arhitektūras triks: inkapsulējiet to nelielā „RandomProvider“-vienībā, kuru testos varat mockot. Tas risina divus gadījumus uzreiz: reproducējami testi (ar fiksētu sēklu mockā) un reāla drošība ražošanā (ar OS-CSPRNG). Tā jūs novēršat, ka kādā hotfixā atkal „ienāk“ Random(), jo tas šķiet ātri izdarāms.
Debugēšana un mantojuma migrācija: versiju vadība nav luksuss
Galvene nav tikai „krypto-skaistumam“, bet svarīga uzturēšanas ērtībai:
- Iteration Tuning: PBKDF2 iterāciju skaits laika gaitā mainās. Ar galvenes lauku vēlāk varat palielināt iterāciju daudzumu, nepadarot vecos datus nelasāmus.
- Formāta maiņa: Piemēram, versija 2 varētu pāriet uz AES-GCM vai papildināt ar HMAC.
- Diagnostika laukā: Magic/Version ļauj ātri veikt pārbaudes žurnālos un rīkos, bez datu atšifrēšanas.
Praktisks padoms: ieviesiet nelielu „Inspector“, kas tikai nolasa galveni (Magic/Version/Iterations) un ieraksta to žurnālā. Tādā veidā atrisināsiet daudzus atbalsta gadījumus („Kura versija šeit ir?“) bez paroles apstrādes.
Tīra migrācija: „Read old, write new“ nevis Big Bang
Ja aizvietojat vecu formātu (piem., fiksēts IV, bez KDF, Blowfish/3DES vai pašu izstrādāts XOR), Delphi projektos ir pierādījies šāds paraugs: Beim Lesen atpazīstat vairākus formātus (Magic/Version vai fallback-heuristika), beim Schreiben ģenerējat tikai jauno formātu. Papildus, sekmīgas atšifrēšanas gadījumā fonā var veikt atkārtotu šifrēšanu („lazy migration“), ja tas atbilst procesam. Tādējādi samazināsiet izvēršanās risku un izvairīsieties no „reiz visa pārsifrēšana“ kā uzturēšanas loga.
Threading un Streaming: tipiskas klupšanas vietas Delphi
Šifrēšana bieži notiek worker-threados (piem., eksportā, augšupielādē uz klientu portālu, rakstot lielus arhīvus). Divi punkti, kas Delphi projektos regulāri izceļas:
- Stream-pozīcijas: pirms šifrēšanas/atšifrēšanas skaidri definējiet noteikumus: Input-Stream tiek lasīts no pašreizējās pozīcijas, Output-Stream tiek rakstīts no pašreizējās pozīcijas. Ja plūsmas tiek izmantotas atkārtoti, obligāti apzināti iestatiet
Position := 0. - Atmiņas pīķi: izvairieties no „viss TB apjomos“. Stream-pieeja ir īpaši svarīga lieliem failiem. Ja jūsu kripto bibliotēka pieņem tikai byte-array, ir vērts ieguldīt papildus darbu, pāriet uz stream-spējīgu implementāciju vai uzbūvēt buferizētu adapteri.
Ja šifrējat servisos (Windows- vai Linux-pakalpojumi), pievērsiet uzmanību arī kārtīgai izņēmumu žurnēšanai: „nepareiza parole“, „galvene bojāta“, „Tag/HMAC nederīgs“ ir atšķirīgi darbības gadījumi un tiem jābūt atšķirāmiem. Svarīgi: ārēji kļūdu ziņojumi nedrīkst būt pārāk detalizēti (nedodiet „Padding nepareizs blokā 7“ kā API kļūdu), bet iekšēji žurnālā tas drīkst parādīties.
Kad pieeja atmaksājas – un kur tā var izgāzties
Ir lietderīgi, ja Jūs: (a) ilgtermiņā glabājat šifrētus eksporta/importa datus, (b) vienlaicīgi darbināt vairākas programmas versijas, (c) apstrādājat datus kā streamus vai (d) vajadzīga tīra kripto-saskarne vairākiem moduļiem (Client/Server/Tooling).
Var izgāzties, ja mēģināt ar to atrisināt „visu“: par transportu atbild TLS, nevis paštaisīts AES-Wrapper. Par secrets (paroles, tokeni) biežāk piemērotāks ir OS-Secret-Store vai Vault. Un ja nepieciešama starpvalodu savietojamība, jādokumentē galvene, Endianness un Encoding precīzi (vai jāizmanto etablēts formāts).
Secinājums: AES in Delphi ir mazāk algoritms, vairāk inženierija
Īstais ieguvums no šī piemēra nav „AES darbojas“, bet gan darbotspējīgs formāts: nejaušs salt un IV, versionēta galvene, PBKDF2 parametri payloadā un stream-spējīga apstrāde. Papildiniet jauniem formātiem iespējami ar integritāti (AES-GCM vai Encrypt-then-HMAC). Tādējādi no „mēs kaut ko šifrējam“ rodas komponents, kas digitālajos uzņēmuma risinājumos arī pēc gadiem paliek uzturams un migrējams.
Ja jums jāintegrē šāds konteiners esošā Delphi-ainavā vai jāveic tīra migrācija no mantojuma formāta, ir vērts veikt īsu arhitektūras pārbaudi (atslēgu pārvaldība, formātu versijas, darbība/žurnēšana). Detalizācijas pēc vajadzības labprāt pārrunāsim sarunā:
Tehniskajā kontekstā arī Delphi Aes un Pbkdf2 Delphi spēlē nozīmīgu lomu, kad integrācijām, datu plūsmām un turpmākajai attīstībai jāstrādā kopā precīzi.
Apspriest projektu vai modernizācijas iniciatīvu ar Net-Base.
Nākamais solis
Wenn aus dem Thema ein reales Projekt wird, sollten Architektur, Bestand und Betrieb frueh zusammen betrachtet werden.
Wir unterstuetzen nicht nur bei Einzelfragen, sondern auch dann, wenn aus Source-Schnipseln, Legacy-Themen oder Portalideen ein belastbares Unternehmensprojekt werden soll.
- Esošais stāvoklis, mērķa stāvoklis un tehniskie riski tiek kopīgi vērtēti.
- REST, datu piekļuve, portāli un izvēršana netiek atlikti kā vēlākas sekas.
- Jūs savlaicīgi redzat, kurš ceļš ir ekonomiski un darbības ziņā dzīvotspējīgs.