Archi Projet Chiffrement

Identification

Infoforall

16 - Projet XOR


Vous avez vu le chiffrement symétrique.

Vous allez aujourd'hui réaliser un module Python de chiffrement symétrique basé sur le système XOR.

Vous aurez donc à la fin de ce mini-projet de quoi chiffrer n'importe lequel de vos fichiers de façon rudimentaire.

C'est une mini-projet vous préparant à l'épreuve pratique de NSI du mois de juin.

  • Mini-projet car c'est un projet très guidé, ou un gros TP, mai sans les réponses.
  • Quelques exercices semblables à l'épreuve 1 de l'épreuve pratique (fonction à réaliser à partir de rien)
  • Quelques exercices semblables à l'épreuve 2 de l'épreuve pratique (fonction à trous)

Evaluation ✎ : -

Documents de cours :

1 - Exercices type 1 : programmation libre

Nous allons vouloir utiliser le chiffrement symétrique basé sur XOR.

01° Compléter la table de vérité du XOR (OU eXclusif).

a b a XOR b
0 0 ?
0 1 ?
1 0 ?
1 1 ?

...CORRECTION...

a b a XOR b
0 0 0
0 1 1
1 0 1
1 1 0

02° Créer une fonction xor() qui reçoit en entrée deux entiers a b ne pouvant valoir que 0 ou 1.

La fonction doit renvoyer la valeur a XOR b.

Voici le prototype :

xor(a:'0|1', b:'0|1') -> '0|1'

PRECONDITIONS : a et b ne peuvent valoir que 0 ou 1.

POSTCONDITIONS : la réponse vaut a XOR b et donc 0 ou 1.

Vous utiliserez une documentation courte utilisant juste le typing comme sur le prototype. N'oubliez pas néanmoins de rajouter la courte DOCSTRING descriptive.

03° Créer une fonction test_xor() qui testera votre fonction en utilisant quelques assertions bien choisies.

Vous rajouterez un appel automatique à cette fonction dans le corps de votre module (dans un if __name__ == "__main__":).

04° Créer une fonction xor_sur_8_bits() qui reçoit en entrée deux tableaux de 8 cases représentant les 8 bits d'un octet. Elle renvoe en sortie un tableau de 8 cases correspondant au XOR bit à bit des deux tableaux de bits.

Voici le prototype :

xor_sur_8_bits(bits1:list[int], bits2:list[int]) -> list[int]

>>> bits1 = [0, 1, 0, 1, 1, 0, 0, 1] >>> bits2 = [1, 1, 0, 1, 1, 0, 1, 1] >>> bits3 = xor_sur_8_bits(bits1, bits2) >>> bits3 [1, 0, 0, 0, 0, 0, 1, 0]

PRECONDITIONS : les deux tableaux d'entrées ont 8 cases qui ne contiennent que 0 ou 1.

POSTCONDITIONS : la réponse est un tableau de 8 cases qui est le résultat du XOR bit à bit sur les valeurs des tableaux précédents.

Vous réalisez ici cette fonction avec une DOCUMENTATION LONGUE : description précise des paramètres et des préconditions, la postcondition et quelques exemples d'utilisation.

05° Utiliser le module doctest de façon à tester votre fonction à l'aide des exemples fournis dans la documentation longue de celle-ci.

06° Rajouter ceci dans votre fonction xor_sur_8_bits(), juste avant de renvoyer votre réponse : cela permettra d'afficher le calcul réalisé par le XOR bit à bit sous condition d'avoir mis la constante AFFICHAGE à True avant le démarrage.

Il faudra bien entendu adapté en fonction des noms que vous avez utilisé pour vos propres tableaux.

1 2 3 4 5 6
AFFICHAGE = False if AFFICHAGE: print("---------------------------") print("OCTET FICHIER : ", bits1) print("OCTET CLE : ", bits2) print("XOR entre les 2 : ", bits3)

2 - Exercice type 2 : code à trous

07° On considère la fonction binaire suivante. Cette fonction prend en paramètre un entier positif a en écriture décimale et renvoie son écriture binaire sous la forme d’une chaine de caractères.

L’algorithme utilise la méthode des divisions euclidiennes par deux successives.

Compléter le code de la fonction binaire.

