Net-Base Περιοδικό

30.05.2026

AES-κρυπτογράφηση σε Delphi: ένα στιβαρό απόσπασμα πηγαίου κώδικα με IV, Salt, Header και Streaming

Ένα πρακτικό απόσπασμα πηγαίου κώδικα Delphi για κρυπτογράφηση AES με τυχαίο salt και IV, σαφή δομή κεφαλίδας αρχείου, PBKDF2 για παραγωγή κλειδιού και ροή δεδομένων — συμπεριλαμβανομένων των τυπικών παγίδων σε legacy μορφές, θεμάτων ακεραιότητας και λειτουργίας.

30.05.2026

Από το θέμα του περιοδικού στην πρακτική εφαρμογή του έργου

Σχετικές σελίδες υπηρεσιών και τεχνολογίας για το άρθρο

Bei Κρυπτογράφηση AES Delphi scheitert es in der Praxis selten an „AES an sich“, sondern an Randbedingungen: Daten müssen als Stream verarbeitet werden (Dateien, BLOBs, Backups), alte Formate müssen weiter lesbar bleiben, und im Betrieb braucht man Debugbarkeit (Header, Versionierung) und sichere Defaults (Salt/IV zufällig, keine Wiederverwendung). Dieser Source-Schnipsel zeigt daher nicht nur „Encrypt/Decrypt“, sondern ein kleines, belastbares Format mit Header, Version, Salt und IV – plus PBKDF2 für die Schlüsselableitung und eine Stelle, an der Integrität sinnvoll ergänzt wird.

Warum „AES-String verschlüsseln“ fast nie reicht

In individueller Unternehmenssoftware taucht Verschlüsselung typischerweise an drei Stellen auf: (1) Konfiguration/Secrets (z. B. Zugangsdaten), (2) Austausch-/Exportdateien und (3) ruhende Daten (z. B. Archive, Dokumentcontainer). Der naive Ansatz „Passwort → AES-Key → String rein/raus“ kippt dabei schnell:

  • IV-Wiederverwendung: Bei Modi wie CBC oder GCM muss ein Initialisierungsvektor (IV) pro Verschlüsselung einzigartig sein. Ein konstantes IV ist ein Leck, auch wenn das Passwort stark ist.
  • Key aus Passwort ohne KDF: Ein Passwort direkt als Key zu verwenden (oder einmalig zu hashen) lädt Offline-Angriffe ein. Eine KDF (Key Derivation Function) wie PBKDF2 bremst Angreifer gezielt.
  • Keine Formatversion: Ohne Header/Version können Sie Iterationszahl, Algorithmus oder Parameter später kaum ändern, ohne Alt-Daten zu „verwaisen“.
  • Keine Integrität: AES-CBC verschlüsselt, aber verhindert keine Manipulation. Ohne Authentifizierung (z. B. HMAC oder AEAD wie GCM) bekommen Sie Bitflipping-/Padding-Probleme und schwer diagnosbare Fehlerbilder.

Der Kern dieses Beitrags: ein kleines Containerformat, das Streaming unterstützt, versionierbar ist und die Standardfehler vermeidet.

Κρυπτογράφηση AES Delphi mit Header, Salt, IV und PBKDF2

Wir definieren ein simples Containerformat, das sich auch in Datenbank-BLOBs oder Message-Payloads nutzen lässt:

  • Magic: 4 Bytes, z. B. NBAE (schneller „Ist das unser Format?“-Check)
  • Version: 1 Byte (ermöglicht Migration)
  • KDF-Parameter: Iterationszahl (4 Bytes)
  • Salt: 16 Bytes (zufällig pro Datei)
  • IV: 16 Bytes (zufällig pro Datei für AES-CBC)
  • Ciphertext: verschlüsselte Nutzdaten (streamingfähig)

Wichtig: Salt und IV sind nicht geheim. Sie müssen nur pro Verschlüsselung neu sein. Das Passwort bleibt geheim; der daraus abgeleitete Key wird nicht gespeichert.

Κρυπτογράφηση AES Delphi im Stream: Container schreiben/lesen

Der Code ist bewusst als „Bauplan“ geschrieben: klar getrennte Funktionen, prüfbare Header, keine Hidden-Globals. Für AES und PBKDF2 nutzen viele Teams eine bewährte Crypto-Bibliothek (z. B. DEC). Der Schnipsel zeigt das Format und das Streaming-Pattern; die AES-/PBKDF2-Aufrufe sind so kapselbar, dass Sie sie je nach Bibliothek austauschen können.

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;

// — Εξαρτήσεις που πρέπει να υλοποιήσετε ανάλογα με το Crypto-Stack —

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 Bytes.
raise ENbCryptoError.Create(‚PBKDF2_HMAC_SHA256: δεν υλοποιήθηκε‘);
end;

