Dal tema della rivista alla pratica di progetto
Pagine di servizi e tecniche correlate all'articolo
In pratica la crittografia AES Delphi raramente fallisce per «AES in sé», ma per le condizioni al contorno: i dati devono essere trattati come stream (file, BLOB, backup), i formati vecchi devono rimanere leggibili e in esercizio servono capacità di debug (header, versioning) e valori predefiniti sicuri (salt/IV casuali, nessuna riutilizzazione). Questo frammento di codice mostra dunque non solo «Encrypt/Decrypt», ma un piccolo formato robusto con header, versione, salt e IV – più PBKDF2 per la derivazione della chiave e un punto in cui ha senso integrare il controllo di integrità.
Perché „crittografare una stringa con AES“ quasi mai è sufficiente
Nella software aziendale personalizzato la crittografia compare tipicamente in tre ambiti: (1) configurazione/secret (es. credenziali), (2) file di scambio/export e (3) dati a riposo (es. archivi, contenitori documentali). L’approccio ingenuo «password → chiave AES → stringa dentro/fuori» si rivela presto insufficiente:
- Riutilizzo dell’IV: in modalità come CBC o GCM il vettore di inizializzazione (IV) deve essere unico per ogni cifratura. Un IV costante è una perdita, anche se la password è forte.
- Chiave ricavata dalla password senza KDF: usare direttamente una password come chiave (o calcolare un hash una tantum) favorisce attacchi offline. Una KDF (Key Derivation Function) come PBKDF2 rallenta intenzionalmente gli aggressori.
- Nessuna versione del formato: senza header/versione sarà difficile cambiare in seguito iterazioni, algoritmo o parametri senza lasciare i dati esistenti «orfani».
- Nessuna integrità: AES-CBC cifra, ma non impedisce la manipolazione. Senza autenticazione (es. HMAC o AEAD come GCM) si espone il sistema a bitflipping/padding e a errori difficili da diagnosticare.
Il nucleo di questo contributo è: un piccolo formato contenitore che supporta lo streaming, è versionabile e evita gli errori comuni.
Crittografia AES Delphi con header, salt, IV e PBKDF2
Definiamo un formato contenitore semplice, utilizzabile anche in BLOB di database o payload di messaggi:
- Magic: 4 byte, p.es.
NBAE(controllo rapido «È il nostro formato?») - Versione: 1 byte (consente migrazioni)
- Parametri KDF: numero di iterazioni (4 byte)
- Salt: 16 byte (casuale per file)
- IV: 16 byte (casuale per file per AES-CBC)
- Ciphertext: dati utente cifrati (adatti allo streaming)
Importante: salt e IV non sono segreti. Devono solo essere nuovi per ogni cifratura. La password rimane segreta; la chiave derivata non viene memorizzata.
Crittografia AES Delphi nello stream: scrittura/lettura del contenitore
Il codice è volutamente scritto come «progetto»: funzioni chiaramente separate, header verificabili, nessuna variabile globale nascosta. Per AES e PBKDF2 molti team usano una libreria crittografica consolidata (es. DEC). Il frammento mostra il formato e il pattern di streaming; le chiamate AES/PBKDF2 sono incapsulate in modo da poterle sostituire a seconda della libreria.
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
// Per l’entropia crittografica: utilizzare il CSPRNG del sistema operativo (Windows BCryptGenRandom,
// Linux getrandom/urandom). Qui intenzionalmente come segnaposto.
raise ENbCryptoError.Create(‚FillRandomBytes: CSPRNG non collegato‘);
end;
function PBKDF2_HMAC_SHA256(const APassword: string; const ASalt: TBytes;
const AIterations, AKeyLen: Cardinal): TBytes;
begin
// Implementazione, ad es. con DEC (PBKDF2) o altra libreria.
// Risultato: AKeyLen byte.
raise ENbCryptoError.Create(‚PBKDF2_HMAC_SHA256: non collegato‘);
end;
procedure AES256_CBC_EncryptStream(const AKey, AIV: TBytes; const AIn, AOut: TStream);
begin
// Implementazione tramite libreria:
// – KeyLen = 32 Bytes
// – IVLen = 16 Bytes
// – PKCS#7 Padding
// Importante: elaborare in modalità stream, non caricare tutto in memoria.
raise ENbCryptoError.Create(‚AES256_CBC_EncryptStream: non collegato‘);
end;
procedure AES256_CBC_DecryptStream(const AKey, AIV: TBytes; const AIn, AOut: TStream);
begin
raise ENbCryptoError.Create(‚AES256_CBC_DecryptStream: non collegato‘);
end;
// — Helper —
procedure WriteHeaderV1(const AOut: TStream; const H: TNbHeaderV1);
begin
if AOut.Write(H, SizeOf(H)) <> SizeOf(H) then
raise ENbCryptoError.Create(‚Impossibile scrivere l“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 incompleto‘);
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(‚Contenitore non valido (Magic non corrisponde)‘);
if H.Version <> CVersion then
raise ENbCryptoError.CreateFmt(‚Versione del container sconosciuta: %d‘, [H.Version]);
if (H.Iterations < 10000) or (H.Iterations > 5000000) then
raise ENbCryptoError.Create(‚Numero di iterazioni fuori dai limiti plausibili‘);
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(‚La password non può essere vuota‘);
// Generazione di Salt/IV
SetLength(Salt, CSaltLen);
SetLength(IV, CIvLen);
FillRandomBytes(Salt);
FillRandomBytes(IV);
// Compilare l’header
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);
// Derivazione della chiave (32 byte per AES-256)
Key := PBKDF2_HMAC_SHA256(APassword, Salt, AIterations, 32);
// Crittografia dei dati (il ciphertext segue direttamente l’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(‚La password non può essere vuota‘);
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);
// Decifrare a partire dalla posizione corrente dello stream (dopo l’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.
Scopo: un contenitore minimale adatto a file e BLOB, con versioning e parametri KDF. Vincoli: dovete appoggiare una reale integrazione CSPRNG (entropia crittografica fornita dal sistema operativo) e un’implementazione AES/PBKDF2 robusta. Trappole: non usare un “random qualsiasi” (non Random()), niente IV fissi, e prevedere una gestione degli errori chiara al Decrypt (password errata vs. dati corrotti). Varianti: invece di CBC preferire AEAD (vedi sotto), oppure estendere l’header con ID algoritmo e HMAC.
Integrità: perché AES-CBC da solo è troppo rischioso in esercizio
AES-CBC è ancora presente in molti contesti legacy e può funzionare se usate in aggiunta una protezione di integrità. Senza integrità un attaccante può manipolare il ciphertext; anche senza attacco attivo, errori di trasmissione o layer di storage difettosi generano errori di «padding» difficili da diagnosticare.
Opzioni pragmatiche:
- Encrypt-then-HMAC: scrivere dopo il ciphertext un HMAC (es. HMAC-SHA-256) su header+ciphertext. Alla lettura verificare prima l’HMAC, poi decifrare. Per questo è preferibile derivare due chiavi da PBKDF2 (es. 64 byte: 32 per AES, 32 per HMAC), invece di riutilizzare la stessa chiave due volte.
- AES-GCM: modalità AEAD (Authenticated Encryption with Associated Data). Produce ciphertext + auth-tag. Oggi spesso è la scelta più pulita, se la vostra Delphi-libreria supporta GCM in modo stabile. I campi header possono essere autenticati come «AAD» senza essere cifrati.
Se dovete restare su CBC (es. per interoperabilità), Encrypt-then-HMAC è l’integrazione robusta. Per formati nuovi conviene GCM, perché fornisce autenticazione integrata e produce errori più netti.
Particolarmente importante: “casualità crittografica” e perché System.Hash non basta
Un reflex legacy comune nei progetti Delphi: «facciamo SHA256 su timestamp + qualcosa e abbiamo random». Non è una base affidabile. Per salt e IV servitevi di un CSPRNG (Cryptographically Secure Pseudo Random Number Generator) del sistema operativo. Sotto Windows questo è tipicamente la BCrypt-API (CNG), sotto Linux un generatore kernel come getrandom() o /dev/urandom. La differenza pratica: un CSPRNG è progettato in modo che dai valori osservati non si possano prevedere i valori successivi.
Accorgimento architetturale: incapsulate tutto in una piccola «RandomProvider»-Unit che potete mockare nei test. Così risolvete due casi: test riproducibili (mock con seed fisso) e sicurezza reale in produzione (OS-CSPRNG). Eviterete che in un hotfix entri di nuovo «per comodità» Random().
Debugging e migrazione legacy: il versionamento non è un lusso
L’header non serve solo per la «bellezza crittografica», ma per la manutenibilità:
- Tuning delle iterazioni: i numeri di iterazione di PBKDF2 cambiano nel tempo. Con un campo header potete aumentarle dopo, senza rendere il dato vecchio illeggibile.
- Cambi di formato: la versione 2 potrebbe ad esempio passare a AES-GCM o aggiungere un HMAC.
- Diagnosi sul campo: Magic/Version permettono controlli rapidi in log e strumenti, senza dover decifrare i dati.
Consiglio pratico: implementate un piccolo “inspector” che legge solo l’header (Magic/Version/Iterations) e lo registra in un log. Con questo chiarite molti casi di supporto (“Quale versione è qui?”) senza gestire le password.
Migrazione pulita: “Read old, write new” invece del Big Bang
Se sostituite un formato vecchio (p.es. IV fisso, nessuna KDF, Blowfish/3DES o XOR fatto in casa), nei progetti Delphi si è dimostrato efficace questo schema: in fase di lettura riconoscete più formati (Magic/Version o euristica di fallback), in fase di scrittura generate solo il nuovo formato. In aggiunta potete, dopo un decrittamento riuscito, ri-cifrare in background (“lazy migration”) se il processo lo consente. In questo modo riducete il rischio di rollout ed evitate la finestra di manutenzione “ricifrare tutto una volta sola”.
Threading e streaming: punti critici tipici in Delphi
La cifratura spesso avviene in worker thread (p.es. durante l’export, l’upload in un portale clienti, la scrittura di grandi archivi). Due punti che nei progetti Delphi emergono regolarmente:
- Posizioni del flusso: prima di cifrare/decifrare definite contratti chiari: l’input-stream va letto a partire dalla posizione corrente, l’output-stream va scritto a partire dalla posizione corrente. Se riutilizzate stream impostate consapevolmente
Position := 0. - Picchi di memoria: evitate il paradigma “tutto in TBytes”. L’approccio basato su stream è particolarmente importante per file di grandi dimensioni. Se la vostra libreria crypto accetta solo array di byte, vale la pena lo sforzo aggiuntivo per migrare a un’implementazione compatibile con i stream o costruire un adattatore bufferizzato.
Se cifrate all’interno di servizi (Windows- o Linux-Services), pRESTate inoltre attenzione a un logging delle eccezioni pulito: “password errata”, “header danneggiato”, “Tag/HMAC non valido” sono casi operativi distinti e dovrebbero essere distinguibili. Importante: i messaggi di errore esposti esternamente non devono essere troppo dettagliati (niente “Padding sbagliato nel blocco 7” come errore API), mentre nei log interni possono esserlo.
Quando conviene l’approccio — e dove può incepparsi
Conviene se voi: (a) memorizzate dati di export/import cifrati in modo duraturo, (b) eseguite versioni diverse del programma in parallelo, (c) processate dati come stream o (d) avete bisogno di una interfaccia crittografica pulita per più moduli (Client/Server/Tooling).
Non è adatto se cercate di risolvere con questo “tutto”: per il trasporto è TLS a farsi carico della sicurezza, non un wrapper AES fatto in casa. Per i secret (password, token) spesso è più sensato utilizzare uno store di secret del sistema operativo o un Vault. E se vi serve interoperabilità con altri linguaggi, dovete documentare in modo preciso header, endianness e encoding (o adottare un formato consolidato).
Conclusione: AES in Delphi è meno algoritmo, più ingegneria
Il reale valore di questo frammento non è “AES funziona”, ma un formato operativo: salt e IV casuali, header versionato, parametri PBKDF2 nel payload e elaborazione compatibile con i stream. Per i nuovi formati integrate, ove possibile, l’integrità (AES-GCM o Encrypt-then-HMAC). In questo modo “stiamo cifrando qualcosa” diventa un componente che, nelle soluzioni digitali aziendali, RESTa manutenibile e migrabile anche dopo anni.
Qualora sia necessario integrare un container simile in un paesaggio Delphi consolidato o migrare in modo pulito da un formato legacy, conviene eseguire un breve controllo architetturale (gestione delle chiavi, versioni del formato, operatività/logging). I dettagli li definiamo volentieri su richiesta in un colloquio:
Nel contesto applicativo anche Delphi Aes e Pbkdf2 Delphi rivestono un ruolo importante quando integrazioni, flussi di dati e sviluppo devono operare insieme in modo coerente.
Discutere un progetto o un intervento di modernizzazione con Net-Base.
Passo successivo
Quando un tema diventa un progetto reale, architettura, sistemi esistenti e gestione operativa dovrebbero essere considerati insieme fin dall'inizio.
Non forniamo solo supporto per questioni isolate, ma anche quando da frammenti di codice sorgente, tematiche legacy o idee di portale deve nascere un progetto aziendale solido.
- Stato attuale, stato obiettivo e rischi tecnici vengono valutati insieme.
- REST, l'accesso ai dati, i portali e il rollout non vengono rimandati a fasi successive.
- Vede in anticipo quale percorso è economicamente ed operativamente sostenibile.