Du thème du magazine à la pratique des projets
Pages de services et techniques pertinentes pour l'article
Dans la pratique, les problèmes liés à Chiffrement AES Delphi ne proviennent que rarement de « l’AES en lui‑même », mais des conditions périphériques : les données doivent être traitées en flux (fichiers, BLOBs, sauvegardes), les anciens formats doivent rester lisibles, et en exploitation on a besoin de débogabilité (header, versioning) et de valeurs par défaut sûres (Salt/IV aléatoires, pas de réutilisation). Ce fragment de code source montre donc non seulement « Encrypt/Decrypt », mais un petit format robuste avec header, version, Salt et IV – plus PBKDF2 pour la dérivation de clé et un endroit où l’intégrité peut être ajoutée de façon pertinente.
Pourquoi « chiffrer une chaîne AES » est rarement suffisant
Dans les logiciels d’entreprise sur mesure, le chiffrement apparaît typiquement à trois endroits : (1) configuration/secrets (p. ex. identifiants), (2) fichiers d’échange/export et (3) données au repos (p. ex. archives, conteneurs de documents). L’approche naïve « mot de passe → clé AES → chaîne entrée/sortie » montre vite ses limites :
- Réutilisation de l’IV : pour des modes comme CBC ou GCM, le vecteur d’initialisation (IV) doit être unique par chiffrement. Un IV constant constitue une fuite, même si le mot de passe est fort.
- Clé issue du mot de passe sans KDF : utiliser un mot de passe directement comme clé (ou le hacher une fois) facilite les attaques hors ligne. Une KDF (Key Derivation Function) comme PBKDF2 ralentit efficacement les attaquants.
- Absence de version de format : sans header/version, il est difficile de modifier ultérieurement le nombre d’itérations, l’algorithme ou les paramètres sans rendre les anciennes données « orphelines ».
- Aucune intégrité : AES‑CBC chiffre, mais n’empêche pas la manipulation. Sans authentification (p. ex. HMAC ou AEAD comme GCM), vous êtes exposé aux problèmes de bitflipping/padding et à des erreurs difficiles à diagnostiquer.
Le cœur de cet article : un petit format conteneur qui prend en charge le streaming, est versionnable et évite les erreurs courantes.
Chiffrement AES Delphi avec header, Salt, IV et PBKDF2
Nous définissons un format conteneur simple qui peut également être utilisé dans des BLOBs de base de données ou des payloads de messages :
- Magic : 4 octets, p. ex.
NBAE(contrôle rapide « est‑ce notre format ?») - Version : 1 octet (permet la migration)
- Paramètres KDF : nombre d’itérations (4 octets)
- Salt : 16 octets (aléatoire par fichier)
- IV : 16 octets (aléatoire par fichier pour AES‑CBC)
- Ciphertext : données utiles chiffrées (compatible streaming)
Important : Salt et IV ne sont pas secrets. Ils doivent seulement être nouveaux pour chaque chiffrement. Le mot de passe reste secret ; la clé dérivée de celui‑ci n’est pas stockée.
Chiffrement AES Delphi en flux : écrire/lire le conteneur
Le code est volontairement conçu comme un « plan » : fonctions clairement séparées, headers vérifiables, pas de variables globales cachées. Pour AES et PBKDF2, de nombreuses équipes utilisent une bibliothèque crypto éprouvée (p. ex. DEC). Le fragment illustre le format et le pattern de streaming ; les appels AES/PBKDF2 sont encapsulés de manière à pouvoir être remplacés selon la bibliothèque utilisée.
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;
// --- Dépendances à implémenter selon la pile crypto ---
procedure FillRandomBytes(var B: TBytes);
begin
// Pour l'aléa cryptographique : utiliser un CSPRNG du système (par ex. Windows BCryptGenRandom,
// Linux getrandom/urandom). Ici intentionnellement placé en tant que placeholder.
raise ENbCryptoError.Create('FillRandomBytes: CSPRNG non implémenté');
end;
function PBKDF2_HMAC_SHA256(const APassword: string; const ASalt: TBytes;
const AIterations, AKeyLen: Cardinal): TBytes;
begin
// Implémentation p.ex. avec DEC (PBKDF2) ou une autre bibliothèque.
// Résultat : AKeyLen octets.
raise ENbCryptoError.Create('PBKDF2_HMAC_SHA256: non implémenté');
end;
procedure AES256_CBC_EncryptStream(const AKey, AIV: TBytes; const AIn, AOut: TStream);
begin
// Implémentation via une bibliothèque :
// - KeyLen = 32 Bytes
// - IVLen = 16 Bytes
// - PKCS#7 Padding
// Important : traiter en mode flux, ne pas tout charger en mémoire.
raise ENbCryptoError.Create('AES256_CBC_EncryptStream: non implémenté');
end;
procedure AES256_CBC_DecryptStream(const AKey, AIV: TBytes; const AIn, AOut: TStream);
begin
raise ENbCryptoError.Create('AES256_CBC_DecryptStream: non implémenté');
end;
// --- Fonctions utilitaires ---
procedure WriteHeaderV1(const AOut: TStream; const H: TNbHeaderV1);
begin
if AOut.Write(H, SizeOf(H)) <> SizeOf(H) then
raise ENbCryptoError.Create('Impossible d''écrire l''en-tête');
end;
function ReadHeaderV1(const AIn: TStream): TNbHeaderV1;
var
H: TNbHeaderV1;
begin
if AIn.Read(H, SizeOf(H)) <> SizeOf(H) then
raise ENbCryptoError.Create('En-tête incomplet');
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('Conteneur non valide (Magic ne correspond pas)');
if H.Version <> CVersion then
raise ENbCryptoError.CreateFmt('Version de conteneur inconnue: %d', [H.Version]);
if (H.Iterations < 10000) or (H.Iterations > 5000000) then
raise ENbCryptoError.Create('Nombre d''itérations hors limites plausibles');
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('Le mot de passe ne peut pas être vide');
// Générer Salt et IV
SetLength(Salt, CSaltLen);
SetLength(IV, CIvLen);
FillRandomBytes(Salt);
FillRandomBytes(IV);
// Remplir l'en-tête
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);
// Dériver la clé (32 octets pour AES-256)
Key := PBKDF2_HMAC_SHA256(APassword, Salt, AIterations, 32);
// Chiffrer les données utiles (le ciphertext suit directement l'en-tê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('Le mot de passe ne peut pas être vide');
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);
// Déchiffrer à partir de la position actuelle du flux (après l'en-tête)
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.Objet : Un conteneur minimal adapté aux fichiers et aux BLOBs, incluant la gestion des versions et les paramètres KDF. Contraintes : Vous devez intégrer une véritable connexion CSPRNG (générateur de nombres pseudo-aléatoires cryptographiquement sûr fourni par le système d’exploitation) et une implémentation AES/PBKDF2 robuste en dessous. Écueils : N’utilisez pas n’importe quel Random (pas de Random()), pas d’IV fixes, et prévoyez un traitement d’erreur distinct au déchiffrement (mot de passe incorrect vs données corrompues). Variantes : plutôt que CBC, préférer AEAD (voir ci‑dessous), ou étendre l’en-tête avec un ID d’algorithme et un HMAC.
Intégrité : pourquoi AES-CBC seul est trop risqué en production
AES-CBC est encore présent dans de nombreux contextes legacy et peut fonctionner si vous utilisez en plus une garantie d’intégrité. Sans intégrité, un attaquant peut manipuler le texte chiffré ; même sans attaquant actif, des erreurs de transmission ou des couches de stockage défaillantes peuvent produire des erreurs de « padding » difficiles à diagnostiquer.
Options pragmatiques :
- Encrypt-then-HMAC : Après le texte chiffré, écrire un HMAC (p. ex. HMAC-SHA-256) sur l’en-tête + le texte chiffré. À la lecture, vérifier d’abord le HMAC, puis déchiffrer. Pour cela, dérivez idéalement deux clés depuis PBKDF2 (par ex. 64 octets : 32 pour AES, 32 pour HMAC), au lieu de réutiliser la même clé deux fois.
- AES-GCM : mode AEAD (Authenticated Encryption with Associated Data). Produit le texte chiffré + tag d’authentification. C’est aujourd’hui souvent le choix le plus propre si votre bibliothèque Delphi prend en charge GCM de manière stable. Les champs d’en-tête peuvent être authentifiés en tant que « AAD » sans que vous ayez à les chiffrer.
Si vous devez rester sur CBC (par ex. pour des raisons d’interopérabilité), Encrypt-then-HMAC est le complément robuste. Pour de nouveaux formats, GCM vaut la peine car il fournit l’authentification et rend les scénarios d’erreur plus clairs.
Particulièrement important : « hasard cryptographique » et pourquoi System.Hash ne suffit pas
Un réflexe legacy fréquent dans les projets Delphi : « On fait juste un SHA256 sur un horodatage + n’importe quoi et on a du Random. » Ce n’est pas une base fiable. Pour le salt et les IV, vous avez besoin d’un CSPRNG (Cryptographically Secure Pseudo Random Number Generator) du système d’exploitation. Sous Windows il s’agit typiquement de l’API BCrypt (CNG), sous Linux d’un générateur noyau comme getrandom() ou /dev/urandom. La différence est pratique : un CSPRNG est conçu pour qu’à partir de valeurs observées il soit impossible de prédire d’autres valeurs.
Astuce d’architecture : encapsulez cela dans une petite unité « RandomProvider » que vous pouvez mocker dans les tests. Ainsi vous traitez deux cas limites à la fois : des tests reproductibles (avec un seed fixe dans le mock) et une vraie sécurité en production (avec le CSPRNG du système). Cela évite qu’un hotfix n’introduise à nouveau « vite fait » un Random() parce que c’est plus rapide.
Débogage et migration legacy : la gestion de versions n’est pas un luxe
L’en-tête n’est pas seulement pour la « beauté cryptographique », mais pour la maintenabilité :
- Réglage des itérations : les nombres d’itérations PBKDF2 évoluent au fil des années. Avec un champ d’en-tête vous pouvez augmenter ces valeurs plus tard, sans rendre les anciennes données illisibles.
- Changement de format : la version 2 pourrait par ex. passer à AES-GCM ou ajouter un HMAC.
- Diagnostic sur le terrain : Magic/Version permettent des vérifications rapides dans les logs et les outils, sans déchiffrer les données.
Conseil pratique : implémentez un petit „Inspector“, qui ne lit que l’en-tête (Magic/Version/Iterations) et l’écrit dans un Log. Ainsi vous résolvez de nombreux cas de support („Quelle version est ici ?“) sans gestion des mots de passe.
Migrer proprement : „Read old, write new“ au lieu du Big Bang
Lorsque vous remplacez un ancien format (p. ex. IV fixe, pas de KDF, Blowfish/3DES, ou XOR artisanal), un schéma s’est avéré efficace dans les projets Delphi : à la lecture vous reconnaissez plusieurs formats (Magic/Version ou heuristique de repli), à la écriture vous ne produisez que le nouveau format. En outre, vous pouvez, après un déchiffrement réussi, ré-encrypter en arrière-plan (« lazy migration ») si cela s’intègre au processus. Cela réduit le risque lors du déploiement et évite la maintenance « tout re-chiffrer en une fois » comme fenêtre d’intervention.
Threading und Streaming: typische Kanten in Delphi
Le chiffrement s’exécute souvent dans des threads worker (p. ex. lors d’un export, d’un upload vers un portail client, ou lors de l’écriture de gros archives). Deux points reviennent régulièrement dans les projets Delphi :
- Positions des flux : avant le chiffrement/déchiffrement définissez des contrats clairs : le flux d’entrée (Input-Stream) est lu à partir de la position courante, le flux de sortie (Output-Stream) est écrit à partir de la position courante. En cas de réutilisation des streams, veillez à positionner explicitement
Position := 0. - Pics mémoire : évitez de « tout charger en To ». L’approche par flux est essentielle pour les fichiers volumineux. Si votre bibliothèque crypto n’accepte que des tableaux d’octets, il vaut la peine de fournir l’effort supplémentaire pour passer à une implémentation compatible stream ou pour construire un adaptateur bufferisé.
Si vous chiffrez dans des services (Windows- ou Linux-Services), soignez également le logging des exceptions : « mot de passe incorrect », « header corrompu », « Tag/HMAC invalide » sont des cas d’exploitation différents et doivent être distinguables. Important : les messages d’erreur ne doivent pas être trop détaillés vers l’extérieur (pas de « padding incorrect dans le bloc 7 » comme erreur d’API), mais peuvent l’être dans le log interne.
Quand l’approche vaut la peine – et où elle peut basculer
Pertinent si vous : (a) stockez de manière durable des données d’export/import chiffrées, (b) exploitez plusieurs versions du programme en parallèle, (c) traitez les données sous forme de flux, ou (d) avez besoin d’une interface crypto propre pour plusieurs modules (Client/Server/Tooling).
Défaillit si vous tentez d’en faire « la » solution à tout : le transport relève de TLS, pas d’un wrapper AES maison. Pour les secrets (mots de passe, tokens), un OS-Secret-Store ou un vault est souvent plus adapté. Et si vous devez assurer l’interopérabilité avec d’autres langages, documentez précisément le header, l’endianness et l’encodage (ou utilisez un format établi).
Conclusion : AES in Delphi est moins un algorithme, plus de l’ingénierie
Le véritable gain de cet extrait n’est pas « AES qui fonctionne », mais un format opérationnel : salt et IV aléatoires, header versionné, paramètres PBKDF2 dans le payload et traitement compatible flux. Pour les nouveaux formats, ajoutez de préférence l’intégrité (AES-GCM ou Encrypt-then-HMAC). Ainsi, « on chiffre n’importe quoi » devient un composant qui RESTe maintenable et migrable dans des solutions d’entreprise numériques, même après des années.
Si vous devez intégrer un tel conteneur dans un Delphi-paysage établi ou migrer proprement depuis un format hérité, un bref contrôle d’architecture (gestion des clés, versions de format, exploitation/journalisation) s’impose. Nous clarifions les détails, si nécessaire, lors d’un entretien :
Dans le contexte métier, Delphi Aes et Pbkdf2 Delphi jouent également un rôle important lorsque les intégrations, les flux de données et l’évolution doivent s’articuler proprement.
Discuter d’un projet ou d’une initiative de modernisation avec Net-Base.
Étape suivante
Lorsque ce sujet devient un projet concret, l'architecture, l'existant et l'exploitation doivent être examinés ensemble dès le départ.
Nous n'intervenons pas seulement sur des questions ponctuelles, mais aussi lorsque des fragments de code source, des problématiques liées aux systèmes legacy ou des concepts de portail doivent se transformer en un projet d'entreprise robuste.
- L'état des lieux, l'état cible et les risques techniques sont évalués conjointement.
- REST, l'accès aux données, les portails et le déploiement ne sont pas repoussés en tant que conséquences ultérieures.
- Vous identifiez tôt quelle voie est viable sur le plan économique et opérationnel.