procedure AES256_CBC_EncryptStream(const AKey, AIV: TBytes; const AIn, AOut: TStream);
begin
// Υλοποίηση μέσω βιβλιοθήκης:
// – KeyLen = 32 Bytes
// – IVLen = 16 Bytes
// – PKCS#7 Padding
// Σημαντικό: Επεξεργασία προσανατολισμένη στο stream, όχι όλα στη μνήμη.
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(‚Δεν ήταν δυνατή η εγγραφή του header‘);
end;

function ReadHeaderV1(const AIn: TStream): TNbHeaderV1;
var
H: TNbHeaderV1;
begin
if AIn.Read(H, SizeOf(H)) <> SizeOf(H) then
raise ENbCryptoError.Create(‚Ο header είναι ελλιπής‘);

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(‚Μη έγκυρος container (το Magic δεν ταιριάζει)‘);

if H.Version <> CVersion then
raise ENbCryptoError.CreateFmt(‚Άγνωστη έκδοση container: %d‘, [H.Version]);

if (H.Iterations < 10000) or (H.Iterations > 5000000) then
raise ENbCryptoError.Create(‚Ο αριθμός επαναλήψεων εκτός εύλογων ορίων‘);

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
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.

Σκοπός: Ένα ελάχιστο δοχείο που ταιριάζει για αρχεία και BLOBs, συμπεριλαμβανομένης της διαχείρισης εκδόσεων και των παραμέτρων KDF. Περιορισμοί: Πρέπει να βασιστείτε σε πραγματική σύνδεση σε CSPRNG (κρυπτογραφικά ασφαλής τυχαιότητα από το λειτουργικό σύστημα) και σε μια ανθεκτική υλοποίηση AES/PBKDF2 από κάτω. Παγίδες: Μην χρησιμοποιείτε «οποιοδήποτε» Random (όχι Random()), μην έχετε σταθερά IV, και σχεδιάστε σαφή χειρισμό σφαλμάτων στο decrypt (λανθασμένο συνθηματικό έναντι κατεστραμμένων δεδομένων). Παραλλαγές: αντί για CBC προτιμότερο AEAD (βλέπε παρακάτω), ή επεκτείνετε το header με Algorithmus-ID και HMAC.

Ακεραιότητα: γιατί το AES-CBC μόνο του στο runtime είναι επικίνδυνο

Το AES-CBC εξακολουθεί να υφίσταται σε πολλούς legacy context και μπορεί να λειτουργήσει, εφόσον χρησιμοποιήσετε επιπλέον μηχανισμό ακεραιότητας. Χωρίς ακεραιότητα ένας επιτιθέμενος μπορεί να τροποποιήσει το ciphertext· ακόμα και χωρίς ενεργό επίθεση, σφάλματα μετάδοσης ή ελαττωματικά στρώματα αποθήκευσης προκαλούν δύσκολα διαγνωστικά σφάλματα «padding».

Πρακτικές επιλογές:

  • Encrypt-then-HMAC: Γράφετε μετά το ciphertext ένα HMAC (π.χ. HMAC-SHA-256) πάνω σε header+ciphertext. Κατά την ανάγνωση ελέγχετε πρώτα το HMAC και μετά αποκρυπτογραφείτε. Για αυτό ιδανικά παράγεται δύο keys από PBKDF2 (π.χ. 64 Bytes: 32 για AES, 32 για HMAC), αντί να χρησιμοποιήσετε το ίδιο key διπλά.
  • AES-GCM: AEAD mode (Authenticated Encryption with Associated Data). Παρέχει ciphertext + auth-tag. Συχνά είναι η καθαρότερη επιλογή σήμερα, εάν η Delphi-βιβλιοθήκη σας υποστηρίζει σταθερά GCM. Πεδία του header μπορούν να θεωρηθούν ως «AAD» και να αυθεντικοποιηθούν χωρίς να τα κρυπτογραφήσετε.

Εάν πρέπει να παραμείνετε σε CBC (π.χ. λόγω interoperabilty), το Encrypt-then-HMAC είναι η ανθεκτική προσθήκη. Για νέα formats αξίζει το GCM, γιατί παρέχει ενσωματωμένη αυθεντικοποίηση και τα σενάρια σφαλμάτων γίνονται πιο σαφή.

Εξαιρετικά σημαντικό: «Κρυπτογραφική τυχαιότητα» και γιατί το System.Hash δεν αρκεί

