Le gestionnaire de mots de passe
Un gestionnaire de mots de passe est un outil qui permet de gérer ses mots de passe de façon efficace et sécurisée. Il en existe plusieurs et de différents types, gratuits ou payants, hébergés en ligne ou en local (sur votre machine). Dans ce tutoriel, je vous propose de construire un gestionnaire de mots de passe basique, accessible via l’invite de commande. A travers ce projet, nous pourrons comprendre le fonctionnement de ce type de programme.
En règle générale, les gestionnaires de mots de passe utilisent des systèmes de gestion de bases de données pour stocker les mots de passe. Dans ce cas pratique, nous allons simplement les stocker dans des fichiers.
Attention : Nous construisons cet outil dans un but purement pédagogique. Je vous déconseille fortement de l’utiliser pour stocker vos mots de passe ! Les données étant stockées en local dans des fichiers, il suffirait de les supprimer pour perdre toutes vos données d’accès.
Fonctionnement de l’outil
Le gestionnaire proposé ici, est un outil accessible en ligne de commande qui permet de stocker et chiffrer les mots de passe dans des fichiers eux-mêmes chiffrés à l’aide d’une clé de chiffrement. Nous utilisons cette même clé pour les opérations de déchiffrement. C’est ce qu’on appelle le chiffrement symétrique (chiffrer et déchiffrer à l’aide d’une même clé). Cette clé est en fait générée à partir d’un mot de passe “maître” (en anglais : master password). Ce dernier est le seul mot de passe qu’il faudra retenir. La clé est produite grâce à ce qu’on appelle une fonction de dérivation de clé (en anglais : Key Derivation Function ou KDF). Les KDF sont utilisées pour le hachage et la vérification de mots de passe.
Rappel
Avant d’aller plus loin, rappelons quelques notions de cryptographie dont nous aurons besoin pour implémenter notre outil de gestion de mots de passe.
Le Chiffrement
Le chiffrement est un procédé cryptographique qui consiste à transformer des données lisibles en un un code inintelligible afin de rendre ces données incompréhensibles à ceux qui ne sont pas autorisés à y accéder. Les données ne pourront être lues à moins d’être déchiffrées. Il existe deux systèmes de chiffrement de l’information :
Le chiffrement symétrique
Comme mentionné plus haut, le chiffrement symétrique repose sur l’utilisation d’une seule et même clé pour les opérations de chiffrement et de déchiffrement des données.
Le chiffrement asymétrique
Ce système consiste à utiliser une paire de clés (publique et privée) pour chiffrer et déchiffrer l’information. La clé publique sert à chiffrer les données et peut être accessible à tous. Quant à la clé privée, celle-ci doit rester secrète et est utilisée pour déchiffrer les données. La clé privée ne peut être déduite à partir de la clé publique.
Le hachage
Une fonction de hachage est un algorithme qui permet de transformer une chaîne caractère en une clé (un hash) de longueur fixe. Contrairement au chiffrement qui est une opération bidirectionnelle, le hachage est une fonction irréversible : on ne peut pas calculer la valeur d’origine à partir du hash.
La fonction de dérivation de clé (KDF)
Une fonction de dérivation de clé est une fonction de hachage qui dérive une ou plusieurs clés secrètes à partir d’une valeur secrète comme un mot de passe. A l’aide de la fonction de dérivation de clé, il est possible d’utiliser la clé secrète générée au lieu de la valeur secrète (dans notre cas le mot de passe) qui serait plus vulnérable aux attaques.
Besoins du projet
Dans le cadre de ce mini-projet, le programme à réaliser devra comprendre les fonctionnalités de base suivantes :
1- Enregistrement d’un nouvel utilisateur
Création d’un utilisateur et production d’une clé de chiffrement pour celui-ci à partir d’un mot de passe principal (Master Password), puis stockage de la clé dans un fichier <username>.key
.
2- Authentification d’un utilisateur
S’il est déjà enregistré, l’utilisateur se connecte avec son nom d’utilisateur et son mot de passe maître.
Une fois authentifié, l’utilisateur pourra accomplir les opérations suivantes :
2-1- Chiffrement de mots de passe
Enregistrement des données liées à un compte internet de l’utilisateur (nom de la plateforme/service, nom d’utilisateur/mail, mot de passe). Le mot de passe est crypté avec la clé de cryptage avant d’être enregistré avec les autres informations dans un fichier <username>.credentials
. Une fois les données enregistrées dans le fichier, ce dernier est aussi crypté avec la clé de cryptage.
2-2- Visualisation des données
Déchiffrement et affichage du fichier contenant les données liées à l’utilisateur (le fichier est ensuite rechiffré après l’affichage de son contenu dans le terminal).
2-3- Récupération d’un mot de passe
Déchiffrement, affichage et copie dans le presse-papier du mot de passe d’un compte utilisateur.
2-4- Sortie
Option pour arrêter le programme.
Mise en œuvre
1- Configuration de base
Créer le répertoire du projet
On commence par créer le répertoire du projet PasswordManager
et on se positionne à sa racine.
E:\>mkdir PasswordManager
E:\>cd PasswordManager
Créer et activer l’environnement virtuel du projet
On crée l’environnement virtuel venv
du projet et on l’active.
E:\PasswordManager>python -m venv venv
E:\PasswordManager>venv\Scripts\activate
(venv) E:\PasswordManager>
2- Installer les modules nécessaires
Cryptography – Fernet
Afin de chiffrer nos mots de passe, nous aurons besoin d’installer le module cryptography
.
(venv) E:\PasswordManager>python -m pip install --upgrade pip
(venv) E:\PasswordManager>pip install cryptography
Pour réaliser le chiffrement symétrique, nous utiliserons la classe Fernet
du module cryptography
. Cette classe nous offre des fonctions de génération de clé secrète, de chiffrement et de déchiffrement de données.
On peut générer la clé de chiffrement soit de façon aléatoire ou en dérivant la clé à partir d’une valeur secrète comme un mot de passe. Pour note outil, nous allons opter pour la dérivation de clé, mais voyons comment on peut utiliser Fernet pour générer une clé aléatoire et l’utiliser pour chiffrer et déchiffrer un message secret.
Générer une clé secrète aléatoire
Pour voir comment générer une clé aléatoire avec Fernet, lancez l’interpréteur python.
(venv) E:\PasswordManager>py
Importez la classe Fernet, et utilisez la méthode generate_key()
pour générer la clé.
>>> from cryptography.fernet import Fernet
>>> key = Fernet.generate_key()
>>> print(key)
b'nyVUsd6hf4RLcoWQIylpMEperqA3_RrR_OnvP2psSZk='
>>> exit()
En affichant la clé, vous remarquerez que celle-ci est au format de bytes (octets). Elle est encodée en URL-safe base64 avec une longueur de 32 octets.
Chiffrer un message
On utilise ensuite la méthode encryt()
pour chiffrer le message secret avec clé générée. Le message doit être être sous forme d’octets. Pour cela, on convertit le message en octets avec la méthode encode()
et on le passe en argument à la fonction de chiffrement. Le chiffre obtenu cipher
est aussi formaté en octets et encodé en URL-safe base64.
>>> f = Fernet(key)
>>> message = 'mon message secret'
>>> cipher = f.encrypt(message.encode())
>>> cipher
b'gAAAAABhIPgTqCi96TA0A32x-5dEAt24fgZWcV8OLWkxeyNhzQQ1G_3_dHn34HVeZ1ZFER4FdlVQL_EjF5rq_W37QedbpH2ihGUSvkxH-Ohn4P9HNj0Xbno='
Déchiffrer un message
La méthode decrypt()
prend le message crypté cipher
en argument et le déchiffre avec la clé secrète pour nous renvoyer le message original encodé en octets. On utilise la méthode decode()
pour convertir le message déchiffré en chaîne de caractère encodée en UTF-8.
>>> decipher = f.decrypt(cipher)
>>> decipher
b'mon message secret'
>>> decipher.decode()
'mon message secret'
Argon2
Comme mentionné plus haut, nous allons générer notre clé secrète à partir d’un mot de passe maître à l’aide d’une fonction de dérivation de clé. Il existe plusieurs fonctions de ce type telles que BPKDF2HMAC, Bcrypt ou Scypt. Pour notre programme, nous allons utiliser la fonction de hachage Argon2, gagnante de la compétition de hachage de mots de passe (PHC – Password Hashing Competition) en 2015.
Contrairement aux autres fonctions, Argon2 est notamment plus résistante à certaines attaques usitées pour le “crackage” de mots de passe. La fonction existe sous trois versions et il est recommandé de l’utiliser dans sa version Argon2id. L’algorithme est considéré comme sure et offre une certaine flexibilité quant à son paramétrage.
Argon2 comporte les paramètres de configuration suivants :
- Mot de passe : chaîne de caractère à hacher
- Sel cryptographique : généré aléatoirement
- Itérations : nombre d’itérations de l’opération de hachage
- Mémoire : espace mémoire utilisé (en kilo-octets)
- Parallélisme : degrés de parallélisme
- Longueur du hash : longueur du hash généré (en octets)
Pour utiliser Argon2, on installe le module argon2-cffi
.
(venv) E:\PasswordManager>pip install argon2-cffi
Pour générer une clé à partir d’une chaîne de caractère, on utilise la méthode hash()
de la classe PasswordHasher()
.
>>> from argon2 import PasswordHasher
>>> ph = PasswordHasher()
>>> hash = ph.hash('musupermasterpassword')
>>> hash
'$argon2id$v=19$m=102400,t=2,p=8$JuACkrZqkFpJKO7m/r6YFQ$oJBvMEf6Af9F0GsN7+Dqxg'
Le hash obtenu contient :
- les paramètres par défaut de la fonction de hachage :
$argon2id$v=19$m=102400,t=2,p=8
- le sel cryptographique utilisé pour le hachage du mot de passe :
JuACkrZqkFpJKO7m/r6YFQ
- le mot de passe haché :
oJBvMEf6Af9F0GsN7+Dqxg
Le hash sera ensuite encodé et réduit pour être utilisé en tant que clé de chiffrement avec la classe Fernet.
Pyperclip
On installe aussi le module pyperclip. Ce module nous sera utile lorsqu’on voudra copier automatiquement un mot de passe déchiffré.
(venv) E:\PasswordManager>pip install pyperclip
Ce module permet de copier une valeur dans le presse-papier.
>>> import pyperclip
>>> pyperclip.copy('message à copier')
>>> pyperclip.paste()
'message à copier'
3- Organisation
Fichier principal password_manager.py
On créer le fichier password_manager.py
à la racine du projet et on y définit les menus de navigation qui nous offrirons les différentes options décrites plus haut. On y décrira ensuite la fonction principale main()
qui fera appel aux différentes fonctions dont nous aurons besoins. Ces fonctions sont importées au début à partir du répertoire utils
qui sera défini plus loin avec les fichiers qui contiendront lesdites fonctions.
Premier menu
La fonction menu_1()
propose trois options :
- Créer un nouvel utilisateur et enregistrer son mot de passe Maître
- S’authentifier et accéder au second menu
- Arrêter le programme
Second menu
La fonction menu_2()
propose les options suivantes :
- Crypter un mot de passe (relatif à un compte en ligne)
- Afficher les données
- Décrypter un mot de passe
- Arrêter le programme
Fonction principale main()
- Si c’est la première fois qu’il lance le programme, l’utilisateur va choisir de s’enregistrer
choice_1==1
. Ses nom d’utilisateur et mot de passe maître lui seront demandésusername, password=new_user()
et sa clé de chiffrement (ou hash) sera générée à partir de ce mot de passehash=generate_hash(username, password)
. La clé est ensuite stockée dans un fichier<username>.key
à l’aide de la fonctionstore_key(username, hash)
. - Dans le cas où l’utilisateur veut s’authentifier
choice_1==2
, son nom d’utilisateur et son mot de passe maître sont vérifiéscheck_user(username, password)
et s’ils sont valides le second menu s’affiche. Il peut choisir de :choice == 1
: chiffrer son mot de passe de compte en ligne. Celui-ci est chiffré et stockéencrypt_data(username, password)
dans un fichier<username>.credentials
qui sera aussi chiffréencrypt_file(username, password)
.choice == 2
: afficher les données du fichier<username>.credentials
. Dans ce cas, le fichier est déchiffrédecrypt_file(username, password)
et son contenu affiché sur dans l’invite de commandeshow_data_file(username)
. Le fichier est ensuite chiffré à nouveauencrypt_file(username, password)
.choice == 3
: récupérer un mot de passe. Pour cela, on déchiffre d’abord le fichier<username>.credentials
avec la fonctiondecrypt_file(username, password)
. On déchiffre ensuite le mot de passe voulu avec la fonctiondecrypt_cipher(username, password)
.choice == 4
: sortirexit()
du programme.
from utils.user_manager import * from utils.hash_manager import generate_hash, store_key from utils.encryptor import encrypt_data from utils.decryptor import decrypt_cipher from utils.file_manager import encrypt_file, decrypt_file, show_data_file def menu_1(): choice = int(input(''' Options ---------------------------------------------------------------------- 1. New user, REGISTER, GENERATE, STORE master password 2. Registered user, LOGIN 3. Exit ---------------------------------------------------------------------- ''')) if choice == 1 : print(f'your choice is : {choice} | ------REGISTER------') elif choice == 2 : print(f'your choice is : {choice} | ------LOGIN------') elif choice == 3 : print(f'your choice is : {choice} | ------SEE YA !------') else : print(f'your choice is : {choice} | ------!!!------') return choice def menu_2(): choice = int(input(''' Options ---------------------------------------------------------------------- 1. CRYPT some passwords 2. VISUALIZE my data 3. DECRYPT my password 4. Exit ---------------------------------------------------------------------- ''')) if choice == 1: print(f'your choice is : {choice} | ------CRYPT------') elif choice == 2 : print(f'your choice is : {choice} | ------VISUALIZE------') elif choice == 3 : print(f'your choice is : {choice} | ------DECRYPT------') elif choice == 4 : print(f'your choice is : {choice} | ------SEE YA !------') else : print(f'your choice is : {choice} | ------!!!------') return choice def main(): print('') print(15*' ',25*'*','PASSWORD MANAGER',27*'*') choice_1 = menu_1() if choice_1 == 1 : username, password = new_user() hash = generate_hash(username, password) store_key(username, hash) elif choice_1 == 2 : username = input('your name : ') password = getpass(prompt='your password : ') if check_user(username, password) : choice = menu_2() while choice != 4 : if choice == 1 : encrypt_data(username, password) encrypt_file(username, password) elif choice == 2 : if decrypt_file(username, password): show_data_file(username) else: print('no data stored yet') encrypt_file(username, password) elif choice == 3 : if decrypt_file(username, password) : pwd_decrypted = decrypt_cipher(username, password) if pwd_decrypted : print(pwd_decrypted,'is copied to clipboard ! ') else : print('no such service recorded') encrypt_file(username, password) else: print('no data stored yet') else: exit() choice = menu_2() exit() else: exit() elif choice_1 == 3 : exit() else : exit() if __name__ == '__main__': main()
Répertoire “utils”
On créer un dossier utils
qui contiendra les fichiers dans lesquels seront définies les fonctions nécessaires à l’implémentation de notre gestionnaire de mots de passe.
Le fichier hash_manager.py
Le fichier comprend les fonctions liées à la gestion de la clé de chiffrement. La fonction generate_hash(username, password)
permet de générer un hash à partir du mot de passe maître à l’aide la méthode PasswordHasher().hash(password)
de argon2
. Ensuite avec la fonction encode_hash(hash)
on peut encoder le hash et en réduire la taille pour pouvoir l’utiliser en tant que clé pour le chiffrement symétrique avec le module de cryptographie Fernet.
On peut stocker la clé de chiffrement dans un fichier <username>.key
à l’aide de la fonction store_key(username, key)
. On récupère la clé avec la fonction read_key(username)
.
La fonction valid_password(hash, password)
permet de valider le mot de passe maître fourni par l’utilisateur au moment de son authentification. Pour ce faire, la fonction utilise la méthode PasswordHasher().verify(hash, password)
du module argon2
.
from argon2 import PasswordHasher import base64 def generate_hash(username, password): hash = PasswordHasher().hash(password) print(f'You are registered ! Key for {username} was created successfully.') print('Restart the program') return hash def encode_hash(hash): encoded_hash = hash.encode() encoded_hash = base64.urlsafe_b64encode(encoded_hash[:32]) return encoded_hash def store_key(username, key): with open(username+'.key','w') as master_pwd: master_pwd.write(key) def read_key(username): with open(username+'.key','r') as master_pwd: key = master_pwd.read() return key def valid_password(hash, password): try: PasswordHasher().verify(hash, password) return True except: return False
Le fichier user_manager.py
Dans ce fichier on mettra les fonctions de création d’un nouvel utilisateur new_user()
et la fonction de vérification check_user(username, password)
.
Avec la fonction de création d’un utilisateur new_user()
, on demande à l’utilisateur d’entrer son nom username = input('your name : ')
et on vérifie s’il existe déjà en recherchant le fichier <username>.key
. Si le fichier existe path.exists(username+'.key') == True
alors on repropose à l’utilisateur d’entrer un autre nom, puis on demande à l’utilisateur d’enter son mot de passe maître. Pour éviter que le mot de passe soit visible lors de la saisie, nous utilisons la méthode getpass()
.
Lorsque l’utilisateur veut s’authentifier, on fait appel à la fonction check_user(username, password)
qui prend le nom de l’utilisateur <username>
et son mot de passe maître <password>
en argument. Si le fichier <username>.key
existe, on récupère le hash contenu dans le fichier avec la fonction read_key(username)
. Cette fonction est importée à partir de utils.hash_manager
en plus de la fonction valid_password(hash, password)
utilisée pour vérifier si le mot de passe est correcte.
from os import path from getpass import getpass from utils.hash_manager import read_key, valid_password def new_user(): username = input('your name : ') while path.exists(username+'.key'): print('this username is already taken. Choose another one !') username = input('your name : ') password = getpass(prompt='your master password : ') return username, password def check_user(username, password): if path.exists(username+'.key'): hash = read_key(username) if valid_password(hash, password) : print(f'user : {username} (correct password, you\'re in)') return True else : print('wrong password') return False else: print('you are not registered yet') return False
Le fichier encryptor.py
Dans ce fichier, on décrit les fonctions de chiffrement encrypt_data(username, password)
et de stockage des mots de passe store_credentials(username, password, service_name, username_service, cipher)
.
Dans la fonction de chiffrement, on récupère le hash relatif à l’utilisateur avec la fonction read_key(username)
. Ensuite, on vérifie que le mot de passe maître de l’utilisateur est valide avant de l’encoder (encoded_hash
) avec la fonction encode_hash(hash)
. Si le mot de passe est valide, on récupère les informations nécessaires, tels que le nom de la plateforme, du site ou du service en ligne <service_name>
et les informations de connexion de l’utilisateur (<username_service>
et <pwd_service>
). On utilise la clé encoded_hash
pour chiffrer le mot de passe pwd_service
à l’aide du module de cryptographie Fernet
. Après cela, avec la fonction de stockage store_credentials()
, on déchiffre le fichier <username>.credentials
s’il existe déjà et on y inscrit les données avec le mot de passe chiffré.
from os import path from cryptography.fernet import Fernet from getpass import getpass from utils.hash_manager import read_key, encode_hash, valid_password from utils.file_manager import decrypt_file def encrypt_data(username, password): hash = read_key(username) if valid_password(hash, password) : service_name = input('service name : ') username_service = input(f'your username for {service_name} : ') pwd_service = getpass(prompt=f'your password for {service_name} : ') encoded_hash = encode_hash(hash) encryptor = Fernet(encoded_hash) cipher = encryptor.encrypt(pwd_service.encode()) store_credentials(username, password, service_name, username_service, cipher) def store_credentials(username, password, service_name, username_service, cipher): if path.exists(username+'.credentials'): decrypt_file(username, password) with open(username+'.credentials','a') as credentials : credentials.write(service_name+'\t'+username_service+'\t'+cipher.decode()+'\n') print("your data has been stored successfully")
Le fichier decryptor.py
Dans ce fichier, on décrit la fonction get_cipher(username, service_name)
qui récupère le mot de passe chiffré à partir du fichier <username>.credentials
, ainsi que la fonction decrypt_cipher(username, password)
qui permet de déchiffrer et de retourner le mot de passe en clair.
La fonction de déchiffrement valide le mot de passe maître et demande à l’utilisateur pour quel service il veut déchiffrer le mot de passe. Le mot de passe chiffré est récupéré à l’aide de la fonction get_cipher
(username, service_name
). Pour déchiffrer le mot de passe, on utilise la fonction decrypt()
du module Fernet
et on le décode. Le mot de passe déchiffré pwd_decrypted
est copié dans presse papier.
from cryptography.fernet import Fernet from utils.hash_manager import read_key, encode_hash, valid_password import pyperclip as pc def get_cipher(username, service_name) : with open(username+'.credentials','r') as credentials : line = credentials.readline() while line : data = line.split('\t') if data[0] == service_name: return data[2] line = credentials.readline() return False def decrypt_cipher(username, password): hash = read_key(username) if valid_password(hash, password) : service_name = input('service_name : ') pwd_encrypted = get_cipher(username, service_name) encoded_hash = encode_hash(hash) decryptor = Fernet(encoded_hash) if pwd_encrypted : pwd_encrypted = pwd_encrypted.encode() pwd_decrypted = decryptor.decrypt(pwd_encrypted) pwd_decrypted = pwd_decrypted.decode() pc.copy(pwd_decrypted) return pwd_decrypted else : return False
Le fichier file_manager.py
Ce fichier comprend les fonctions qui permettent de manipuler les fichiers de stockage des mots de passe. On définit les fonctions de lecture read_file(username)
, d’écriture write_file(username, data)
, d’affichage show_data_file(username)
, de chiffrement encrypt_file(username, password)
et de déchiffrement decrypt_file(username, password)
du fichier <username>.credentials
.
Pour chiffrer le fichier, on récupère les données lines = read_file(username)
, on les chiffre cipher=encryptor.encrypt(lines)
et on réécrit le fichier avec les données chiffrées write_file(username, cipher)
. Le déchiffrement du fichier se fait de façon similaire : on lit le fichier chiffré, on déchiffre le contenu data=decryptor.decrypt(lines)
et on réécrit le fichier avec les données déchiffrées.
from os import path from cryptography.fernet import Fernet from utils import hash_manager from utils.hash_manager import read_key, encode_hash, valid_password def read_file(username): with open(username+'.credentials','rb') as file : lines = file.read() return lines def write_file(username, data): with open(username+'.credentials','wb') as file : file.write(data) def encrypt_file(username, password): hash = read_key(username) if valid_password(hash, password) : encoded_hash = encode_hash(hash) encryptor = Fernet(encoded_hash) lines = read_file(username) cipher = encryptor.encrypt(lines) write_file(username, cipher) def show_data_file(username): with open(username+'.credentials','r') as file : lines = file.readlines() for line in lines : print(line) def decrypt_file(username, password): if path.exists(username+'.credentials'): hash = read_key(username) if valid_password(hash, password) : encoded_hash = encode_hash(hash) decryptor = Fernet(encoded_hash) lines = read_file(username) data = decryptor.decrypt(lines) write_file(username, data) return True else: return False
Aller plus loin
Dans cet article, vous avez vu comment construire un gestionnaire de mots de passe basique avec Python. Vous pouvez partir de là et enrichir ce projet en ajoutant d’autres fonctionnalités comme :
- ajouter un générateur de mots de passe
- utiliser une base de données (MongoDB ou MySQL) pour stocker les données
- ajouter la possibilité de modifier ou réinitialiser un mot de passe
Sachez enfin que le code source pour ce projet est accessible sur GitHub.