IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Vous êtes nouveau sur Developpez.com ? Créez votre compte ou connectez-vous afin de pouvoir participer !

Vous devez avoir un compte Developpez.com et être connecté pour pouvoir participer aux discussions.

Vous n'avez pas encore de compte Developpez.com ? Créez-en un en quelques instants, c'est entièrement gratuit !

Si vous disposez déjà d'un compte et qu'il est bien activé, connectez-vous à l'aide du formulaire ci-dessous.

Identifiez-vous
Identifiant
Mot de passe
Mot de passe oublié ?
Créer un compte

L'inscription est gratuite et ne vous prendra que quelques instants !

Je m'inscris !

Python : apprendre le chiffrement symétrique par portion
Un billet de Julien Garderon

Le , par Nothus

0PARTAGES

Principes et état d’esprit

Rappel : un signe est un élément d’imprimerie. Les signes correspondent à l’ensemble des caractères imprimables ainsi que les espaces et les caractères spéciaux.

Il s’agit d'observer aujourd’hui une méthode de chiffrement symétrique à transposition : on modifie mathématiquement l’ordre du contenu, sans en changer les symboles représentés. Elle utilise un principe similaire à celui utilisé par le chiffrement AES. Ce type de chiffrement symétrique n’augmente pas la taille totale transférée du message, en dehors de la clé utilisée (qui peut être utilisée à plusieurs reprises, même si ce n'est pas conseillé...).

Cette méthode semble indéchiffrable (le contenu est temporairement « perdu ») à une condition : que ladite clé reste secrète. C’est pour cette raison que l’utilisation sur Internet n’est pas possible – tout du moins sans utiliser d’’abord un chiffrement asymétrique à clés publiques ! Dans ce cas, la sécurité totale du système d’échange repose sur la fiabilité du chiffrement asymétrique : l’interception de la clé symétrique sur une connexion chiffrée asymétriquement mais écoutée et piratée, fait perdre toute la confidentialité. Le couple symétrique/asymétrique n’a donc intérêt (sauf saucuneous certaines conditions trop particulières, comme sur des lignes optiques polarisées) car la fiabilité du chiffrement asymétrique suffit à garantir la sécurité du message.

Il existe de nombreuses raisons de chiffrer symétriquement ses messages : la confidentialité bien sûr, mais parfois aussi des raisons techniques, en obligeant à des opérations sur le message qui permet d’en garantir « totalement » le contenu. Ne rentrons pas ici sur les limites juridiques ainsi que les questions illégales que peuvent cacher le chiffrement : il s’agit d’un outil, comme un couteau - qui peut aider l’homme à manger et à se défendre, comme à tuer son prochain ou à agresser. C’est moins la fonction de l’outil que l’usage qui en détermine une forme de moralité…

Dans mon exemple, un texte (long) et une phrase « passe » (donc la longueur doit être raisonnable, au moins égale au quart de la taille de la matrice utilisée et où cette matrice est dimensionnée au paquet de signes à traiter à un instant t), seront utilisés conjointement.

L’intérêt de la phrase « passe » est d’en retenir une suite de chiffres – ça tombe bien, Python utilise ce principe pour gérer ses strings -, qui sera associée à des opérations (en réalité à des fonctions bêtes). Ces opérations peuvent être potentiellement publiques : c’est leur ordre et leur fréquence qui comptent. La connaissance de l’opération individuelle (où même d’une partie) ne permet pas de connaître le chiffrement général. Le code source de cet outil peut être diffusé sans restriction.

La phrase « passe » est donc la recette du chiffrement : elle donne à la fois l’ordre et le nombre d’opérations à effectuer une matrice de caractères/signes, eux aussi convertis en nombre. Ainsi, tel que les noms de domaine sur le web, on retient une phrase humainement compréhensible (donc plus facile à retenir) qu’une suite de chiffres ou d'expédients / d'indications…

Exemple :
Code python : Sélectionner tout
1
2
3
>>> list(map(ord,list("toto va au marché"))) # ---> la fonction native de Python "ord", donne la représentation le code Unicode 
[116, 111, 116, 111, 32, 118, 97, 32, 97, 117, 32, 109, 97, 114, 99, 104, 233] 
{ effectuer l’opération 116 puis 111 puis 116 puis 111 puis 32}

Pour simplifier, nous utiliserons seulement trois caractères différents, qui resteront des caractères, correspondant à une opération « simple » de matrice sous Numpy :

Code python : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
def operation_A(npArray): 
    return np.roll(npArray,1)   
  
def operation_B(npArray): 
    return np.rot90(npArray) 
  
def operation_C(npArray): 
    return np.flip(npArray,1) 
  
OPERATIONS = { 
    "a" : operation_A, 
    "b" : operation_B, 
    "c" : operation_C 
}