1 2 3 4 5 6 7 8 9 10
def binaire(a): '''convertit un nombre entier a en sa representation binaire sous forme de chaine de caractères.''' if a == 0: return ... bin_a = ... while ... : bin_a = ... + bin_a a = ... return bin_a

Exemples :

>>> binaire(83) '1010011' >>> binaire(6) '110' >>> binaire(127) '1111111' >>> binaire(0) '0

08° La fonction précédente possède néanmoins deux défauts pour notre projet : il ne s'agit pas d'un tableau et il n'y a pas nécessairement 8 cases uniquement.

Expliquer précisement comment fonctionnent les deux transtypes des lignes 4 et 5. Attention, on ne demande pas de paraphrases vis à vis des simples commentaires présents. On veut une réelle explication.

1 2 3 4 5 6
def obtenir_8_bits(entier:int) -> list[int]: """Transtype l'octet unique en un tableau de 8 cases contenant 0 ou 1 """ str_8_bits = f"{bin(entier)[2:]:0>8}" # On transforme l'entier en str de 8 cases 0 ou 1 t_int = [int(c) for c in str_8_bits] # Transtype str -> list[int] return t_int # On renvoie le tableau correspondant

Notez bien que les explications sur le formatage des f-strings n'est pas au programme du BAC.

09°Compléter maintenant la fonction chiffrer() qui reçoit deux entiers notés vk_fic pour valeur de l'octet en position k dans le fichier, comprise entre 0 et 255 et vk_cle pour valeur de l'octet k dans la clé, comprise elle aussi entre 0 et 255.

Il faudra bien entendu utiliser certaines fonctions précédentes et se souvenir qu'on doit copier-coller la clé si elle est trop courte.

On peut simuler cela en utilisant le reste de la division euclidienne sur la longueur de la clé :

Imaginons que notre clé soit de longueur 10 et possède donc 10 cases d'indice 0 à 9.

On veut agir sur l'octet k=18 du fichier. Comment trouver la valeur de l'octet qui correspond dans la clé ? Facile : on calcule 18 % 10, ce qui donne un reste de 8 : l'octet 18 du fichier sera situé sous l'octet 8 de la clé.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
def chiffrer(vk_fic:int, vk_cle:int) -> int: """Renvoie une valeur 0-255 qui est le chiffrement XOR bit à bit des 2 valeurs 0-255""" # On calcule les deux tableaux de 8 bits correspondant bits_fic = obtenir_8_bits(...) # Tableau de bits de l'octet k du fichier bits_cle = obtenir_8_bits(...) # Tableau de bits de l'octet k de la clé # On récupère le tableau des bits chiffrés bit à bit par un XOR bits_chi = ...(..., ...) # Calcule la valeur que représente ces 8 bits valeur = ... # on initialise la somme for i in ...: # pour chaque indice du tableau des bits chiffrés v = bits_chi[i] # on lit la valeur 0 ou 1 dans cette case p = 2**(...) # on calcule avec i le poids de la case (128-64-32...) valeur = valeur + ... # on rajoute la valeur obtenue à la somme # Renvoie la valeur 0-255 qui remplacera celle du fichier return valeur

10° Enregistrer votre module sous le nom chiffrement_symetrique.py de façon à le conserver pour la dernière partie du mini-projet.

Fermer ensuite ce fichier et ouvrir Thonny sur un nouveau programme pour ne pas risquer d'écraser votre travail par accident.

Le mieux est certainenement de le placer sur un GIT.

3 - Quelques exemples de manipulation du type bytes

Vous allez maintenant réaliser votre chiffrement sur les octets d'un fichier. Python possède un type natif permettant de manipuler une séquence d'octets, octet se traduisant par bytes en anglais. Il s'agit du type... bytes.

Notez bien la présence du s, il s'agit d'un type structuré ressemblant à un string. Le type bytes est donc un conteneur comme le type str : un string peut être le string vide, ou ne contenir qu'un seul caractère ou plusieur caractères.

11° Lire les bouts de cours fournis ci-dessus, en testant à votre convenance les exemples pour comprendre comment se manipulent les octets-mémoire en Python.

3.1 BYTES : définition

Le type gérant les octets se nomme bytes. C'est un conteneur formant une collection ordonnée d'octets.

