CentraleSupélecDépartement informatique
Plateau de Moulon
3 rue Joliot-Curie
F-91192 Gif-sur-Yvette cedex
1CC1000 - Systèmes d'Information et Programmation - TD : Sécurité, Cryptographie

Table of contents



Gestionnaire de mot de passe

L'objectif de ce sujet est d'aborder les questions de sécurité. Nous allons voir :

  • qu'est ce qu'un mot de passe fort ;
  • comment traiter des données sous forme binaire ;
  • comment chiffrer des données.

Toutes ces questions seront traitées en réalisant un petit logiciel de gestion de mots de passe.

En France, deux organisations sont particulièrement impliquées dans la sécurité des données. D'un coté la CNIL (Commission Nationale de l'Informatique et des Libertés) contribue au cadre réglementaire et veille à ce qu'il soit respecté. De l'autre, l'ANSSI (Agence Nationnale de la Sécurité des Systèmes d'Information) assiste les administrations, les entreprises et les particuliers pour la technique.

Ces organisations sont d'excellentes sources pour trouver les bonnes pratiques afin de lutter contre le piratage de donneés. Ce TD s'appuie sur leurs recommandations.

Utiliser des mots de passe forts

Choisir un bon mot de passe est crucial pour la protection de ses données. De plus, il est souvent préférable de choisir des mots de passe différents pour les différents services que nous utilisons en ligne. Cela circonscrit les risques en cas de fuite de données.

Un point de vigilance important concerne le mot de passe de votre boite mail qui permet souvent de ré-initialiser tous les autres.

Force d'un mot de passe

La force (ou robustesse) d’un mot de passe désigne sa capacité à résister à une énumération de tous les mots de passe possibles. Elle dépend de la longueur {$L$} du mot de passe et de la taille {$N$} de l'alphabet. Dans le cas où le mot de passe est choisi de façon aléatoire, cette mesure correspond à l'entropie et se calcule par la formule {$F=\log_2(N^L)$}.

On estime que la force d'un mot de passe est :

  • très faible si {$F < 64$};
  • faible si {$F < 80$};
  • moyen si {$F < 100$};
  • fort si {$F \geq 100$}.

En utilisant une fonction Python, on peut se rendre compte de la longueur nécessaire d'un mot passe en fonction de la taille de l'alphabet. Pour un mot de passe uniquement basé sur des chiffres il faut 25 symboles pour obtenir un mot de passe moyen. Il faut seulement 13 symboles pour un mot de passe utilisant chiffres, lettres minuscules, majuscules et ponctuations.

from math import pow, log2

def force(L, N):
    return int(log2(pow(N, L)))

Tester les critères de force

La plus part des sites imposent qu'un mot de passe contiennent une minuscule, une majuscule, un chiffre et un symbole de ponctuation. Il faut donc au moins 13 symboles pour qu'un mot de passe choisit aléatoirement soit de force moyenne.

Écrivez une fonction is_strong_enough(password) qui prend en paramètre un mot de passe et renvoie un booléan indiquant si ce mot de passe vérifie les contraintes ci-dessus.

Voici les symboles de ponctuations que nous allons considérer : punct = ".:!?/()&#@&_-*%".


Vous pouvez utiliser les méthodes isxxx() décrites sur cette page pour tester la catégorie d'un caractère.


ANSWER ELEMENTS

punct = ".:!?/()&#@&_-*%"

def is_strong_enough(password):
    return any(c.islower() for c in password) \
       and any(c.isupper() for c in password) \
       and any(c in punct  for c in password) \
       and any(c.isdigit() for c in password)

Génération d'un mot de passe

Pour générer des mots de passe, nous aimerions pouvoir sélectionner aléatoirement des symboles parmi un ensemble de symboles. C'est ce que permet de faire la fonction choice du module random (voir ici). Cette fonction prend en paramètre une séquence et renvoie un élément de cette dernière.

Pour implémenter cette fonction, Python doit générer des valeurs aléatoires. Le problème est que nos machines sont déterministes et donc peu adaptées à la génération des suites de valeurs aléatoires. En pratique on ne génère d'ailleurs pas de suites aléatoires mais pseudo-aléatoires, c'est à dire qui ressemblent à de l'aléa sans en être. Le module random fournit des fonctions pseudo-aléatoires rapides donc adaptées à la simulation mais mauvaises du point de vue cryptographique.

