Exercices - Fonctions de hachage et modes opératoires
Travaux pratiques sur les fonctions de hachage modernes et les modes opératoires de chiffrement
TP 1: Analyse des fonctions de hachage cryptographiques
- Comparer les caractéristiques des différentes fonctions de hachage
- Analyser l'effet d'avalanche des fonctions de hachage
- Évaluer les performances des fonctions de hachage modernes
Partie 1: Génération et comparaison des valeurs de hachage
Créez un script Python pour comparer les différentes fonctions de hachage :
#!/usr/bin/env python3
import hashlib
import time
import binascii
import sys
# Fonction pour afficher un hash de manière formatée
def display_hash(hash_name, hash_func, data):
start_time = time.time()
hash_value = hash_func(data).hexdigest()
end_time = time.time()
print(f"{hash_name:10} | {hash_value} | {len(hash_value)/2*8} bits | {(end_time-start_time)*1000:.2f} ms")
# Test avec différentes entrées
def test_hash_functions(input_data):
data = input_data.encode('utf-8')
print(f"\nEntrée: '{input_data}'")
print("-" * 80)
print(f"{'Fonction':<10} | {'Valeur de hachage':64} | {'Taille':<10} | {'Temps'}")
print("-" * 80)
# Tester différentes fonctions de hachage
display_hash("MD5", hashlib.md5, data)
display_hash("SHA-1", hashlib.sha1, data)
display_hash("SHA-256", hashlib.sha256, data)
display_hash("SHA-384", hashlib.sha384, data)
display_hash("SHA-512", hashlib.sha512, data)
display_hash("SHA3-256", hashlib.sha3_256, data)
display_hash("BLAKE2b", hashlib.blake2b, data)
display_hash("BLAKE2s", hashlib.blake2s, data)
# Tester avec différentes entrées
test_hash_functions("Hello, World!")
test_hash_functions("hello, world!") # Notez la différence de casse
# Test avec une grande quantité de données
large_data = "A" * 1000000
print(f"\nTest de performance avec 1 Mo de données")
print("-" * 80)
print(f"{'Fonction':<10} | {'Temps pour 1 Mo'}")
print("-" * 80)
for hash_name, hash_func in [
("MD5", hashlib.md5),
("SHA-1", hashlib.sha1),
("SHA-256", hashlib.sha256),
("SHA-512", hashlib.sha512),
("SHA3-256", hashlib.sha3_256),
("BLAKE2b", hashlib.blake2b),
("BLAKE2s", hashlib.blake2s),
]:
start_time = time.time()
hash_func(large_data.encode('utf-8')).hexdigest()
end_time = time.time()
print(f"{hash_name:10} | {(end_time-start_time)*1000:.2f} ms")
Partie 2: Effet d'avalanche
Créez un script pour illustrer et mesurer l'effet d'avalanche dans les fonctions de hachage :
#!/usr/bin/env python3
import hashlib
import binascii
def bit_diff_count(hex1, hex2):
"""Calcule le nombre de bits différents entre deux valeurs hexadécimales."""
bin1 = bin(int(hex1, 16))[2:].zfill(len(hex1) * 4)
bin2 = bin(int(hex2, 16))[2:].zfill(len(hex2) * 4)
diff_count = sum(b1 != b2 for b1, b2 in zip(bin1, bin2))
return diff_count, len(bin1)
def test_avalanche(hash_func, text1, text2):
"""Teste l'effet d'avalanche d'une fonction de hachage entre deux textes."""
h1 = hash_func(text1.encode()).hexdigest()
h2 = hash_func(text2.encode()).hexdigest()
diff_count, total_bits = bit_diff_count(h1, h2)
percentage = (diff_count / total_bits) * 100
print(f"Texte 1: '{text1}'")
print(f"Texte 2: '{text2}'")
print(f"Hash 1: {h1}")
print(f"Hash 2: {h2}")
print(f"Différence: {diff_count}/{total_bits} bits ({percentage:.2f}%)")
print()
# Tester l'effet d'avalanche pour différentes fonctions de hachage
hash_functions = [
("MD5", hashlib.md5),
("SHA-1", hashlib.sha1),
("SHA-256", hashlib.sha256),
("SHA3-256", hashlib.sha3_256),
("BLAKE2b", hashlib.blake2b)
]
# Test avec une modification d'un seul caractère
print("=== Test de l'effet d'avalanche avec une modification d'un seul caractère ===")
for name, func in hash_functions:
print(f"--- {name} ---")
test_avalanche(func, "Hello, World!", "Hello, World.")
# Test avec une modification d'un seul bit
print("=== Test de l'effet d'avalanche avec une modification d'un seul bit ===")
single_bit = bytes([0b00000001])
zero_bit = bytes([0b00000000])
for name, func in hash_functions:
print(f"--- {name} ---")
h1 = func(single_bit).hexdigest()
h2 = func(zero_bit).hexdigest()
diff_count, total_bits = bit_diff_count(h1, h2)
percentage = (diff_count / total_bits) * 100
print(f"Entrée 1: 1 bit")
print(f"Entrée 2: 0 bit")
print(f"Hash 1: {h1}")
print(f"Hash 2: {h2}")
print(f"Différence: {diff_count}/{total_bits} bits ({percentage:.2f}%)")
print()
Partie 3: Résistance aux collisions
Testez la résistance aux collisions de MD5 avec un script simple :
#!/usr/bin/env python3
import hashlib
import random
import string
import time
def generate_random_string(length):
"""Génère une chaîne aléatoire de la longueur spécifiée."""
return ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(length))
def find_partial_collision(hash_func, prefix_length, max_attempts=10000000):
"""
Tente de trouver une collision partielle (premiers octets) dans une fonction de hachage.
Retourne le nombre d'essais nécessaires et les deux entrées qui génèrent la collision.
"""
hash_dict = {}
attempts = 0
start_time = time.time()
for i in range(max_attempts):
attempts += 1
s = generate_random_string(10) # Chaînes de 10 caractères
h = hash_func(s.encode()).hexdigest()
prefix = h[:prefix_length]
if prefix in hash_dict:
end_time = time.time()
return {
'attempts': attempts,
'time': end_time - start_time,
'string1': s,
'string2': hash_dict[prefix],
'hash1': h,
'hash2': hash_func(hash_dict[prefix].encode()).hexdigest(),
'prefix': prefix
}
hash_dict[prefix] = s
if i % 100000 == 0:
print(f"Essai {i}, table de hachage: {len(hash_dict)} entrées")
return None
# Tester avec différentes longueurs de préfixes pour MD5
for prefix_length in [2, 4, 6]:
print(f"\nRecherche de collision sur les premiers {prefix_length} caractères (MD5):")
result = find_partial_collision(hashlib.md5, prefix_length)
if result:
print(f"Collision trouvée après {result['attempts']} tentatives ({result['time']:.2f} secondes)")
print(f"String 1: '{result['string1']}'")
print(f"String 2: '{result['string2']}'")
print(f"Hash 1: {result['hash1']}")
print(f"Hash 2: {result['hash2']}")
print(f"Les deux hachés commencent par: {result['prefix']}")
else:
print(f"Aucune collision trouvée après {max_attempts} tentatives")
prefix_length
. Avec 6 caractères hexadécimaux, l'attaque peut prendre plusieurs minutes.
Partie 4: Questions d'analyse
- Sur la base des résultats de performance, quelle fonction de hachage est la plus rapide ? Classez-les par ordre de vitesse.
- Comparez les pourcentages d'effet d'avalanche pour les différentes fonctions. Quelle fonction montre le meilleur effet d'avalanche ?
- Quelle est la relation entre la longueur du préfixe et le nombre de tentatives nécessaires pour trouver une collision partielle ? Les résultats correspondent-ils à la théorie du paradoxe des anniversaires ?
- Expliquez pourquoi MD5 et SHA-1 ne sont plus considérés comme sécurisés pour les applications cryptographiques. Quelles sont les attaques spécifiques qui ont été démontrées contre ces fonctions ?
TP 2: Mise en œuvre et analyse des modes opératoires
- Implémenter et comparer différents modes opératoires de chiffrement
- Observer les propriétés de propagation d'erreurs
- Analyser la sécurité de chaque mode face à différentes menaces
Partie 1: Démonstration des propriétés des modes avec OpenSSL
Créez un script Bash pour comparer visuellement les modes ECB et CBC :
#!/bin/bash
# Ce script démontre la différence visuelle entre ECB et CBC
# sur un fichier image
# Vérifier que nous avons convert (de ImageMagick)
if ! command -v convert &> /dev/null; then
echo "Ce script nécessite ImageMagick. Installez-le avec:"
echo "apt-get install imagemagick"
exit 1
fi
# Générer une clé et un IV
openssl rand -out key.bin 16
openssl rand -out iv.bin 16
# Créer une image simple pour démontrer le problème
echo "Création d'une image test..."
convert -size 512x512 gradient:black-white gradient.ppm
# Chiffrer avec ECB
echo "Chiffrement avec ECB..."
openssl enc -aes-128-ecb -in gradient.ppm -out gradient_ecb.bin \
-K $(xxd -p -c 32 key.bin) -nosalt
# Chiffrer avec CBC
echo "Chiffrement avec CBC..."
openssl enc -aes-128-cbc -in gradient.ppm -out gradient_cbc.bin \
-K $(xxd -p -c 32 key.bin) -iv $(xxd -p -c 32 iv.bin) -nosalt
# Convertir les fichiers chiffrés en images
# (en ajoutant un en-tête PPM simple)
cat > header.ppm << EOL
P6
512 512
255
EOL
echo "Création des images chiffrées..."
cat header.ppm > tmp_ecb.ppm
dd if=gradient_ecb.bin of=tmp_ecb.ppm bs=1 seek=12 conv=notrunc
convert tmp_ecb.ppm gradient_ecb.png
cat header.ppm > tmp_cbc.ppm
dd if=gradient_cbc.bin of=tmp_cbc.ppm bs=1 seek=12 conv=notrunc
convert tmp_cbc.ppm gradient_cbc.png
# Nettoyage
rm header.ppm tmp_ecb.ppm tmp_cbc.ppm
echo "Terminé. Comparez les images:"
echo " - gradient.ppm (original)"
echo " - gradient_ecb.png (chiffré avec ECB)"
echo " - gradient_cbc.png (chiffré avec CBC)"
Partie 2: Implémentation de différents modes opératoires
Implémentez les modes ECB, CBC et CTR en Python pour mieux comprendre leur fonctionnement interne :
#!/usr/bin/env python3
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
import os
import time
import binascii
# Fonctions utilitaires
def xor_bytes(a, b):
"""XOR deux séquences d'octets de même longueur."""
return bytes(x ^ y for x, y in zip(a, b))
def pad_data(data, block_size):
"""Ajoute un padding PKCS#7 aux données."""
pad_len = block_size - (len(data) % block_size)
return data + bytes([pad_len]) * pad_len
def unpad_data(data):
"""Supprime le padding PKCS#7 des données."""
pad_len = data[-1]
return data[:-pad_len]
# Implémentation des modes opératoires
class BlockCipherModes:
def __init__(self, key, backend=default_backend()):
"""Initialise avec une clé AES."""
self.key = key
self.block_size = 16 # AES a un bloc de 16 octets (128 bits)
self.backend = backend
def encrypt_block(self, block):
"""Chiffre un seul bloc avec AES."""
cipher = Cipher(algorithms.AES(self.key), modes.ECB(), backend=self.backend)
encryptor = cipher.encryptor()
return encryptor.update(block) + encryptor.finalize()
def decrypt_block(self, block):
"""Déchiffre un seul bloc avec AES."""
cipher = Cipher(algorithms.AES(self.key), modes.ECB(), backend=self.backend)
decryptor = cipher.decryptor()
return decryptor.update(block) + decryptor.finalize()
def ecb_encrypt(self, plaintext):
"""Chiffre avec le mode ECB."""
padded_data = pad_data(plaintext, self.block_size)
ciphertext = b''
# Chiffre chaque bloc individuellement
for i in range(0, len(padded_data), self.block_size):
block = padded_data[i:i + self.block_size]
ciphertext += self.encrypt_block(block)
return ciphertext
def ecb_decrypt(self, ciphertext):
"""Déchiffre avec le mode ECB."""
plaintext = b''
# Déchiffre chaque bloc individuellement
for i in range(0, len(ciphertext), self.block_size):
block = ciphertext[i:i + self.block_size]
plaintext += self.decrypt_block(block)
# Supprime le padding
return unpad_data(plaintext)
def cbc_encrypt(self, plaintext, iv):
"""Chiffre avec le mode CBC."""
if len(iv) != self.block_size:
raise ValueError(f"IV doit être de taille {self.block_size} octets")
padded_data = pad_data(plaintext, self.block_size)
ciphertext = b''
prev_block = iv
# Chiffre chaque bloc, en chaînant avec le bloc précédent
for i in range(0, len(padded_data), self.block_size):
block = padded_data[i:i + self.block_size]
xored_block = xor_bytes(block, prev_block)
encrypted_block = self.encrypt_block(xored_block)
ciphertext += encrypted_block
prev_block = encrypted_block
return ciphertext
def cbc_decrypt(self, ciphertext, iv):
"""Déchiffre avec le mode CBC."""
if len(iv) != self.block_size:
raise ValueError(f"IV doit être de taille {self.block_size} octets")
plaintext = b''
prev_block = iv
# Déchiffre chaque bloc, en chaînant avec le bloc chiffré précédent
for i in range(0, len(ciphertext), self.block_size):
block = ciphertext[i:i + self.block_size]
decrypted_block = self.decrypt_block(block)
plaintext += xor_bytes(decrypted_block, prev_block)
prev_block = block
# Supprime le padding
return unpad_data(plaintext)
def ctr_encrypt(self, plaintext, nonce):
"""Chiffre avec le mode CTR."""
if len(nonce) != self.block_size // 2: # Nonce de 8 octets pour AES
raise ValueError(f"Nonce doit être de taille {self.block_size // 2} octets")
ciphertext = b''
counter = 0
# Parcourir le plaintext bloc par bloc
for i in range(0, len(plaintext), self.block_size):
# Construire le compteur avec le nonce
counter_block = nonce + counter.to_bytes(self.block_size // 2, byteorder='big')
# Chiffrer le compteur
keystream = self.encrypt_block(counter_block)
# Extraire le bloc courant (peut être incomplet à la fin)
block = plaintext[i:i + self.block_size]
# XOR avec la partie correspondante du keystream
encrypted_block = xor_bytes(block, keystream[:len(block)])
ciphertext += encrypted_block
counter += 1
return ciphertext
def ctr_decrypt(self, ciphertext, nonce):
"""Déchiffre avec le mode CTR (même algorithme que le chiffrement)."""
return self.ctr_encrypt(ciphertext, nonce) # CTR est symétrique
# Démonstration d'utilisation
def demo_modes():
# Générer une clé AES-128
key = os.urandom(16)
# Générer un IV aléatoire
iv = os.urandom(16)
# Générer un nonce aléatoire
nonce = os.urandom(8)
# Message à chiffrer
message = b"En cryptographie, un mode operatoire est un algorithme qui decrit comment appliquer l'operation d'un chiffrement par bloc a une quantite de donnees superieure a la taille d'un bloc."
# Instancier notre classe
cipher_modes = BlockCipherModes(key)
print("Message original:")
print(message.decode())
print(f"Longueur: {len(message)} octets\n")
# Tester ECB
print("=== Mode ECB ===")
start_time = time.time()
ecb_encrypted = cipher_modes.ecb_encrypt(message)
end_time = time.time()
print(f"Temps de chiffrement: {(end_time - start_time) * 1000:.2f} ms")
print(f"Chiffré (hex): {binascii.hexlify(ecb_encrypted)[:64]}...")
start_time = time.time()
ecb_decrypted = cipher_modes.ecb_decrypt(ecb_encrypted)
end_time = time.time()
print(f"Temps de déchiffrement: {(end_time - start_time) * 1000:.2f} ms")
print(f"Déchiffré: {ecb_decrypted.decode()}")
print(f"Vérification: {'OK' if ecb_decrypted == message else 'ÉCHEC'}\n")
# Tester CBC
print("=== Mode CBC ===")
start_time = time.time()
cbc_encrypted = cipher_modes.cbc_encrypt(message, iv)
end_time = time.time()
print(f"Temps de chiffrement: {(end_time - start_time) * 1000:.2f} ms")
print(f"Chiffré (hex): {binascii.hexlify(cbc_encrypted)[:64]}...")
start_time = time.time()
cbc_decrypted = cipher_modes.cbc_decrypt(cbc_encrypted, iv)
end_time = time.time()
print(f"Temps de déchiffrement: {(end_time - start_time) * 1000:.2f} ms")
print(f"Déchiffré: {cbc_decrypted.decode()}")
print(f"Vérification: {'OK' if cbc_decrypted == message else 'ÉCHEC'}\n")
# Tester CTR
print("=== Mode CTR ===")
start_time = time.time()
ctr_encrypted = cipher_modes.ctr_encrypt(message, nonce)
end_time = time.time()
print(f"Temps de chiffrement: {(end_time - start_time) * 1000:.2f} ms")
print(f"Chiffré (hex): {binascii.hexlify(ctr_encrypted)[:64]}...")
start_time = time.time()
ctr_decrypted = cipher_modes.ctr_decrypt(ctr_encrypted, nonce)
end_time = time.time()
print(f"Temps de déchiffrement: {(end_time - start_time) * 1000:.2f} ms")
print(f"Déchiffré: {ctr_decrypted.decode()}")
print(f"Vérification: {'OK' if ctr_decrypted == message else 'ÉCHEC'}\n")
return key, iv, nonce, message, ecb_encrypted, cbc_encrypted, ctr_encrypted
if __name__ == "__main__":
demo_modes()
Partie 3: Test de propagation d'erreurs
Continuez le script précédent pour tester la propagation d'erreurs dans les différents modes :
def test_error_propagation(key, iv, nonce, message):
"""Teste la propagation d'erreurs dans les différents modes."""
print("=== Test de propagation d'erreurs ===")
cipher_modes = BlockCipherModes(key)
# Erreur dans ECB
print("\n--- Propagation d'erreur en mode ECB ---")
ecb_encrypted = cipher_modes.ecb_encrypt(message)
# Modifier un seul bit dans le premier bloc
corrupted_ecb = bytearray(ecb_encrypted)
corrupted_ecb[5] ^= 1 # Flip un bit
corrupted_ecb = bytes(corrupted_ecb)
try:
ecb_decrypted = cipher_modes.ecb_decrypt(corrupted_ecb)
print("Premier bloc déchiffré (corrompu):")
print(ecb_decrypted[:16])
print("Reste du message:")
print(ecb_decrypted[16:])
except Exception as e:
print(f"Erreur lors du déchiffrement: {e}")
# Erreur dans CBC
print("\n--- Propagation d'erreur en mode CBC ---")
cbc_encrypted = cipher_modes.cbc_encrypt(message, iv)
# Modifier un seul bit dans le premier bloc
corrupted_cbc = bytearray(cbc_encrypted)
corrupted_cbc[5] ^= 1 # Flip un bit
corrupted_cbc = bytes(corrupted_cbc)
try:
cbc_decrypted = cipher_modes.cbc_decrypt(corrupted_cbc, iv)
print("Premier bloc déchiffré (corrompu):")
print(cbc_decrypted[:16])
print("Deuxième bloc (affecté par la corruption):")
print(cbc_decrypted[16:32])
print("Reste du message:")
print(cbc_decrypted[32:])
except Exception as e:
print(f"Erreur lors du déchiffrement: {e}")
# Erreur dans CTR
print("\n--- Propagation d'erreur en mode CTR ---")
ctr_encrypted = cipher_modes.ctr_encrypt(message, nonce)
# Modifier un seul bit dans le premier bloc
corrupted_ctr = bytearray(ctr_encrypted)
corrupted_ctr[5] ^= 1 # Flip un bit
corrupted_ctr = bytes(corrupted_ctr)
try:
ctr_decrypted = cipher_modes.ctr_decrypt(corrupted_ctr, nonce)
# Localiser exactement le bit modifié
print("Octet corrompu:")
for i in range(len(message)):
if i < len(ctr_decrypted) and message[i] != ctr_decrypted[i]:
print(f"Position {i}: Original: {message[i]} vs Corrompu: {ctr_decrypted[i]}")
print("Message déchiffré (avec erreur):")
print(ctr_decrypted.decode(errors='replace'))
except Exception as e:
print(f"Erreur lors du déchiffrement: {e}")
# Ajouter à la fin du script principal
if __name__ == "__main__":
key, iv, nonce, message, ecb_encrypted, cbc_encrypted, ctr_encrypted = demo_modes()
test_error_propagation(key, iv, nonce, message)
Partie 4: Test d'authentification avec GCM
Créez un script pour démontrer l'importance de l'authentification avec AES-GCM :
#!/usr/bin/env python3
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
import os
import binascii
def demo_aes_gcm():
# Générer une clé AES-256
key = os.urandom(32)
# Générer un nonce/IV pour GCM
nonce = os.urandom(12) # 96 bits recommandés pour GCM
# Données associées (non chiffrées mais authentifiées)
aad = b"Header information or metadata"
# Message à chiffrer
message = b"En cryptographie, le mode GCM (Galois/Counter Mode) est un mode d'operation pour chiffrements par bloc symetriques qui permet a la fois d'assurer la confidentialite et l'authenticite des donnees."
print("=== AES-GCM Demo ===")
print(f"Message original: {message.decode()}")
print(f"Données associées (AAD): {aad.decode()}")
# Chiffrement avec GCM
encryptor = Cipher(
algorithms.AES(key),
modes.GCM(nonce),
backend=default_backend()
).encryptor()
# Mettre à jour avec AAD (avant le message)
encryptor.authenticate_additional_data(aad)
# Chiffrer le message
ciphertext = encryptor.update(message) + encryptor.finalize()
# Récupérer le tag d'authentification
tag = encryptor.tag
print(f"\nMessage chiffré: {binascii.hexlify(ciphertext)[:64]}...")
print(f"Tag d'authentification: {binascii.hexlify(tag)}")
# Déchiffrement avec GCM
print("\n--- Déchiffrement normal ---")
try:
decryptor = Cipher(
algorithms.AES(key),
modes.GCM(nonce, tag),
backend=default_backend()
).decryptor()
decryptor.authenticate_additional_data(aad)
plaintext = decryptor.update(ciphertext) + decryptor.finalize()
print(f"Message déchiffré: {plaintext.decode()}")
print("Authentification: RÉUSSIE")
except Exception as e:
print(f"Erreur d'authentification: {e}")
# Démonstration de la détection de manipulation
print("\n--- Tentative de manipulation du message ---")
# Modifier un bit du message chiffré
corrupted_ciphertext = bytearray(ciphertext)
corrupted_ciphertext[10] ^= 1 # Inverser un bit
corrupted_ciphertext = bytes(corrupted_ciphertext)
try:
decryptor = Cipher(
algorithms.AES(key),
modes.GCM(nonce, tag),
backend=default_backend()
).decryptor()
decryptor.authenticate_additional_data(aad)
plaintext = decryptor.update(corrupted_ciphertext) + decryptor.finalize()
print(f"Message déchiffré (manipulé): {plaintext.decode()}")
print("Authentification: RÉUSSIE (ÉCHEC ATTENDU!)")
except Exception as e:
print(f"Erreur d'authentification (attendue): {e}")
# Démonstration de la détection de manipulation des AAD
print("\n--- Tentative de manipulation des AAD ---")
corrupted_aad = aad + b"extra data"
try:
decryptor = Cipher(
algorithms.AES(key),
modes.GCM(nonce, tag),
backend=default_backend()
).decryptor()
decryptor.authenticate_additional_data(corrupted_aad)
plaintext = decryptor.update(ciphertext) + decryptor.finalize()
print(f"Message déchiffré (AAD manipulée): {plaintext.decode()}")
print("Authentification: RÉUSSIE (ÉCHEC ATTENDU!)")
except Exception as e:
print(f"Erreur d'authentification (attendue): {e}")
# Démonstration de la réutilisation de nonce (à éviter!)
print("\n--- Démonstration du danger de réutilisation de nonce ---")
message2 = b"Ceci est un nouveau message chiffre avec le meme nonce!"
# Premier chiffrement (déjà fait ci-dessus avec le premier message)
# Deuxième chiffrement avec le même nonce
encryptor2 = Cipher(
algorithms.AES(key),
modes.GCM(nonce), # Réutilisation du même nonce!
backend=default_backend()
).encryptor()
encryptor2.authenticate_additional_data(aad)
ciphertext2 = encryptor2.update(message2) + encryptor2.finalize()
tag2 = encryptor2.tag
print(f"Deuxième message chiffré: {binascii.hexlify(ciphertext2)[:64]}...")
# Analyse des risques de la réutilisation de nonce
print("\nAnalyse de la réutilisation de nonce (DANGEREUX):")
# XOR des deux textes chiffrés
min_len = min(len(ciphertext), len(ciphertext2))
xor_result = bytes(a ^ b for a, b in zip(ciphertext[:min_len], ciphertext2[:min_len]))
print(f"XOR des deux chiffrés: {binascii.hexlify(xor_result)[:64]}...")
# XOR des deux textes clairs (pour comparaison)
xor_plaintexts = bytes(a ^ b for a, b in zip(message[:min_len], message2[:min_len]))
print(f"XOR des deux textes clairs: {binascii.hexlify(xor_plaintexts)[:64]}...")
print("\nObservation: Les XOR sont identiques, ce qui signifie qu'un attaquant peut calculer")
print("XOR(message1, message2) directement à partir de XOR(ciphertext1, ciphertext2)!")
print("C'est pourquoi la réutilisation de nonce en GCM est CATASTROPHIQUE pour la sécurité.")
if __name__ == "__main__":
demo_aes_gcm()
Partie 5: Questions d'analyse
- Basé sur vos observations de l'image chiffrée avec ECB, expliquez pourquoi ce mode est considéré comme non sécurisé pour la plupart des applications. Quels types de données sont particulièrement vulnérables aux analyses basées sur ECB ?
- Comparez les résultats de propagation d'erreurs entre ECB, CBC et CTR. Quel mode a l'impact le plus localisé et pourquoi cela peut-il être un avantage dans certains cas ?
- Selon le test GCM, quels sont les avantages d'un mode authentifié (AEAD) par rapport aux modes traditionnels ? Dans quelles situations l'authentification des données est-elle cruciale ?
- D'après la démonstration sur la réutilisation de nonce en GCM, expliquez précisément pourquoi cette pratique est dangereuse et comment un attaquant pourrait exploiter cette vulnérabilité.
- Si vous deviez choisir un mode opératoire pour (a) un système de fichiers chiffré et (b) une communication TLS, quels modes recommanderiez-vous et pourquoi ?
TP 3: Applications avancées des fonctions de hachage
- Implémenter et analyser HMAC et HKDF
- Construire et utiliser un arbre de Merkle
- Implémenter une preuve de travail simple (style Bitcoin)
Partie 1: Implémentation de HMAC
Implémentez HMAC (Hash-based Message Authentication Code) à partir de zéro pour comprendre son fonctionnement interne :
#!/usr/bin/env python3
import hashlib
import hmac as hmac_lib
def my_hmac(key, message, hash_func=hashlib.sha256, block_size=64):
"""
Implémentation personnalisée de HMAC.
Args:
key: Clé secrète pour l'authentification (bytes)
message: Message à authentifier (bytes)
hash_func: Fonction de hachage à utiliser
block_size: Taille du bloc de la fonction de hachage
Returns:
HMAC du message
"""
# Si la clé est plus longue que la taille de bloc, la hacher
if len(key) > block_size:
key = hash_func(key).digest()
# Si la clé est plus courte que la taille de bloc, la compléter avec des zéros
if len(key) < block_size:
key = key + b'\x00' * (block_size - len(key))
# Définir les constantes opad et ipad
o_key_pad = bytes([0x5c ^ b for b in key]) # XOR avec 0x5c
i_key_pad = bytes([0x36 ^ b for b in key]) # XOR avec 0x36
# Calculer le HMAC: H((K' ⊕ opad) || H((K' ⊕ ipad) || m))
inner_hash = hash_func(i_key_pad + message).digest()
outer_hash = hash_func(o_key_pad + inner_hash).digest()
return outer_hash
def test_hmac():
# Test avec SHA-256
key = b"clef_secrete"
message = b"Cette chaine doit etre authentifiee avec HMAC"
# Calcul avec notre implémentation
my_hmac_result = my_hmac(key, message)
print(f"Notre HMAC: {my_hmac_result.hex()}")
# Calcul avec la bibliothèque Python standard pour comparaison
std_hmac_result = hmac_lib.new(key, message, hashlib.sha256).digest()
print(f"HMAC std : {std_hmac_result.hex()}")
# Vérification
print(f"HMACs identiques: {'Oui' if my_hmac_result == std_hmac_result else 'Non'}")
# Tests avec différentes clés et messages
test_cases = [
(b"", b""), # Cas limite: clé vide, message vide
(b"a" * 100, b"message"), # Clé plus grande que block_size
(b"cle", b"a" * 1000) # Message long
]
for i, (test_key, test_message) in enumerate(test_cases):
my_result = my_hmac(test_key, test_message)
std_result = hmac_lib.new(test_key, test_message, hashlib.sha256).digest()
match = my_result == std_result
print(f"Test {i+1}: {'OK' if match else 'ÉCHEC'}")
test_hmac()
Partie 2: Implémentation de HKDF
Implémentez HKDF (HMAC-based Key Derivation Function) qui permet de dériver des clés cryptographiques à partir d'un matériel initial :
#!/usr/bin/env python3
import hashlib
import hmac
import binascii
def hkdf_extract(salt, ikm, hash_func=hashlib.sha256):
"""
Phase d'extraction de HKDF.
Args:
salt: Sel (bytes)
ikm: Matériel initial de la clé (Input Key Material) (bytes)
hash_func: Fonction de hachage à utiliser
Returns:
Clé pseudo-aléatoire (PRK)
"""
if salt is None or len(salt) == 0:
salt = bytes([0] * hash_func().digest_size)
return hmac.new(salt, ikm, hash_func).digest()
def hkdf_expand(prk, info, length, hash_func=hashlib.sha256):
"""
Phase d'expansion de HKDF.
Args:
prk: Clé pseudo-aléatoire (sortie de hkdf_extract)
info: Contexte et information d'application (bytes)
length: Longueur du matériel dérivé en octets
hash_func: Fonction de hachage à utiliser
Returns:
Matériel de clé en sortie (OKM) de longueur spécifiée
"""
digest_size = hash_func().digest_size
n = (length + digest_size - 1) // digest_size # Arrondi supérieur
if n > 255:
raise ValueError("Length too long (>255 * HashLen)")
if info is None:
info = b""
t = b""
okm = b""
for i in range(1, n + 1):
t = hmac.new(prk, t + info + bytes([i]), hash_func).digest()
okm += t
return okm[:length]
def hkdf(ikm, length, salt=None, info=None, hash_func=hashlib.sha256):
"""
HKDF complet.
Args:
ikm: Matériel initial de la clé (Input Key Material) (bytes)
length: Longueur du matériel dérivé en octets
salt: Sel (bytes ou None)
info: Contexte et information d'application (bytes ou None)
hash_func: Fonction de hachage à utiliser
Returns:
Matériel de clé en sortie (OKM) de longueur spécifiée
"""
prk = hkdf_extract(salt, ikm, hash_func)
return hkdf_expand(prk, info, length, hash_func)
def demo_hkdf():
print("=== Démonstration de HKDF ===")
# Matériel initial de la clé (pourrait être un DH partagé ou un secret)
ikm = b"input key material"
# Sel (optionnel)
salt = b"salt value"
# Informations contextuelles (optionnel)
info1 = b"encryption key"
info2 = b"authentication key"
# Dériver différentes clés pour différentes fins
encryption_key = hkdf(ikm, 32, salt, info1) # 256 bits key
auth_key = hkdf(ikm, 32, salt, info2) # 256 bits key
print(f"IKM: {binascii.hexlify(ikm)}")
print(f"Salt: {binascii.hexlify(salt)}")
print(f"\nClé de chiffrement: {binascii.hexlify(encryption_key)}")
print(f"Clé d'authentification: {binascii.hexlify(auth_key)}")
# Démontrer comment l'info affecte la sortie
print("\nDémonstration de l'impact du paramètre 'info':")
keys = []
for i in range(5):
info = f"context-{i}".encode()
key = hkdf(ikm, 16, salt, info)
keys.append(key)
print(f"Info={info}, Clé={binascii.hexlify(key)}")
# Démontrer l'impact du sel
print("\nDémonstration de l'impact du sel:")
key1 = hkdf(ikm, 16, None, b"info") # Pas de sel (utilise zéros)
key2 = hkdf(ikm, 16, b"sel1", b"info")
key3 = hkdf(ikm, 16, b"sel2", b"info")
print(f"Pas de sel: {binascii.hexlify(key1)}")
print(f"Sel 1: {binascii.hexlify(key2)}")
print(f"Sel 2: {binascii.hexlify(key3)}")
demo_hkdf()
Partie 3: Implémentation d'un arbre de Merkle
Créez une implémentation fonctionnelle d'un arbre de Merkle et démontrez ses propriétés de vérification :
#!/usr/bin/env python3
import hashlib
import binascii
from math import log2, ceil
class MerkleTree:
def __init__(self, data_blocks, hash_func=hashlib.sha256):
"""
Initialise un arbre de Merkle avec les blocs de données fournis.
Args:
data_blocks: Liste de blocs de données (chaque bloc est un bytes)
hash_func: Fonction de hachage à utiliser
"""
self.hash_func = hash_func
# Assurer un nombre de blocs qui est une puissance de 2
self.num_leaves = 2 ** ceil(log2(len(data_blocks)))
# Padding: répliquer le dernier bloc si nécessaire
padded_blocks = data_blocks + [data_blocks[-1]] * (self.num_leaves - len(data_blocks))
# Calculer les hachés des feuilles
self.leaves = [self._hash(block) for block in padded_blocks]
# Construire l'arbre
self.tree = self._build_tree(self.leaves)
# Stocker les données d'origine pour référence
self.data_blocks = data_blocks
def _hash(self, data):
"""Calcule le hash d'un bloc de données."""
return self.hash_func(data).digest()
def _build_tree(self, leaves):
"""
Construit récursivement l'arbre de Merkle.
Args:
leaves: Hashes des feuilles
Returns:
Liste de listes représentant l'arbre
"""
tree = [leaves]
# Construire chaque niveau de l'arbre
level = leaves
while len(level) > 1:
new_level = []
# Combiner les nœuds par paires
for i in range(0, len(level), 2):
if i + 1 < len(level):
new_level.append(self._hash(level[i] + level[i + 1]))
else:
# S'il y a un nœud impair, le dupliquer
new_level.append(self._hash(level[i] + level[i]))
# Ajouter ce niveau à l'arbre
tree.append(new_level)
level = new_level
return tree
def get_root(self):
"""Retourne la racine de l'arbre de Merkle."""
return self.tree[-1][0]
def get_proof(self, index):
"""
Génère une preuve pour le bloc à l'index spécifié.
Args:
index: Index du bloc (0-based)
Returns:
Liste de tuples (hash, position) où position indique si le hash
doit être concaténé à gauche (0) ou à droite (1)
"""
if index >= len(self.data_blocks):
raise ValueError("Index out of range")
proof = []
node_index = index
for level in range(len(self.tree) - 1):
is_right = node_index % 2 == 1
pair_index = node_index - 1 if is_right else node_index + 1
# S'assurer que le pair_index est dans les limites
if pair_index < len(self.tree[level]):
proof.append((self.tree[level][pair_index], 0 if is_right else 1))
# Passer au niveau suivant
node_index //= 2
return proof
def verify_proof(self, index, proof):
"""
Vérifie une preuve pour un bloc donné.
Args:
index: Index du bloc
proof: Preuve générée par get_proof
Returns:
Le hash calculé en suivant la preuve
"""
if index >= len(self.data_blocks):
raise ValueError("Index out of range")
current_hash = self._hash(self.data_blocks[index])
for proof_hash, position in proof:
if position == 0: # hash va à gauche
current_hash = self._hash(proof_hash + current_hash)
else: # hash va à droite
current_hash = self._hash(current_hash + proof_hash)
return current_hash
def print_tree(self):
"""Affiche une représentation visuelle de l'arbre."""
print("=== Arbre de Merkle ===")
for level_idx, level in enumerate(reversed(self.tree)):
level_name = "Racine" if level_idx == 0 else f"Niveau {len(self.tree) - level_idx - 1}"
print(f"{level_name}: {len(level)} nœuds")
# Limiter l'affichage pour les grands arbres
if len(level) > 6:
print(f" {binascii.hexlify(level[0])[:8]}... et {len(level) - 2} autres ... {binascii.hexlify(level[-1])[:8]}...")
else:
for node_idx, node in enumerate(level):
print(f" {node_idx}: {binascii.hexlify(node)[:16]}...")
print(f"\nRacine: {binascii.hexlify(self.get_root())}")
def demo_merkle_tree():
# Créer quelques blocs de données
blocks = [
b"Bloc 1: Donnees importantes",
b"Bloc 2: Autres informations",
b"Bloc 3: Encore des donnees",
b"Bloc 4: Dernier bloc",
b"Bloc 5: Un bloc supplementaire"
]
# Créer l'arbre de Merkle
merkle_tree = MerkleTree(blocks)
# Afficher l'arbre
merkle_tree.print_tree()
# Démontrer la vérification
print("\n=== Vérification d'un bloc ===")
block_index = 2 # Vérifions le bloc 3
proof = merkle_tree.get_proof(block_index)
print(f"Bloc à vérifier: '{blocks[block_index].decode()}'")
print("Preuve:")
for i, (hash_val, pos) in enumerate(proof):
position = "gauche" if pos == 0 else "droite"
print(f" Élément {i+1}: {binascii.hexlify(hash_val)[:16]}... (à {position})")
# Vérifier la preuve
calculated_root = merkle_tree.verify_proof(block_index, proof)
actual_root = merkle_tree.get_root()
print(f"\nRacine calculée: {binascii.hexlify(calculated_root)}")
print(f"Racine actuelle: {binascii.hexlify(actual_root)}")
print(f"Vérification: {'RÉUSSIE' if calculated_root == actual_root else 'ÉCHOUÉE'}")
# Démontrer la détection de modification
print("\n=== Détection de modification ===")
modified_blocks = blocks.copy()
modified_blocks[block_index] = b"Bloc 3: Donnees modifiees malicieusement"
print(f"Bloc original: '{blocks[block_index].decode()}'")
print(f"Bloc modifié: '{modified_blocks[block_index].decode()}'")
# Vérifier avec le bloc modifié (devrait échouer)
modified_tree = MerkleTree([modified_blocks[block_index]])
modified_leaf_hash = modified_tree.leaves[0]
print(f"Hash original: {binascii.hexlify(merkle_tree.leaves[block_index])}")
print(f"Hash modifié: {binascii.hexlify(modified_leaf_hash)}")
# Tenter de vérifier avec la preuve originale
current_hash = modified_leaf_hash
for proof_hash, position in proof:
if position == 0:
current_hash = hashlib.sha256(proof_hash + current_hash).digest()
else:
current_hash = hashlib.sha256(current_hash + proof_hash).digest()
print(f"\nRacine calculée avec bloc modifié: {binascii.hexlify(current_hash)}")
print(f"Racine originale: {binascii.hexlify(actual_root)}")
print(f"Vérification: {'RÉUSSIE' if current_hash == actual_root else 'ÉCHOUÉE (modification détectée)'}")
demo_merkle_tree()
Partie 4: Implémentation d'une preuve de travail
Créez une implémentation simplifiée d'un algorithme de preuve de travail similaire à celui utilisé dans Bitcoin :
#!/usr/bin/env python3
import hashlib
import time
import json
import binascii
import multiprocessing
class SimpleBlock:
def __init__(self, index, timestamp, data, previous_hash):
"""
Initialise un bloc simplifié.
Args:
index: Numéro du bloc
timestamp: Horodatage du bloc
data: Données du bloc
previous_hash: Hash du bloc précédent
"""
self.index = index
self.timestamp = timestamp
self.data = data
self.previous_hash = previous_hash
self.nonce = 0
self.hash = self.calculate_hash()
def calculate_hash(self):
"""Calcule le hash du bloc avec les données actuelles."""
block_string = json.dumps({
"index": self.index,
"timestamp": self.timestamp,
"data": self.data,
"previous_hash": self.previous_hash,
"nonce": self.nonce
}, sort_keys=True).encode()
return hashlib.sha256(block_string).hexdigest()
def mine_block(self, difficulty):
"""
Mine le bloc en trouvant un nonce qui donne un hash avec le nombre
spécifié de zéros au début.
Args:
difficulty: Nombre de zéros requis au début du hash
"""
target = "0" * difficulty
start_time = time.time()
iterations = 0
while self.hash[:difficulty] != target:
self.nonce += 1
self.hash = self.calculate_hash()
iterations += 1
# Afficher la progression toutes les 100 000 itérations
if iterations % 100000 == 0:
elapsed = time.time() - start_time
hash_rate = iterations / elapsed if elapsed > 0 else 0
print(f"Recherche... Nonce: {self.nonce}, Hash: {self.hash[:10]}..., {hash_rate:.0f} hash/s")
end_time = time.time()
elapsed = end_time - start_time
hash_rate = self.nonce / elapsed if elapsed > 0 else 0
print(f"\nBloc miné! Nonce: {self.nonce}")
print(f"Hash: {self.hash}")
print(f"Temps: {elapsed:.2f} secondes")
print(f"Taux de hachage: {hash_rate:.0f} hash/s")
return self
def __str__(self):
return (f"Bloc #{self.index}\n"
f"Timestamp: {self.timestamp}\n"
f"Données: {self.data}\n"
f"Hash précédent: {self.previous_hash}\n"
f"Nonce: {self.nonce}\n"
f"Hash: {self.hash}")
class SimpleBlockchain:
def __init__(self, difficulty=4):
"""
Initialise une blockchain simple.
Args:
difficulty: Difficulté de minage (nombre de zéros au début du hash)
"""
self.chain = [self.create_genesis_block()]
self.difficulty = difficulty
def create_genesis_block(self):
"""Crée et retourne le bloc de genèse."""
return SimpleBlock(0, time.time(), "Bloc de genèse", "0")
def get_latest_block(self):
"""Retourne le dernier bloc de la chaîne."""
return self.chain[-1]
def add_block(self, new_block):
"""
Ajoute un nouveau bloc à la chaîne après l'avoir miné.
Args:
new_block: Bloc à ajouter
"""
new_block.previous_hash = self.get_latest_block().hash
new_block.mine_block(self.difficulty)
self.chain.append(new_block)
def is_chain_valid(self):
"""Vérifie si la chaîne est valide."""
for i in range(1, len(self.chain)):
current_block = self.chain[i]
previous_block = self.chain[i - 1]
# Vérifier que le hash du bloc est correct
if current_block.hash != current_block.calculate_hash():
print(f"Hash invalide pour le bloc {i}")
return False
# Vérifier que le bloc pointe vers le hash du bloc précédent
if current_block.previous_hash != previous_block.hash:
print(f"Lien cassé entre les blocs {i-1} et {i}")
return False
# Vérifier que le hash respecte la difficulté
if current_block.hash[:self.difficulty] != "0" * self.difficulty:
print(f"Le bloc {i} n'a pas été miné correctement")
return False
return True
def print_chain(self):
"""Affiche tous les blocs de la chaîne."""
for block in self.chain:
print(block)
print("-" * 50)
def demo_proof_of_work():
# Niveau de difficulté (ajuster selon la puissance de calcul)
difficulty = 4
print(f"=== Démonstration de Preuve de Travail (difficulté: {difficulty}) ===")
# Créer une nouvelle blockchain
blockchain = SimpleBlockchain(difficulty)
# Ajouter quelques blocs
print("Mining Bloc 1...")
blockchain.add_block(SimpleBlock(1, time.time(), "Données du bloc 1", ""))
print("\nMining Bloc 2...")
blockchain.add_block(SimpleBlock(2, time.time(), "Données du bloc 2", ""))
print("\nMining Bloc 3...")
blockchain.add_block(SimpleBlock(3, time.time(), "Données du bloc 3", ""))
# Vérifier la validité de la chaîne
print("\n=== Vérification de la blockchain ===")
if blockchain.is_chain_valid():
print("La blockchain est valide!")
else:
print("La blockchain n'est PAS valide!")
# Tenter de modifier un bloc
print("\n=== Tentative de modification malicieuse ===")
blockchain.chain[1].data = "Données modifiées malicieusement"
if blockchain.is_chain_valid():
print("La blockchain est valide! (Ce n'est pas ce qu'on attendait)")
else:
print("Modification détectée! La blockchain n'est plus valide.")
# Afficher la chaîne
print("\n=== Blockchain complète ===")
blockchain.print_chain()
demo_proof_of_work()
Partie 5: Questions d'analyse
- Quel est le rôle des constantes
opad
etipad
dans HMAC, et pourquoi sont-elles utilisées plutôt que de simplement concaténer la clé et le message avant le hachage ? - Expliquez l'utilité de la phase d'extraction dans HKDF. Pourquoi ne pas simplement utiliser directement la phase d'expansion sur le matériel de clé initial ?
- Dans un arbre de Merkle, comment la taille de la preuve évolue-t-elle par rapport au nombre de feuilles de l'arbre ? Quels sont les avantages pratiques de cette propriété ?
- Si la difficulté de la preuve de travail augmente d'une unité, combien de fois plus de travail (en moyenne) sera nécessaire pour miner un bloc ? Comment cela se reflète-t-il dans les temps de calcul observés ?
- Comparez et contrastez les arbres de Merkle et les chaînes de blocs en tant que structures de données. Quelles propriétés partagent-ils et en quoi diffèrent-ils ?
TP 4: Étude comparative des fonctions de hachage et des modes opératoires
- Réaliser une analyse comparative approfondie des performances et de la sécurité
- Produire un rapport d'analyse basé sur des benchmarks et des tests pratiques
- Formuler des recommandations pour différents contextes d'utilisation
Consignes générales
Ce TP est un projet plus ouvert où vous devrez réaliser une étude complète des algorithmes vus dans ce chapitre. Vous produirez un rapport d'analyse qui comprendra :
- Introduction
- Contexte et objectifs de l'étude
- Méthodologie employée
- Analyse des fonctions de hachage
- Benchmark de performance (temps, mémoire, CPU) pour différentes tailles de données
- Comparaison des propriétés cryptographiques (collisions, effet d'avalanche)
- Analyse des attaques connues et de leur faisabilité pratique
- Analyse des modes opératoires
- Benchmark de performance pour différentes tailles de données
- Analyse de la propagation d'erreurs et de la résilience
- Évaluation de la sécurité face à différentes attaques
- Comparaison des fonctionnalités (authentification, parallélisme)
- Applications pratiques
- Cas d'usage adaptés à chaque algorithme et mode
- Exemples concrets d'implémentation dans des protocoles modernes
- Limitations et considérations particulières
- Recommandations
- Par contexte d'utilisation (stockage, communication, IoT, etc.)
- Par niveau de sécurité requis
- Par contrainte de performance
- Conclusion
- Synthèse des résultats
- Perspectives d'évolution
Éléments à intégrer
- Fonctions de hachage
- Test de vitesse sur différentes tailles de données (1 Ko, 1 Mo, 10 Mo, 100 Mo)
- Mesure de l'utilisation mémoire
- Quantification de l'effet d'avalanche
- Test de résistance aux collisions partielles
- Modes opératoires
- Benchmark de chiffrement/déchiffrement sur différentes tailles
- Performance en mode séquentiel vs parallèle
- Impact sur les performances de l'authentification (pour les modes AEAD)
- Overhead de communication (taille des IV, nonces, tags)
- Bibliothèques Python
- cryptography
- pycryptodome
- hashlib (standard)
- matplotlib/seaborn (pour les graphiques)
- pandas (pour l'analyse des données)
- Outils système
- OpenSSL (pour les benchmarks)
- time, perf (pour les mesures)
- Références
- Standards NIST
- RFCs pertinents
- Articles scientifiques récents
Exemple de script pour les benchmarks
Voici un exemple de script pour réaliser des benchmarks automatisés :
#!/usr/bin/env python3
import hashlib
import time
import os
import json
import matplotlib.pyplot as plt
import numpy as np
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
import resource
import gc
# Configuration des tests
TEST_SIZES = [1024, 10*1024, 100*1024, 1024*1024, 10*1024*1024] # Tailles en octets
HASH_FUNCTIONS = [
('MD5', hashlib.md5),
('SHA-1', hashlib.sha1),
('SHA-256', hashlib.sha256),
('SHA3-256', hashlib.sha3_256),
('BLAKE2b', hashlib.blake2b),
]
CIPHER_MODES = [
('AES-ECB', modes.ECB()),
('AES-CBC', modes.CBC(os.urandom(16))),
('AES-CTR', modes.CTR(os.urandom(16))),
('AES-GCM', modes.GCM(os.urandom(12))),
]
def generate_test_data(size):
"""Génère des données de test de la taille spécifiée."""
return os.urandom(size)
def benchmark_hash(hash_func, data):
"""Mesure le temps et la mémoire pour hacher les données."""
gc.collect() # Force garbage collection
# Mesure de la mémoire avant
mem_before = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
# Mesure du temps
start_time = time.time()
hash_result = hash_func(data).digest()
end_time = time.time()
# Mesure de la mémoire après
mem_after = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
return {
'time': end_time - start_time,
'memory': mem_after - mem_before,
'size': len(data),
'hash_size': len(hash_result)
}
def benchmark_cipher(cipher_mode, data):
"""Mesure le temps pour chiffrer/déchiffrer les données."""
key = os.urandom(32) # AES-256
if isinstance(cipher_mode, modes.GCM):
encryptor = Cipher(
algorithms.AES(key),
cipher_mode,
backend=default_backend()
).encryptor()
# Chiffrement
start_time = time.time()
ciphertext = encryptor.update(data) + encryptor.finalize()
encryption_time = time.time() - start_time
# Récupérer le tag
tag = encryptor.tag
# Déchiffrement
decryptor = Cipher(
algorithms.AES(key),
modes.GCM(cipher_mode.nonce, tag),
backend=default_backend()
).decryptor()
start_time = time.time()
decryptor.update(ciphertext) + decryptor.finalize()
decryption_time = time.time() - start_time
else:
if isinstance(cipher_mode, modes.ECB):
# Padding pour ECB et CBC
pad_length = 16 - (len(data) % 16)
padded_data = data + bytes([pad_length]) * pad_length
else:
padded_data = data
# Chiffrement
encryptor = Cipher(
algorithms.AES(key),
cipher_mode,
backend=default_backend()
).encryptor()
start_time = time.time()
ciphertext = encryptor.update(padded_data) + encryptor.finalize()
encryption_time = time.time() - start_time
# Déchiffrement
if isinstance(cipher_mode, modes.CTR):
# Pour CTR, on peut réutiliser le même objet
iv_or_nonce = cipher_mode.nonce
elif isinstance(cipher_mode, modes.CBC):
iv_or_nonce = cipher_mode.initialization_vector
else:
iv_or_nonce = None
if iv_or_nonce:
if isinstance(cipher_mode, modes.CTR):
new_mode = modes.CTR(iv_or_nonce)
else:
new_mode = modes.CBC(iv_or_nonce)
else:
new_mode = modes.ECB()
decryptor = Cipher(
algorithms.AES(key),
new_mode,
backend=default_backend()
).decryptor()
start_time = time.time()
decryptor.update(ciphertext) + decryptor.finalize()
decryption_time = time.time() - start_time
return {
'encryption_time': encryption_time,
'decryption_time': decryption_time,
'size': len(data),
'ciphertext_size': len(ciphertext)
}
def run_benchmarks():
"""Exécute tous les benchmarks et retourne les résultats."""
results = {
'hash': [],
'cipher': []
}
# Tester les fonctions de hachage
for size in TEST_SIZES:
data = generate_test_data(size)
for name, func in HASH_FUNCTIONS:
print(f"Benchmark {name} sur {size} octets...")
result = benchmark_hash(func, data)
result['function'] = name
results['hash'].append(result)
# Tester les modes de chiffrement
for size in TEST_SIZES:
if size > 10*1024*1024: # Limiter la taille pour les tests de chiffrement
continue
data = generate_test_data(size)
for name, mode in CIPHER_MODES:
print(f"Benchmark {name} sur {size} octets...")
result = benchmark_cipher(mode, data)
result['mode'] = name
results['cipher'].append(result)
return results
def plot_results(results):
"""Génère des graphiques à partir des résultats."""
# Conversion en format plus facile à utiliser
hash_df = []
for r in results['hash']:
hash_df.append({
'function': r['function'],
'size': r['size'] / 1024, # KB
'time': r['time'] * 1000, # ms
'throughput': r['size'] / (r['time'] * 1024 * 1024) if r['time'] > 0 else 0 # MB/s
})
cipher_df = []
for r in results['cipher']:
cipher_df.append({
'mode': r['mode'],
'size': r['size'] / 1024, # KB
'encryption_time': r['encryption_time'] * 1000, # ms
'decryption_time': r['decryption_time'] * 1000, # ms
'encryption_throughput': r['size'] / (r['encryption_time'] * 1024 * 1024) if r['encryption_time'] > 0 else 0, # MB/s
'decryption_throughput': r['size'] / (r['decryption_time'] * 1024 * 1024) if r['decryption_time'] > 0 else 0 # MB/s
})
# Créer les graphiques
plt.figure(figsize=(12, 6))
# Graphique des fonctions de hachage
ax1 = plt.subplot(1, 2, 1)
for func in set(r['function'] for r in hash_df):
data = [r for r in hash_df if r['function'] == func]
sizes = [r['size'] for r in data]
throughputs = [r['throughput'] for r in data]
ax1.plot(sizes, throughputs, marker='o', label=func)
ax1.set_title('Performance des fonctions de hachage')
ax1.set_xlabel('Taille des données (KB)')
ax1.set_ylabel('Débit (MB/s)')
ax1.legend()
ax1.grid(True, linestyle='--', alpha=0.7)
# Graphique des modes de chiffrement
ax2 = plt.subplot(1, 2, 2)
for mode in set(r['mode'] for r in cipher_df):
data = [r for r in cipher_df if r['mode'] == mode]
sizes = [r['size'] for r in data]
throughputs = [r['encryption_throughput'] for r in data]
ax2.plot(sizes, throughputs, marker='o', label=f"{mode} (chiffrement)")
ax2.set_title('Performance des modes de chiffrement')
ax2.set_xlabel('Taille des données (KB)')
ax2.set_ylabel('Débit (MB/s)')
ax2.legend()
ax2.grid(True, linestyle='--', alpha=0.7)
plt.tight_layout()
plt.savefig('benchmark_results.png')
plt.close()
print("Graphiques générés et sauvegardés dans 'benchmark_results.png'")
def save_results(results):
"""Sauvegarde les résultats dans un fichier JSON."""
with open('benchmark_results.json', 'w') as f:
json.dump(results, f, indent=2)
print("Résultats sauvegardés dans 'benchmark_results.json'")
if __name__ == "__main__":
print("Démarrage des benchmarks...")
results = run_benchmarks()
save_results(results)
try:
plot_results(results)
except ImportError:
print("Matplotlib non installé. Les graphiques n'ont pas été générés.")
print("Benchmarks terminés!")
Critères d'évaluation
- Rigueur des tests et de la méthodologie
- Profondeur de l'analyse des résultats
- Pertinence des recommandations selon les contextes
- Qualité des illustrations et des graphiques
- Compréhension des aspects sécuritaires et des compromis performance/sécurité
- Capacité à extrapoler vers les tendances futures
Navigation
Dépendances requises
Pour réaliser ces TPs, vous aurez besoin d'installer les bibliothèques Python suivantes :
# Installation des dépendances pip install cryptography pip install matplotlib pip install numpy pip install pycryptodome # Pour les graphiques du TP 4 pip install seaborn pandas
Et les outils système suivants :
- OpenSSL (généralement préinstallé sur Linux)
- ImageMagick (pour la visualisation de l'ECB)