Fonctionnement

Nous allons tout d’abord créer une matrice avec Numpy, en veillant à ne pas dépasser une certaine taille de matrice, qui correspond à une portion arbitraire du texte à chiffrer. Si la portion est trop petite, on ajoute à DROITE le nombre de signes nécessaires par un signe pris au hasard DANS L’ENSEMBLE du texte à chiffre. Gardons à l'esprit que ce signe ajouté se doit d'être plutôt dans une fréquence d’apparition basse.

Création de la matrice :

Code python : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
MATRICES_taille = (5,5) 
MATRICES_nbre = MATRICES_taille[0] * MATRICES_taille[1]  
  
import numpy as np 
  
def matrice_creer(texte_portion,ajout="*"): 
    if len(texte_portion)<MATRICES_nbre: 
        texte_portion = texte_portion.rjust(MATRICES_nbre, ajout) 
    elif len(texte_portion)>MATRICES_nbre: 
        raise ValueError( 
            "la portion de texte ne peut excédent %s signes" % MATRICES_nbre 
        ) 
    texte_portion = np.asarray( 
        list(map(ord,list(texte_portion))) 
    ) 
    texte_portion = np.reshape( 
        texte_portion, 
        MATRICES_taille 
    ) 
    return texte_portion

Au choix, trouver l’ajout :

Code python : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
def occurences_chgt_frequence(texte): # ---> « hasard » total 
    return ''.join(random.choice(list(texte)) for _ in range(1)) 
  
def occurences_chgt_frequence(texte): # ---> le premier des signes les moins fréquents 
    frequence = list( 
        ( 
            lettre, texte.count(lettre) 
        ) for lettre in list(texte) 
    ) 
    return sorted( 
        frequence, 
        key = lambda couple: couple[1] 
    )[0][0]

Le corps du script, ici pour seulement les 20 premiers caractères afin de garder de se placer dans un cas un peu spécial :

Code python : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
phrase = "abcaabacaabacccaba" 
  
texte_original = "Ceci est un message totalement confidentiel: \ 
les coordonnées du paquet de cookies sont au placard haut, \ 
étagère de droite. Stop." 
  
ajout = occurences_chgt_frequence( 
    texte_original 
) 
  
texte_np = matrice_creer( 
    texte_original[0:20], 
    ajout 
) 
  
for signe in phrase: 
    texte_np = OPERATIONS[signe]( 
        texte_np 
    ) 
  
texte_np = np.reshape( 
    texte_np, 
    (1, MATRICES_nbre) 
) 
  
print("".join(list(map(chr,texte_np.tolist()[0]))))

Lorsque j’exécute le script, en laissant le signe d’ajout à l’opérateur étoile (« * »), voici ce que cela donne pour la portion 0:20 :

Code : Sélectionner tout
1
2
3
Phrase "abcaabacaabacccaba" → ****** ic*Cu t*e***ens ** 
 
Phrase "abcaabacaabaccaba" → eu t*e ic*C***** ***sn***
… donc le changement d’un seul caractère dans la phrase « passe » a profondément changé la portion chiffrée.

Voici pour la totalité du message :

Code : Sélectionner tout
1
2
3
Phrase "abcaabacaabacccaba" → uot egaCttnemel tedifnos l :leiandrooc ced seéntoteuqapsukooc en tnos s icalp ud tuah dearègatéa,ord edreotS .e ise ice pt.sem n 
 
Phrase "abcaabacaabaccaba" →  otS .e iord edrerègatéa,tuah deacalp ud tnos s ikooc en teuqapsud seéntodrooc cel :leianedifnos tnemel tot egaCt.sem nutpse ice
Principales observations

– Il est possible d’associer plusieurs phrases « passe », en fonction du nombre de relais : chacun peut ainsi avoir un « maillon de la chaîne » (principe du block-chain) lisible et le reste du contenu indéchiffrable. Utile pour avoir un relais sans confiance (il n’a pas la possibilité de connaître le contenu) ou par exemple donner des indications (envoyer le reste du contenu à tel relais si on envoie des paquets, effectuer un traitement / une vérification, etc).

– Le travail non directement sur des nombres sous Numpy, mais des matrices de bits, change l’ordre du contenu et aussi la représentation envoyées : en effet, chaque bit étant rangé dans un ordre différent, la matrice revenue à une seule dimension, donnera des signes RADICALEMENT différents.

– Rendre publiques les opérations (et notamment la taille générique de la matrice), pour ne transférer secrètement que les phrases « passe » et le texte chiffré, économise BEAUCOUP de données transférées.