Exemple avec la séquence 66 65 66 65 :

Indice 0 1 2 3
Elément 8 bits
0100 0010
(66 en décimal)
8 bits
0100 0001
(65 en décimal)
8 bits
0100 0010
(66 en décimal)
8 bits
0100 0010
(65 en décimal)

Indice se traduit par index en anglais.

flowchart LR D([Variable]) --> M([conteneur-bytes]) M -- indice 0 --> A([octet 0]) M -- indice 1 --> B([octet 1]) M -- indice 2 --> C([octet 2])

Notez bien que la première case est la case d'indice 0, pas celle d'indice 1. Les indices disponibles dans un bytes de 7 octets vont donc de 0 à 6.

3.2 BYTES : déclaration avec Python

Type bytes

En Python, les octets en mémoire sont gérés par le type nommé bytes.

Déclaration en fournissant les valeurs 0-255 dans un tableau

On peut fournit à la fonction native bytes() un tableau ne contenant que des valeurs entières dans l'intervalle [0; 255].

  • Pour déclarer un bytes vide :
  • >>> memoire = bytes() # Un bytes vide >>> memoire b'' >>> memoire = bytes( [] ) # Un bytes vide aussi >>> memoire b''
  • Pour déclarer un bytes contenant un octet correspondant à 65 (0100 0001), on doit fournir un tableau d'une seule case contenant ce 65 :
  • >>> memoire = bytes( [65] ) >>> memoire b'A'

    Comme on peut le voir, le concepteur de Python, Guido van Rossum, a décidé d'afficher la suite d'octets COMME SI ils encodaient un texte en ASCII. De cette façon, la représentation est plus courte comme vous allez le voir ci-dessous. Mais attention : ce n'est pas un "A" qui est mémorisé, c'est bien 0100 00012, 65.

  • Pour déclarer un bytes contenant une séquence d'octets (par exemple 66, 65, 66, 65), on doit fournir un tableau :
  • >>> memoire = bytes( [66, 65, 66, 65] ) >>> memoire b'BAAA'
  • On peut fournir n'importe quelle valeur comprise entre 0 et 255. Néanmoins, lorsqu'on fournit un nombre entre 128 et 255, ces octets seront représentés en fournissant leurs valeurs en hexadécimales puisque ASCII ne donne d'équivalence qu'entre 0 et 127.
  • >>> memoire = bytes([67, 126, 130, 200, 255]) >>> memoire b'C~\x82\xc8\xff'

    Cette façon de représenter les octets n'est donc pas si intuitive que cela.

  • Formuler un stockage d'octet dont la valeur n'est pas dans [0;255] povoque bien entendu une erreur :
  • >>> memoire = bytes([66, 65, 312]) ValueError: bytes must be in range(0, 256)
Déclaration en fournissant les valeurs 0-127 dans un string

On peut utiliser le fait que Python délimite bytes de la même façon d'un string mais avec le prefixe b.

Pour les valeurs ASCII (de 0 à 127), on donne juste le caractère ASCII correspondant à l'octet voulu. Sinon, il faut utiliser la notation hexadécimale.

  • Le bytes vide : b''
  • Le bytes ne contenant que 65 (code du caractère A) : b'A'
  • Le bytes ne contenant que 66, 65, 66, 65 : b'BABA'
  • Le bytes ne contenant que 66, 65, 255 : b'BA\xFF'

Avec cette méthode de déclaration, vous déclenchez une erreur si vous utilisez un caractère non-ASCII (128-255). Dans ce cas là, il faut passer pas la notation hexadécimal et donc connaitre la valeur hexa.

>>> memoire = b'BABAA' >>> memoire b'BABA' >>> memoire = b"Et là ?" memoire = b"Et là ?" ^^^^^^^^^^ SyntaxError: bytes can only contain ASCII literal characters

Le à n'est pas accepté car aucun code 0-127 n'est associé à ce caractère dans la table ASCII.

Le à n'apparaît que dans certaines tables ASCII étendue.

3.3 BYTES : visualiser les valeurs des octets

Obtenir la valeur d'un octet en particulier dans un type bytes

Puisqu'un bytes est un conteneur indexé, on peut utiliser l'opérateur [].

L'interpréteur va alors vous renvoyer la valeur de cet octet précis.