Μια συνηθισμένη reflex σε legacy Delphi-προγράμματα: «Παίρνουμε απλά SHA256 πάνω σε timestamp + κάτι και έχουμε Random.» Αυτό δεν είναι αξιόπιστη βάση. Για salt και IV χρειάζεστε έναν CSPRNG (Cryptographically Secure Pseudo Random Number Generator) του λειτουργικού. Υπό Windows αυτό τυπικά είναι το BCrypt-API (CNG), υπό Linux ένας kernel generator όπως getrandom() ή /dev/urandom. Η πρακτική διαφορά είναι: ένας CSPRNG σχεδιάζεται ώστε από παρατηρούμενες τιμές να μην είναι δυνατή η πρόβλεψη επόμενων τιμών.

Αρχιτεκτονικό τρικ: Ενθυλακώστε το σε μια μικρή μονάδα «RandomProvider» που μπορείτε να mockάρετε στα tests. Με αυτό λύνουν δύο περιπτώσεις ταυτόχρονα: επαναλήψιμα tests (με σταθερό seed στο mock) και πραγματική ασφάλεια στο production (με OS-CSPRNG). Έτσι αποφεύγετε να μπει ξανά «γρήγορα» ένα Random() σε κάποιο hotfix επειδή είναι βολικό.

Debugging και legacy-migration: η versioning δεν είναι πολυτέλεια

Το header δεν είναι μόνο για «κρυπτογραφική ομορφιά», αλλά για τη συντηρησιμότητα:

  • Iteration tuning: Οι τιμές iterations του PBKDF2 αλλάζουν με τα χρόνια. Με πεδίο στο header μπορείτε αργότερα να τις αυξήσετε χωρίς να καταστήσετε τα παλιά δεδομένα αδιάβαστα.
  • Formatwechsel: Η έκδοση 2 μπορεί π.χ. να περάσει σε AES-GCM ή να προσθέσει HMAC.
  • Διάγνωση στο πεδίο: Magic/Version επιτρέπουν γρήγορους ελέγχους σε logs και εργαλεία χωρίς να χρειάζεται αποκρυπτογράφηση των δεδομένων.

Πρακτική συμβουλή: Υλοποιήστε έναν μικρό «Inspector», ο οποίος διαβάζει μόνο το Header (Magic/Version/Iterations) και το γράφει σε ένα log. Έτσι διευκρινίζετε πολλές υποθέσεις υποστήριξης («Ποια έκδοση υπάρχει εδώ;») χωρίς διαχείριση κωδικών.

Καθαρή μετανάστευση: „Read old, write new“ statt Big Bang

Αν αντικαθιστάτε ένα παλιό φορμάτ (π.χ. σταθερό IV, χωρίς KDF, Blowfish/3DES, ή αυτοσχέδιο XOR), στα Delphi-προγράμματα έχει αποδειχθεί ένα μοτίβο: κατά το Διάβασμα αναγνωρίζετε πολλαπλά φορμάτ (Magic/Version ή fallback-ευρετική), κατά το Γράψιμο παράγετε πλέον μόνο το νέο φορμάτ. Επιπλέον, μετά από επιτυχή αποκρυπτογράφηση, μπορείτε στο παρασκήνιο να κάνετε re-encrypt («lazy migration»), αν ταιριάζει στη διαδικασία. Έτσι μειώνετε τον κίνδυνο rollout και αποφεύγετε το «μια φορά ξανά κρυπτογράφηση όλων» ως παράθυρο συντήρησης.

Threading und Streaming: typische Kanten in Delphi

Η κρυπτογράφηση συχνά εκτελείται σε worker-threads (π.χ. κατά την εξαγωγή, κατά το ανέβασμα σε μια πύλη πελατών, κατά τη δημιουργία μεγάλων αρχείων). Δύο σημεία που παρατηρούνται τακτικά σε Delphi-προγράμματα:

  • Stream-Positionen: Πριν την κρυπτογράφηση/αποκρυπτογράφηση σαφείς συμβάσεις: ο Input-Stream διαβάζεται από την τρέχουσα θέση, ο Output-Stream γράφεται από την τρέχουσα θέση. Σε επαναχρησιμοποίηση streams φροντίστε να ορίζετε συνειδητά Position := 0.
  • Memory-Spitzen: Αποφύγετε το «alles in TBytes». Η προσέγγιση με stream είναι κρίσιμη για μεγάλες αρχεία. Αν η βιβλιοθήκη κρυπτογράφησής σας δέχεται μόνο byte-arrays, αξίζει η επιπλέον εργασία να μεταβείτε σε μια stream‑φιλική υλοποίηση ή να κατασκευάσετε έναν προσαρμογέα με buffer.

Εάν κρυπτογραφείτε μέσα σε Services (Windows- ή Linux-Services), προσέξτε επίσης το καθαρό exception-logging: «λάθος κωδικός», «σπασμένο Header», «Tag/HMAC μη έγκυρο» είναι διαφορετικές λειτουργικές περιπτώσεις και πρέπει να διαχωρίζονται. Σημαντικό: τα σφάλματα που προβάλλονται εξωτερικά δεν πρέπει να είναι υπερβολικά αναλυτικά (χωρίς «Padding λάθος στο Block 7» ως API-σφάλμα), ενώ εσωτερικά στο log μπορούν να είναι λεπτομερή.