– Utiliser tous les signes usuels de l’alphabet, pour n’envoyer que des indications de ressources entre deux personnes, qui échangeront ensuite numériquement : ainsi la page 112 de telle édition de tel ouvrage sert de phrase « passe »… D’ailleurs la phrase peut être envoyée généralement par tout moyen sécurisé (connexion TSL, liaison optique polarisée, pigeon avec un entraînement militaire, bouteille à la mer… ah non pas la bouteille).

– Avoir un jeu de plusieurs alternatives pour une même opération et en avoir, à l’avance, déterminé l’usage : ainsi l’opération 1-1 est utilisée une fois, puis si elle est appelée à nouveau (pour une nouvelle portion) l’opération 1-2, puis si elle est appelée à nouveau l'opération 1-3, puis on revient à 1-1, etc. L’intérêt est de rajouter un (gros) grain de sable et de rendre pratiquement impossible à déterminer une série d’opérations, pourtant toutes connues par le potentiel espion...

– Une telle méthode est idéale pour « couper » le chiffrement d’un contenu très important en une multitude de portions, permettant l’usage facilité de l’asynchronisme.

Edition - ajout d'un exemple plus complet

Voici un exemple complet, asynchrone, qui n'utilise pas Numpy et à vocation pédagogique :

Code python : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
  
import re  
  
class MatriceSimple: 
  
    debug = False  
  
    MATRICE_taille_colonne = 8   
    OPERATIONS = {}  
  
    operations_binaires = False  
  
    def __init__(self, contenu_original, binaire=False ): 
        self.contenu_original = contenu_original 
        self.matriceComplexe = False 
        self.operations_binaires = True if binaire is True else False  
  
    def __log__(self, *args): 
        """méthode de log interne ; à surcharger""" 
        if self.debug: 
            print(*args) 
  
    def peupler(self): 
        if self.operations_binaires:  
            matriceSimple = list( 
                "".join( 
                    ( 
                        bin(ord(signe))[2:] 
                    ).rjust(8,"0") for signe in self.contenu_original  
                ) 
            )  
        else: 
            matriceSimple = list(self.contenu_original) 
        yield 
        self.matriceComplexe = [ 
            []  
        ]  
        y = 0  
        for bit in matriceSimple:  
            if len(self.matriceComplexe[y]) >= self.MATRICE_taille_colonne: 
                self.matriceComplexe.append(  
                    []  
                ) 
                y += 1  
            self.matriceComplexe[y].append(  
                bit 
            ) 
            yield  
  
    def crypter(self, phrase, inverse=False):  
        yield from self.__matrice_transformer__( 
            phrase if inverse is False else phrase[::-1], 
            inverse  
        )  
  
    def decrypter(self, phrase): 
        yield from self.crypter(  
            phrase, 
            True   
        )  
  
    def __matrice_creer__(self):  
        yield 
        matriceSimple = list( 
            "".join( 
                (  
                    bin(ord(signe))[2:] 
                ).rjust(8,"0") for signe in self.contenu_original  
            ) 
        )  
        yield 
        self.matriceComplexe = [ 
            []  
        ]  
        y = 0  
        for bit in matriceSimple:  
            if len(self.matriceComplexe[y]) > self.MATRICES_taille[1]: 
                self.matriceComplexe.append( 
                    []  
                ) 
                y += 1  
            self.matriceComplexe[y].append(  
                bit 
            ) 
            yield  
  
    def __matrice_transformer__(self, phrase, inverse): 
        for op_id in phrase: 
            try: 
                fct = getattr(  
                    self,  
                    "operation_%s" % op_id 
                )  
                self.__log__( 
                    "opération '%s' demandée : %s " % ( 
                        op_id, 
                        fct.__doc__ 
                    ) 
                ) 
                yield from fct(  
                    op_id,  
                    inverse  
                ) 
            except: 
                raise ValueError("cette opération '%s' n'existe pas" % op_id)  
  
    def extraire(self): 
        matrice = yield from self.__matrice_reduire__()  
        message = yield from self.__matrice_traduire__( 
            matrice   
        ) 
        return message  
  
    def __matrice_reduire__(self):  
        if self.matriceComplexe is False: 
            raise Exception(  
                "aucun contenu encore crypté" 
            )  
        bits_modifies = []  
        for ligne in self.matriceComplexe:  
            for bit in ligne: 
                bits_modifies.append(bit)  
                yield 
        return bits_modifies 
  
    def __matrice_traduire__(self, matriceSimple):  
        message = []  
        if self.operations_binaires: 
            for bit in re.finditer( 
                "([01]{8})", 
                "".join(matriceSimple) 
            ):  
                    message.append( 
                        chr( 
                            int( 
                                bit.group(0), 
                                2 
                            ) 
                        )  
                    ) 
                    yield 
        else: 
            for signe in matriceSimple:  
                message.append( 
                    signe   
                ) 
                yield   
        return "".join(message) 
  