>>> memoire = b"BONJOUR A TOUS" >>> memoire[0] 66
Obtenir toutes les valeurs d'un bytes

Avec la fonction list()

On peut utiliser la fonction native list() pour transtyper le bytes en list.

Attention : ensuite, il s'agira bien d'un simple tableau d'entiers, tous compris entre 0 et 255.

>>> memoire = b"BONJOUR A TOUS" >>> t = list(memoire) >>> t [66, 79, 78, 74, 79, 85, 82, 32, 65, 32, 84, 79, 85, 83] 66

Avec la création par compréhension

>>> memoire = b"BONJOUR A TOUS" >>> t = [v for v in memoire] >>> t [66, 79, 78, 74, 79, 85, 82, 32, 65, 32, 84, 79, 85, 83] 66
3.5 BYTES : déterminer la longueur d'une séquence

La longueur d'un bytes correspond au nombre d'octets dans cette mémoire.

On utilise la fonction native len().

Indice 0123456 >>> mem = b"Bonjour" >>> len(mem) 7

Comme on voit qu'il y a 7 octets, on sait alors qu'on peut demander un octet d'indice allant de 0 à ...6.

3.6 BYTES : immuable en Python (pas de modification)

Immuable

En Python, les bytes sont immuables (ou non mutables en franglais) : on ne peut pas modifier le contenu d'une case après la création du string.

>>> memoire = b"bonjour" >>> memoire[0] = 66 TypeError: 'bytes' object does not support item assignment
Ce qui ne veut pas dire qu'on ne peut pas les écraser

En transtypant le bytes en tableau list 'entiers 0-255, on peut modifier le tableau.

On peut alors utiliser bytes() pour créer un bytes portant le même nom et écrasant l'ancienne version. Mais attention : ils n'auront pas la même adresse mémoire : ce n'est pas le contenu du conteneur initial qui a été modifié, on a recrée tout le conteneur.

>>> memoire = b"bonjour" >>> id(memoire) 134417245611344 >>> t = list(memoire) >>> t[0] = 66 >>> memoire = bytes(t) >>> id(memoire) 134417245613696 >>> memoire b'Bonjour'

La variable semble avoir été "modifiée" sauf que

  • Au début, l'espace des noms contenait une liaison entre la variable memoire et l'adresse-mémoire 134417245611344
  • A la fin, l'espace des noms contient une liaison entre la variable memoire et l'adresse-mémoire 134417245613696

On a donc changé de conteneur et pas juste changé le contenu du conteneur.

3.7 BYTES et encodage des textes

La fonction bytes() peut recevoir un tableau d'entiers compris entre 0 et 255 et renvoie alors un nouveau bytes correspondant à ces valeurs.

>>> memoire = bytes([12, 65, 200, 155, 72])

Mais elle peut aussi recevoir un str sous condition d'indiquer avec quel système d'encodage on veut encoder nos caractères.

>>> memoire = bytes("Bonjour à tous") TypeError: string argument without an encoding >>> memoire = bytes("Bonjour à tous", encoding="ascii") UnicodeEncodeError: 'ascii' codec can't encode character '\xe0' in position 8: ordinal not in range(128) >>> memoire = bytes("Bonjour à tous", encoding="latin-1") >>> list(memoire) [66, 111, 110, 106, 111, 117, 114, 32, 224, 32, 116, 111, 117, 115] >>> memoire = bytes("Bonjour à tous", encoding="utf-8") >>> list(memoire) [66, 111, 110, 106, 111, 117, 114, 32, 195, 160, 32, 116, 111, 117, 115] >>> memoire = bytes("Bonjour à tous", encoding="utf-16") >>> list(memoire) [255, 254, 66, 0, 111, 0, 110, 0, 106, 0, 111, 0, 117, 0, 114, 0, 32, 0, 224, 0, 32, 0, 116, 0, 111, 0, 117, 0, 115, 0] >>> memoire = bytes("Bonjour à tous", encoding="utf-32") >>> list(memoire) [255, 254, 0, 0, 66, 0, 0, 0, 111, 0, 0, 0, 110, 0, 0, 0, 106, 0, 0, 0, 111, 0, 0, 0, 117, 0, 0, 0, 114, 0, 0, 0, 32, 0, 0, 0, 224, 0, 0, 0, 32, 0, 0, 0, 116, 0, 0, 0, 111, 0, 0, 0, 117, 0, 0, 0, 115, 0, 0, 0]