Πότε αξίζει η προσέγγιση – και πού μπορεί να αποτύχει

Αξίζει, όταν: (a) αποθηκεύετε μακροχρόνια κρυπτογραφημένα δεδομένα εξαγωγής/εισαγωγής, (b) λειτουργείτε διαφορετικές εκδόσεις προγράμματος παράλληλα, (c) επεξεργάζεστε δεδομένα ως streams ή (d) χρειάζεστε μια καθαρή κρυπτογραφική διεπαφή για πολλαπλά modules (Client/Server/Tooling).

Αποτυγχάνει, όταν προσπαθείτε να λύσετε «τα πάντα» με αυτό: για Transport υπεύθυνο είναι το TLS, όχι ένας αυτοσχέδιος AES‑wrapper. Για Secrets (κωδικοί, tokens) συχνά πιο κατάλληλος είναι ένας OS‑secret‑store ή ένα Vault. Και αν χρειάζεστε διαλειτουργικότητα με άλλες γλώσσες, πρέπει να τεκμηριώσετε με ακρίβεια Header, Endianness και Encoding (ή να χρησιμοποιήσετε ένα καθιερωμένο φορμάτ).

Συμπέρασμα: Το AES in Delphi ist weniger Algorithmus, mehr Engineering

Το πραγματικό κέρδος αυτού του αποσπάσματος δεν είναι «το AES τρέχει», αλλά ένα λειτουργικό φορμάτ: τυχαίο Salt και IV, versioned Header, παράμετροι PBKDF2 μέσα στο Payload και επεξεργασία με υποστήριξη stream. Συμπληρώστε για νέα φορμάτ κατά το δυνατόν μηχανισμούς ακεραιότητας (AES‑GCM ή Encrypt‑then‑HMAC). Έτσι, από το «κρυπτογραφούμε κάτι» προκύπτει ένα δομικό στοιχείο που σε ψηφιακές επιχειρησιακές λύσεις παραμένει συντηρήσιμο και μετανάστευσιμο ακόμα και μετά από χρόνια.

Αν πρέπει να ενσωματώσετε έναν τέτοιο Container σε ένα υφιστάμενο Delphi-περιβάλλον ή να μεταφέρετε καθαρά δεδομένα από ένα Legacy-Format, αξίζει ένας σύντομος έλεγχος αρχιτεκτονικής (διαχείριση κλειδιών, εκδόσεις μορφής, λειτουργία/Logging). Τις λεπτομέρειες τις διευκρινίζουμε πρόθυμα σε συζήτηση εφόσον χρειαστεί:

Στο τεχνικό περιβάλλον παίζουν επίσης ρόλο τα Delphi AES και PBKDF2 Delphi, όταν οι ενσωματώσεις, οι ροές δεδομένων και η περαιτέρω ανάπτυξη πρέπει να συνεργάζονται ομαλά.

Συζητήστε έργο ή σχέδιο εκσυγχρονισμού με Net-Base.

Επόμενο βήμα

Όταν από το θέμα προκύψει ένα πραγματικό έργο, η αρχιτεκτονική, η υφιστάμενη κατάσταση και η λειτουργία πρέπει να εξεταστούν έγκαιρα από κοινού.

Υποστηρίζουμε όχι μόνο σε μεμονωμένα ζητήματα, αλλά και όταν από αποσπάσματα πηγαίου κώδικα, θέματα legacy ή ιδέες για πύλες πρέπει να προκύψει ένα αξιόπιστο εταιρικό έργο.

  • Η υφιστάμενη κατάσταση, το επιθυμητό μελλοντικό μοντέλο και οι τεχνικοί κίνδυνοι αξιολογούνται από κοινού.
  • REST, η πρόσβαση στα δεδομένα, οι πύλες και το rollout δεν αναβάλλονται ως μετέπειτα συνέπειες.
  • Αναγνωρίζετε έγκαιρα ποια προσέγγιση είναι οικονομικά και λειτουργικά βιώσιμη.

Κοινοποίηση δημοσίευσης

Μοιραστείτε αυτήν την ανάρτηση απευθείας

LinkedIn, X, XING, Facebook, WhatsApp und E‑Mail είναι άμεσα διαθέσιμα. Για το Instagram ετοιμάζουμε άμεσα τον σύνδεσμο και το σύντομο κείμενο.

Ηλεκτρονικό ταχυδρομείο

Το Instagram ανοίγει σε μια νέα καρτέλα. Ο σύνδεσμος και το σύντομο κείμενο αντιγράφονται πρώτα στο πρόχειρο.