Introduction
Le chiffrement AES (Advanced Encryption Standard) est un des algorithmes les plus utilisés actuellement et il est à l’heure actuelle considéré comme sûr.
Dans les utilisations les plus fréquentes de l’AES, on peut citer :
– le chiffrement des données lors de l’ajout d’un mot de passe à un fichier PDF ou ZIP ;
– la sécurisation des connexions via l’utilisation de VPN ;
– la sécurisation des données utilisateurs lors de l’utilisation d’outils de gestion de mots de passe ;
– la protection de serveurs multi-joueurs contre les attaques. C’est le cas par exemple de l’entreprise Rockstar, développeur et éditeur de la série des Grand Theft Auto (GTA) ;
– le chiffrement des communications lors de l’utilisation de messageries instantanées. C’est le cas de l’application WhatsApp.
Ce chiffrement symétrique utilise une clef de 128, 192 ou 256 bits et elle est utilisée pour paramétrer une suite de transformations qui permettent de chiffrer ou de déchiffrer le message.
Le fonctionnement plus complexe de ce chiffrement par bloc dépasse le cadre des programmes mais est à votre portée, vous allez le constater dans les lignes qui suivent ...
Histoire et concepts fondamentaux
La cryptologie
La cryptologie est la science du secret. Elle englobe deux disciplines antagonistes mais indissociables :
– La cryptographie : l’art de concevoir des systèmes d’écriture secrète (fabriquer des serrures).
– La cryptanalyse : l’art de casser ces systèmes (crocheter les serrures).
Considérée comme une science récente (années 1970), elle s’appuie aujourd’hui sur des mathématiques avancées et l’informatique.
La cryptographie
La cryptographie protège la confidentialité des messages. Contrairement à la stéganographie (qui cache l’existence du message), la cryptographie rend le message incompréhensible tout en le laissant visible.
Vocabulaire :
– Texte clair : message original.
– Chiffrement : transformation du texte clair en texte chiffré grâce à une clé.
– Déchiffrement : opération inverse, qui nécessite la clé pour retrouver le texte clair.
– Cryptosystème : algorithme de chiffrement (comme AES).
La cryptanalyse
La cryptanalyse est la science qui cherche à « décrypter » un message (retrouver le texte clair sans connaître la clé). La cryptographie et la cryptanalyse sont en constante évolution : chaque nouvel algorithme pousse les analystes à trouver de nouvelles failles, et vice-versa.
La stéganographie
La stéganographie consiste à dissimuler un message dans un autre support (image, son, etc.). Elle repose souvent sur le principe de « sécurité par l’obscurité », qui est aujourd’hui considéré comme fragile par rapport aux exigences modernes de sécurité (principe de Kerckhoffs).
Antiquité
Le chiffre le plus connu de l’Antiquité est sans doute le chiffre de César, utilisé par Jules César pour ses correspondances militaires.
Il s’agit d’un chiffrement par décalage : chaque lettre de l’alphabet est remplacée par une lettre située un certain nombre de rangs plus loin (ou plus tôt).
Exemple avec un décalage de 3
On applique un décalage de 3 lettres.
Ainsi « bonjour » devient « ERQMRXU »
Pour déchiffrer, il suffit d’appliquer le décalage inverse (décalage de -3) tel que ;
Remarque : Ce chiffrement est très faible car il n’existe que 25 décalages possibles (hors décalage nul). Une attaque par force brute (tester tous les décalages) suffit à le casser en quelques secondes.
Exercice 1 Chiffre de César- La cellule de code ci-dessous propose une fonction qui illustre le chiffrement de César :
# Exemple simple pour illustrer le vocabulaire
def chiffrement_cesar(texte_clair, decalage):
"""
Chiffre un texte avec le code de César.
Ne gère que les lettres majuscules/minuscules, les autres caractères sont inchangés.
"""
texte_chiffre = ""
for lettre in texte_clair:
if lettre.isalpha():
# Déterminer si la lettre est majuscule ou minuscule
if lettre.isupper():
base = ord('A')
else:
base = ord('a')
# Appliquer le décalage avec gestion du cycle (modulo 26)
code = (ord(lettre) - base + decalage) % 26 + base
texte_chiffre += chr(code)
else:
texte_chiffre += lettre
return texte_chiffre
Copiez-coller ce code dans une console basthon ou tout autre environnement qui permet de compiler du python.
Utilisez-le pour chiffrer :
message = "NSI"
cle = 3
chiffre = chiffrement_cesar(message, cle)
print(f"Texte clair : {message}")
print(f"Clé : {cle}")
print(f"Texte chiffré : {chiffre}")
déchiffrer :
# Compléter les ...
dechiffre = ...
print(f"Texte déchiffré : {...}")
des messages différents avec des décalages variés.
- Script à tester
La cellule de code ci-dessous illustre une attaque par la force brute sur le chiffre de César pour déchiffrer un message.
Utiliser ce script pour déchiffrer « QDVDIP » et donner la valeur de la clé utilisée.
def casser_cesar(texte_chiffre):
"""Tente les 25 décalages possibles"""
for decalage in range(1, 26):
texte_clair = ""
for lettre in texte_chiffre:
if lettre.isalpha():
code = (ord(lettre) - ord('A') - decalage) % 26 + ord('A')
texte_clair += chr(code)
else:
texte_clair += lettre
print(f"Décalage {decalage:2d} : {texte_clair}")
# Exemple avec le message précédent
casser_cesar("...")
Moyen Âge
Avec le développement des mathématiques et de l’analyse statistique, la cryptanalyse progresse considérablement.
Les chiffrements par substitution monoalphabétique (où chaque lettre est remplacée systématiquement par une autre) deviennent vulnérables face à l’analyse fréquentielle.
Principe de l’analyse fréquentielle :
– Dans une langue donnée, chaque lettre apparaît avec une fréquence caractéristique. En français, par exemple, la lettre E est la plus fréquente (environ 15 %), suivie de A, S, etc.
– En comparant les fréquences des lettres dans un texte chiffré avec celles attendues dans la langue du texte clair, on peut deviner la substitution et décrypter le message.
Pour contrer cette attaque, des chiffrements plus sophistiqués apparaissent, comme le chiffre de Vigenère (XVIe siècle), qui utilise une clé et plusieurs alphabets de substitution. Ce chiffre restera inviolé pendant près de trois siècles, jusqu’aux travaux de Charles Babbage et Friedrich Kasiski au XIXe siècle.

Charles Babbage from Unknown author, Public domain, via Wikimedia Commons
Voici une implémentation Python du chiffre de Vigenère, suivie d’un exemple d’attaque par analyse fréquentielle sur un monoalphabétique, pour illustrer pourquoi Vigenère a été une avancée majeure.
- Implémentation du chiffre de Vigenère
def vigenere_encrypt(plaintext, key):
ciphertext = []
key = key.upper()
key_len = len(key)
key_index = 0
for char in plaintext.upper():
if char.isalpha():
shift = ord(key[key_index % key_len]) - ord('A')
encrypted_char = chr((ord(char) - ord('A') + shift) % 26 + ord('A'))
ciphertext.append(encrypted_char)
key_index += 1
else:
ciphertext.append(char) # Garde les caractères non alphabétiques
return ''.join(ciphertext)
def vigenere_decrypt(ciphertext, key):
plaintext = []
key = key.upper()
key_len = len(key)
key_index = 0
for char in ciphertext.upper():
if char.isalpha():
shift = ord(key[key_index % key_len]) - ord('A')
decrypted_char = chr((ord(char) - ord('A') - shift) % 26 + ord('A'))
plaintext.append(decrypted_char)
key_index += 1
else:
plaintext.append(char)
return ''.join(plaintext)
# Exemple
message = "Moyen Age"
cle = "HISTOIRE"
chiffre = vigenere_encrypt(message, cle)
dechiffre = vigenere_decrypt(chiffre, cle)
print(f"Message original : {message}")
print(f"Clé : {cle}")
print(f"Chiffré : {chiffre}")
print(f"Déchiffré : {dechiffre}")
- Pourquoi Vigenère résiste mieux à l’analyse fréquentielle ?
Contrairement au chiffrement monoalphabétique (ex : César), Vigenère utilise plusieurs alphabets différents selon la position dans le texte.
Un même caractère en clair peut être chiffré en différentes lettres, lissant ainsi les fréquences.
Exemple d’attaque sur un monoalphabétique (simple décalage)
from collections import Counter
def cesar_encrypt(text, shift):
result = []
for char in text.upper():
if char.isalpha():
result.append(chr((ord(char) - ord('A') + shift) % 26 + ord('A')))
else:
result.append(char)
return ''.join(result)
def frequency_attack(ciphertext):
# Compte les fréquences des lettres dans le texte chiffré
letters = [c for c in ciphertext if c.isalpha()]
if not letters:
return ""
freq = Counter(letters)
most_common_letter = freq.most_common(1)[0][0]
# Hypothèse : la lettre la plus fréquente correspond à 'E' (fréquent en français/anglais)
shift = (ord(most_common_letter) - ord('E')) % 26
return cesar_encrypt(ciphertext, -shift)
# Exemple
plain = "LE MOYEN AGE EST UNE PERIODE HISTORIQUE"
shift = 5
cipher = cesar_encrypt(plain, shift)
print(f"Chiffré César : {cipher}")
decrypted_guess = frequency_attack(cipher)
print(f"Attaque fréquentielle : {decrypted_guess}")
L’attaque fonctionne car la lettre la plus fréquente (J ici) est bien remplacée par E après correction.
- Pourquoi cette attaque échoue sur Vigenère ?
Si on applique la même attaque sur un texte chiffré avec Vigenère, la lettre la plus fréquente ne correspond pas à E de façon constante, car le décalage change.
cipher_vig = vigenere_encrypt(plain, "CLE")
print(f"Vigenère chiffré : {cipher_vig}")
decrypted_bad = frequency_attack(cipher_vig)
print(f"Attaque naïve : {decrypted_bad}")
>>> Vigenère chiffré : NP QQJIP LKG PWV FRG AITTSFP LKDXQCMSFI
>>> Attaque naïve : CE FFYXE AZV ELK UGV PXIIHUE AZSMFRBHUX
Conclusion
– Monoalphabétique → vulnérable à l’analyse fréquentielle.
– Vigenère (polyalphabétique) → résiste à cette attaque simple car il répartit les fréquences.
Il faudra attendre Kasiski (XIXe) pour casser Vigenère par détection de la période de la clé.
XXe siècle
La Seconde Guerre mondiale constitue un tournant majeur. La cryptographie et la cryptanalyse deviennent des enjeux stratégiques.
Enigma
Les Allemands utilisent la machine Enigma, un système de chiffrement électromécanique considéré à l’époque comme inviolable. Sa force réside dans sa complexité et le changement quotidien des réglages.
La percée d’Alan Turing
Alan Turing et son équipe à Bletchley Park conçoivent les « bombes », des machines électromécaniques capables de tester des millions de configurations pour casser Enigma. Ces travaux marquent les prémices de la cryptographie automatisée et posent les bases de l’informatique moderne.