Comme on peut le constater, l'encodage UTF-8 est celui qui permet de limiter le nombre d'octets nécessaires tout en permettant d'enregistrer n'importe quel caractère défini dans la table universelle UNICODE.

12° Expliquer pourquoi on obtient ces trois erreurs (et pourquoi la dernière fonctionne en utf-8) :

>>> memoire = bytes("Bonjour à tous") TypeError: string argument without an encoding >>> memoire = bytes("Bonjour à tous", encoding="ascii") UnicodeEncodeError: 'ascii' codec can't encode character '\xe0' in position 8: ordinal not in range(128) >>> memoire = bytes("Le signe → veut dire IMPLIQUE en logique mathématique", encoding="latin-1") UnicodeEncodeError: 'latin-1' codec can't encode character '\u2192' in position 9: ordinal not in range(256) >>> memoire = bytes("Le signe → veut dire IMPLIQUE en logique mathématique", encoding="utf-8") >>> list(memoire) [76, 101, 32, 115, 105, 103, 110, 101, 32, 226, 134, 146, 32, 118, 101, 117, 116, 32, 100, 105, 114, 101, 32, 73, 77, 80, 76, 73, 81, 85, 69, 32, 101, 110, 32, 108, 111, 103, 105, 113, 117, 101, 32, 109, 97, 116, 104, 195, 169, 109, 97, 116, 105, 113, 117, 101]

13° Afficher et trouver le nombre d'octets permettant d'encoder le texte "Avec ça, on ne devrait plus se perdre en forêt" en latin-1, utf-8, utf-16 et utf-32.

14° Quel est le petit avantage de latin-1 sur utf-8 sur le texte précédent ? Quel est le gros défaut de latin-1 sur utf-8 sur un texte quelconque ?

4 - Lecture et chiffrement d'un fichier binaire

15° Récupérer chiffrement_symetrique.py. Votre module doit comporter les fonctions suivantes bien documentées normalement.

xor(a:'0|1', b:'0|1') -> '0|1'

xor_sur_8_bits(bits1:list[int], bits2:list[int]) -> list[int]

obtenir_8_bits(entier:int) -> list[int]

chiffrer(vk_fic:int, vk_cle:int) -> int

16° Créer un petit fichier-texte que vous nommerez texte.txt et que vous placerez dans le même répertoire que votre module.

17° Rajouter et compléter ces deux dernière fonctions qui vont nous permettre :

  • D'ouvrir l'un de vos fichiers en mode lecture binaire ('br').
  • D'ouvrir un autre fichier en mode écriture binaire ('bw').
  • De lire octet par octet votre premier fichier, chiffré chaque octet à l'aide de l'octet correspondant de la clé et d'enregistrer ce nouvel octet dans le deuxième fichier
  • De fermer les deux fichiers à la fin.

Le programme de test vous permettre de tester l'ensemble.

