Les nouvelles technologies pour l’enseignement des mathématiques
Intégration des TICE dans l’enseignement des mathématiques

MathémaTICE, première revue en ligne destinée à promouvoir les TICE à travers l’enseignement des mathématiques.

Le chiffrement symétrique AES
Article mis en ligne le 13 juin 2026
dernière modification le 24 juin 2026

par Benjamin Clerc

NDLR : Cet article est proche de sa rédaction finale. Son état d’achèvement avancé nous permet de l’offrir dès maintenant aux lectrices et lecteurs curieux !

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.

A → D
B → E
…
Z → C

Ainsi « bonjour » devient « ERQMRXU »
Pour déchiffrer, il suffit d’appliquer le décalage inverse (décalage de -3) tel que ;

C → Z
D → A
...
H → E

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

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

  1. 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}")
    
  2. 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.

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

1. Modulo et congruences

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

2. Le corps binaire $\mathbb{F}_2$ (ou $\mathbb{Z}/2\mathbb{Z}$)

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

3. Les polynômes sur $\mathbb{F}_2$

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.

4. Notion de groupe

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}$
$\text{Fermeture}$ $\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$

5. Groupe abélien

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 :

Opérations sur le corps fini $GF(2^8)$ Corps de Galois

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)$ :

  1. Multiplier les polynômes (produit pouvant aller jusqu’au degré 14)
  2. 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$$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$ $\cdot$ $r_{-1} = x^8 + x^4 + x^3 + x + 1 = m(x)$ $u_{-1} = 0$
$0$ $\cdot$ $r_0 = x^6 + x^4 + x^2 + x + 1 = a(x)$ $u_0 = 1$
$1$ $1 + x^2$ $r_1 = x^4$ $u_1 = 1 + x^2$
$2$ $1 + x^2$ $r_2 = 1 + x + x^2$ $u_2 = x^4$
$3$ $x + x^2$ $r_3 = x$ $u_3 = 1 + x^2 + x^5 + x^6$
$4$ $1 + x$ $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\}$

$A=10$, $B = 11$, $C = 12$, $D = 13$, $E = 14$, $F = 15$.

Exemple

Prenons l’octet binaire : 10011011

  1. Séparation : 1001 et 1011
  2. Conversion du premier nibble : $h_1 = 1\cdot8 + 0\cdot4 + 0\cdot2 + 1\cdot1 = 8 + 0 + 0 + 1 = 9$
  3. Conversion du second nibble : $h_2 = 1\cdot8 + 0\cdot4 + 1\cdot2 + 1\cdot1 = 8 + 0 + 2 + 1 = 11$
  4. $11$ en hexadécimal s’écrit B
  5. 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$, les autres sont bien sûr à 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 $4 \times 4$ d’octets (ordre colonne par colonne)
  • 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}$
  1. 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)
    
  2. 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 :

  3. Pourquoi le dernier round ne comporte-t-il pas l’étape MixColumns ? (Indice : pensez à la symétrie entre chiffrement et déchiffrement)

Réponse courte

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.

Réponse détaillée

La raison est liée à la symétrie entre chiffrement et déchiffrement.

  1. Structure du chiffrement
    • Pour les rounds 1 à 9 (sur AES-128) : SubBytes → ShiftRows → MixColumns → AddRoundKey
    • Pour le round 10 (dernier) : SubBytes → ShiftRows → AddRoundKey
  2. 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.
  3. 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
  4. 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 :

Substitution des octets (SubBytes (ByteSub))

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 :

  1. Calcul de l’inverse multiplicatif dans $\mathbb{F}_{2^8}$
  2. 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 :

  1. Inverse de la transformation affine $f^{-1}$
  2. 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

Décalage des lignes (ShiftRows)

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_{10}$ $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) :

AvantAprè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

  1. Visualisation interactive  : Afficher la matrice avant/après côte à côte.
  2. 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.
  3. 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 ?

Mélange ou Combinaison des colonnes (MixColumns)

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 à :

  1. Décaler tous les bits de l’octet d’un cran vers la gauche (multiplication polynomiale par $x$)
  2. 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} $$

$\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

  1. Cellule interactive : Visualiser l’effet de MixColumns sur une colonne.
  2. Exercice : Vérifier que la multiplication par 0x03 correspond bien à $(\text{0x02} \cdot X) \oplus X$.
  3. Vérification expérimentale : Appliquer MixColumns puis InvMixColumns et vérifier qu’on retrouve le bloc original.
  4. 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}$ ?

Ajout de la clé de tour (AddRoundKey)

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

  1. Associativité : $(a \oplus b) \oplus c = a \oplus (b \oplus c)$
  2. Élément neutre : $a \oplus 0 = a$
  3. 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

  1. Cellule interactive : Visualiser l’effet de AddRoundKey sur un bloc.
  2. Exercice : Vérifier que add_round_key(etat, rk) appliqué deux fois redonne l’état initial.
  3. Mini-projet : Implémenter une version simplifiée de l’AES en combinant toutes les transformations vues jusqu’ici.

Expansion de clé (Key Schedule)

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}’)$$

$\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}$$

$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)$ :

  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))$
  2. $W₄ = W₀ ⊕ temp$
  3. $W₅ = W₄ ⊕ W₁$
  4. $W₆ = W₅ ⊕ W₂$
  5. $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 :

  1. Précalcul : Générer toutes les RoundKeys au début du déchiffrement et les stocker.
  2. 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

  1. Cellule interactive : Afficher l’évolution des mots lors de l’expansion.
  2. Exercice : Implémenter la Key Expansion pour AES-128 et vérifier avec des vecteurs de test connus.
  3. Visualisation : Représenter graphiquement les dépendances entre les mots.
  4. 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 :

  1. AddRoundKey (RoundKey initiale)
  2. SubBytes (Substitution)
  3. ShiftRows (Décalage des lignes)
  4. MixColumns (Mélange des colonnes)
  5. 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 ...