1. Introduction▲
Le namespace System.Security.Cryptography contient de nombreuses classes implémentant entre autre les principaux algorithmes de chiffrement symétriques et asymétriques. Au cours de cet article nous allons voir le fonctionnement de base de l'implémentation .Net / C# d'AES - Rijndael, l'algorithme de chiffrement symétrique de référence à l'heure actuelle.
1-1. Chiffrement▲
1-1-1. Clé de chiffrement▲
La confidentialité des données chiffrées avec un algorithme symétrique repose uniquement sur la protection de la clé de chiffrement, si cette clé est découverte alors les données peuvent être déchiffrées. Cette clé permet de chiffrer et de déchiffrer les données. Avec AES - Rijndael la longueur de la clé peut être soit de 128, 196 ou 256 bits. Plus la longueur de cette clé est importante, plus il sera difficile de déchiffrer les données par "brut force". Pour une plus grande simplicité nous représentons une clé de chiffrement sous forme de texte, encodée en UTF-8 une clé :
- de 128 bits représentera 16 caractères
- de 196 bits représentera 24 caractères
- de 256 bits représentera 32 caractères
Remarque : si nous employons en plus un vecteur d'initialisation il faudra détenir le couple clé de chiffrement / vecteur d'initialisation pour déchiffrer les données.
Une clé de chiffrement de 128 bits pourrait être par exemple "abc123deaoezdf77". Cette clé doit rester secrète, et être aussi aléatoire que possible. Une clé de 256 bits sera beaucoup difficile à "casser" par attaque de type "brut force", cependant les performances seront moindres pour chiffrer des données avec une clé de 256 bits. Il faut donc trouver le bon équilibre entre robustesse de la clé et rapidité du chiffrement.
1-1-2. Vecteur d'initialisation▲
Un vecteur d'initialisation est un bloc de données aléatoires utilisé dans le processus de chiffrement du premier bloc de données. Cette séquence de bits, généralement de la même longueur que celle des blocs de données chiffrées, est souvent stockée sous forme de texte pour plus de facilité. Comme pour la clé de chiffrement, un vecteur d'initialisation de 128 bits représente 16 caractères UTF-8.
1-2. AES - Rijndael▲
Rijndael est l'algorithme utilisé pour le chiffrement AES ("Advanced Encryption Standard"). Cet algorithme
a été sélectionné en 2000 par le gouvernement des Etats-Unis. Le gouvernement américain était à
la recherche d'un algorithme sûr et performant pour sécuriser ses données. Parmi près d'une quinzaine
d'algorithmes c'est celui de deux chercheurs belges (Joan Daemen et Vincent Rijmen) qui fut choisi.
Cet algorithme utilise des clés de 128, 196 ou 256 bits et une taille fixe de blocs de 128 bits.
Advanced Encryption Standard fut pour le gouvernement américain le successeur de DES qui datait
des années 70. Jusqu'à aujourd'hui l'algorithme Rijndael d'AES n'a pas été cassé.
1-3. AES - Rijndael et le Framework .Net▲
Le Framework .Net propose une implémentation de l'algorithme AES - Rijndael dans le namespace "System.Security.Cryptography". La classe implémentant cet algorithme ne nomme "RijndaelManaged". Voici les principales méthodes et propriétés de cette classe :
- Méthode CreateEncryptor() : permet de créer un chiffreur AES.
- Méthode CreateDecryptor() : permet de créer un déchiffreur AES.
- Propriété Key : permet de définir ou d'obtenir la clé secrète, c'est elle qui permet le chiffrement et le déchiffrement.
- Propriété IV : permet de définir ou d'obtenir le vecteur d'initialisation. Un vecteur d'initialisation n'est pas utilisé systématiquement, il dépend du mode, le mode ECB n'en utilise pas.
- Propriété BlockSize : permet de définir ou d'obtenir la taille des blocs utilisés lors d'une opération de chiffrement ou de déchiffrement. Cette taille de bloc définit la taille maximale de données qui peuvent être chiffrées ou déchiffrées en une seule opération.
- Propriété Padding : permet de définir ou d'obtenir le mode de padding, c'est-à-dire le mode de remplissage des blocs quand un bloc n'est pas complet. Ces différents types de padding sont contenus dans une énumération. Voici la documentation MSDN des types de Padding :
Member Name | Description |
---|---|
None | No padding is done. |
PKCS7 | The PKCS #7 padding string consists of a sequence of bytes, each of which is equal to the total number of padding bytes added. |
Zeros | The padding string consists of bytes set to zero. |
ANSIX923 | The ANSIX923 padding string consists of a sequence of bytes filled with zeros before the length. |
ISO10126 | The ISO10126 padding string consists of random data before the length. |
- Propriété Mode : permet de définir ou d'obtenir le mode de fonctionnement de l'algorithme, les modes disponibles (dans une énumération) sont les suivants : CBC; CFB; CTS; CBS; ECB et OFB. Voici la documentation MSDN des modes de chiffrement disponibles :
Member Name | Description | |
---|---|---|
CBC | Le mode CBC (Cipher Block Chaining) introduit des commentaires. Avant que chaque bloc de texte brut ne soit chiffré, il est combiné au texte de chiffrement du bloc précédent à l'aide d'une opération de bits XOR. Cela permet de garantir que même si le texte brut contient de nombreux blocs identiques, chacun d'eux sera chiffré à l'aide d'un bloc de texte de chiffrement différent. Le vecteur d'initialisation est combiné au premier bloc de texte brut à l'aide d'une opération de bits XOR, avant que le bloc ne soit chiffré. Si un seul bit du bloc de texte de chiffrement est tronqué, le bloc de texte brut correspondant est également tronqué. En outre, un bit du bloc suivant, situé au même emplacement que le bit tronqué d'origine, est lui aussi tronqué. | |
ECB | Le mode ECB (Electronic Codebook) chiffre chaque bloc individuellement. En d'autres termes, tous les blocs de texte brut identiques qui se trouvent dans le même message, ou un message différent chiffré à l'aide de la même clé, sont transformés en blocs de texte de chiffrement identiques. Si le texte brut à chiffrer contient de nombreuses répétitions, le texte de chiffrement peut être déchiffré bloc par bloc. De même, tout pirate suffisamment tenace peut substituer des blocs individuels sans risque de détection. Si un seul bit du bloc de texte de chiffrement est tronqué, l'ensemble du bloc de texte brut correspondant est également tronqué. | |
OFB | Le mode OFB (Output Feedback) convertit de petits incréments de texte brut en texte de chiffrement, au lieu de convertir tout un bloc. Ce mode est similaire au mode CFB ; la seule différence entre ces deux modes réside dans la méthode de remplissage du registre à décalage. Si un bit du texte de chiffrement est tronqué, le bit de texte brut correspondant est également tronqué. Cependant, si des bits manquent dans le texte de chiffrement, ou si ce dernier contient des bits supplémentaires, le texte brut est tronqué à partir de cet emplacement. | |
CFB | Le mode CFB (Cipher Feedback) convertit de petits incréments de texte brut en texte de chiffrement, au lieu de convertir tout un bloc. Ce mode utilise un registre à décalage d'un bloc de longueur, qui se décompose en sections. Par exemple, si la taille du bloc est de huit octets, et qu'un seul octet est converti à la fois, le registre à décalage se décompose en huit sections. Si un bit du texte de chiffrement est tronqué, un bit du texte brut est également tronqué ; en outre, le registre à décalage est endommagé. Par conséquent, les incréments de texte brut suivants sont tronqués jusqu'à ce que le bit incorrect soit exclu du registre à décalage. | |
CTS | Le mode CTS (Cipher Text Stealing) gère toutes les longueurs de texte brut et produit un texte de chiffrement dont la longueur correspond à celle du texte brut. Ce mode a un comportement similaire au mode CBC, sauf en ce qui concerne les deux derniers blocs de texte brut. |
2. Opérations sur des chaînes de caractères▲
Dans cette partie nous allons voir comment chiffrer et déchiffrer des chaînes de caractères avec AES - Rijndael.
2-1. Chiffrer des chaines de caractères▲
Nous allons maintenant écrire une méthode prenant en entrée une chaîne de caractères, une clé de chiffrement et un vecteur d'initialisation et qui retourne une chaîne de caractères chiffrée encodée en base64. Voici le code de cette méthode :
///
/// Chiffre une chaîne de caractère
///
/// Texte clair à chiffrer
/// Clé de chiffrement
/// Vecteur d'initialisation
/// Retourne le texte chiffré
private
static
string
EncryptString
(
string
clearText,
string
strKey,
string
strIv)
{
// Place le texte à chiffrer dans un tableau d'octets
byte
[]
plainText =
Encoding.
UTF8.
GetBytes
(
clearText);
// Place la clé de chiffrement dans un tableau d'octets
byte
[]
key =
Encoding.
UTF8.
GetBytes
(
strKey);
// Place le vecteur d'initialisation dans un tableau d'octets
byte
[]
iv =
Encoding.
UTF8.
GetBytes
(
strIv);
RijndaelManaged rijndael =
new
RijndaelManaged
(
);
// Définit le mode utilisé
rijndael.
Mode =
CipherMode.
CBC;
// Crée le chiffreur AES - Rijndael
ICryptoTransform aesEncryptor =
rijndael.
CreateEncryptor
(
key,
iv);
MemoryStream ms =
new
MemoryStream
(
);
// Ecris les données chiffrées dans le MemoryStream
CryptoStream cs =
new
CryptoStream
(
ms,
aesEncryptor,
CryptoStreamMode.
Write);
cs.
Write
(
plainText,
0
,
plainText.
Length);
cs.
FlushFinalBlock
(
);
// Place les données chiffrées dans un tableau d'octet
byte
[]
CipherBytes =
ms.
ToArray
(
);
ms.
Close
(
);
cs.
Close
(
);
// Place les données chiffrées dans une chaine encodée en Base64
return
Convert.
ToBase64String
(
CipherBytes);
}
Une petite remarque, vous voyez que l'on spécifie l'encodage utilisé pour les différentes chaines (texte, clé, vecteur d'initialisation...), vous devez utiliser le plus adapté à vos besoins. Prenez bien garde, l'encodage est un problème extrêmement récurrent et peut faire souvent perdre de longues heures de débogage.
2-2. Déchiffrer des chaines de caractères▲
Après le chiffrement voici le déchiffrement. Comme d'habitude en C#, et en .Net plus généralement, c'est extrêmement simple :
///
<
summary
>
/// Déchiffre une chaîne de caractère
///
<
/summary
>
///
<
param
name
=
"cipherText"
>
Texte chiffré
<
/param
>
///
<
param
name
=
"strKey"
>
Clé de déchiffrement
<
/param
>
///
<
param
name
=
"strIv"
>
Vecteur d'initialisation
<
/param
>
///
<
returns
><
/returns
>
public
static
string
DecryptString
(
string
cipherText,
string
strKey,
string
strIv)
{
// Place le texte à déchiffrer dans un tableau d'octets
byte
[]
cipheredData =
Convert.
FromBase64String
(
cipherText);
// Place la clé de déchiffrement dans un tableau d'octets
byte
[]
key =
Encoding.
UTF8.
GetBytes
(
strKey);
// Place le vecteur d'initialisation dans un tableau d'octets
byte
[]
iv =
Encoding.
UTF8.
GetBytes
(
strIv);
RijndaelManaged rijndael =
new
RijndaelManaged
(
);
rijndael.
Mode =
CipherMode.
CBC;
// Ecris les données déchiffrées dans le MemoryStream
ICryptoTransform decryptor =
rijndael.
CreateDecryptor
(
key,
iv);
MemoryStream ms =
new
MemoryStream
(
cipheredData);
CryptoStream cs =
new
CryptoStream
(
ms,
decryptor,
CryptoStreamMode.
Read);
// Place les données déchiffrées dans un tableau d'octet
byte
[]
plainTextData =
new
byte
[
cipheredData.
Length];
int
decryptedByteCount =
cs.
Read
(
plainTextData,
0
,
plainTextData.
Length);
ms.
Close
(
);
cs.
Close
(
);
return
Encoding.
UTF8.
GetString
(
plainTextData,
0
,
decryptedByteCount);
}
3. Opérations sur des fichiers▲
Dans la partie précédente nous chiffrions et déchiffrions des chaînes de caractères, voyons maintenant comment procéder avec des fichiers entiers. Le principe de base et toute la gestion du chiffrement restent les mêmes, seules les méthodes d'accès au fichier et de traitement des flux en mémoire vont différer.
3-1. Chiffrer des fichiers▲
Chiffrer un fichier va consister à ouvrir ce fichier, le parcourir, chiffrer son contenu à la volée et écrire le résultat dans un second fichier. Voici un bout de code permettant de réaliser une telle opération sur un fichier texte :
public
static
void
EncryptFile
(
string
strKey,
string
strIv,
string
pathPlainTextFile,
string
pathCypheredTextFile)
{
// Place la clé de déchiffrement dans un tableau d'octets
byte
[]
key =
Encoding.
UTF8.
GetBytes
(
strKey);
// Place le vecteur d'initialisation dans un tableau d'octets
byte
[]
iv =
Encoding.
UTF8.
GetBytes
(
strIv);
FileStream fsCypheredFile =
new
FileStream
(
pathCypheredTextFile,
FileMode.
Create);
RijndaelManaged rijndael =
new
RijndaelManaged
(
);
rijndael.
Mode =
CipherMode.
CBC;
rijndael.
Key =
key;
rijndael.
IV =
iv;
ICryptoTransform aesEncryptor =
rijndael.
CreateEncryptor
(
);
CryptoStream cs =
new
CryptoStream
(
fsCypheredFile,
aesEncryptor,
CryptoStreamMode.
Write);
FileStream fsPlainTextFile =
new
FileStream
(
pathPlainTextFile,
FileMode.
OpenOrCreate);
int
data;
while
((
data =
fsPlainTextFile.
ReadByte
(
)) !=
-
1
)
{
cs.
WriteByte
((
byte
)data);
}
fsPlainTextFile.
Close
(
);
cs.
Close
(
);
fsCypheredFile.
Close
(
);
}
3-2. Déchiffrer des fichiers▲
Pour déchiffrer un fichier il suffit de procéder de la même façon que pour une chaine de caractères, la mécanique de l'ouverture et du parsing de fichier en plus. Voici comment procéder :
public
static
void
DecryptFile
(
string
strKey,
string
strIv,
string
pathCypheredTextFile,
string
pathPlainTextFile)
{
// Place la clé de déchiffrement dans un tableau d'octets
byte
[]
key =
Encoding.
UTF8.
GetBytes
(
strKey);
// Place le vecteur d'initialisation dans un tableau d'octets
byte
[]
iv =
Encoding.
UTF8.
GetBytes
(
strIv);
// Filestream of the new file that will be decrypted.
FileStream fsCrypt =
new
FileStream
(
pathPlainTextFile,
FileMode.
Create);
RijndaelManaged rijndael =
new
RijndaelManaged
(
);
rijndael.
Mode =
CipherMode.
CBC;
rijndael.
Key =
key;
rijndael.
IV =
iv;
ICryptoTransform aesDecryptor =
rijndael.
CreateDecryptor
(
);
CryptoStream cs =
new
CryptoStream
(
fsCrypt,
aesDecryptor,
CryptoStreamMode.
Write);
// FileStream of the file that is currently encrypted.
FileStream fsIn =
new
FileStream
(
pathCypheredTextFile,
FileMode.
OpenOrCreate);
int
data;
while
((
data =
fsIn.
ReadByte
(
)) !=
-
1
)
cs.
WriteByte
((
byte
)data);
cs.
Close
(
);
fsIn.
Close
(
);
fsCrypt.
Close
(
);
}
4. Allons plus loin, utilisons une "PassPhrase"▲
Jusqu'à maintenant nous avons utilisé des clés et des vecteurs d'initialisation que nous avions créée préalablement. Bien que cette méthode soit la plus utilisée il existe une autre manière de procéder. Les "passphrases", se sont des "mots" de passe qui vont nous permettre de générer une clé et un vecteur d'initialisation. Le secret ne sera alors plus un couple clé et vecteur d'initialisation, mais tout simplement un mot de passe et un sel, bien plus facile à retenir et à partager. La génération de cette clé et de ce vecteur seront effectuées grâce à un algorithme dédié reposant sur la RFC 2898. Ce procédé de génération est appelé dérivation, il nécessite un mot de passe, un sel (plus connu dans sa version anglaise "salt") et éventuellement un entier déterminant le nombre de "tours à réaliser".
Remarque : attention, la classe Rfc2898DeriveBytes est nouvelle dans le Framework .Net 2.0, elle n'est pas compatible avec la classe PasswordDeriveBytes assurant la dérivation de clé. Prenez donc garde aux problèmes d'interopérabilités d'application .Net plus anciennes reposant sur cette dernière classe.
Voyons maintenant comment obtenir une clé et un vecteur à l'aide de cette nouvelle classe pour chiffrer nos données :
private
static
List<
byte
[]>
GenerateAlgotihmInputs
(
string
password)
{
byte
[]
key;
byte
[]
iv;
List<
byte
[]>
result =
new
List<
byte
[]>(
);
Rfc2898DeriveBytes rfcDb =
new
Rfc2898DeriveBytes
(
password,
System.
Text.
Encoding.
UTF8.
GetBytes
(
password));
key =
rfcDb.
GetBytes
(
16
);
iv =
rfcDb.
GetBytes
(
16
);
result.
Add
(
key);
result.
Add
(
iv);
return
result;
}
Il faut donc fournir une chaine de caractères en entrées, l'on va à partir de ce "mot de passe" générer
une clé et un vecteur d'initialisation. Cette chaine de caractères doit mesurer 8 octets au moins,
sinon une exception sera levée.
La méthode GetBytes() permet de récupérer notre clé et de notre vecteur d'initialisation. Cette méthode
prend en paramètres la longueur de données en octets à récupérer. Une clé AES - Rijndael peut avoir
une longueur de 128, 196 ou 256 bits, soit 16, 24 ou 32 octets.
Remarque : 8 octets représentent 8 caractères encodés en UTF-8 ou encore 4 caractères encodés en Unicode.
Grâce à cette classe il est possible de générer des clés et des vecteurs à partir de simples mots de passe qui sont bien plus facile à gérer pour des utilisateurs finaux que des complexes couples vecteur d'initialisation / clés. Attention cependant à exiger des "passphrases" suffisamment complexes, sans quoi il pourrait être plus ou moins facile de retrouver une clé et un vecteur par "brut force".
5. Conclusion▲
Nous avons vu dans cette introduction au chiffrement AES en .Net / C# que sécuriser des données est relativement simple à réaliser. AES - Rijndael est à l'heure actuelle l'algorithme de référence, il n'a pas été craqué pour le moment. Comme la plus part des algorithmes symétriques (clé de chiffrement et de déchiffrement identiques) il offre de bonnes performances. Vous n'avez maintenant plus aucune excuse pour laisser des informations sensibles en clair dans vos applications. La sécurisation de vos développements doit être considérée au niveau de chaque couche, se reposer sur la sécurité d'autres couches (communications réseaux, système d'exploitation, base de données...) serait une grave erreur de conception.