Quelques compléments d'informations sur les fonctions utilisables sur les fichier ouverts en mode 'b' :

  • La ligne k = fb.tell() veut dire de récupérer l'indice/la position de l'octet que nous allons lire dans l'objet-fichier fb, le fichier de base, non-chiffré initialement. k correspond donc au "numéro de colonne" du chiffrement que vous avez réalisé et complété à la main en fin d'activité sur le chiffrement symétrique.
  • La ligne k = fb.tell() veut dire de récupérer le numéro/l'indice/la position du curseur qui pointe le prochain octet que nous allons lire dans l'objet-fichier fb, le fichier de base, non-chiffré initialement. k correspond donc au "numéro de colonne" du chiffrement que vous avez réalisé et complété à la main en fin d'activité sur le chiffrement symétrique.
  • La ligne lecture = fb.read(1) veut deux choses : lire 1 seul octet et mémoriser le bytes (de 1 case) obtenu dans la variable lecture. Après cette lecture, le curseur du fichier avance automatiquement d'une position. La prochainement fois qu'on utilisera tell(), la variable k sera donc augmenté de 1, puisqu'on ne lit qu'un octet à la fois.
  • On écrit avec write() dans un fichier binaire, comme avec un fichier-texte. La seule différence est qu'on doit fournir ici un bytes et pas un str comme pour les fichiers-texte.
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
def chiffrement_octet_par_octet(fb, fc, cle:bytes): """Lit le fichier et renvoie l'octet lu""" lecture = ... # Fausse 1er lecture NON VIDE pour rentrer dans le while while ...: # TQ le bytes lecture n'est pas vide k = fb.tell() # On mesure la position du pointeur de lecture lecture = fb.read(1) # On lit un unique octet qu'on stocke dans ce bytes if lecture: # Si on a lu autre chose que VIDE / EOF (End Of File) # lecture est alors un bytes, une sorte de "tableau d'entiers 0-255" # De plus, on lit octet par octet, lecture est ici un tableau d'une case. # On mémorise l'octet k du fichier et de la clé vk_fic = ...[0] # Valeur 0-255 de l'octet k dans le fichier vk_cle = cle[k % ...] # Valeur 0-255 de l'octet k de la clé # On calcule la valeur chiffrée par l'octet k dans le nouveau fichier vk_chiffre = ... # la valeur chiffrée à placer octet_chiffre = ... # l'octet correspondant à cette valeur AFFICHAGE = True if AFFICHAGE: print(vk_fic, " --> ", vk_chiffre) # On écrit cela dans le fichier fc ... def chiffrement_du_fichier(nom_1:str, nom_2:str, octets_cle:bytes) -> None: """Chiffre le fichier 1 avec les octets de la clé et crée le fichier 2""" fb = open(..., "br") # Le fichier de base en lecture binaire fc = open(..., "bw") # La copie chiffrée en écriture binaire chiffrement_octet_par_octet(fb, fc, octets_cle) ... # Fermeture du premier fichier ... # Fermeture du deuxième fichier if __name__ == "__main__": import doctest doctest.testmod() NOM_LECTURE = "texte.txt" NOM_ECRITURE = "fichier_chiffre.txt" print(f"Chiffrement demandé pour le fichier {NOM_LECTURE}") print(f"Le fichier chiffré se nommera {NOM_ECRITURE}. Attention, si un fichier portant ce nom existe, il sera écrasé.") phrase = input("Quelle est votre clé symétrique ? (Fournir le texte de la clé ici) :") octets_cle = bytes(phrase, encoding='utf-8') print("Démarrage du chiffrement") chiffrement_du_fichier(NOM_LECTURE, NOM_ECRITURE, octets_cle) print("Chiffrement effectué")

18° Chiffrer votre fichier puis chiffrer le fichier-chiffré.

Puisque les constantes d'afficage sont activées, vous devriez voir toutes les variations d'octets effectuées.

Expliquer ce que veut dire cette expression symbolisant l'action que vous venez de faire :

f = chiffrer( chiffrer(f, cle) , cle)

19° Désactiver l'affichage de la transformation des octets car le temps d'exécution d'un print() est très très lent du point du vue du processeur.

Chiffrer ensuite un fichier quelqueconque (image, vidéo...).

Chiffrer le fichier chiffré avec une clé différente.

Chiffrer le fichier chiffré avec la bonne clé.

Conclusion ?

20° Vous n'avez plus qu'à vous mettre d'accord avec quelqu'un sur le choix d'une clé et échanger des données de façon sécurisée. Bien entendu, le choix de la clé est fondamentale.

Répondre à ces dernières questions :

  • Combien de possibilités de clé à tester avec une clé inconnue de 8 bits (un seul octet) ?
  • Combien de possibilités de clé à tester avec une clé inconnue de 128 octets ?
  • Que peut-il se passer si la clé possède trop de motifs qui se répète ? Est-ce bon pour la sécurité ou pas ?
  • Puisque la clé n'est qu'un bytes, aura-t-on pu utiliser les octets d'un fichier quelconque pour faire office de clé ?

Cela pourrait faire un beau sujet de projet amélioré : réaliser une nouvelle fonctionnalité : chiffrer un fichier en utilisant les octets d'un autre fichier (partagé entre deux individus) en tant que clés.

5 - FAQ

6 -

7 -

8 -

Activité publiée le 04 10 2020
Dernière modification : 04 10 2020
Auteur : ows. h.