Pour notre besoin, nous utiliserons la fonction choice du module secrets (documentation). Elle fonctionne de manière identique à son homologue du module random mais est moins prédictible.

Écrivez une fonction generate_password(size=13) qui renvoie un mot de passe généré aléatoirement et respectant les critères de longueur et de force.

  • La solution la plus simple et conservant le plus d'entropie est de générer aléatoirement un mot de passe tant qu'il ne respecte pas la contrainte de force.

Le module string intègre les séquences suivantes qui facilitent l'écriture : digits et ascii_letters.

Remarque

Un mot de passe généré de cette manière est robuste, cependant il peut être dur à mémoriser. À la fin de ce sujet, nous vous proposons une autre manière de générer des mots de passe robustes mais plus faciles à mémoriser : la méthode Diceware.

Si vous êtes rapide, vous aurez l'opportunité de la découvrir. Sinon, nous vous encourageons à vous renseigner ultérieurement.


ANSWER ELEMENTS

def generate_password(size=13):
    alphabet = string.digits + string.ascii_letters + symbols
    password = ""
    while not is_strong_enough(password):
        password = "".join([secrets.choice(alphabet) for _ in range(size)])
    return password

Encodage des caractères

Avant de nous intéresser au chiffrement, nous devons nous attarder quelque peu sur l'encodage des caractères.

Un fichier sur un disque dur est une suite de bits. Cette suite n'a pas de sens en soi, tout dépend de la manière dont on l'interprète. Lorsque l'on parle de fichier "texte", on choisit d'interpréter les octets d'un fichier comme des lettres. Pour autant, il n'existe pas une unique manière de le faire. L'encodage des caractères est la table qui associe à chaque caractère un ou plusieurs octets déterminés.

Pour des raisons historique, liées à la gestion des alphabets des différentes langues, il existe plusieurs encodages. Parmi ceux que l'on rencontre le plus en Europe, on trouve latin-1 et utf-8.

On peut convertir une chaîne de caractères en octets avec la fonction encode qui prend en argument l'encodage souhaité. En l'abssence d'argument, l'encodage utilisé est utf-8 qui est aujourd'hui la référence. Par exemple si l'on tape l'instruction 'E-e-é'.encode() dans un interpréteur, on obtient le résultat suivant : b'E-e-\xc3\xa9'.

Ce que l'on observe n'est pas une chaine de caractère mais une série d'octets. On le sait grace à la lettre b à gauche du premier apostrophe. Entre les apostrophes, l'interpréteur représente les octets sous forme condensée qui ressemble à une chaîne de caractères mais qui n'en n'est pas une.

La manière d'interpréter cette "chaine" est la suivante. Les lettres qui ne sont pas précédées d'un \ valent l'octet correspondant dans l'encodage utf-8. Par exemple, la lettre a est codé en utf-8 par la valeur décimale 97 (61 en héxadecimal), donc b"a" vaut 01100001 en binaire.

Les deux caractères suivant un \x représentent en hexadécimal un octet. Les valeurs b"\x00" et b"\xff" correspond aux deux octets 00000000 et 11111111. Remarquez que b"\x61" vaut 01100001 tout comme b"a"...

À l'inverse il est possible d'utiliser decode pour convertir une suite d'octets en chaîne de caractères. S'il n'est pas précisé, par défaut, l'encodage utilisé est utf-8.

Ouvrez un shell Python et exécutez les instructions suivantes. Vous verrez que, suivant l'encodage, on ne représente pas toujours les lettres de la même manière. On utilise parfois même plus ou moins d'octets.

[bin(byte) for byte in "E-e-é".encode("latin-1")]
[bin(byte) for byte in "E-e-é".encode("utf-8")]

Trousseau d'accès

Pour introduire le chiffrement, nous allons créer un gestionnaire de mots de passe rudimentaire.

Il fonctionnera en ligne de commande de la manière suivante :

  • python pykey.py get name retrouvera le mot de passe associé au site name ;
  • python pykey.py set name génèrera (et éventuellement remplacera) un mot de passe associé au site name.

Toute autre utilisation fera apparaître un message d'aide, expliquant comment utiliser le programme.

Téléchargez le fichier Python suivant pykey.py. Nous le compléterons progressivement.

Quelques commentaires sur le squelette

Les fonctions is_strong_enough, generate_password, get_password et set_password sont pour le moment vides, nous les compléterons ultérieurement.

