Menu

Un Petit Gestionnaire de Mots de Passe avec Python

Password Manager using Python

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és username, password=new_user() et sa clé de chiffrement (ou hash) sera générée à partir de ce mot de passe hash=generate_hash(username, password). La clé est ensuite stockée dans un fichier <username>.key à l’aide de la fonction store_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és check_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 commande show_data_file(username). Le fichier est ensuite chiffré à nouveau encrypt_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 fonction decrypt_file(username, password). On déchiffre ensuite le mot de passe voulu avec la fonction decrypt_cipher(username, password).
    • choice == 4 : sortir exit() 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.