L’après-guerre
– Claude Shannon (1949) jette les fondements théoriques de la cryptographie moderne avec son article « Communication Theory of Secrecy Systems ».
– Années 1970 : apparition des premiers chiffrements symétriques modernes comme le DES (Data Encryption Standard), adopté comme standard aux États-Unis.
– 1977 : invention du RSA (Rivest-Shamir-Adleman), premier algorithme de cryptographie asymétrique (ou à clé publique). Cette innovation révolutionne la sécurité des communications en permettant d’échanger des clés de manière sécurisée sur des canaux non protégés.
Aujourd’hui
Le DES, avec sa clé de 56 bits, devient vulnérable face à l’augmentation de la puissance de calcul. Dans les années 1990, des attaques par force brute le compromettent définitivement. Le 3DES (Triple DES) prolonge sa durée de vie, mais reste trop lent.
En 1997, le NIST (National Institute of Standards and Technology) lance un appel à candidatures pour un nouvel algorithme de chiffrement symétrique, plus rapide et plus sûr.
Le concours AES
– 1999 : 5 finalistes sont sélectionnés parmi 15 candidats.
– Octobre 2000 : l’algorithme Rijndael, conçu par les cryptographes belges Joan Daemen et Vincent Rijmen, remporte le concours.
– 2001 : le standard est publié sous le nom d’AES (Advanced Encryption Standard).
Caractéristiques de l’AES
– Chiffrement symétrique : la même clé sert à chiffrer et déchiffrer.
– Longueurs de clé : 128, 192 ou 256 bits.
– Taille de bloc : 128 bits (contrairement à Rijndael qui autorisait d’autres tailles).
– Sécurité : aujourd’hui, aucune attaque pratique ne permet de casser un chiffrement AES bien implémenté.
AES est aujourd’hui présent partout : Wi‑Fi (WPA2/WPA3), communications sécurisées (TLS/HTTPS), disques durs chiffrés, cartes bancaires, etc.
Prérequis mathématiques
Pour bien comprendre le fonctionnement de l’AES, il est nécessaire de maîtriser quelques concepts mathématiques simples mais essentiels.
L’opération modulo (notée % dans la plupart des langages de programmation) retourne le reste d’une division entière (division euclidienne).
Exemple :
$13 = 2 × 5 + 3$ donc $12 \% 5 = 3$ (Il faut bien sûr que $3 < 5$ sinon ce n’est pas une division euclidienne)
Congruence
On dit que deux nombres $A$ et $B$ sont congrus modulo $P$ s’ils ont le même reste dans la division euclidienne par $P$. Cela se note : $A \equiv B [P]$.
Cela équivaut à dire que la différence $A - B$ est un multiple de $P$, c’est-à-dire qu’il existe un entier $k$ tel que : $A = B + k⋅P$
Exemples avec $P = 7$ :
– $12 \equiv 5 [7]$ car $12 = 5 + 1 \times 7$
– $12 \equiv 33 [7]$ car $12 = 33 + (-3) \times 7$
– $12 \equiv -2 [7]$ car $12 = -2 + 2 \times 7$
Remarque :
En programmation, le résultat du modulo est généralement compris entre 0 et P - 1 (reste positif).
Ainsi, $12 \% 7 = 5$, et $-2 \% 7 = 5$ (dans la plupart des langages).
Dans l’AES, toutes les opérations de somme se font modulo 2.
Cela signifie que les coefficients manipulés ne peuvent être que $0$ ou $1$, et l’addition se fait selon les règles suivantes :
| $a$ |
$b$ |
$a + b [2]$ |
| $0$ |
$0$ |
$0$ |
| $0$ |
$1$ |
$1$ |
| $1$ |
$0$ |
$1$ |
| $1$ |
$1$ |
$0$ |
Propriétés importantes :
– Dans $\mathbb{F}_2$, additionner et soustraire, c’est la même chose :
$a - b ≡ a + b [2]$. En effet, $-b \equiv b [2]$ car $-1 \equiv 1 [2]$.
– L’addition modulo 2 correspond à l’opérateur logique XOR (ou exclusif) des bits.
– L’ensemble $\{0 ; 1\}$ muni de l’addition modulo 2 forme un groupe abélien (nous verrons cette notion plus loin).
Dans l’AES, les octets sont représentés comme des polynômes à coefficients dans $\mathbb{F}_2$.
Par exemple, l’octet $0b11010011$ (soit $211$ en décimal) peut s’écrire : $1⋅x^7 + 1⋅x^6 + 0⋅x^5 + 1⋅x^4 + 0⋅x^3 + 0⋅x^2 + 1⋅x + 1$ soit plus simplement : $x^7 + x^6 + x^4 + x + 1$
Opérations sur ces polynômes :
– Addition : on additionne les coefficients de mêmes degrés, modulo 2.
– Multiplication : classique, mais les coefficients sont additionnés modulo 2.
– Réduction : pour rester dans des polynômes de degré inférieur à 8, on utilise un polynôme irréductible (dans AES : $x^8 + x^4 + x^3 + x + 1$).
Cette structure mathématique s’appelle un corps de Galois, noté $\mathbb{F}_{2^8}$ ou $GF(2^8)$. C’est ce qui permet de manipuler des octets tout en conservant des propriétés algébriques essentielles pour le chiffrement.
Un groupe $(G,\circ)$ est un ensemble $G$ muni d’une opération $\circ$ (appelée « loi de composition interne ») qui vérifie quatre propriétés fondamentales :
| $\text{Propriété}$ |
$\text{Définition mathématique}$ |
$\text{Explication}$ |
|
$\forall a,b \in G,\ a \circ b \in G$ |
$\text{Le résultat de l’opération reste dans l’ensemble.}$ |
| $\text{Associativité}$ |
$\forall a,b,c \in G,\ (a \circ b) \circ c = a \circ (b \circ c)$ |
$\text{L’ordre des calculs n’a pas d’importance.}$ |
| $\text{Élément neutre}$ |
$\exists e \in G,\ \forall a \in G,\ a \circ e = e \circ a = a$ |
$\text{Il existe un élément qui ne modifie aucun autre.}$ |
| $\text{Inverse}$ |
$\forall a \in G,\ \exists a^{-1} \in G,\ a \circ a^{-1} = a^{-1} \circ a = e$ |
$\text{Chaque élément possède un "symétrique".}$ |
Exemples de groupes :
- $(\mathbb{Z}, +)$ : l’ensemble des entiers relatifs avec l’addition.
- Neutre : $0$
- Inverse de $a$ : $-a$
- $(\mathbb{F}_2, +)$ : l’ensemble $\{0,1\}$ avec l’addition modulo 2.
- Neutre : $0$
- Inverse : $1$ est son propre inverse car $1+1=0$
Si, en plus des quatre propriétés ci-dessus, l’opération est commutative, on dit que le groupe est abélien (en hommage au mathématicien Niels Abel) : $\forall a,b \in G, a \circ b = b \circ a$
- Exemple : $(\mathbb{Z}, +)$ est un groupe abélien.
- Contre-exemple : l’ensemble des matrices carrées inversibles avec la multiplication (non commutative).
Pour aller plus loin :
Le corps fini $GF(2^8)$ (Galois Field) est un espace mathématique fondamental utilisé dans de nombreux algorithmes de chiffrement, notamment AES (Rijndael). Il permet de manipuler des octets comme des éléments mathématiques avec des opérations bien définies.
1. Structure générale
$GF(2^8)$ est un corps à $2^8 = 256$ éléments.
Chaque élément est représenté par un octet (8 bits) :
– 0x00, 0x01, 0x02, ..., 0xFF en héxadécimal ;
– 0b00000000, 0b00000001, 0b00000010, ..., 0b11111111 en binaire.
Chaque octet peut être vu comme :
– Un polynôme à coefficients dans $GF(2^8)$
– Un vecteur binaire de dimension 8
2. Représentation polynomiale
Un octet $b_7 b_6 b_5 b_4 b_3 b_2 b_1 b_0$ correspond au polynôme :
$b_7 x^7 + b_6 x^6 + b_5 x^5 + b_4 x^4 + b_3 x^3 + b_2 x^2 + b_1 x + b_0$
Exemple : 0x57 = 0b01010111 = 87
0x57 = 0·x⁷ + 1·x⁶ + 0·x⁵ + 1·x⁴ + 0·x³ + 1·x² + 1·x + 1
= x⁶ + x⁴ + x² + x + 1
3. Addition
L’addition dans $GF(2^8)$ est simplement le XOR bit à bit.
$a + b = a \oplus b$
Propriétés :
- L’élément neutre est 0
- Chaque élément est son propre inverse : $a + a = 0$
Exemple :
0x57 + 0x83 = 0x57 ⊕ 0x83 = 0xD4
01010111
⊕10000011
---------
11010100
En polynômes :
$(x⁶ + x⁴ + x² + x + 1) + (x⁷ + x + 1) = x⁷ + x⁶ + x⁴ + x²$ en effet, dans $GF(2^8)$, l’addition est un XOR : les coefficients s’additionnent modulo 2 ($1 + 1 \equiv 0$) donc $x + x = 2x \equiv 0$ et $1 + 1 = 2 \equiv 0$
4. Multiplication
La multiplication est plus complexe : on multiplie les polynômes, puis on réduit modulo un polynôme irréductible de degré 8.
Polynôme irréductible pour AES
AES utilise le polynôme :
$m(x) = x^8 + x^4 + x^3 + x + 1 \quad (\text{0x11B})$
Le $(\text{0x11B})$ est la représentation hexadécimale du polynôme irréductible utilisé par AES :
On encode le polynôme en hexadécimal en ne considérant que les coefficients (0 ou 1) :
| Puissance |
$x^8$ |
$x^7$ |
$x^6$ |
$x^5$ |
$x^4$ |
$x^3$ |
$x^2$ |
$x^1$ |
$x^0$ |
| Coefficient |
$1$ |
$0$ |
$0$ |
$0$ |
$1$ |
$1$ |
$0$ |
$1$ |
$1$ |
Soit en binaire : 0b1 0001 1011
En regroupant par 4 bits : 1 0001 1011 = 0001 0001 1011 (en ajoutant un 0 devant pour avoir un nombre de bits multiple de 4)
Soit :
0001 = 0x1
0001 = 0x1
1011 = 0xB
→ 0x11B
Pourquoi 9 bits ?
Le polynôme est de degré 8, donc il a 9 coefficients (de $x^8$ à $x^0$). Sa représentation binaire nécessite 9 bits, que l’on écrit en hexadécimal sur 3 chiffres :
0x11B = 0b1 0001 1011
↑ ↑ ↑ ↑↑
x⁸ x⁴,x³,x¹,x⁰
Dans la pratique
Lors des calculs dans $GF(2^8)$, on travaille uniquement sur des octets (8 bits). Le polynôme irréductible 0x11B sert à réduire les résultats qui dépassent 8 bits :
def reduction(x):
"""Réduction modulo 0x11B pour un résultat sur 9 bits"""
if x & 0x100: # si le 9ème bit est 1
x ^= 0x11B # soustraire le polynôme (XOR)
return x & 0xFF
Exemple :
Si un produit donne 0x1A3 (binaire : 1 1010 0011)
On réduit : 0x1A3 ^ 0x11B = 0x0B8
C’est ce mécanisme qui permet de maintenir tous les résultats dans l’intervalle $[0x00, 0xFF]$, soit $[0 ; 256]$, tout en conservant la structure de corps fini.
Algorithme de multiplication
Pour multiplier $a$ et $b$ dans $GF(2^8)$ :
- Multiplier les polynômes (produit pouvant aller jusqu’au degré 14)
- Prendre le reste de la division euclidienne par $m(x)$
Exemple simplifié
Soit $a = 0x57$, $b = 0x83$ :
Étape 1 : Multiplication polynomiale
$a(x) \times b(x) = (x⁶ + x⁴ + x² + x + 1) \times (x⁷ + x + 1) = x^{13} + x^{11} + x^9 + x^8 + x^7 + x^7 + x^5 + x^3 + x^2 + x + x^6 + x^4 + x^2 + x + 1$
Après simplification (car $x^7 + x^7 \equiv 0$) :
$a(x) \times b(x) = x^{13} + x^{11} + x^9 + x^8 + x^6 + x^5 + x^4 + x^3 + 1$
Étape 2 : Réduction modulo $m(x) = x^8 + x^4 + x^3 + x + 1$ (On utilise la division des polynômes)
$a(x) \times b(x) [m(x)] \equiv -x^7 - x^6 + x^0 [m(x)] \equiv x^7 + x^6 + x^0 [m(x)]$, soit 0b11000001 = 0xC1 = 193
Après calcul : $a \times b = 0xC1$ (résultat du calcul complet)
5. Multiplication rapide par xtime
AES utilise une multiplication optimisée par 2, notée xtime() :
xtime(x) = (x << 1) ⊕ 0x1B (si bit de poids fort = 1 sinon 0)
xtime() est une fonction qui effectue la multiplication par le polynôme x (c’est-à-dire par 2 en décimal) dans le corps fini $GF(2^8)$. C’est une opération fondamentale qui sert de brique de base pour toutes les multiplications dans AES.
5.1. Principe mathématique
Multiplier un polynôme par $x$ revient à décaler tous les bits vers la gauche :
$a(x)×x=(b_7x^7 + b_6x^6 + ... + b_0)×x = b_7x^8 + b_6x^7 + ... + b_0x$
Mais en $GF(2^8)$, on travaille modulo un polynôme irréductible de degré 8. Si le résultat dépasse le degré 7, on réduit en soustrayant XOR le polynôme irréductible.
5.2. Algorithme
def xtime(x):
"""
Multiplication par 2 (par x) dans GF(2⁸)
Polynôme : x⁸ + x⁴ + x³ + x + 1 (0x11B)
"""
# Étape 1 : décaler à gauche
result = (x << 1) & 0xFF
# Étape 2 : si le bit perdu était 1, on réduit
if x & 0x80: # si le bit de poids fort est 1
result ^= 0x11B
return result& 0xFF
# xtime(0x57) = 0xAE
# xtime(0x83) = 0x1D
5.3. Visualisation
– 0x80 est la représentation hexadécimale du nombre 128, qui en binaire s’écrit :
0x80 = 0b10000000.
C’est un masque qui a un seul bit à 1 : le 8ème bit (bit de poids fort).
– Le & est l’opérateur ET binaire (AND). Il compare chaque bit de x avec le bit correspondant du masque 0x80.
– Exemples
- Cas 1 : Le bit de poids fort est 1
x = 0x83 = 0b10000011
x & 0x80 = 0b10000011 & 0b10000000 = 0b10000000 = 128 # Le résultat est non nul → condition vraie
xtime(0x83) = (0x83 << 1) = 0x106 (0b100000110)
On masque sur 8 bits : 0x06
Puis on XOR avec 0x11B : 0x06 ^ 0x11B = 0x1D
Si le bit de poids fort de x est 1 : le décalage fait apparaître un 9ème bit (débordement) → on doit réduire en XORant avec 0x11B
- Cas 2 : Le bit de poids fort est 0
x = 0x57 = 0b01010111
x & 0x80 = 0b01010111 & 0b10000000 = 0b00000000 = 0 # Le résultat est nul → condition fausse
xtime(0x57) = (0x57 << 1) = 0xAE (0b10101110)
Si le bit de poids fort de x est 0 : le décalage ne provoque pas de débordement → pas de réduction
5.4. Pourquoi cette optimisation ?
xtime() est optimisée car :
– Rapide : Un seul décalage et un XOR conditionnel
– Matériel : Se fait en quelques cycles d’horloge
– Réutilisable : Sert à construire toutes les multiplications
5.5. Multiplication générale avec xtime()
On peut construire n’importe quelle multiplication en combinant des xtime() et des XOR :
def gf256_mult(a, b):
"""Multiplication utilisant xtime()"""
result = 0
for _ in range(8):
if b & 1:
result ^= a
a = xtime(a) # a = a × 2
b >>= 1
return result
gf256_mult(0x57, 0xBF)
La multiplication générale s’effectue par combinaison d’additions et de xtime.
5.6. Code Python pour la multiplication dans $GF(2^8)$
def gf256_mult(a, b):
"""Multiplication dans GF(2^8) - version explicite"""
result = 0
for i in range(8):
if b & 1:
result ^= a
# xtime sur a
high_bit = (a >> 7) & 1
a = (a << 1) & 0xFF
if high_bit:
a ^= 0x11B
b >>= 1
return result
# Test
print(hex(gf256_mult(0x57, 0x83))) # 0xc1
6. L’inverse dans $GF(2^8)$
L’inverse d’un élément $a$ dans $GF(2^8)$ est l’élément $a^{-1}$ tel que :
$a \times a^{-1} = 1 \quad \text{(dans} GF(2^8)\text{)}$
6.1. Pourquoi l’inverse est-il important ?
Dans AES, l’opération SubBytes (la seule non-linéaire) utilise l’inverse dans $GF(2^8)$ :
$\text{SubBytes}(a) = \text{Inv}(a) \times M + b$ où $M$ est une matrice affine et $b$ un vecteur constant.
L’inverse introduit une forte non-linéarité qui rend la cryptanalyse différentielle et linéaire beaucoup plus difficile.
6.2. Propriétés de l’inverse
| $\text{Propriété}$ |
Explication |
| Existence |
Tout élément non nul a un unique inverse |
| $a \times a^{-1} = 1$ |
Définition même |
| $a^{-1} \times a = 1$ |
Commutatif (corps) |
| $0$ n’a pas d’inverse |
Dans AES, $0$ est traité séparément (image = 0) |
| Involution partielle |
$(a^{-1})^{-1} = a$ |
6.3. Exemples concrets
| Élément $a$ |
Binaire |
Inverse $a^{-1}$ |
$\text{Vérification}$ |
| $0x01$ |
$00000001$ |
$0x01$ |
$1 \times 1 = 1$ |
| $0x02$ |
$00000010$ |
$0x8D$ |
$0x02 \times 0x8D = 0x01$ |
| $0x03$ |
$00000011$ |
$0x0F$ |
$0x03 \times 0x0F = 0x01$ |
| $0x57$ |
$01010111$ |
$0x5D$ |
$0x57 \times 0x5D = 0x01$ |
| $0x83$ |
$10000011$ |
$0x0E$ |
$0x83 \times 0x0E = 0x01$ |
6.4. Vérification avec Python
# Vérification de l'inverse de 0x57
a = 0x57
inv_a = 0xBF
print(hex(gf256_mult(a, inv_a))) # 0x1
6.5. Comment calculer l’inverse ?
Plusieurs méthodes existent :
Méthode 1 : Algorithme d’Euclide étendu (dans $GF(2^8)$)
On cherche $u(x)$ et $v(x)$ tels que : $a(x) \cdot u(x) + m(x) \cdot v(x) = 1$.
Alors $u(x)$ est l’inverse de $a(x)$ modulo $m(x)$.
Exemple :
Trouver l’inverse de $a(x) = x^6 + x^4 + x^2 + x + 1 (0x57)$ avec $m(x) = x^8 + x^4 + x^3 + x + 1 (0x11B)$
Voici le développement pour trouver l’inverse de 0x57 modulo 0x11B.
1. Rappel des données
- $m(x) = x^8 + x^4 + x^3 + x + 1$ (0x11B)
- $a(x) = x^6 + x^4 + x^2 + x + 1$ (0x57)
2. Algorithme d’Euclide étendu pas à pas
| Étape |
Quotient $q_i$ |
Reste $r_i$ |
Coefficient $u_i$ |
| $-1$ |
|
$r_{-1} = x^8 + x^4 + x^3 + x + 1 = m(x)$ |
$u_{-1} = 0$ |
| $0$ |
|
$r_0 = x^6 + x^4 + x^2 + x + 1 = a(x)$ |
$u_0 = 1$ |
| $1$ |
|
$r_1 = x^4$ |
$u_1 = 1 + x^2$ |
| $2$ |
|
$r_2 = 1 + x + x^2$ |
$u_2 = x^4$ |
| $3$ |
|
$r_3 = x$ |
$u_3 = 1 + x^2 + x^5 + x^6$ |
| $4$ |
|
$r_4 = 1$ |
$u_4 = 1 + x + x^2 + x^3 + x^4 + x^5 + x^7$ |
Itération 1 : $\dfrac{r_{-1}}{r_0}$
- $q_1 = \mathbf{x^2 + 1}$
- $r_1 = r_{-1} \oplus (x^2+1)r_0 = \mathbf{x^4}$
- $u_1 = u_{-1} \oplus (x^2+1)u_0 = 0 \oplus (x^2+1) = \mathbf{x^2+1}$
Itération 2 : $\dfrac{r_0}{r_1}$
- $q_2 = \mathbf{x^2 + 1}$
- $r_2 = r_0 \oplus (x^2+1)x^4 = (x^6+x^4+x^2+x+1) \oplus (x^6+x^4) = \mathbf{x^2+x+1}$
- $u_2 = u_0 \oplus (x^2+1)u_1 = 1 \oplus (x^2+1)^2 = 1 \oplus (x^4+1) = \mathbf{x^4}$
Itération 3 : $\dfrac{r_1}{r_2}$
- $q_3 = \mathbf{x^2 + x}$ (car $(x^2+x)(x^2+x+1) = x^4+x^3+x^2 + x^3+x^2+x = x^4+x$)
- $r_3 = x^4 \oplus (x^4+x) = \mathbf{x}$
- $u_3 = u_1 \oplus (x^2+x)u_2 = (x^2+1) \oplus (x^2+x)x^4 = (x^2+1) \oplus (x^6+x^5) = \mathbf{x^6+x^5+x^2+1}$
Itération 4 : $\dfrac{r_2}{r_3}$
- $q_4 = \mathbf{x + 1}$ (car $(x+1)x = x^2+x$)
- $r_4 = (x^2+x+1) \oplus (x^2+x) = \mathbf{1}$
- $u_4 = u_2 \oplus (x+1)u_3 = x^4 \oplus(x+1) \cdot (x^6+x^5+x^2+1) = x^4 \oplus x^7+x^6+x^3+x + x^6+x^5+x^2+1 = \mathbf{x^7 + x^4 + x^5 + x^3 + x^2 + x + 1}$
Puisque le reste $r_4$ est nul, l’inverse de $x^6 + x^4 + x^2 + x + 1$ est $u_4 = \mathbf{x^7 + x^4 + x^5 + x^3 + x^2 + x + 1}$.
Résultat
En binaire : 0b10111111
En hexadécimal : 0xBF
Version Python de l’algorithme d’Euclide étendu
def gf256_inv(a, m=0x11b):
if a == 0:
return 0
# Initialisation : u0*m + v0*a = r0 | u1*m + v1*a = r1
r0, r1 = m, a
v0, v1 = 0, 1 # On cherche v tel que v*a = 1 mod m
while r1 != 0:
# 1. Calculer le quotient complet q de r0 / r1
# Dans GF(2), c'est une division polynomiale
q = 0
temp_r1 = r1
deg_r0 = r0.bit_length() - 1
deg_r1 = temp_r1.bit_length() - 1
# Division longue pour trouver le quotient q
while deg_r0 >= deg_r1:
shift = deg_r0 - deg_r1
q ^= (1 << shift)
r0 ^= (temp_r1 << shift)
deg_r0 = r0.bit_length() - 1
# 2. Mettre à jour r et v (v correspond à u dans votre code)
# r_suivant = r0 (qui est devenu le reste après le XOR au-dessus)
# v_suivant = v0 ^ q*v1
# Pour v0 ^ (q * v1), on doit faire une multiplication polynomiale GF(2)
prod = 0
for i in range(q.bit_length()):
if (q >> i) & 1:
prod ^= (v1 << i)
v0, v1 = v1, v0 ^ prod
r0, r1 = r1, r0
return v0
print(f"L'inverse de 0x57 est : {hex(gf256_inv(0x57))}") # 0xbf
Méthode 2 : Exponentiation (petit théorème de Fermat)
Dans un corps fini $GF(2^8)$ :
$a^{-1} = a^{254}$ car $a^{255} = 1$ pour tout $a \neq 0$.
def gf256_inv_pow(a):
"""Inverse par exponentiation (a²⁵⁴)"""
if a == 0:
return 0
result = 1
for _ in range(254): # a^254
result = gf256_mult(result, a)
return result
hex(gf256_inv_pow(0x57))
>>>'0xbf'
Méthode 3 : Table pré-calculée
La méthode la plus rapide en pratique : pré-calculer une table de 256 entrées.
def gf256_inv_table():
"""
Prépare une table d'inverses multiplicatifs dans GF(2⁸)
avec le polynôme x⁸ + x⁴ + x³ + x + 1 (0x11B)
Returns:
list: Table de 256 entrées où table[a] = a⁻¹
"""
# Initialisation de la table
inv_table = [0] * 256
# Cas particulier : 0 n'a pas d'inverse (on lui donne 0 par convention)
inv_table[0] = 0
# Pour chaque élément non nul, calculer son inverse
for a in range(1, 256):
# Recherche de l'inverse par force brute
# (une seule fois à l'initialisation)
for b in range(1, 256):
if gf256_mult(a, b) == 1:
inv_table[a] = b
break
return inv_table
# Construction de la table
INV_TABLE = gf256_inv_table()
def gf256_inv_table_lookup(a):
"""
Retourne l'inverse multiplicatif de a dans GF(2⁸)
Utilise une table pré-calculée
Args:
a: entier entre 0 et 255
Returns:
int: a⁻¹ dans GF(2⁸)
"""
return INV_TABLE[a & 0xFF]
# Affichage des premiers inverses
print("Table des inverses (début) :")
for i in range(16):
print(f"0x{i:02X} → 0x{INV_TABLE[i]:02X}")
>>>Table des inverses (début) :
0x00 → 0x00
0x01 → 0x01
0x02 → 0x8D
0x03 → 0xF6
0x04 → 0xCB
0x05 → 0x52
0x06 → 0x7B
0x07 → 0xD1
0x08 → 0xE8
0x09 → 0x4F
0x0A → 0x29
0x0B → 0xC0
0x0C → 0xB0
0x0D → 0xE1
0x0E → 0xE5
0x0F → 0xC7
hex(gf256_inv_table_lookup(0x57))
>>>'0xbf'
6.6. Rôle dans AES
Dans AES, l’inverse est utilisé dans SubBytes :
┌─────────────┐
│ Octet a │
└──────┬──────┘
▼
┌─────────────┐
│ a⁻¹ dans │ ← inversion dans GF(2⁸)
│ GF(2⁸) │
└──────┬──────┘
▼
┌─────────────┐
│ Affine │ ← transformation affine
│ M·x + b │
└──────┬──────┘
▼
┌─────────────┐
│ S-Box(a) │
└─────────────┘
L’inversion dans $GF(2^8)$ assure que :
- La transformation est non-linéaire (aucune relation affine entre l’entrée et la sortie)
- Tout changement d’un bit en entrée modifie en moyenne 4 bits en sortie (avalanche)
- Résistance aux cryptanalyses différentielle et linéaire
7. Table récapitulative
| Opération |
Description |
Exemple (0x57, 0x83) |
| Addition |
XOR bit à bit |
0x57 ⊕ 0x83 = 0xD4 |
| Multiplication |
Produit polynomial mod m(x) |
0x57 × 0x83 = 0xC1 |
| Inverse |
Élément $a⁻¹$ tel que $a × a⁻¹ = 1$ |
inv(0x57) = 0xBF |
xtime |
Multiplication par x (2) |
xtime(0x57) = 0xAE |
8. Utilisation dans AES
$GF(2^8)$ est au cœur d’AES pour :
- SubBytes : inversion dans $GF(2^8)$ suivie d’une transformation affine
- MixColumns : multiplication de colonnes par un polynôme fixe dans $GF(2^8)$
- KeyExpansion : génération des sous-clés
Cette structure algébrique permet d’implémenter des opérations de chiffrement à la fois rapides en matériel et résistantes aux attaques grâce à la non-linéarité de l’inversion dans le corps.
Application dans l’AES
Ces notions mathématiques sont utilisées dans l’AES de la manière suivante :
| Concept |
Application dans AES |
| Addition modulo 2 |
L’opération XOR entre octets (utilisée dans AddRoundKey). |
| Corps $\mathbb{F}_{2^8}$ |
Les octets sont manipulés comme des polynômes. |
| Multiplication dans $\mathbb{F}_{2^8}$ |
Utilisée dans la transformation MixColumns. |
| Structure de groupe |
L’ensemble des octets avec le XOR forme un groupe abélien. |
Pour la suite
Gardez en mémoire que :
-
Addition = XOR dans tout l’algorithme AES. -
Multiplication dans AES n’est pas la multiplication entière usuelle, mais une multiplication polynomiale dans $\mathbb{F}_{2^8}$. - L’élément neutre pour l’addition est l’octet
0x00, et tout élément est son propre inverse pour l’addition.
Exécutez le script ci-dessous dans une console Python pour une illustration du XOR :
# Illustration de l'addition modulo 2 (XOR)
a = 0b10110011 # 179 en décimal
b = 0b11001010 # 202 en décimal
resultat = a ^ b # XOR en Python
print(f"{a:08b} (0x{a:02X})")
print(f"{b:08b} (0x{b:02X})")
print(f"{'-'*8}")
print(f"{resultat:08b} (0x{resultat:02X})")
Principe de l’AES
1. Généralités
L’AES (Advanced Encryption Standard) est un algorithme de chiffrement symétrique : la même clé est utilisée à la fois pour chiffrer et pour déchiffrer un message.
Il repose entièrement sur des notions mathématiques liées aux ensembles (notamment la théorie des corps finis) et à l’arithmétique modulaire.
Objectif de cette section : comprendre le déroulement global de l’algorithme avant d’étudier en détail chaque composant.
2. Structure des données : le bloc
L’AES travaille avec des blocs de données de taille fixe : 128 bits, soit 16 octets.
Ces 16 octets sont organisés en un tableau carré de 4 lignes × 4 colonnes. Chaque cellule du tableau contient un octet (8 bits) et est appelée un mot (ou byte).
Visualisation : un bloc de 16 octets $A_1, A_2, ... , A_{16}$ est disposé colonne par colonne :
|
Colonne 0 |
Colonne 1 |
Colonne 2 |
Colonne 3 |
| Ligne 0 |
$A_1$ |
$A_5$ |
$A_9$ |
$A_{13}$ |
| Ligne 1 |
$A_2$ |
$A_6$ |
$A_{10}$ |
$A_{14}$ |
| Ligne 2 |
$A_3$ |
$A_7$ |
$A_{11}$ |
$A_{15}$ |
| Ligne 3 |
$A_4$ |
$A_8$ |
$A_{12}$ |
$A_{16}$ |
Pourquoi cette disposition ? L’ordre colonne par colonne (ordre *column-major*) est celui utilisé dans la spécification officielle de l’AES. C’est important pour comprendre les transformations comme ShiftRows et MixColumns.
3. Vocabulaire
- Plain Text (texte clair) : le bloc d’entrée, non chiffré.
- Cipher Text (texte chiffré) : le bloc de sortie, après application de l’algorithme.
- Round : une itération complète des transformations.
- Round Key : une clé dérivée de la clé principale, utilisée pour un round spécifique.
4. Vue d’ensemble de l’algorithme
Pour chiffrer un bloc, l’AES applique une série de transformations réversibles dans un ordre précis. Ces transformations sont répétées un certain nombre de fois, appelé nombre de rounds.
Le nombre de rounds dépend de la taille de la clé :
| Taille de clé |
Nombre de rounds |
| 128 bits |
10 rounds |
| 192 bits |
12 rounds |
| 256 bits |
14 rounds |
Schéma3. Vocabulaire général de l’algorithme
┌─────────────────────────────────────────────────────────────────┐
│ ALGORITHME AES │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Plain Text (128 bits) │
│ ↓ │
│ ┌─────────────────┐ │
│ │ AddRoundKey (0) │ ← Clé initiale │
│ └─────────────────┘ │
│ ↓ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Pour i = 1 à NbRounds-1 │ │
│ │ ┌──────────────┐ │ │
│ │ │ SubBytes │ │ │
│ │ └──────────────┘ │ │
│ │ ↓ │ │
│ │ ┌──────────────┐ │ │
│ │ │ ShiftRows │ │ │
│ │ └──────────────┘ │ │
│ │ ↓ │ │
│ │ ┌──────────────┐ │ │
│ │ │ MixColumns │ ← Sauf au dernier round │ │
│ │ └──────────────┘ │ │
│ │ ↓ │ │
│ │ ┌──────────────┐ │ │
│ │ │ AddRoundKey │ ← RoundKey i │ │
│ │ └──────────────┘ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌─────────────────┐ │
│ │ SubBytes │ │
│ └─────────────────┘ │
│ ↓ │
│ ┌─────────────────┐ │
│ │ ShiftRows │ │
│ └─────────────────┘ │
│ ↓ │
│ ┌─────────────────┐ │
│ │ AddRoundKey (N) │ ← Dernière RoundKey │
│ └─────────────────┘ │
│ ↓ │
│ Cipher Text (128 bits) │
│ │
└─────────────────────────────────────────────────────────────────┘
Remarques importantes :
- Le dernier round ne comporte pas de transformation
MixColumns. - La première opération est un
AddRoundKey avec la clé initiale. - Chaque transformation est réversible, ce qui permet le déchiffrement.
5. Les quatre transformations
| Transformation |
Description |
| SubBytes |
Substitution non linéaire : chaque octet du bloc est remplacé par un autre suivant une table fixe (S-box). |
| ShiftRows |
Décalage circulaire des lignes du tableau. |
|MixColumns | Mélange des colonnes par multiplication polynomiale dans $\mathbb{F}_{2^8}$. |
| AddRoundKey| Addition (XOR) du bloc avec la RoundKey. |
Chacune de ces transformations sera détaillée dans une section dédiée.
6. Rappel : hexadécimal et binaire
Dans les sections suivantes, les octets seront représentés en hexadécimal pour plus de lisibilité.
Attention : cette représentation ne correspond pas à l’entier en base 10 usuel !
Conversion binaire → hexadécimal
Un octet (8 bits) peut être séparé en deux nibbles de 4 bits (Un nibble en informatique (parfois orthographié nybble) désigne un regroupement de 4 bits, soit très exactement un demi-octet. En français, on le traduit couramment par le terme quartet ou demi-octet) :
- Les 4 bits de poids fort (bits $b_7$ à $b_4$)
- Les 4 bits de poids faible (bits $b_3$ à $b_0$)
b₇ b₆ b₅ b₄ b₃ b₂ b₁ b₀
┌────────────────┬────────────────┐
│ h₁ │ h₂ │
└────────────────┴────────────────┘
Chaque nibble est converti en chiffre hexadécimal :
$h_1 = b_7 \cdot 2^3 + b_6 \cdot 2^2 + b_5 \cdot 2^1 + b_4 \cdot 2^0$
$h_2 = b_3 \cdot 2^3 + b_2 \cdot 2^2 + b_1 \cdot 2^1 + b_0 \cdot 2^0$
Les chiffres hexadécimaux sont :
$\{0,1,2,3,4,5,6,7,8,9,A,B,C,D,E,F\}$
où $A=10$, $B = 11$, $C = 12$, $D = 13$, $E = 14$, $F = 15$.
Exemple
Prenons l’octet binaire : 10011011
- Séparation :
1001 et 1011 - Conversion du premier nibble : $h_1 = 1\cdot8 + 0\cdot4 + 0\cdot2 + 1\cdot1 = 8 + 0 + 0 + 1 = 9$
- Conversion du second nibble : $h_2 = 1\cdot8 + 0\cdot4 + 1\cdot2 + 1\cdot1 = 8 + 0 + 2 + 1 = 11$
- $11$ en hexadécimal s’écrit
B - L’octet complet s’écrit :
0x9B
Note : le préfixe 0x est utilisé dans de nombreux langages (Python, C, JavaScript) pour indiquer une valeur hexadécimale.
Représentation polynomiale
Dans le cadre mathématique de l’AES, cet octet 0x9B correspond au polynôme :
$x^7 + x^4 + x^3 + x + 1$
(les bits à 1 sont les poids $b_7$, $b_4$, $b_3$, $b_1$, $b_0$,
7. Gestion des données volumineuses
L’AES traite des blocs de 128 bits. Pour chiffrer un message plus long, on utilise un mode d’opération (comme ECB, CBC, CTR, etc.). Le choix du mode détermine comment les blocs sont chaînés entre eux.
8. À retenir
- AES = chiffrement symétrique par blocs (128 bits)
- Bloc organisé en matrice
- 10, 12 ou 14 rounds selon la taille de clé
- 4 transformations :
SubBytes, ShiftRows, MixColumns, AddRoundKey - Représentation hexadécimale = écriture compacte de l’octet
- L’octet correspond à un polynôme dans $\mathbb{F}_{2^8}$
- Script Python à compiler pour visualiser la structure du bloc :
# Affichage de la structure d'un bloc AES
def afficher_bloc(bloc):
"""Affiche un bloc de 16 octets sous forme de matrice 4x4 (ordre colonne par colonne)"""
for ligne in range(4):
ligne_str = ""
for col in range(4):
idx = ligne + 4 * col
ligne_str += f"{bloc[idx]:02X} "
print(ligne_str)
# Exemple : remplir avec des valeurs de 0 à 15
exemple = list(range(16))
afficher_bloc(exemple)
- Script Python à compiler pour la conversion binaire ↔ hexadécimal :
def binaire_vers_hex(binaire):
"""Convertit une chaîne de 8 bits en hexadécimal"""
if len(binaire) != 8:
raise ValueError("Doit contenir exactement 8 bits")
h1 = int(binaire[:4], 2)
h2 = int(binaire[4:], 2)
return f"0x{h1:01X}{h2:01X}"
# Test
print(binaire_vers_hex("10011011")) # Affiche 0x9B
Question de réflexion :
- Pourquoi le dernier round ne comporte-t-il pas l’étape
MixColumns ? (Indice : pensez à la symétrie entre chiffrement et déchiffrement)
Le dernier round ne comporte pas MixColumns pour que la structure du déchiffrement soit parfaitement symétrique et utilise exactement les mêmes transformations inverses que le chiffrement, sans avoir à gérer un cas particulier.
La raison est liée à la symétrie entre chiffrement et déchiffrement.
- Structure du chiffrement
- Pour les rounds 1 à 9 (sur AES-128) :
SubBytes → ShiftRows → MixColumns → AddRoundKey - Pour le round 10 (dernier) :
SubBytes → ShiftRows → AddRoundKey
- Structure du déchiffrement
Pour que le déchiffrement fonctionne, on applique les transformations inverses dans l’ordre inverse :
- Pour les rounds 1 à 9 (déchiffrement) :
InvShiftRows → InvSubBytes → AddRoundKey → InvMixColumns - Pour le dernier round du déchiffrement (qui correspond au premier round du chiffrement) :
InvShiftRows → InvSubBytes → AddRoundKey
On constate que le InvMixColumns n’est pas présent au dernier round du déchiffrement.
- Pourquoi cette asymétrie serait un problème ?
- Si le dernier round du chiffrement contenait
MixColumns, alors le déchiffrement devrait commencer par InvMixColumns. Cela fonctionnerait mathématiquement, mais créerait une incohérence structurelle : - Le premier round du déchiffrement devrait traiter
InvMixColumns sans avoir d’AddRoundKey avant (car l’ordre serait inversé) - Cela compliquerait l’implémentation et romprait l’alternance régulière
AddRoundKey / MixColumns
- La règle d’or
Pour qu’un chiffrement par blocs soit facilement réversible, on veut que la structure du déchiffrement soit identique à celle du chiffrement, mais avec :
- Les transformations inverses
- Les clés utilisées dans l’ordre inverse
En supprimant MixColumns au dernier round, on garantit que AddRoundKey est la dernière opération du chiffrement et la première opération du déchiffrement. Ainsi, aucune transformation de diffusion (qui mélange les octets) ne se trouve « à découvert » sans clé.
En résumé
MixColumns est absent du dernier round pour que le déchiffrement puisse s’enchaîner de manière symétrique : on termine le chiffrement par AddRoundKey (la seule opération qui dépend de la clé) et on commence le déchiffrement par AddRoundKey (avec la même clé). Ainsi, la structure reste élégante, régulière et facile à implémenter.
Description détaillée des méthodes :
1. Introduction
SubBytes est la première transformation appliquée à chaque round de l’AES (après l’AddRoundKey initial).
C’est une substitution non linéaire qui agit indépendamment sur chaque octet du bloc.
Propriétés fondamentales
| Propriété |
Définition |
Pourquoi c’est important |
| Bijectivité |
$\forall y \in \mathbb{F}_{2^8},\ \exists ! x \in \mathbb{F}_{2^8}\ \text{tel que}\ f(x)=y$ |
Garantit que le déchiffrement est possible : chaque octet chiffré correspond à un unique octet clair. |
| Non-linéarité |
$\text{SubBytes}(A) + \text{SubBytes}(B) \neq \text{SubBytes}(A + B)$ |
Protège contre les attaques cryptanalytiques (notamment la cryptanalyse différentielle et linéaire). |
Remarque : Dans cette section, l’ensemble d’arrivée et de départ est le même : le corps de Galois $\mathbb{F}_{2^8}$ (ou $GF(2^8)$).
2. Fonctionnement général
SubBytes s’effectue en deux étapes distinctes :
- Calcul de l’inverse multiplicatif dans $\mathbb{F}_{2^8}$
- Application d’une transformation affine (sur $\mathbb{F}_2$)
Prenons comme exemple tout au long de cette section :
$A = \texttt{10011011}_2 = \texttt{0x9B}$
3. Étape 1 : Calcul de l’inverse multiplicatif
On remplace l’octet $A = (a_7 a_6 a_5 a_4 a_3 a_2 a_1 a_0)$ par son inverse multiplicatif dans $\mathbb{F}_{2^8}$.
Précision importante : Nous sommes dans le groupe multiplicatif de $\mathbb{F}_{2^8}$, dont l’élément neutre est $\texttt{0x01}$.
L’élément $\texttt{0x00}$ n’a pas d’inverse multiplicatif ; par convention, on pose $\texttt{0x00}^{-1} = \texttt{0x00}$.
L’inverse multiplicatif peut être calculé par l’algorithme d’Euclide étendu dans les corps finis, mais en pratique, on utilise une table pré-calculée (la S-box) car elle est fixe.
Soit $B’ = A^{-1}$.
Exemple :
$A = \texttt{0x9B} \quad \Rightarrow \quad A^{-1} = \texttt{0x88}$
Soit en binaire :
$A^{-1} = \texttt{10001000}_2$
4. Étape 2 : Transformation affine
On applique ensuite une transformation affine sur $B’$ pour obtenir $B = \text{SubBytes}(A)$.
Cette transformation est définie par :
$\boxed{
\begin{pmatrix}
b_0 \\
b_1 \\
b_2 \\
b_3 \\
b_4 \\
b_5 \\
b_6 \\
b_7
\end{pmatrix}
\equiv
\begin{pmatrix}
1 & 0 & 0 & 0 & 1 & 1 & 1 & 1 \\
1 & 1 & 0 & 0 & 0 & 1 & 1 & 1 \\
1 & 1 & 1 & 0 & 0 & 0 & 1 & 1 \\
1 & 1 & 1 & 1 & 0 & 0 & 0 & 1 \\
1 & 1 & 1 & 1 & 1 & 0 & 0 & 0 \\
0 & 1 & 1 & 1 & 1 & 1 & 0 & 0 \\
0 & 0 & 1 & 1 & 1 & 1 & 1 & 0 \\
0 & 0 & 0 & 1 & 1 & 1 & 1 & 1
\end{pmatrix}
\cdot
\begin{pmatrix}
b’_0 \\
b’_1 \\
b’_2 \\
b’_3 \\
b’_4 \\
b’_5 \\
b’_6 \\
b’_7
\end{pmatrix}
+
\begin{pmatrix}
1 \\
1 \\
0 \\
0 \\
0 \\
1 \\
1 \\
0
\end{pmatrix}
\pmod{2}
}$
Interprétation : Il s’agit d’une multiplication matricielle (modulo 2) suivie d’une addition d’un vecteur constant (appelé *constante affine*). Cette opération est réalisée dans $\mathbb{F}_2$ (addition = XOR).
Exemple avec $B’ = \texttt{10001000}$ :
Calculons étape par étape. $B’$ en binaire : $b’_7=1$, $b’_6=0$, $b’_5=0$, $b’_4=0$, $b’_3=1$, $b’_2=0$, $b’_1=0$, $b’_0=0$.
Pour $b_0$ (première ligne) :
$b_0 = 1\cdot b’_0 + 0\cdot b’_1 + 0\cdot b’_2 + 0\cdot b’_3 + 1\cdot b’_4 + 1\cdot b’_5 + 1\cdot b’_6 + 1\cdot b’_7 + 1 \pmod{2}$
$b_0 = b’_0 + b’_4 + b’_5 + b’_6 + b’_7 + 1$
$b_0 = 0 + 1 + 0 + 0 + 1 + 1 = 1 \pmod{2}$
Après calcul complet (à faire en exercice), on obtient :
$B = \texttt{00010100}_2 = \texttt{0x14}$
5. Simplification : la S-box
Les deux étapes (inverse multiplicatif + transformation affine) sont combinées en une seule table de correspondance : la S-box (Substitution box).
La S-box est une table de $16 \times 16$ (256 entrées) qui donne directement $\text{SubBytes}(x)$ pour tout octet $x$. Elle est pré-calculée et identique pour toutes les implémentations.
|
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
A |
B |
C |
D |
E |
F |
| 0 |
63 |
7C |
77 |
7B |
F2 |
6B |
6F |
C5 |
30 |
01 |
67 |
2B |
FE |
D7 |
AB |
76 |
| 1 |
CA |
82 |
C9 |
7D |
FA |
59 |
47 |
F0 |
AD |
D4 |
A2 |
AF |
9C |
A4 |
72 |
C0 |
| 2 |
B7 |
FD |
93 |
26 |
36 |
3F |
F7 |
CC |
34 |
A5 |
E5 |
F1 |
71 |
D8 |
31 |
15 |
| 3 |
04 |
C7 |
23 |
C3 |
18 |
96 |
05 |
9A |
07 |
12 |
80 |
E2 |
EB |
27 |
B2 |
75 |
| 4 |
09 |
83 |
2C |
1A |
1B |
6E |
5A |
A0 |
52 |
3B |
D6 |
B3 |
29 |
E3 |
2F |
84 |
| 5 |
53 |
D1 |
00 |
ED |
20 |
FC |
B1 |
5B |
6A |
CB |
BE |
39 |
4A |
4C |
58 |
CF |
| 6 |
D0 |
EF |
AA |
FB |
43 |
4D |
33 |
85 |
45 |
F9 |
02 |
7F |
50 |
3C |
9F |
A8 |
| 7 |
51 |
A3 |
40 |
8F |
92 |
9D |
38 |
F5 |
BC |
B6 |
DA |
21 |
10 |
FF |
F3 |
D2 |
| 8 |
CD |
0C |
13 |
EC |
5F |
97 |
44 |
17 |
C4 |
A7 |
7E |
3D |
64 |
5D |
19 |
73 |
| 9 |
60 |
81 |
4F |
DC |
22 |
2A |
90 |
88 |
46 |
EE |
B8 |
14 |
DE |
5E |
0B |
DB |
| A |
E0 |
32 |
3A |
0A |
49 |
06 |
24 |
5C |
C2 |
D3 |
AC |
62 |
91 |
95 |
E4 |
79 |
| B |
E7 |
C8 |
37 |
6D |
8D |
D5 |
4E |
A9 |
6C |
56 |
F4 |
EA |
65 |
7A |
AE |
08 |
| C |
BA |
78 |
25 |
2E |
1C |
A6 |
B4 |
C6 |
E8 |
DD |
74 |
1F |
4B |
BD |
8B |
8A |
| D |
70 |
3E |
B5 |
66 |
48 |
03 |
F6 |
0E |
61 |
35 |
57 |
B9 |
86 |
C1 |
1D |
9E |
| E |
E1 |
F8 |
98 |
11 |
69 |
D9 |
8E |
94 |
9B |
1E |
87 |
E9 |
CE |
55 |
28 |
DF |
| F |
8C |
A1 |
89 |
0D |
BF |
E6 |
42 |
68 |
41 |
99 |
2D |
0F |
B0 |
54 |
BB |
16 |
**Comment lire la table** :
– Le premier chiffre hexadécimal (ligne) donne les 4 bits de poids fort.
– Le second chiffre hexadécimal (colonne) donne les 4 bits de poids faible.
Exemple avec $A = \texttt{0x9B}$ :
– Ligne 9, colonne B → on lit 0x14
On retrouve bien le résultat précédent !
6. Propriétés de la S-box
Observons quelques propriétés intéressantes :
| Propriété |
Observation |
| Aucun point fixe |
$\text{SubBytes}(x) = x$ n’a pas de solution (sauf éventuellement quelques cas, à vérifier) |
| Aucune symétrie |
La table ne présente pas de motif apparent |
| Bijectivité |
Chaque valeur de 0x00 à 0xFF apparaît exactement une fois dans la table |
Vérification : Compter le nombre de fois que chaque valeur apparaît dans la S-box permet de confirmer la bijectivité.
7. Inverse de SubBytes (InvSubBytes)
Pour déchiffrer, on a besoin de la transformation inverse. Elle se construit en deux étapes :
- Inverse de la transformation affine $f^{-1}$
- Calcul de l’inverse multiplicatif du résultat
La transformation affine inverse est donnée par :
$\boxed{
\begin{pmatrix}
b’_0 \\
b’_1 \\
b’_2 \\
b’_3 \\
b’_4 \\
b’_5 \\
b’_6 \\
b’_7
\end{pmatrix}
\equiv
\begin{pmatrix}
0 & 1 & 0 & 1 & 0 & 0 & 1 & 0 \\
0 & 0 & 1 & 0 & 1 & 0 & 0 & 1 \\
1 & 0 & 0 & 1 & 0 & 1 & 0 & 0 \\
0 & 1 & 0 & 0 & 1 & 0 & 1 & 0 \\
0 & 0 & 1 & 0 & 0 & 1 & 0 & 1 \\
1 & 0 & 0 & 1 & 0 & 0 & 1 & 0 \\
0 & 1 & 0 & 0 & 1 & 0 & 0 & 1 \\
1 & 0 & 1 & 0 & 0 & 1 & 0 & 0
\end{pmatrix}
\cdot
\begin{pmatrix}
b_0 \\
b_1 \\
b_2 \\
b_3 \\
b_4 \\
b_5 \\
b_6 \\
b_7
\end{pmatrix}
+
\begin{pmatrix}
0 \\
0 \\
0 \\
0 \\
0 \\
1 \\
0 \\
1
\end{pmatrix}
\pmod{2}
}$
En pratique, on utilise aussi une table pré-calculée : la InvS-box (inverse S-box), qui donne directement $\text{InvSubBytes}(x)$.
8. Récapitulatif : SubBytes en pratique
| Étape |
Opération |
Exemple (0x9B) |
| 1. Entrée |
Octet A |
0x9B |
| 2. Inverse multiplicatif |
$A^{-1}$ dans $\mathbb{F}_{2^8}$ |
0x88 |
| 3. Transformation affine |
$f(A^{-1})$ |
0x14 |
| Simplification |
Lecture dans la S-box |
0x14 |
9. Pourquoi cette complexité ?
La combinaison de l’inverse multiplicatif (structure algébrique forte) et de la transformation affine (qui brise la structure algébrique) permet d’obtenir :
– Une forte non-linéarité (résistance aux attaques différentielles et linéaires)
– L’absence de points fixes ou de symétries
– Une implémentation efficace via table pré-calculée
1. Introduction
ShiftRows est la deuxième transformation de chaque round (après SubBytes). Avec MixColumns, elle constitue la couche de diffusion de l’AES.
Objectif : introduire de la confusion dans les données en modifiant l’ordre des octets au sein du bloc. La diffusion permet de répartir l’influence de chaque octet du texte clair sur un grand nombre d’octets du texte chiffré.
La transformation est simple : on applique un décalage circulaire (rotation) sur chaque ligne (row) de la matrice représentant le bloc.
2. Rappel : organisation du bloc
Rappelons que le bloc de 16 octets est organisé en matrice $4 \times 4$ colonne par colonne :
|
Col 0 |
Col 1 |
Col 2 |
Col 3 |
| Ligne 0 |
$A_1$ |
$A_5$ |
$A_9$ |
$A_{13}$ |
| Ligne 1 |
$A_2$ |
$A_6$ |
$A_{10}$ |
$A_{14}$ |
| Ligne 2 |
$A_3$ |
$A_7$ |
$A_{11}$ |
$A_{15}$ |
| Ligne 3 |
$A_4$ |
$A_8$ |
$A_{12}$ |
$A_{16}$ |
Important : Les indices $A_1$ à $A_{16}$ correspondent à l’ordre de lecture colonne par colonne. C’est essentiel pour comprendre les décalages.
3. Principe de ShiftRows
ShiftRows applique un décalage circulaire vers la gauche sur chaque ligne. Le nombre de décalages dépend du numéro de la ligne :
| Ligne |
Décalage |
| Ligne 0 |
0 octet (inchangée) |
| Ligne 1 |
1 octet |
| Ligne 2 |
2 octets |
| Ligne 3 |
3 octets |
Remarque : Un décalage circulaire signifie que les octets qui « sortent » à gauche réapparaissent à droite.
Visualisation
Avant ShiftRows (matrice avec indices) :
|
Col 0 |
Col 1 |
Col 2 |
Col 3 |
| L0 |
$A_1$ |
$A_5$ |
$A_9$ |
$A_{13}$ |
| L1 |
$A_2$ |
$A_6$ |
$A_{10}$ |
$A_{14}$ |
| L2 |
$A_3$ |
$A_7$ |
$A_{11}$ |
$A_{15}$ |
| L3 |
$A_4$ |
$A_8$ |
$A_{12}$ |
$A_{16}$ |
Après ShiftRows :
|
Col 0 |
Col 1 |
Col 2 |
Col 3 |
| L0 |
$A_1$ |
$A_5$ |
$A_9$ |
$A_{13}$ |
← décalage 0 |
| L1 |
$A_6$ |
|
$A_{14}$ |
$A_2$ |
← décalage 1 |
| L2 |
$A_{11}$ |
$A_{15}$ |
$A_3$ |
$A_7$ |
← décalage 2 |
| L3 |
$A_{16}$ |
$A_4$ |
$A_8$ |
$A_{12}$ |
← décalage 3 |
4. Représentation sous forme de tableau d’indices
Pour implémenter ShiftRows en programmation, on peut utiliser un tableau de correspondance (permutation) qui donne, pour chaque position d’origine, sa nouvelle position.
En considérant les 16 positions indexées de 0 à 15 (dans l’ordre colonne par colonne), la permutation est :
| Position origine |
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
| Position cible |
0 |
5 |
10 |
15 |
4 |
9 |
14 |
3 |
8 |
13 |
2 |
7 |
12 |
1 |
6 |
11 |
Ou plus simplement, en termes de correspondance des indices $A_1$ à $A_{16}$ (en partant de 1) :
Avant → Après :
$$
[A_1, A_2, A_3, A_4, A_5, A_6, A_7, A_8, A_9, A_{10}, A_{11}, A_{12}, A_{13}, A_{14}, A_{15}, A_{16}]
$$
$$
\Downarrow
$$
$$
[A_1, A_6, A_{11}, A_{16}, A_5, A_{10}, A_{15}, A_4, A_9, A_{14}, A_3, A_8, A_{13}, A_2, A_7, A_{12}]
$$
Exemple : $A_2$ (ligne 1, col 0) se déplace en position 1 (ligne 1, col 1) après décalage.
5. Exemple concret
Prenons un bloc d’exemple avec des valeurs simples pour visualiser le décalage :
Bloc initial (en hexadécimal, indices pour repérage) :
|
Col 0 |
Col 1 |
Col 2 |
Col 3 |
| L0 |
01 |
05 |
09 |
0D |
| L1 |
02 |
06 |
0A |
0E |
| L2 |
03 |
07 |
0B |
0F |
| L3 |
04 |
08 |
0C |
10 |
Après ShiftRows :
|
Col 0 |
Col 1 |
Col 2 |
Col 3 |
| L0 |
01 |
05 |
09 |
0D |
← inchangée |
| L1 |
06 |
0A |
0E |
02 |
← décalage 1 vers la gauche |
| L2 |
0B |
0F |
03 |
07 |
← décalage 2 vers la gauche |
| L3 |
10 |
04 |
08 |
0C |
← décalage 3 vers la gauche |
6. Réversibilité
ShiftRows est facilement réversible : il suffit d’appliquer un décalage circulaire vers la droite sur chaque ligne.
| Ligne |
Décalage inverse |
| Ligne 0 |
0 octet |
| Ligne 1 |
1 octet vers la droite |
| Ligne 2 |
2 octets vers la droite |
| Ligne 3 |
3 octets vers la droite |
Vérification : Si on reprend l’exemple précédent et qu’on applique le décalage inverse, on retrouve bien la matrice d’origine.
En termes de tableau de correspondance inverse :
| Position cible |
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
| Position origine |
0 |
13 |
10 |
7 |
4 |
1 |
14 |
11 |
8 |
5 |
2 |
15 |
12 |
9 |
6 |
3 |
7. Pourquoi ce décalage ?
Le choix des décalages (0, 1, 2, 3) n’est pas anodin :
– Ligne 0 inchangée : permet de préserver une partie de la structure pour le déchiffrement.
– Décalages croissants : assurent qu’après plusieurs rounds, chaque octet de la colonne a été distribué dans toutes les colonnes.
Cette diffusion, combinée avec MixColumns, garantit qu’après quelques rounds, chaque octet du texte chiffré dépend de tous les octets du texte clair.
8. Implémentation simple
def shift_rows(bloc):
"""
Applique ShiftRows sur un bloc de 16 octets (liste d'entiers)
Le bloc est organisé en 4 lignes de 4 octets (ordre colonne par colonne)
"""
# Transformer la liste plate en matrice 4x4 (colonne par colonne)
matrice = [[0]*4 for _ in range(4)]
for ligne in range(4):
for col in range(4):
matrice[ligne][col] = bloc[ligne + 4*col]
# Appliquer les décalages
decalages = [0, 1, 2, 3]
nouvelle_matrice = [[0]*4 for _ in range(4)]
for ligne in range(4):
for col in range(4):
# Décalage circulaire vers la gauche
nouvelle_col = (col - decalages[ligne]) % 4
nouvelle_matrice[ligne][col] = matrice[ligne][nouvelle_col]
# Reconstruire la liste plate
resultat = [0]*16
for ligne in range(4):
for col in range(4):
resultat[ligne + 4*col] = nouvelle_matrice[ligne][col]
return resultat
# Version simplifiée avec permutation directe
def shift_rows_permutation(bloc):
"""Version utilisant la permutation directe"""
permutation = [0, 5, 10, 15, 4, 9, 14, 3, 8, 13, 2, 7, 12, 1, 6, 11]
return [bloc[p] for p in permutation]
# Test
bloc_test = list(range(1, 17))
print("Avant :", bloc_test)
print("Après :", shift_rows_permutation(bloc_test))
9. Récapitulatif
| Aspect |
Description |
| Rôle |
Diffusion : réorganisation des octets dans le bloc |
| Opération |
Décalage circulaire vers la gauche de chaque ligne |
| Décalages |
L0 : 0, L1 : 1, L2 : 2, L3 : 3 |
| Inverse |
Décalage circulaire vers la droite |
| Complexité |
Très faible (simple permutation) |
Pour aller plus loin
- Visualisation interactive : Afficher la matrice avant/après côte à côte.
- Exercice : Appliquer ShiftRows à un bloc concret (par exemple en reprenant les valeurs hexadécimales d’un vrai bloc AES) et vérifier le résultat avec une implémentation de référence.
- Question de réflexion :
> Pourquoi est-il important que la ligne 0 ne soit pas décalée ? Que se passerait-il si toutes les lignes étaient décalées du même nombre ?
1. Introduction
MixColumns est la troisième transformation de chaque round (après ShiftRows). Avec ShiftRows, elle constitue la couche de diffusion de l’AES.
Objectif : faire en sorte que chaque octet de la colonne de sortie dépende de tous les octets de la colonne d’entrée. Cela permet de propager rapidement l’influence de chaque bit à travers tout le bloc.
Contrairement à ShiftRows qui est une simple permutation, MixColumns est une transformation algébrique qui mélange réellement les bits entre eux.
2. Organisation des données
Rappelons l’organisation du bloc en matrice $4 \times 4$ (ordre colonne par colonne) :
|
Col 0 |
Col 1 |
Col 2 |
Col 3 |
| Ligne 0 |
$A_1$ |
$A_5$ |
$A_9$ |
$A_{13}$ |
| Ligne 1 |
$A_2$ |
$A_6$ |
$A_{10}$ |
$A_{14}$ |
| Ligne 2 |
$A_3$ |
$A_7$ |
$A_{11}$ |
$A_{15}$ |
| Ligne 3 |
$A_4$ |
$A_8$ |
$A_{12}$ |
$A_{16}$ |
MixColumns agit indépendamment sur chaque colonne. Pour une colonne donnée, on note $A_i$ les quatre octets de la colonne d’entrée, et $C_i$ les quatre octets de la colonne de sortie.
3. Principe de la transformation
MixColumns applique une multiplication matricielle dans $\mathbb{F}_{2^8}$ (le corps de Galois) entre une matrice fixe $S$ et chaque colonne du bloc.
Pour une colonne d’indice $i$ (avec $i \in \{0, 1, 2, 3\}$) :
$$
\begin{pmatrix}
C_{i} \\
C_{i+4} \\
C_{i+8} \\
C_{i+12}
\end{pmatrix}
=
\begin{pmatrix}
\text{0x02} & \text{0x03} & \text{0x01} & \text{0x01} \\
\text{0x01} & \text{0x02} & \text{0x03} & \text{0x01} \\
\text{0x01} & \text{0x01} & \text{0x02} & \text{0x03} \\
\text{0x03} & \text{0x01} & \text{0x01} & \text{0x02}
\end{pmatrix}
\cdot
\begin{pmatrix}
A_{i} \\
A_{i+4} \\
A_{i+8} \\
A_{i+12}
\end{pmatrix}
$$
Remarque importante : Les coefficients de la matrice sont des éléments de $\mathbb{F}_{2^8}$. Ils sont représentés en hexadécimal :
- $\text{0x01} = 00000001_2$ (élément neutre)
- $\text{0x02} = 00000010_2$
- $\text{0x03} = 00000011_2$
4. Détail des opérations dans $\mathbb{F}_{2^8}$
Multiplication par 0x01
C’est l’élément neutre : $\text{0x01} \cdot X = X$
Multiplication par 0x02
Dans $\mathbb{F}_{2^8}$, multiplier par $\text{0x02}$ équivaut à :
- Décaler tous les bits de l’octet d’un cran vers la gauche (multiplication polynomiale par $x$)
- Si le bit de poids fort (bit 7) était à 1 avant le décalage, on réduit en effectuant un XOR avec $\text{0x1B}$ (le polynôme irréductible $x^8 + x^4 + x^3 + x + 1$)
En Python, cela donne :
def mul_by_02(octet):
if octet & 0x80: # bit de poids fort = 1
return ((octet << 1) ^ 0x1B) & 0xFF
else:
return (octet << 1) & 0xFF
Multiplication par 0x03
On utilise la propriété : $\text{0x03} = \text{0x02} \oplus \text{0x01}$ (car $3 = 2 + 1$ dans $\mathbb{F}_{2^8}$)
Ainsi :
$$\text{0x03} \cdot X = (\text{0x02} \cdot X) \oplus X$$
5. Développement des équations
Pour une colonne donnée, les quatre octets de sortie s’écrivent :
$$
\begin{cases}
C_i = (\text{0x02} \cdot A_i) \oplus (\text{0x03} \cdot A_{i+4}) \oplus A_{i+8} \oplus A_{i+12} \\
C_{i+4} = A_i \oplus (\text{0x02} \cdot A_{i+4}) \oplus (\text{0x03} \cdot A_{i+8}) \oplus A_{i+12} \\
C_{i+8} = A_i \oplus A_{i+4} \oplus (\text{0x02} \cdot A_{i+8}) \oplus (\text{0x03} \cdot A_{i+12}) \\
C_{i+12} = (\text{0x03} \cdot A_i) \oplus A_{i+4} \oplus A_{i+8} \oplus (\text{0x02} \cdot A_{i+12})
\end{cases}
$$
où $\oplus$ désigne l’opération XOR (addition dans $\mathbb{F}_{2^8}$).
6. Exemple concret
Prenons une colonne d’exemple (première colonne, $i=0$) :
| Octet |
Valeur (hex) |
Valeur (binaire) |
| $A_0$ (ligne 0) |
0x9B |
10011011 |
| $A_4$ (ligne 1) |
0x88 |
10001000 |
| $A_8$ (ligne 2) |
0x14 |
00010100 |
| $A_{12}$ (ligne 3) |
0x2A |
00101010 |
Calculs préliminaires :
- $\text{0x02} \cdot A_0$ : $0x9B = 0x9B$ (bit 7 = 1) → décalage + XOR 0x1B
- $0x9B << 1 = 0x136$ → $0x36$ après masque
- $0x36 \oplus 0x1B = 0x2D$
- Donc $\text{0x02} \cdot 0x9B = 0x2D$
- $\text{0x03} \cdot A_4 = (\text{0x02} \cdot 0x88) \oplus 0x88$
- $\text{0x02} \cdot 0x88$ : $0x88$ (bit 7=1) → décalage + XOR 0x1B
- $0x88 << 1 = 0x110 → 0x10$
- $0x10 \oplus 0x1B = 0x0B$
- $0x0B \oplus 0x88 = 0x83$
- $\text{0x02} \cdot A_8 : 0x14$ (bit 7=0) → simple décalage
- $0x14 << 1 = 0x28$
- $\text{0x02} \cdot A_{12} : 0x2A$ (bit 7=0) → simple décalage
- $0x2A << 1 = 0x54$
- $\text{0x03} \cdot A_8 = (\text{0x02} \cdot 0x14) \oplus 0x14 = 0x28 \oplus 0x14 = 0x3C$
- $\text{0x03} \cdot A_{12} = (\text{0x02} \cdot 0x2A) \oplus 0x2A = 0x54 \oplus 0x2A = 0x7E$
Calcul des octets de sortie :
- $C_0 = 0x2D \oplus 0x83 \oplus 0x14 \oplus 0x2A$
- $0x2D \oplus 0x83 = 0xAE$
- $0xAE \oplus 0x14 = 0xBA$
- $0xBA \oplus 0x2A = 0x90$
- $C_4 = 0x9B \oplus 0x0B \oplus 0x3C \oplus 0x2A$
- $0x9B \oplus 0x0B = 0x90$
- $0x90 \oplus 0x3C = 0xAC$
- $0xAC \oplus 0x2A = 0x86$
- $C_8 = 0x9B \oplus 0x88 \oplus 0x28 \oplus 0x7E$
- $0x9B \oplus 0x88 = 0x13$
- $0x13 \oplus 0x28 = 0x3B$
- $0x3B \oplus 0x7E = 0x45$
- $C_{12} = 0x7E \oplus 0x88 \oplus 0x14 \oplus 0x54$
- $0x7E \oplus 0x88 = 0xF6$
- $0xF6 \oplus 0x14 = 0xE2$
- $0xE2 \oplus 0x54 = 0xB6$
La colonne de sortie est donc : $[0x90, 0x86, 0x45, 0xB6]$
7. Réversibilité : InvMixColumns
MixColumns est une transformation linéaire et bijective. Pour déchiffrer, on applique la matrice inverse $S’$ :
$$
\begin{pmatrix}
A_{i} \\
A_{i+4} \\
A_{i+8} \\
A_{i+12}
\end{pmatrix}
=
\begin{pmatrix}
\text{0x0E} & \text{0x0B} & \text{0x0D} & \text{0x09} \\
\text{0x09} & \text{0x0E} & \text{0x0B} & \text{0x0D} \\
\text{0x0D} & \text{0x09} & \text{0x0E} & \text{0x0B} \\
\text{0x0B} & \text{0x0D} & \text{0x09} & \text{0x0E}
\end{pmatrix}
\cdot
\begin{pmatrix}
C_{i} \\
C_{i+4} \\
C_{i+8} \\
C_{i+12}
\end{pmatrix}
$$
Vérification : En reprenant l’exemple précédent, on devrait retrouver $[0x9B, 0x88, 0x14, 0x2A]$.
8. Implémentation Python
def mul_by_02(octet):
"""Multiplication par 0x02 dans GF(2^8)"""
if octet & 0x80:
return ((octet << 1) ^ 0x1B) & 0xFF
return (octet << 1) & 0xFF
def mul_by_03(octet):
"""Multiplication par 0x03 dans GF(2^8)"""
return mul_by_02(octet) ^ octet
def mix_columns(bloc):
"""
Applique MixColumns sur un bloc de 16 octets
Le bloc est organisé en 4 lignes (ordre colonne par colonne)
"""
resultat = [0] * 16
for col in range(4): # Pour chaque colonne
i = col # indice de base pour la colonne
a0 = bloc[i + 0] # ligne 0
a1 = bloc[i + 4] # ligne 1
a2 = bloc[i + 8] # ligne 2
a3 = bloc[i + 12] # ligne 3
# Calcul des 4 octets de sortie
resultat[i + 0] = mul_by_02(a0) ^ mul_by_03(a1) ^ a2 ^ a3
resultat[i + 4] = a0 ^ mul_by_02(a1) ^ mul_by_03(a2) ^ a3
resultat[i + 8] = a0 ^ a1 ^ mul_by_02(a2) ^ mul_by_03(a3)
resultat[i + 12] = mul_by_03(a0) ^ a1 ^ a2 ^ mul_by_02(a3)
return resultat
# Version avec la matrice inverse pour le déchiffrement
def inv_mix_columns(bloc):
"""InvMixColumns - déchiffrement"""
resultat = [0] * 16
for col in range(4):
i = col
c0 = bloc[i + 0]
c1 = bloc[i + 4]
c2 = bloc[i + 8]
c3 = bloc[i + 12]
# Multiplication par les coefficients de la matrice inverse
# 0x0E, 0x0B, 0x0D, 0x09
resultat[i + 0] = mul_by_0e(c0) ^ mul_by_0b(c1) ^ mul_by_0d(c2) ^ mul_by_09(c3)
resultat[i + 4] = mul_by_09(c0) ^ mul_by_0e(c1) ^ mul_by_0b(c2) ^ mul_by_0d(c3)
resultat[i + 8] = mul_by_0d(c0) ^ mul_by_09(c1) ^ mul_by_0e(c2) ^ mul_by_0b(c3)
resultat[i + 12] = mul_by_0b(c0) ^ mul_by_0d(c1) ^ mul_by_09(c2) ^ mul_by_0e(c3)
return resultat
Note : Les fonctions mul_by_09, mul_by_0b, mul_by_0d, mul_by_0e peuvent être implémentées par combinaisons de multiplications par 0x02 (par exemple, $0x09 = 0x02 \cdot 0x02 \cdot 0x02 \oplus 0x01$).
9. Récapitulatif
| Aspect |
Description |
| Rôle |
Diffusion : mélange des octets au sein de chaque colonne |
| Opération |
Multiplication matricielle dans $\mathbb{F}_{2^8}$ |
| Matrice |
$4 \times 4$ avec coefficients 0x01, 0x02, 0x03 |
| Inverse |
Matrice avec coefficients 0x0E, 0x0B, 0x0D, 0x09 |
| Complexité |
Nécessite la multiplication dans $\mathbb{F}_{2^8}$ |
10. Pourquoi cette matrice ?
Le choix de cette matrice particulière (appelée matrice de MDS - Maximum Distance Separable) garantit une diffusion optimale : après une seule application de MixColumns, la moindre modification d’un octet d’entrée affecte tous les octets de sortie de la colonne.
Pour aller plus loin
- Cellule interactive : Visualiser l’effet de MixColumns sur une colonne.
- Exercice : Vérifier que la multiplication par 0x03 correspond bien à $(\text{0x02} \cdot X) \oplus X$.
- Vérification expérimentale : Appliquer MixColumns puis InvMixColumns et vérifier qu’on retrouve le bloc original.
- Question de réflexion : Pourquoi la matrice inverse contient-elle des coefficients plus grands (0x0E, 0x0B, 0x0D, 0x09) ? Quelle est leur signification dans $\mathbb{F}_{2^8}$ ?
1. Introduction
AddRoundKey est la quatrième et dernière transformation de chaque round. C’est la seule opération de l’AES qui fait intervenir directement la clé de chiffrement.
Fondamental : C’est véritablement la seule transformation qui « chiffre » les données. Les autres (SubBytes, ShiftRows, MixColumns) ne font que mélanger et substituer les octets, mais sans la clé, elles sont parfaitement réversibles. La sécurité de l’AES repose donc sur la clé et sur sa gestion.
2. Principe
AddRoundKey réalise une addition (XOR) entre :
- Le bloc de données courant (état)
- La RoundKey courante (clé dérivée pour ce round)
Pour chaque octet de l’état, on effectue :
$$\text{État}_{\text{sortie}} = \text{État}_{\text{entrée}} \oplus \text{RoundKey}$$
Rappel : Dans $\mathbb{F}_{2^8}$, l’addition est l’opération XOR. L’élément neutre est $\text{0x00}$.
3. Détail de l’opération
Prenons un exemple sur un seul octet (un mot du bloc) :
| RoundKey |
1 |
1 |
1 |
0 |
1 |
1 |
0 |
0 |
| Mot |
1 |
0 |
0 |
1 |
1 |
0 |
1 |
1 |
XOR |
0 |
1 |
1 |
1 |
0 |
1 |
1 |
1 |
En binaire :
RoundKey : 11101100
Mot : 10011011
XOR : 01110111
Soit en hexadécimal :
– 0xEC ⊕ 0x9B = 0x77
4. Visualisation sur le bloc complet
Sur l’ensemble du bloc de 16 octets (matrice $4 \times 4$), AddRoundKey s’applique octet par octet :
État_entrée RoundKey État_sortie
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ a00 a01 a02 a03 │ │ k00 k01 k02 k03 │ │ b00 b01 b02 b03 │
│ a10 a11 a12 a13 │ ⊕ │ k10 k11 k12 k13 │ = │ b10 b11 b12 b13 │
│ a20 a21 a22 a23 │ │ k20 k21 k22 k23 │ │ b20 b21 b22 b23 │
│ a30 a31 a32 a33 │ │ k30 k31 k32 k33 │ │ b30 b31 b32 b33 │
└──────────────┘ └──────────────┘ └──────────────┘
avec :
$$b_{ij} = a_{ij} \oplus k_{ij}$$
5. Réversibilité
AddRoundKey est parfaitement réversible grâce aux propriétés de l’opération XOR.
Propriétés utilisées
- Associativité : $(a \oplus b) \oplus c = a \oplus (b \oplus c)$
- Élément neutre : $a \oplus 0 = a$
- Auto-inverse : $a \oplus a = 0$
Démonstration
Soit $M_i$ le mot courant, $K_i$ la RoundKey, et $C_i$ le résultat du chiffrement :
$$C_i = M_i \oplus K_i$$
Pour déchiffrer, on applique à nouveau la même RoundKey :
$$C_i \oplus K_i = (M_i \oplus K_i) \oplus K_i$$
Par associativité :
$$= M_i \oplus (K_i \oplus K_i)$$
Or $K_i \oplus K_i = 0$ (élément neutre), donc :
$$= M_i \oplus 0 = M_i$$
On retrouve bien le message original.
6. Schéma de la compensation
Chiffrement : Déchiffrement :
M_i C_i
│ │
⊕ K_i ⊕ K_i
↓ ↓
C_i M_i
Exemple numérique avec l’octet précédent :
| Opération |
Valeur |
| Mot initial $M_i$ |
0x9B (10011011) |
| RoundKey $K_i$ |
0xEC (11101100) |
| $C_i = M_i \oplus K_i$ |
0x77 (01110111) |
| $C_i \oplus K_i$ |
0x77 ⊕ 0xEC = 0x9B |
7. Rôle dans l’algorithme
AddRoundKey est appliquée :
- Au début :
AddRoundKey initial (avec la clé principale)
– **À la fin de chaque round** (sauf le dernier ?) : En réalité, dans la structure standard, AddRoundKey est appliquée à la fin de **chaque round**, y compris le dernier.
Rappel de l’ordre pour un round complet (sauf le premier et le dernier qui sont particuliers) :
1. SubBytes
2. ShiftRows
3. MixColumns (sauf au dernier round)
4. AddRoundKey
Important : Le dernier round ne contient pas MixColumns, mais contient bien AddRoundKey.
8. Gestion des RoundKeys
Les RoundKeys sont dérivées de la clé principale via un processus appelé Key Expansion (ou routine d’expansion).
Pour AES-128 (clé de 128 bits) :
- Clé initiale : 128 bits = 4 mots de 32 bits
- 10 rounds → besoin de 11 RoundKeys (une initiale + une par round)
- Soit $11 \times 4 = 44$ mots de 32 bits
La Key Expansion sera détaillée dans la section suivante.
9. Implémentation Python
def add_round_key(etat, round_key):
"""
Applique AddRoundKey sur l'état (liste de 16 octets)
round_key est une liste de 16 octets
"""
return [etat[i] ^ round_key[i] for i in range(16)]
# Exemple avec un seul octet
mot = 0x9B
round_key = 0xEC
resultat = mot ^ round_key
print(f"0x{mot:02X} ⊕ 0x{round_key:02X} = 0x{resultat:02X}")
# Exemple sur un bloc complet (simplifié)
etat = [0x9B, 0x88, 0x14, 0x2A] * 4 # bloc exemple
rk = [0xEC] * 16 # round_key factice
nouvel_etat = add_round_key(etat, rk)
10. Récapitulatif
| Aspect |
Description |
| Rôle |
Introduire la clé dans le processus de chiffrement |
| Opération |
XOR bit à bit entre l’état et la RoundKey |
| Réversibilité |
Parfaite car $X \oplus K \oplus K = X$ |
| Dépendance |
Seule opération (avec Key Expansion) dépendant de la clé |
| Fréquence |
Appliquée au début et à la fin de chaque round |
11. Pourquoi AddRoundKey est cruciale
Sans AddRoundKey, l’AES ne serait qu’une permutation fixe des données (SubBytes, ShiftRows, MixColumns), parfaitement connue et donc totalement vulnérable. C’est l’ajout de la clé qui rend l’algorithme sûr.
La sécurité repose sur :
- La longueur de la clé (128, 192 ou 256 bits)
- La non-linéarité de SubBytes
- La diffusion de ShiftRows et MixColumns
- La complexité de la Key Expansion
12. Question de réflexion
Pourquoi l’opération XOR est-elle particulièrement adaptée pour ce type de chiffrement ? Quelles propriétés mathématiques la rendent indispensable ?
Éléments de réponse :
- Elle est rapide (une seule instruction processeur)
- Elle est parfaitement réversible
- Elle préserve l’entropie (pas de perte d’information)
- Elle est associative et commutative, ce qui simplifie l’implémentation
Pour aller plus loin
- Cellule interactive : Visualiser l’effet de AddRoundKey sur un bloc.
- Exercice : Vérifier que
add_round_key(etat, rk) appliqué deux fois redonne l’état initial. - Mini-projet : Implémenter une version simplifiée de l’AES en combinant toutes les transformations vues jusqu’ici.
1. Introduction
Le Key Schedule (ou routine d’expansion de clé) est le processus qui génère l’ensemble des RoundKeys à partir de la clé principale.
Pourquoi ? Utiliser directement la même clé à chaque round serait dangereux : cela créerait des corrélations entre les rounds et faciliterait certaines attaques cryptanalytiques. L’expansion permet de :
- Disposer d’une clé différente par round
- Introduire de la non-linéarité dans la génération des clés
- Rendre l’attaque par related-key plus difficile
2. Généralités
| Taille de clé |
Nombre de rounds |
Nombre de RoundKeys |
Taille totale générée |
| 128 bits |
10 |
11 (initiale + 10) |
1408 bits (11 × 128) |
| 192 bits |
12 |
13 |
1664 bits (13 × 128) |
| 256 bits |
14 |
15 |
1920 bits (15 × 128) |
Remarque : La clé initiale est utilisée comme première RoundKey (avant le premier round). On génère ensuite $N_r$ RoundKeys supplémentaires, où $N_r$ est le nombre de rounds.
3. Organisation des clés
Une clé de 128 bits est constituée de 16 octets. Pour faciliter le traitement, on les regroupe par mots de 4 octets (32 bits).
Soit une clé $K$ de 128 bits :
$$
K \iff b_0 b_1 \ldots b_{126} b_{127} \quad (b_i \in \{0,1\})
$$
On organise en octets $V_i$ (8 bits) :
$$
K \iff V_0 V_1 \ldots V_{14} V_{15}
$$
Puis en mots $W_i$ (32 bits = 4 octets) :
$$
K \iff W_0 W_1 W_2 W_3
$$
avec :
- $W_0 = (V_0, V_1, V_2, V_3)$
- $W_1 = (V_4, V_5, V_6, V_7)$
- $W_2 = (V_8, V_9, V_{10}, V_{11})$
- $W_3 = (V_{12}, V_{13}, V_{14}, V_{15})$
4. Principe général de l’expansion
Pour AES-128, on doit générer $11 \times 4 = 44$ mots $W_0$ à $W_{43}$.
La relation de récurrence est :
$$
W_i =
\begin{cases}
W_{i-4} \oplus g(W_{i-1}) & \text{si } i \equiv 0 \pmod{4} \\
W_{i-4} \oplus W_{i-1} & \text{sinon}
\end{cases}
$$
Interprétation : Tous les 4 mots, on applique la fonction non linéaire $g()$ au mot précédent avant de l’ajouter.
5. La fonction $g()$
La fonction $g()$ s’applique sur un mot de 4 octets (32 bits) et se décompose en trois étapes :
Étape 1 : Rotation (RotWord)
Décalage circulaire vers la gauche d’un octet :
$$g_1(W) = \text{RotWord}(W) = (W_1, W_2, W_3, W_0)$$
Exemple avec $W = (V_{12}, V_{13}, V_{14}, V_{15})$ :
$$g_1(W) = (V_{13}, V_{14}, V_{15}, V_{12})$$
Étape 2 : Substitution (SubWord)
Application de la S-box (SubBytes) à chaque octet du mot :
$$g_2(W) = (\text{SubBytes}(V_{13}), \text{SubBytes}(V_{14}), \text{SubBytes}(V_{15}), \text{SubBytes}(V_{12}))$$
Étape 3 : Ajout de la constante de round (Rcon)
On ajoute (XOR) une constante de round uniquement au premier octet du mot :
$$g_3(W) = (V_{13}’ \oplus \text{Rcon}[j], V_{14}’, V_{15}’, V_{12}’)$$
où $\text{Rcon}[j]$ (Round Constant) est un octet dépendant du round $j$.
6. Les constantes de round (Rcon)
Les constantes $\text{Rcon}[j]$ sont définies dans $\mathbb{F}_{2^8}$ comme des puissances de $x$ (polynôme $x^1$) :
$$\text{Rcon}[j] = x^{j-1} \pmod{P}$$
où $P = x^8 + x^4 + x^3 + x + 1$ est le polynôme irréductible de l’AES.
| Round $j$ |
$Rcon[j]$ (hex) |
$Rcon[j]$ (binaire) |
| 1 |
0x01 |
00000001 |
| 2 |
0x02 |
00000010 |
| 3 |
0x04 |
00000100 |
| 4 |
0x08 |
00001000 |
| 5 |
0x10 |
00010000 |
| 6 |
0x20 |
00100000 |
| 7 |
0x40 |
01000000 |
| 8 |
0x80 |
10000000 |
| 9 |
0x1B |
00011011 |
| 10 |
0x36 |
00110110 |
Remarque : À partir de $j=9$, on dépasse $x^8$ et on réduit par le polynôme irréductible.
7. Schéma complet pour AES-128
Clé initiale (128 bits) = W₀ W₁ W₂ W₃
│
↓
┌──────────────────────────────────────────────────────────┐
│ Pour j = 1 à 10 (rounds) │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ temp = g(W₄ⱼ₋₁) avec g = RotWord → SubWord → XOR Rcon │
│ └─────────────────────────────────────────────────┘ │
│ │ │
│ W₄ⱼ = W₄ⱼ₋₄ ⊕ temp │
│ W₄ⱼ₊₁ = W₄ⱼ₋₃ ⊕ W₄ⱼ │
│ W₄ⱼ₊₂ = W₄ⱼ₋₂ ⊕ W₄ⱼ₊₁ │
│ W₄ⱼ₊₃ = W₄ⱼ₋₁ ⊕ W₄ⱼ₊₂ │
│ │
└──────────────────────────────────────────────────────────┘
Visualisation :
Round 0 (clé initiale) : [W₀] [W₁] [W₂] [W₃]
│ │ │ │
Round 1 : [W₄] ← g(W₃) │ │ │ │
[W₅] ← W₄ ⊕ W₁ ↓ ↓ ↓ ↓
[W₆] ← W₅ ⊕ W₂
[W₇] ← W₆ ⊕ W₃
Round 2 : [W₈] ← g(W₇) ⊕ W₄
[W₉] ← W₈ ⊕ W₅
[W₁₀] ← W₉ ⊕ W₆
[W₁₁] ← W₁₀ ⊕ W₇
... et ainsi de suite jusqu'à W₄₃
8. Exemple concret (simplifié)
Prenons un exemple simplifié avec des valeurs fictives pour illustrer.
Soit la clé initiale (128 bits) découpée en 4 mots :
| Mot |
Octets |
Valeur (hex) |
| W₀ |
V₀-V₃ |
0x2B 0x7E 0x15 0x16 |
| W₁ |
V₄-V₇ |
0x28 0xAE 0xD2 0xA6 |
| W₂ |
V₈-V₁₁ |
0xAB 0xF7 0x15 0x88 |
| W₃ |
V₁₂-V₁₅ |
0x09 0xCF 0x4F 0x3C |
Pour le premier round $(j = 1)$ :
- $temp = g(W₃)$ :
- RotWord : (0xCF, 0x4F, 0x3C, 0x09)
- SubWord : appliquer S-box à chaque octet
- $Rcon[1] = 0x01$
- $temp = (S(0xCF) ⊕ 0x01, S(0x4F), S(0x3C), S(0x09))$
- $W₄ = W₀ ⊕ temp$
- $W₅ = W₄ ⊕ W₁$
- $W₆ = W₅ ⊕ W₂$
- $W₇ = W₆ ⊕ W₃$
9. Cas particulier : clé de 192 bits
Pour AES-192, on part de 6 mots initiaux (192 bits = 6 × 32 bits). La récurrence devient :
$$
W_i =
\begin{cases}
W_{i-6} \oplus g(W_{i-1}) & \text{si } i \equiv 0 \pmod{6} \\
W_{i-6} \oplus W_{i-1} & \text{sinon}
\end{cases}
$$
On génère $13 \times 4 = 52$ mots (W₀ à W₅₁).
Particularité : La dernière itération ne nécessite que 4 mots supplémentaires (au lieu de 6).
10. Cas particulier : clé de 256 bits
Pour AES-256, on part de 8 mots initiaux (256 bits = 8 × 32 bits). La récurrence est :
$$
W_i =
\begin{cases}
W_{i-8} \oplus g(W_{i-1}) & \text{si } i \equiv 0 \pmod{8} \\
W_{i-8} \oplus W_{i-1} & \text{si } i \equiv 4 \pmod{8} \\
W_{i-8} \oplus W_{i-1} & \text{sinon}
\end{cases}
$$
Particularité : Tous les 4 mots (lorsque $i \equiv 4 \pmod{8}$), on applique également la S-box avant l’addition (sans rotation ni Rcon).
On génère $15 \times 4 = 60$ mots (W₀ à W₅₉).
11. Récapitulatif par taille de clé
| Taille |
Mots initiaux |
Mots totaux |
Application de $g()$ |
Particularité |
| 128 bits |
4 |
44 |
Tous les 4 mots |
- |
| 192 bits |
6 |
52 |
Tous les 6 mots |
Dernière itération incomplète |
| 256 bits |
8 |
60 |
Tous les 8 mots + S-box tous les 4 mots |
Deux types d’expansion |
12. Implémentation Python (AES-128)
def sub_word(mot):
"""Applique SubBytes sur un mot de 4 octets"""
return [sbox[octet] for octet in mot]
def rot_word(mot):
"""Rotation circulaire vers la gauche d'un octet"""
return mot[1:] + [mot[0]]
def rcon(j):
"""Retourne la constante de round pour le round j (1-indexé)"""
rcon_table = [
0x01, 0x02, 0x04, 0x08, 0x10,
0x20, 0x40, 0x80, 0x1B, 0x36
]
return rcon_table[j-1]
def key_expansion_128(cle_initiale):
"""
Expansion de clé pour AES-128
cle_initiale : liste de 16 octets
Retourne une liste de 44 mots (4 octets chacun)
"""
# Initialisation : 4 premiers mots
mots = []
for i in range(4):
mots.append(cle_initiale[4*i:4*(i+1)])
# Génération des 40 mots suivants
for i in range(4, 44):
temp = mots[i-1].copy()
if i % 4 == 0:
# Application de g() tous les 4 mots
temp = rot_word(temp)
temp = sub_word(temp)
temp[0] ^= rcon(i // 4)
# Ajout du mot i-4
nouveau = [mots[i-4][j] ^ temp[j] for j in range(4)]
mots.append(nouveau)
return mots
def key_expansion_192(cle_initiale):
"""Expansion de clé pour AES-192 (similaire, avec modulo 6)"""
# À implémenter
pass
def key_expansion_256(cle_initiale):
"""Expansion de clé pour AES-256 (avec cas particulier i%8 == 4)"""
# À implémenter
pass
13. Réversibilité et déchiffrement
Pour le déchiffrement, on a besoin des RoundKeys dans l’ordre inverse (de la dernière à la première).
Deux approches possibles :
- Précalcul : Générer toutes les RoundKeys au début du déchiffrement et les stocker.
- Génération inversée : Calculer les clés à partir de la dernière (plus complexe, rarement utilisée).
En pratique, on précalcule toutes les RoundKeys à l’initialisation.
14. Pourquoi ne pas utiliser une seule grande clé ?
| Question |
Réponse |
| Pourquoi ne pas utiliser une clé de 128 bits × 11 ? |
La Key Expansion permet d’introduire de la non-linéarité et d’éviter les corrélations entre les rounds. Une simple répétition serait vulnérable. |
| Pourquoi des clés de 192 ou 256 bits ? |
Plus la clé est longue, plus la Key Expansion est complexe et plus l’espace de recherche est grand, rendant les attaques par force brute plus difficiles. |
15. Récapitulatif final
| Concept |
Description |
| Objectif |
Générer des RoundKeys distinctes pour chaque round |
| Entrée |
Clé principale (128, 192 ou 256 bits) |
| Sortie |
$N_r+1$ RoundKeys de 128 bits |
| Opérations |
RotWord, SubWord, XOR avec Rcon, XOR entre mots |
| Sécurité |
Non-linéarité, diffusion, dépendance entre les rounds |
Pour aller plus loin
- Cellule interactive : Afficher l’évolution des mots lors de l’expansion.
- Exercice : Implémenter la Key Expansion pour AES-128 et vérifier avec des vecteurs de test connus.
- Visualisation : Représenter graphiquement les dépendances entre les mots.
- Question de réflexion :
Pourquoi utilise-t-on des constantes de round (Rcon) et pourquoi changent-elles à chaque round ?
Application pratique de l’AES, à la main !
1. Objectif
Cette section vous permet d’appliquer les transformations fondamentales de l’AES manuellement, afin de bien comprendre leur fonctionnement.
Les données initiales (texte clair et clé) ont été générées aléatoirement (ils n’ont pas forcément de signification). Vous pouvez :
- Calculer vous-même chaque étape ;
- Vérifier vos résultats en affichant la correction.
2. Structure d’un round
Pour un round complet (hors dernier round), l’ordre des opérations est :
- AddRoundKey (RoundKey initiale)
- SubBytes (Substitution)
- ShiftRows (Décalage des lignes)
- MixColumns (Mélange des colonnes)
- AddRoundKey (RoundKey suivante)
Dans ce qui suit, nous travaillons sur un seul round (hors dernier), avec une clé de 128 bits.
3. Données initiales
Texte clair (PlainText) - 16 octets, soit 128 bits
| Octet |
Valeur |
Octet |
Valeur |
Octet |
Valeur |
Octet |
Valeur |
| 0 |
0x95 |
4 |
0x0E |
8 |
0x3B |
12 |
0x24 |
| 1 |
0x7C |
5 |
0xBD |
9 |
0x47 |
13 |
0x5E |
| 2 |
0xEA |
6 |
0x55 |
10 |
0x4A |
14 |
0x57 |
| 3 |
0x66 |
7 |
0xA9 |
11 |
0x75 |
15 |
0xFD |
Clé initiale (RoundKey 0) - 16 octets
| Octet |
Valeur |
Octet |
Valeur |
Octet |
Valeur |
Octet |
Valeur |
| 0 |
0xCC |
4 |
0xC2 |
8 |
0xF9 |
12 |
0xDD |
| 1 |
0x27 |
5 |
0x9A |
9 |
0xDD |
13 |
0x83 |
| 2 |
0xBE |
6 |
0xEB |
10 |
0xA1 |
14 |
0xF6 |
| 3 |
0x50 |
7 |
0xF9 |
11 |
0x8C |
15 |
0x71 |
4. Étape 1 : AddRoundKey (RoundKey initiale)
Formule : $\text{État} = \text{PlainText} \oplus \text{RoundKey}_0$
Calcul à effectuer
Pour chaque octet, effectuez un XOR bit à bit entre le texte clair et la clé.
| Pos |
PlainText |
RoundKey |
Résultat (à calculer) |
| 0 |
0x95 |
0xCC |
|
| 1 |
0x7C |
0x27 |
|
| 2 |
0xEA |
0xBE |
|
| 3 |
0x66 |
0x50 |
|
| 4 |
0x0E |
0xC2 |
|
| 5 |
0xBD |
0x9A |
|
| 6 |
0x55 |
0xEB |
|
| 7 |
0xA9 |
0xF9 |
|
| 8 |
0x3B |
0xF9 |
|
| 9 |
0x47 |
0xDD |
|
| 10 |
0x4A |
0xA1 |
|
| 11 |
0x75 |
0x8C |
|
| 12 |
0x24 |
0xDD |
|
| 13 |
0x5E |
0x83 |
|
| 14 |
0x57 |
0xF6 |
|
| 15 |
0xFD |
0x71 |
|
📌 Correction : résultat d’AddRoundKey
Exemple avec le premier : 0x95 = ’0b10010101’ et 0xCC = ’0b11001100’ donc 0x95^0xCC = ’0b10010101^0b11001100’ = ’0b1011001’ = 89 = 0x59
| 0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
0x59 |
0x5B |
0x54 |
0x36 |
0xCC |
0x27 |
0xBE |
0x50 |
0xC2 |
0x9A |
0xEB |
0xF9 |
0xF9 |
0xDD |
0xA1 |
0x8C |
5. Étape 2 : SubBytes
Formule : Pour chaque octet $x$, appliquer la S-box : $y = \text{Sbox}[x]$.
Utilisez la table S-box (voir section précédente) pour substituer chaque octet.
Tableau des résultats
| Pos |
Entrée (après AddRK) |
S-box |
Résultat (à calculer) |
| 0 |
0x59 |
S(0x59) |
|
| 1 |
0x5B |
S(0x5B) |
|
| 2 |
0x54 |
S(0x54) |
|
| 3 |
0x36 |
S(0x36) |
|
| 4 |
0xCC |
S(0xCC) |
|
| 5 |
0x27 |
S(0x27) |
|
| 6 |
0xBE |
S(0xBE) |
|
| 7 |
0x50 |
S(0x50) |
|
| 8 |
0xC2 |
S(0xC2) |
|
| 9 |
0x9A |
S(0x9A) |
|
| 10 |
0xEB |
S(0xEB) |
|
| 11 |
0xF9 |
S(0xF9) |
|
| 12 |
0xF9 |
S(0xF9) |
|
| 13 |
0xDD |
S(0xDD) |
|
| 14 |
0xA1 |
S(0xA1) |
|
| 15 |
0x8C |
S(0x8C) |
|
📌 Correction : résultat de SubBytes
| 0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
| 0xCB |
0x39 |
0x20 |
0x05 |
0x8B |
0xCC |
0xAE |
0x53 |
0x25 |
0xB8 |
0xE9 |
0x99 |
0x99 |
0xC1 |
0x32 |
0x64 |
On lit sur la ligne 5 et la colonne 9 de la S-box : 0xCB que l’on associe donc au 0x59 de la position 0.
6. Étape 3 : ShiftRows
Formule : Décalage circulaire vers la gauche de chaque ligne
- Ligne 0 : décalage 0 (inchangée)
- Ligne 1 : décalage 1
- Ligne 2 : décalage 2
- Ligne 3 : décalage 3
Organisation en matrice (ordre colonne par colonne)
Avant ShiftRows (après SubBytes) :
|
Col 0 |
Col 1 |
Col 2 |
Col 3 |
| L0 |
0xCB |
0x8B |
0x25 |
0x99 |
| L1 |
0x39 |
0xCC |
0xB8 |
0x99 |
| L2 |
0x20 |
0xAE |
0xE9 |
0xC1 |
| L3 |
0x05 |
0x53 |
0x99 |
0x32 |
Après ShiftRows :
|
Col 0 |
Col 1 |
Col 2 |
Col 3 |
| L0 |
|
|
|
|
| L1 |
|
|
|
|
| L2 |
|
|
|
|
| L3 |
|
|
|
|
📌 Correction : résultat de ShiftRows (ordre en tableau puis ordre plat)
|
Col 0 |
Col 1 |
Col 2 |
Col 3 |
| L0 |
0xCB |
0x8B |
0x25 |
0x99 |
← inchangée |
| L1 |
0xCC |
0xB8 |
0x99 |
0x39 |
← décalage 1 |
| L2 |
0xE9 |
0xC1 |
0x20 |
0xAE |
← décalage 2 |
| L3 |
0x32 |
0x05 |
0x53 |
0x99 |
← décalage 3 |
| Pos |
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
| Val |
0xCB |
0xCC |
0xE9 |
0x32 |
0x8B |
0xB8 |
0xC1 |
0x05 |
0x25 |
0x99 |
0x20 |
0x53 |
0x99 |
0x39 |
0xAE |
0x99 |
7. Étape 4 : MixColumns
Formule : Pour chaque colonne, multiplication matricielle par la matrice de MixColumns.
Note : Dans cet exemple, les valeurs après ShiftRows donnent un résultat particulier. Effectuez le calcul ou vérifiez la correction.
📌 Correction : résultat de MixColumns
| Pos |
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
| Val |
0x00 |
0x00 |
0x00 |
0x00 |
0x00 |
0x00 |
0x00 |
0x00 |
0x00 |
0x00 |
0x00 |
0x00 |
0x00 |
0x00 |
0x00 |
0x00 |
8. Étape 5 : AddRoundKey (RoundKey suivante)
Formule : $\text{État}_{\text{final}} = \text{État}_{\text{MixColumns}} \oplus \text{RoundKey}_1$
RoundKey 1 (à partir de l’expansion)
| Pos |
Valeur |
| 0 |
0xBB |
| 1 |
0x55 |
| 2 |
0x1C |
| 3 |
0x77 |
| 4 |
0x8D |
| 5 |
0xBD |
| 6 |
0x2C |
| 7 |
0x55 |
| 8 |
0x3C |
| 9 |
0xBB |
| 10 |
0x0A |
| 11 |
0xF7 |
| 12 |
0xE3 |
| 13 |
0x22 |
| 14 |
0x79 |
| 15 |
0x4A |
📌 Correction : résultat final du round
Le résultat après AddRoundKey est le même qu’après MixColumns (tous les octets à 0x00), car 0x00 ⊕ K = K. Le résultat est donc exactement la RoundKey 1 :
| 0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
| 0xBB |
0x55 |
0x1C |
0x77 |
0x8D |
0xBD |
0x2C |
0x55 |
0x3C |
0xBB |
0x0A |
0xF7 |
0xE3 |
0x22 |
0x79 |
0x4A |
9. Remarque sur l’exemple
Dans cet exemple, le résultat de MixColumns est intégralement nul. Cela peut arriver lorsque la colonne d’entrée présente une propriété particulière (par exemple, tous les octets identiques après application des transformations). C’est un cas intéressant pour observer le comportement de MixColumns.
Et ce n’est pas fini ! La suite bientôt ...