Vigenère Cipher Project

Robin Boucher - Cryptography Portfolio

1. Fonctions de Base du Chiffrement

Le chiffrement de Vigenère est une méthode classique de chiffrement par substitution polyalphabétique. Contrairement au chiffre de César, il utilise un mot-clé pour appliquer des décalages variables sur chaque lettre du message.

  • Principe de base : Chaque lettre du message clair est décalée dans l’alphabet selon la valeur correspondante de la clé. Par exemple, si la clé est "CLE", alors : La première lettre du message est décalée selon "C" (2), La deuxième selon "L" (11), La troisième selon "E" (4), etc.
  • Démonstration:

    def vigenere_chiffrer(texte_clair: str, cle: str, conserver_casse: bool = True) -> str:
        texte_chiffre = []
        ## Répète la clé pour qu'elle ait la même longueur que le message
        cle_repetee = (cle * (len(texte_clair) // len(cle) + 1)).lower()
        index_cle = 0
    
        for caractere in texte_clair:
            if caractere.isalpha():
                ## Détermine la base ASCII selon la casse (majuscule ou minuscule)
                base = ord('A') if caractere.isupper() else ord('a')
                ## Récupère la lettre correspondante dans la clé répétée
                caractere_cle = cle_repetee[index_cle]
                ## Calcul du décalage en ASCII
                decalage = ord(caractere_cle) - ord('a')
                ## Applique le décalage au caractère
                caractere_chiffre = chr((ord(caractere.lower()) - ord('a') + decalage) % 26 + base)
                texte_chiffre.append(caractere_chiffre if conserver_casse else caractere_chiffre.upper())
                index_cle += 1
            else:
                ## Conserve les caractères non alphabétiques tels quels
                texte_chiffre.append(caractere)
    
        return ''.join(texte_chiffre)
    
    
    def vigenere_dechiffrer(texte_chiffre: str, cle: str, conserver_casse: bool = True) -> str:
        texte_clair = []
        ## Répète la clé pour qu'elle ait la même longueur que le message chiffré
        cle_repetee = (cle * (len(texte_chiffre) // len(cle) + 1)).lower()
        index_cle = 0
    
        for caractere in texte_chiffre:
            if caractere.isalpha():
                ## Détermine la base ASCII selon la casse (majuscule ou minuscule)
                base = ord('A') if caractere.isupper() else ord('a')
                ## Récupère la lettre correspondante dans la clé répétée
                caractere_cle = cle_repetee[index_cle]
                ## Calcul du décalage en ASCII
                decalage = ord(caractere_cle) - ord('a')
                ## Retire le décalage pour obtenir le caractère original
                caractere_dechiffre = chr((ord(caractere.lower()) - ord('a') - decalage) % 26 + base)
                texte_clair.append(caractere_dechiffre if conserver_casse else caractere_dechiffre.upper())
                index_cle += 1
            else:
                ## Conserve les caractères non alphabétiques tels quels
                texte_clair.append(caractere)
    
        return ''.join(texte_clair)
    

    2. Limites du chiffre de Vigenère

    La clé est répétée

    Attaque possible via la méthode de répétitions de séquences ou indice de coïncidence, ce qui permet d’identifier la longueur de la clé.

    Une fois la longueur de la clé connue, on peut découper le texte en sous-textes et utiliser l’analyse fréquentielle pour retrouver la clé, rendant le message facile à déchiffrer.

    Démonstration:

    import re
    from collections import Counter
    
    def calculer_indice_coincidence(texte: str) -> float:
    
        ## Supprime tous les caractères non alphabétiques et passe en minuscules
    
        texte = re.sub(r'[^a-zA-Z]', '', texte).lower()
        if len(texte) < 2:
            return 0.0
        freq = Counter(texte)
        total = len(texte)
    
        ## Calcul de l'indice de coïncidence : somme des fréquences combinatoires divisée par le 
        nombre total de combinaisons possibles
    
        ic = sum(count * (count - 1) for count in freq.values()) / (total * (total - 1))
        return ic
    
    
    def examen_kasiski(texte_chiffre: str, longueur_cle_max: int = 10) -> list:
    
        ## Nettoyage du texte : uniquement lettres, majuscules
    
        texte_chiffre = re.sub(r'[^a-zA-Z]', '', texte_chiffre).upper()
        sequences = {}
    
        ## On cherche toutes les séquences de 3 à 5 lettres dans le texte chiffré
    
        for longueur in range(3, 6):
            for i in range(len(texte_chiffre) - longueur):
                seq = texte_chiffre[i:i + longueur]
                if seq in sequences:
                    sequences[seq].append(i)
                else:
                    sequences[seq] = [i]
        distances = []
    
        ## Pour chaque séquence répétée, on calcule la distance entre ses positions
    
        for seq, positions in sequences.items():
            if len(positions) > 1:
                for i in range(len(positions)):
                    for j in range(i + 1, len(positions)):
                        distances.append(positions[j] - positions[i])
        facteurs = Counter()
    
        ## On compte combien de fois chaque longueur possible divise les distances trouvées
    
        for distance in distances:
            for longueur_possible in range(2, min(distance, longueur_cle_max) + 1):
                if distance % longueur_possible == 0:
                    facteurs[longueur_possible] += 1
    
        ## On retourne les longueurs les plus probables de la clé
    
        return [longueur for longueur, _ in facteurs.most_common()]
    
    
    def attaque_frequence(texte_chiffre: str, longueur_cle_probable: int = None, langue: str = 'fr') -> str:
        tables_freq = {
            'en': 'etaoinshrdlcumwfgypbvkjxqz',
            'fr': 'easnrtoulidcmpévqfbghjàxèyêzwk'
        }
        texte_chiffre_propre = re.sub(r'[^a-zA-Z]', '', texte_chiffre).upper()
        if longueur_cle_probable is None:
    
            ## Si aucune longueur n’est fournie, on utilise l’examen de Kasiski
    
            longueur_cle_probable = examen_kasiski(texte_chiffre)[0] if len(texte_chiffre) > 50 else 1
        groupes = [''] * longueur_cle_probable
    
        ## On divise le texte chiffré en sous-groupes selon la longueur probable de la clé
    
        for i, caractere in enumerate(texte_chiffre_propre):
            groupes[i % longueur_cle_probable] += caractere
        cle_devinee = []
        for groupe in groupes:
            freq = Counter(groupe.lower())
    
            ## La lettre la plus fréquente dans le groupe est supposée correspondre à la lettre la plus 
            fréquente dans la langue
    
            plus_frequent = freq.most_common(1)[0][0]
            decalage_devine = (ord(plus_frequent) - ord(tables_freq[langue][0])) % 26
            cle_devinee.append(chr(decalage_devine + ord('a')))
        cle_devinee = ''.join(cle_devinee)
    
        ## On déchiffre le message avec la clé devinée
        
        return vigenere_dechiffrer(texte_chiffre, cle_devinee)
    

    3. Améliorations possibles

    Niveau 1 : Inversion du texte

  • Principe : Inverser le texte clair avant de lancer le chiffrement. Avantage : Perturbe la structure linguistique et rend plus difficile l’analyse fréquentielle.
  • Niveau 2 : Padding aléatoire

  • Principe : Diviser le texte chiffré en blocs et ajouter entre eux des caractères de bruit aléatoires. Avantage : Rend le texte chiffré moins exploitable statistiquement.
  • Niveau 3 : Double chiffrement avec clé transformée

  • Principe : Chiffrer deux fois, avec une clé inversée et décalée. Avantage : Augmente la complexité du système sans nécessiter une clé très longue.
  • Autres améliorations possible:

  • Utiliser une clé aléatoire et non répétée → One-Time Pad (théoriquement inviolable).
  • Combiner avec d’autres méthodes (permutation, codage ASCII étendu).
  • Utiliser une clé auto-générée à partir du message (Autokey Cipher).
  • 4. Démonstration

    Caesar Cipher Toolkit
    Ici pour voir le fichier vigenere.py complet
    # -*- coding: utf-8 -*- """ Outil de chiffrement Vigenère - Version avec améliorations ========================================================= Fonctionnalités : - Chiffrement Vigenère classique - Amélioration Niveau 1 : Inversion du texte - Amélioration Niveau 2 : Ajout de padding aléatoire - Amélioration Niveau 3 : Double chiffrement avec clé transformée """ import re import random def vigenere_chiffrer(texte_clair: str, cle: str) -> str: """ Chiffre un texte avec le chiffrement de Vigenère. ## Version classique utilisant une clé répétée. """ ciphertext = [] cle_repetee = (cle * (len(texte_clair) // len(cle) + 1)).lower() idx_cle = 0 for char in texte_clair: if char.isalpha(): base = ord('A') if char.isupper() else ord('a') shift = ord(cle_repetee[idx_cle]) - ord('a') chiffrer_char = chr((ord(char.lower()) - ord('a') + shift) % 26 + base) ciphertext.append(chiffrer_char.upper()) idx_cle += 1 else: ciphertext.append(char) return ''.join(ciphertext) def vigenere_dechiffrer(texte_chiffre: str, cle: str) -> str: """ Déchiffre un texte chiffré par Vigenère. ## Fonction symétrique à `vigenere_chiffrer`. """ plaintext = [] cle_repetee = (cle * (len(texte_chiffre) // len(cle) + 1)).lower() idx_cle = 0 for char in texte_chiffre: if char.isalpha(): base = ord('A') if char.isupper() else ord('a') shift = ord(cle_repetee[idx_cle]) - ord('a') decodage_char = chr((ord(char.lower()) - ord('a') - shift) % 26 + base) plaintext.append(decodage_char.upper()) idx_cle += 1 else: plaintext.append(char) return ''.join(plaintext) def vigenere_ameliorer_chiffrement(texte_clair: str, cle: str, niveau_securite: int = 1) -> str: """ Chiffre avec des améliorations de sécurité selon le niveau choisi. ## Options : - Niveau 1 : Inversion du texte - Niveau 2 : Padding aléatoire - Niveau 3 : Double chiffrement avec clé transformée """ ### AMÉLIORATION NIVEAU 1 : INVERSION DU TEXTE AVANT CHIFFREMENT if niveau_securite == 1: texte_inverse = texte_clair[::-1] # Inverser le message return vigenere_chiffrer(texte_inverse, cle) ### AMÉLIORATION NIVEAU 2 : AJOUT DE CARACTÈRES DE PADDING ALÉATOIRES elif niveau_securite == 2: texte_inverse = texte_clair[::-1] texte_chiffre = vigenere_chiffrer(texte_inverse, cle) chars_padding = '!@#$%^&*()_+-=[]{}|;:,.<>?' resultat = [] i = 0 while i < len(texte_chiffre): taille_chunk = random.randint(3, 5) chunk = texte_chiffre[i:i+taille_chunk] resultat.append(chunk) if i + taille_chunk < len(texte_chiffre): resultat.append(random.choice(chars_padding)) i += taille_chunk return ''.join(resultat) ### AMÉLIORATION NIVEAU 3 : DOUBLE CHIFFREMENT AVEC CLÉ TRANSFORMÉE elif niveau_securite == 3: premier_passage = vigenere_chiffrer(texte_clair, cle) # Transformation de la clé : inversion + décalage de +1 sur chaque caractère cle_transformee = ''.join( chr((ord(c.lower()) - ord('a') + 1) % 26 + ord('a')) for c in cle[::-1] ) # Deuxième passage de chiffrement avec la clé modifiée return vigenere_chiffrer(premier_passage, cle_transformee) else: raise ValueError("Niveau de sécurité invalide. Utiliser 1, 2 ou 3.") def vigenere_ameliorer_dechiffrement(texte_chiffre: str, cle: str, niveau_securite: int = 1) -> str: """ Déchiffre un texte selon le niveau d'amélioration utilisé. ## Options : - Niveau 1 : Inversion du texte - Niveau 2 : Padding aléatoire - Niveau 3 : Double chiffrement avec clé transformée """ ### AMÉLIORATION NIVEAU 1 : INVERSION DU TEXTE APRÈS DÉCHIFFREMENT if niveau_securite == 1: texte_dechiffre = vigenere_dechiffrer(texte_chiffre, cle) return texte_dechiffre[::-1] ### AMÉLIORATION NIVEAU 2 : SUPPRESSION DES CARACTÈRES DE PADDING elif niveau_securite == 2: # Supprimer les caractères de padding texte_nettoyé = re.sub(r'[!@#$%^&*()_+\-=$\[\]{}|;:,.<>?]', '', texte_chiffre) # Déchiffrer le texte nettoyé texte_dechiffre = vigenere_dechiffrer(texte_nettoyé, cle) # Inverser le résultat final return texte_dechiffre[::-1] ### AMÉLIORATION NIVEAU 3 : DÉCHIFFREMENT DOUBLE AVEC CLÉ TRANSFORMÉE elif niveau_securite == 3: # Première déchiffrement avec la clé transformée cle_transformee = ''.join( chr((ord(c.lower()) - ord('a') + 1) % 26 + ord('a')) for c in cle[::-1] ) premier_passage = vigenere_dechiffrer(texte_chiffre, cle_transformee) # Deuxième déchiffrement avec la clé originale return vigenere_dechiffrer(premier_passage, cle) else: raise ValueError("Niveau de sécurité invalide. Utiliser 1, 2 ou 3.") # Exemple d'utilisation if __name__ == "__main__": texte = "Le chiffrement de Vigenère est une méthode polyalphabétique." cle = "securite" print("=== CHIFFREMENT CLASSIQUE ===") print(vigenere_chiffrer(texte, cle)) print("\n=== AMÉLIORATION NIVEAU 1 (INVERSION) ===") chiffré_niv1 = vigenere_ameliorer_chiffrement(texte, cle, 1) print("Chiffré:", chiffré_niv1) print("Déchiffré:", vigenere_ameliorer_dechiffrement(chiffré_niv1, cle, 1)) print("\n=== AMÉLIORATION NIVEAU 2 (PADDING ALÉATOIRE) ===") chiffré_niv2 = vigenere_ameliorer_chiffrement(texte, cle, 2) print("Chiffré:", chiffré_niv2) print("Déchiffré:", vigenere_ameliorer_dechiffrement(chiffré_niv2, cle, 2)) print("\n=== AMÉLIORATION NIVEAU 3 (DOUBLE CHIFFREMENT) ===") chiffré_niv3 = vigenere_ameliorer_chiffrement(texte, cle, 3) print("Chiffré:", chiffré_niv3) print("Déchiffré:", vigenere_ameliorer_dechiffrement(chiffré_niv3, cle, 3))
    Démonstration du chiffrement César

    À retenir

    Ces techniques complexifient l'attaque mais ne garantissent pas la sécurité. Pour une vraie protection :

    • Vigenère reste également uniquement pédagogique
    • Vigenère offre un niveau supérieur de sécurité
    • AES/RSA sont indispensables pour des données sensibles