class CryptRapide(MatriceSimple): 
  
    def operation_A(self, op_id, inverse=False): 
        """décalage d'une ligne""" 
        ancienne_matrice = self.matriceComplexe[:] 
        yield 
        if inverse: 
            self.matriceComplexe = [ancienne_matrice[-1],]+ancienne_matrice[0:-1]  
        else:  
            self.matriceComplexe = ancienne_matrice[1:]+[ancienne_matrice[0],]  
  
    def operation_B(self, op_id, inverse=False): 
        """décalage d'une colonne dans chaque ligne"""  
        ancienne_matrice = self.matriceComplexe[:]  
        yield 
        nouvelle_matrice = []  
        if inverse: 
            for ligne in ancienne_matrice: 
                ligne = ligne[:] 
                nouvelle_matrice.append( 
                    [ligne[-1],] + ligne[0:-1]  
                )  
                yield 
        else:  
            for ligne in ancienne_matrice: 
                ligne = ligne[:] 
                nouvelle_matrice.append( 
                    [*ligne[1:], ligne[0]]  
                )  
                yield 
        self.matriceComplexe = nouvelle_matrice  
  
    def operation_C(self, op_id, inverse=False): 
        """inversion des lignes"""  
        ancienne_matrice = self.matriceComplexe[:]  
        yield 
        nouvelle_matrice = []  
        for ligne in ancienne_matrice: 
            nouvelle_matrice.append( 
                ligne[::-1] 
            ) 
            yield  
        self.matriceComplexe = nouvelle_matrice   
  
def auto(binaire, phrase_id, phrase, texte_original): 
    global verrou_affichage  
    ma_matrice = CryptRapide( 
        texte_original 
    )  
    ma_matrice.operations_binaires = binaire 
  
    yield from ma_matrice.peupler() 
    print("! log (%s) : La matrice du contenu a été créée" % phrase_id) 
  
    yield from ma_matrice.crypter( 
        phrase  
    )  
    print("! log (%s) : le contenu a été crypté" % phrase_id)  
  
    valeur_message = yield from ma_matrice.extraire()  
    print("! log (%s) : message crypté :" % phrase_id, valeur_message) 
  
    yield from ma_matrice.decrypter( 
        phrase  
    )  
    print("! log (%s) : message décrypté :" % phrase_id) 
  
    valeur_message = yield from ma_matrice.extraire()  
    print("! log (%s) : message décrypté :" % phrase_id, valeur_message) 
  
    return "Fin de la tache %s" % phrase_id  
  
def boucle_asynchrone(listes_taches): 
    taches_generateurs = []  
    for tache in listes_taches: 
        fct, *args = tache  
        taches_generateurs.append( 
            fct(*args) 
        )  
    while True: 
        try: 
            tache = taches_generateurs.pop(0) 
            next(tache) 
            taches_generateurs.append( 
                tache 
            )  
        except StopIteration as err: 
            print(">>> Le générateur a terminé et dit : ", err)  
        if len(taches_generateurs)==0: 
            break  
  
if __name__=="__main__":  
  
  
    #VERSION NON-BINAIRE -> moins efficace  
    boucle_asynchrone( 
        [ 
            ( 
                auto, # fct appelée dans la boucle événementielle  
                False, # ne sera pas traitement binairement  
                1, # id de la tache  
                "AAACBABCBCABACBBB", # phrase passe  
                "Ceci est un super message secret, plus long \ 
mais proportionnellement il y a moins de traitements, \ 
c'est bizarre hein ? Mais c'est comme ça, il fallait un exemple..." # contenu du message  
            ),  
            ( 
                auto, 
                False, 
                2, 
                "AAACBABCBCABACBACBBCAAABBBB",  
                "Ceci est un message secret de Nothus (Julien Garderon, août 2017)."  
            )  
        ] 
    )  
  
    print("- - - - - - - - - - - - - - -") 
  
    #VERSION BINAIRE -> plus efficace (car la matrice générée est plus grande ; plus de dimensions (3, 4, ... au lieu de 2D utilisées dans cet exemple) ou une taille de phrase plus grande = plus de sécurité)  
    boucle_asynchrone( 
        [ 
            ( 
                auto, 
                True,  
                1, 
                "AAACBABCBCABACBBB",  
                "Ceci est un super message secret, plus long \ 
mais proportionnellement il y a moins de traitements, \ 
c'est bizarre hein ? Mais c'est comme ça, il fallait un exemple..."  
            ),  
            ( 
                auto, 
                True,  
                2, 
                "AAACBABCBCABACBACBBCAAABBBB",  
                "Ceci est un message secret de Nothus (Julien Garderon, août 2017)."  
            )  
        ] 
    )

Une erreur dans cette actualité ? Signalez-nous-la !