Nous reviendrons sur le fonctionnement des fonctions load_passwords et save_passwords mais pour le moment leurs noms sont suffisamment explicites. Elles permettent évidemment de charger et sauver une base de données de mots de passes. Cette base est simplement un dictionnaire qui associe à une chaine de caractères (le nom d'un site) une autre chaine de caractères (un mot de passe). Par exemple :

{
    "google.fr" : "?NFiuhe875",
    "centralesupelec.fr" : "746405nIUbdeQCp!"
}

La fonction main contient l'ensemble des appels de fonctions. Elle commence par tester la liste des paramètres lors de l'appel du programme (regroupés par l'interpréteur dans la liste sys.argv). Si leur nombre ou leur nature n'est pas bon, on affiche un message d'aide (print_help) et on quitte le programme. Si tout est bon, alors on exécutera les trois instructions suivantes :

db = load_passwords(key)
action(db, sys.argv[2])
save_passwords(db, key)

Ces instructions permettent :

  • de charger la base de données contenue dans le fichier keychain.dat ;
  • de réaliser une action qui est soit get_password soit set_password (notez l'utilisation d'une variable pour stocker une fonction et l'appeler plus tard) ;
  • de sauvegarder la base de données des mots de passe.

Pour le moment, tout est stocké en clair, la variable key n'est pas encore importante.

Générer un nouveau mot de passe

Complétez les fonctions generate_password(size=13) et is_strong_enough(password) avec les réponses données aux questions précédentes.


Implémentez la fonction set_password(db, name) qui prend en paramètres le dictionnaire de mots de passe db et la chaine de caractères name. Cette fonction génère un nouveau mot de passe aléatoirement. Elle l'associe à name dans le dictionnaire db et affiche le mot de passe à l'écran. S'il existe déjà une entrée dans le dictionnaire pour la valeur name alors on la remplace.

Vous devriez pouvoir vérifier que votre fonction marche correctement en regardant le contenu du fichier keychain.dat. Pour le dictionnaire d'exemples montré plus haut, votre fichier devrait contenir le texte suivant (le symbole § délimite les sites et les mots de passe) :

google.fr§?NFiuhe875
centralesupelec.fr§746405nIUbdeQCp!

ANSWER ELEMENTS

def set_password(db, name):
    p = generate_password()
    db[name] = p
    print(f'New password for "{name}" is set: {p}')

Récupérer un mot de passe

Compléter la fonction get_password(db, name) qui va afficher le mot de passe associé à name dans le dictionnaire db.

Si ce mot de passe n'existe pas, alors vous affichez la liste des entrées présentes dans le dictionnaire (les sites disponibles sans les mots de passe).


ANSWER ELEMENTS

def get_password(db, name):
    if name in db:
        print(f'Password for "{name}" : {db[name]}')
    else:
        print(f'No password found for "{name}". Available entries are:')
        for n in db:
            print(f"- {n}")

Chiffrement/Déchiffrement

Nous avons maintenant un logiciel rudimentaire mais fonctionnel. Le problème est que nos mots de passe sont stockés en clair, ce qui est une mauvaise pratique. Nous utiliserons donc un algorithme de chiffrement symétrique pour protéger nos données. La clef de chiffrement est donc la même que la clef de déchiffrement.

Avançons par étape, nous commencerons avec une clef en dur générée au préalable puis nous verrons comment la générer à partir d'un mot de passe.

Regardons le code de la fonction de sauvegarde. Le principe est

  1. de transformer chaque entrée du dictionnaire en une ligne contenant la clef et la valeur séparées par le délimiteur @§@ ;
  2. d'encoder l'ensemble des lignes sous forme binaire ;
  3. d'ouvrir le fichier de sauvegarde en mode écriture binaire ;
  4. d'écrire les données.
def save_passwords(db, key):
    data = "\n".join(w + delimiter + p for w, p in db.items())   #1.
    data = data.encode()                                         #2.

    with open(keychain_path, 'wb') as f:                         #3.
        f.write(data)                                            #4.

Pour chiffrer une suite d'octets avec Python on peut utiliser le module Fernet de la bibliothèque cryptography (voir ici) qui implémente l'algorithme AES avec des clefs de 128 bits.

Pour le chiffrement il suffit des deux instructions suivantes. Avant leurs exécutions, la variable data contient des données en clair, sous forme d'octets. Après leurs exécutions, la variable data contiendra des données chiffrées.

fernet = Fernet(key)
data = fernet.encrypt(data)

Chiffrement

Modifiez la fonction save_passwords(db, key) en introduisant le chiffrement.

Si tout est fait correctement, après l'exécution de votre programme, vous ne devriez plus pouvoir comprendre ce qui se trouve dans le fichier keychain.dat.

Attention, la modification de la fonction load_passwords(key) n'étant pas faite, le programme génère une erreur si on cherche à lire un mot de passe.


ANSWER ELEMENTS

def save_passwords(db, key):
    data = "\n".join(w + delimiter + p for w, p in db.items())
    data = data.encode()

    fernet = Fernet(key)
    encrypted = fernet.encrypt(data)

    with open(keychain_path, 'wb') as f:
        f.write(encrypted)

Déchiffrement

La fonction load_passwords(key) est un peu plus complexe que la précédente mais la modification à apporter ne sera pas plus difficile. Voici son fonctionnement.

  1. Si le fichier keychain.dat n'existe pas alors on en crée un sauvegardant un dictionnaire vide.
  2. On ouvre le fichier en mode lecture binaire.
  3. On lit l'ensemble des données binaires.
  4. On convertit les octets lus en une chaîne de caractères.
  5. On initialise un dictionnaire vide.
  6. On indique à Python de transformer la chaîne de caractères en "flot de lignes" que l'on peut itérer.
  7. On nettoie les lignes et on les coupe sur le délimiteur.
  8. On remplit le dictionnaire avec les morceaux des lignes.
def load_passwords(key):
    if not os.path.isfile(keychain_path):       #1.             
        save_passwords({}, key)

    with open(keychain_path, 'rb') as f:        #2.
        data = f.read()                         #3.
    data = data.decode()                        #4. 

    db = {}                                     #5.
    for l in io.StringIO(data):                 #6.
        s = l.strip().split(delimiter)          #7.
        db[s[0]] = s[1]                         #8.
    return db

Les deux instructions nécessaires pour modifier ce code et décoder un fichier qui serait chiffré sont les suivantes :

fernet = Fernet(key)
data = fernet.decrypt(data)

Modifiez la fonction load_passwords(key). Si tout est fait correctement, votre programme devrait fonctionner à nouveau.


ANSWER ELEMENTS

def load_passwords(key):
    if not os.path.isfile(keychain_path):
        save_passwords({}, key)

    with open(keychain_path, 'rb') as f:
        data = f.read()

    fernet = Fernet(key)
    data = fernet.decrypt(data)
    data = data.decode()

    db = {}
    for l in io.StringIO(data):
        s = l.strip().split(delimiter)
        db[s[0]] = s[1]
    return db

Générer une clef

Notre programme est pour le moment inefficace puisque la clef de chiffrement apparaît en clair dans le code source... La fonction suivante permet justement de nous aider, elle génère une clef à partir un mot de passe.

def generate_key(password):
    password = password.encode()
    kdf = PBKDF2HMAC(algorithm=hashes.SHA256(),
                     length=32,
                     salt=salt,
                     iterations=100000,
                     backend=default_backend())
    return base64.urlsafe_b64encode(kdf.derive(password))

On retrouve ces lignes telles quelles dans la documentation du module. Sans trop rentrer dans les détails, PBKDF2HMAC génère une clef. Pour cela, la fonction concatène le mot de passe avec une donnée que l'on appelle le "sel". C'est une donnée de configuration qui peut rester publique. Le mot de passe salé est ensuite hashé itérativement 100000 fois.

Ouvrez un shell Python et générez un sel avec l'instruction suivante.

import os
os.urandom(16)

Attention, deux clefs générées à partir d'un unique mot de passe mais de deux sels différents ne sont pas identiques. Donc il ne faut pas changer de sel une fois les données chiffrées au risque de ne plus pouvoir rien déchiffrer.

Sel et génération de clef

Ajoutez la fonction generate_key(password) à votre programme et changez la valeur de la variable salt avec la valeur que vous avez générée dans le shell Python.


ANSWER ELEMENTS

def generate_key(password):
    password = password.encode()
    kdf = PBKDF2HMAC(algorithm=hashes.SHA256(),
                     length=32,
                     salt=salt,
                     iterations=100000,
                     backend=default_backend())
    return base64.urlsafe_b64encode(kdf.derive(password))

Saisir le mot de passe maître

Pour utiliser la fonction generate_key(password), nous avons besoin de demander à l'utilisateur de saisir un mot de passe maitre.

Il serait possible d'utiliser la fonction input de la bibliothèque standard de Python mais le mot de passe serait alors visible. Nous préférons utiliser la fonction getpass du module éponyme.

from getpass import getpass
password = getpass("Enter your master password:")

Remplacez l'instruction key = b'WNlS4K1hLhAVl8JiYV0Fj8e92EiSEQi5VS4KNGNPQCc=' dans la fonction main par des instructions permettant de saisir un mot de passe au clavier et générer une clef.

Attention, le fichier de mots de passe keychain.dat ayant été généré avec une autre clef, vous devriez obtenir le message d'erreur suivant à l'exécution : cryptography.fernet.InvalidToken.

Nous vous conseillons simplement de supprimer le fichier keychain.dat, il sera recréé avec la nouvelle clef.


ANSWER ELEMENTS

def main():
    if __name__ == "__main__":
        print("Pykey - Password manager")

        if len(sys.argv) <= 1:
            print_help()
        elif sys.argv[1] == "get" and len(sys.argv) == 3:
            action = get_password
        elif sys.argv[1] == "set" and len(sys.argv) == 3:
            action = set_password
        else:
            print_help()

        password = getpass("Enter your master password.")
        key = generate_key(password)

        db = load_passwords(key)
        action(db, sys.argv[2])
        save_passwords(db, key)


Conclusion

À ce stade, nous avons réalisé l'ébauche d'un gestionnaire de mots de passe. Il existe aujourd'hui de nombreux logiciels rendant de tels services. Nous vous conseillons bien entendu leur utilisation plutôt qu'une solution maison. Ils auront l'avantage d'être développés par des spécialistes de la sécurité informatique et maintenus à jour.

Concernant notre logiciel, voici une liste non exhaustive d'améliorations qui pourraient être apportées et font l'objet de questions optionnelles.

  • Comment gérer la saisie d'un mauvais mot de passe qui fait planter le programme ?
  • Comment imposer la double saisie du mot de passe à la création de la base de données ?
  • Comment copier le mot de passe directement dans le presse papier pour éviter qu'il ne soit affiché à l'écran ?
  • Comment générer un mot de passe maitre simple à mémoriser ?

Améliorations (optionnel)

Gestion des exceptions

Lorsque l'on saisit un mauvais mot de passe, la clef générée pour le déchiffrement ne correspond pas à celle utilisée pour le chiffrement. Dans ce cas, la fonction fernet.decrypt génère une exception et le programme s'arrête.

Il est possible de capturer cette exception avec un bloc try - except. Pour cela, on doit importer l'exception (from cryptography.fernet import InvalidToken) et encapsuler la fonction susceptible de la produire.

Incorporer les instructions suivantes à votre programme.

try:
    db = load_passwords(key)
except InvalidToken :
    print("Wrong master password.")
    sys.exit()

ANSWER ELEMENTS

from cryptography.fernet import InvalidToken


def main():
    if __name__ == "__main__":
        print("Pykey - Password manager")

        if len(sys.argv) <= 1:
            print_help()
        elif sys.argv[1] == "get" and len(sys.argv) == 3:
            action = get_password
        elif sys.argv[1] == "set" and len(sys.argv) == 3:
            action = set_password
        else:
            print_help()

        password = getpass("Enter your master password.")
        key = generate_key(password)

        try:
            db = load_passwords(key)
        except InvalidToken :
            print("Wrong master password.")
            sys.exit()

        action(db, sys.argv[2])
        save_passwords(db, key)


Copie dans le presse papier

Pour éviter qu'un mot de passe en clair ne soit afficher sur l'écran, on peut utiliser le module pyperclip pour copier une chaîne de caractères directement dans le presse papier.

Le module pyperclip devrait déjà être installé si vous avez suivi nos instructions d'installation au début du cours.

Sinon, vous pouvez l'installer en saisissant la commande suivante :

  • Sous Windows : python -m pip install pyperclip OU py -m pip install pyperclip
  • Sur macOS : python3 -m pip install pyperclip

Modifiez la fonction get_password pour copier directement le mot de passe dans le presse papier. Inspirez vous de l'instruction suivante : pyperclip.copy("pass").

Intégrez cette fonctionnalité.


ANSWER ELEMENTS

import pyperclip

def get_password(db, name):
    if name in db:
        print(f'Password for "{name}" has been copied to the clipboard.')
        pyperclip.copy(db[name])
    else:
        print(f'No password found for "{name}". Available entries are:')
        for n in db:
            print(f"- {n}")

Double saisie du mot de passe

La fonction load_passwords(key) permet de créer une nouvelle base de données si le fichier keychain.dat n'existe pas. Ce sont les deux premières instructions qui permettent de le faire.

def load_passwords(key):
    if not os.path.isfile(keychain_path):
        save_passwords({}, key)

    with open(keychain_path, 'rb') as f:
        data = f.read()
    data = data.decode()

    db = {}
    for l in io.StringIO(data):
        s = l.strip().split(delimiter)
        db[s[0]] = s[1]
    return db

Au moment de l'appel de cette fonction, l'utilisateur a déjà saisi le mot de passe maitre et la clef associée est passée en paramètre de la fonction load_passwords(key).

Dans le cas où le fichier keychain.dat n'existe pas, on peut adapter cette fonction pour demander à l'utilisateur de saisir une nouvelle fois son mot de passe maitre. Cela nous permet de générer une seconde clef. Si cette clef est identique à la clef passée en paramètre, alors l'utilisateur a saisi deux fois le même mot de passe et on peut recréer le fichier keychain.dat. Si ce n'est pas le cas, on peut indiquer à l'utilisateur qu'il s'est trompé et quitter le programme (avec sys.exit()).

Intégrez cette fonctionnalité.


ANSWER ELEMENTS

def load_passwords(key):
    if not os.path.isfile(keychain_path):
        print("No keychain file found.")
        password = getpass("Enter your master password again to recreate a keychain file.")
        key2 = generate_key(password)
        if key.hex() == key2.hex():
            print("Empty keychain file generated.")
            save_keychain({}, key)
        else:
            print("You entered two different passwords. No keychain file generated.")
            sys.exit()

    with open(keychain_path, 'rb') as f:
        data = f.read()

    fernet = Fernet(key)
    data = fernet.decrypt(data)
    data = data.decode()

    db = {}
    for l in io.StringIO(data):
        s = l.strip().split(delimiter)
        db[s[0]] = s[1]
    return db


Diceware (optionnel)

La méthode que nous avons vue plus haut pour fabriquer des mots de passe est robuste mais elle a deux inconvénients :

  • elle nécessite de faire confiance à l'ordinateur pour la génération d'aléa ;
  • elle produit des mots de passe difficiles à retenir...

On pourrait être tenté d'imaginer des mots de passe par d'autres moyens. Par exemple, s'inspirer de noms communs et introduire des substitutions pour fabriquer des mots de passe respectant les critères de force. Malheureusement, ces mots de passes n'ont qu'une faible entropie et doivent être proscrits.

XKCD l'illustre bien et suggère une méthode connue pour la génération de mots de passe : la méthode Diceware.

Cette méthode s'appuie sur une liste d'environ 8000 mots. Un mot de passe est une sélection aléatoire de plusieurs mots parmi cette liste. On parle aussi de passphrase.

Un mot choisi au hasard parmi une liste de 8000 mots correspond à environ 13 bits d'entropie. Pour obtenir un mot de passe de force moyenne il faut donc choisir au moins 6 mots.

Cette méthode peut être réalisée sans l'assistance d'ordinateur. En effet, chaque mot de la liste est identifié par un quintuplet de valeurs comprises entre 1 et 6. Autrement dit, chaque mot peut être tiré au sort avec un lancé de 5 dés.

Il existe aujourd'hui des listes alternatives. En particulier Electronic Frontier Foundation a développé des listes issues de la pop culture (Star wars, Harry Potter, Game of Thrones, Star Trek). Le principe est le même mais ici on tire au sort un mot avec 3 dés à 20 faces.

Générer une passphrase

La fonction suivante, permet de lire un fichier Diceword que nous avons nettoyé (suppression des lignes d'entête, de l'identification des lignes et utilisation de l'encodage utf-8). Le fichier est disponible ici : starwars_8k_2018.txt.

def read_diceware():
    words = []
    with open("starwars_8k_2018.txt", "r") as f:
        for l in f:
            words.append(l.strip())
    return words

Vous pouvez maintenant créer une fonction generate_passphrase qui génère aléatoirement un mot de passe. Pour plus de lisibilité, vous pouvez passer la première lettre de chaque mot en majuscule avec la fonction capitalize.