428 lines
12 KiB
Markdown
428 lines
12 KiB
Markdown
|
|
# TP : Gestionnaire de mots de passe
|
||
|
|
|
||
|
|
## Contexte
|
||
|
|
|
||
|
|
Vous développez un gestionnaire de mots de passe simplifié, similaire à **Bitwarden**, **1Password** ou **KeePass**. Ce type d'application permet de stocker de manière sécurisée tous ses identifiants en les chiffrant avec un mot de passe maître.
|
||
|
|
|
||
|
|
L'objectif est de comprendre les mécanismes de chiffrement symétrique et les bonnes pratiques de sécurité.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Objectifs
|
||
|
|
|
||
|
|
- Implémenter un chiffrement symétrique simple
|
||
|
|
- Comprendre le rôle du mot de passe maître
|
||
|
|
- Stocker et récupérer des données chiffrées
|
||
|
|
- Générer des mots de passe robustes
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Partie 1 : Chiffrement XOR
|
||
|
|
|
||
|
|
### 1.1. Principe du XOR
|
||
|
|
|
||
|
|
L'opération XOR (ou exclusif) est fondamentale en cryptographie. Elle possède une propriété intéressante :
|
||
|
|
|
||
|
|
```
|
||
|
|
A XOR B XOR B = A
|
||
|
|
```
|
||
|
|
|
||
|
|
Autrement dit, appliquer deux fois XOR avec la même clé redonne le message original.
|
||
|
|
|
||
|
|
### 1.2. Implémentation
|
||
|
|
|
||
|
|
Implémenter la fonction de chiffrement XOR :
|
||
|
|
|
||
|
|
```python
|
||
|
|
def xor_chiffrement(message, cle):
|
||
|
|
"""
|
||
|
|
Chiffre ou déchiffre un message avec XOR.
|
||
|
|
|
||
|
|
:param message: (str) Le message à traiter
|
||
|
|
:param cle: (str) La clé de chiffrement
|
||
|
|
:return: (str) Le message chiffré/déchiffré
|
||
|
|
"""
|
||
|
|
resultat = ""
|
||
|
|
for i, caractere in enumerate(message):
|
||
|
|
# Récupérer le caractère de la clé (cyclique)
|
||
|
|
cle_char = cle[i % len(cle)]
|
||
|
|
# Appliquer XOR
|
||
|
|
nouveau_char = chr(ord(caractere) ^ ord(cle_char))
|
||
|
|
resultat += nouveau_char
|
||
|
|
return resultat
|
||
|
|
```
|
||
|
|
|
||
|
|
**Test** :
|
||
|
|
```python
|
||
|
|
>>> message = "Bonjour le monde"
|
||
|
|
>>> cle = "secret"
|
||
|
|
>>> chiffre = xor_chiffrement(message, cle)
|
||
|
|
>>> print(chiffre) # Caractères illisibles
|
||
|
|
>>> xor_chiffrement(chiffre, cle) # Déchiffrement
|
||
|
|
'Bonjour le monde'
|
||
|
|
```
|
||
|
|
|
||
|
|
### 1.3. Questions
|
||
|
|
|
||
|
|
1. Pourquoi la clé est-elle utilisée de manière cyclique ?
|
||
|
|
2. Que se passe-t-il si la clé est aussi longue que le message ?
|
||
|
|
3. Pourquoi XOR seul n'est-il pas suffisamment sécurisé ?
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Partie 2 : Classe MotDePasse
|
||
|
|
|
||
|
|
### 2.1. Structure d'une entrée
|
||
|
|
|
||
|
|
Créer une classe pour représenter un mot de passe stocké :
|
||
|
|
|
||
|
|
```python
|
||
|
|
class MotDePasse:
|
||
|
|
"""Représente un identifiant stocké."""
|
||
|
|
|
||
|
|
def __init__(self, site, identifiant, mot_de_passe):
|
||
|
|
"""
|
||
|
|
Initialise une entrée.
|
||
|
|
|
||
|
|
:param site: (str) Nom du site/service
|
||
|
|
:param identifiant: (str) Nom d'utilisateur ou email
|
||
|
|
:param mot_de_passe: (str) Mot de passe en clair
|
||
|
|
"""
|
||
|
|
self.site = site
|
||
|
|
self.identifiant = identifiant
|
||
|
|
self.mot_de_passe = mot_de_passe
|
||
|
|
|
||
|
|
def __repr__(self):
|
||
|
|
# Ne pas afficher le mot de passe en clair !
|
||
|
|
return f"MotDePasse({self.site}, {self.identifiant}, ****)"
|
||
|
|
|
||
|
|
def to_string(self):
|
||
|
|
"""Convertit en chaîne pour le stockage."""
|
||
|
|
return f"{self.site}|{self.identifiant}|{self.mot_de_passe}"
|
||
|
|
|
||
|
|
@staticmethod
|
||
|
|
def from_string(chaine):
|
||
|
|
"""Crée un objet depuis une chaîne."""
|
||
|
|
parties = chaine.split("|")
|
||
|
|
return MotDePasse(parties[0], parties[1], parties[2])
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Partie 3 : Gestionnaire principal
|
||
|
|
|
||
|
|
### 3.1. Classe GestionnaireMDP
|
||
|
|
|
||
|
|
Implémenter le gestionnaire complet :
|
||
|
|
|
||
|
|
```python
|
||
|
|
class GestionnaireMDP:
|
||
|
|
"""Gestionnaire de mots de passe sécurisé."""
|
||
|
|
|
||
|
|
def __init__(self, mot_de_passe_maitre):
|
||
|
|
"""
|
||
|
|
Initialise le gestionnaire.
|
||
|
|
|
||
|
|
:param mot_de_passe_maitre: (str) Clé principale de chiffrement
|
||
|
|
"""
|
||
|
|
self.cle = mot_de_passe_maitre
|
||
|
|
self.coffre = [] # Liste des MotDePasse
|
||
|
|
|
||
|
|
def ajouter(self, site, identifiant, mot_de_passe):
|
||
|
|
"""
|
||
|
|
Ajoute un nouveau mot de passe au coffre.
|
||
|
|
|
||
|
|
:param site: (str) Nom du site
|
||
|
|
:param identifiant: (str) Identifiant
|
||
|
|
:param mot_de_passe: (str) Mot de passe
|
||
|
|
"""
|
||
|
|
# À compléter
|
||
|
|
pass
|
||
|
|
|
||
|
|
def rechercher(self, site):
|
||
|
|
"""
|
||
|
|
Recherche un mot de passe par site.
|
||
|
|
|
||
|
|
:param site: (str) Nom du site à rechercher
|
||
|
|
:return: (MotDePasse ou None) L'entrée trouvée
|
||
|
|
"""
|
||
|
|
# À compléter
|
||
|
|
pass
|
||
|
|
|
||
|
|
def supprimer(self, site):
|
||
|
|
"""
|
||
|
|
Supprime une entrée du coffre.
|
||
|
|
|
||
|
|
:param site: (str) Nom du site à supprimer
|
||
|
|
:return: (bool) True si supprimé, False sinon
|
||
|
|
"""
|
||
|
|
# À compléter
|
||
|
|
pass
|
||
|
|
|
||
|
|
def lister_sites(self):
|
||
|
|
"""
|
||
|
|
Liste tous les sites enregistrés (sans les mots de passe).
|
||
|
|
|
||
|
|
:return: (list) Liste des noms de sites
|
||
|
|
"""
|
||
|
|
# À compléter
|
||
|
|
pass
|
||
|
|
```
|
||
|
|
|
||
|
|
### 3.2. Sauvegarde chiffrée
|
||
|
|
|
||
|
|
Ajouter les méthodes de sauvegarde et chargement :
|
||
|
|
|
||
|
|
```python
|
||
|
|
def sauvegarder(self, fichier):
|
||
|
|
"""
|
||
|
|
Sauvegarde le coffre dans un fichier chiffré.
|
||
|
|
|
||
|
|
:param fichier: (str) Chemin du fichier
|
||
|
|
"""
|
||
|
|
# Convertir toutes les entrées en une chaîne
|
||
|
|
contenu = "\n".join([mdp.to_string() for mdp in self.coffre])
|
||
|
|
# Chiffrer le contenu
|
||
|
|
contenu_chiffre = xor_chiffrement(contenu, self.cle)
|
||
|
|
# Écrire dans le fichier
|
||
|
|
with open(fichier, 'w', encoding='utf-8') as f:
|
||
|
|
f.write(contenu_chiffre)
|
||
|
|
|
||
|
|
def charger(self, fichier):
|
||
|
|
"""
|
||
|
|
Charge le coffre depuis un fichier chiffré.
|
||
|
|
|
||
|
|
:param fichier: (str) Chemin du fichier
|
||
|
|
"""
|
||
|
|
# À compléter
|
||
|
|
pass
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Partie 4 : Génération de mots de passe
|
||
|
|
|
||
|
|
### 4.1. Générateur simple
|
||
|
|
|
||
|
|
Créer une fonction de génération de mots de passe robustes :
|
||
|
|
|
||
|
|
```python
|
||
|
|
import random
|
||
|
|
import string
|
||
|
|
|
||
|
|
def generer_mot_de_passe(longueur=16, majuscules=True, chiffres=True, speciaux=True):
|
||
|
|
"""
|
||
|
|
Génère un mot de passe aléatoire robuste.
|
||
|
|
|
||
|
|
:param longueur: (int) Longueur du mot de passe
|
||
|
|
:param majuscules: (bool) Inclure des majuscules
|
||
|
|
:param chiffres: (bool) Inclure des chiffres
|
||
|
|
:param speciaux: (bool) Inclure des caractères spéciaux
|
||
|
|
:return: (str) Mot de passe généré
|
||
|
|
"""
|
||
|
|
caracteres = string.ascii_lowercase
|
||
|
|
|
||
|
|
if majuscules:
|
||
|
|
caracteres += string.ascii_uppercase
|
||
|
|
if chiffres:
|
||
|
|
caracteres += string.digits
|
||
|
|
if speciaux:
|
||
|
|
caracteres += "!@#$%^&*()_+-=[]{}|;:,.<>?"
|
||
|
|
|
||
|
|
# À compléter : générer le mot de passe
|
||
|
|
pass
|
||
|
|
```
|
||
|
|
|
||
|
|
### 4.2. Vérification de robustesse
|
||
|
|
|
||
|
|
Implémenter une fonction d'évaluation :
|
||
|
|
|
||
|
|
```python
|
||
|
|
def evaluer_robustesse(mot_de_passe):
|
||
|
|
"""
|
||
|
|
Évalue la robustesse d'un mot de passe.
|
||
|
|
|
||
|
|
:param mot_de_passe: (str) Le mot de passe à évaluer
|
||
|
|
:return: (str) Niveau de robustesse (Faible, Moyen, Fort, Très fort)
|
||
|
|
"""
|
||
|
|
score = 0
|
||
|
|
|
||
|
|
# Longueur
|
||
|
|
if len(mot_de_passe) >= 8:
|
||
|
|
score += 1
|
||
|
|
if len(mot_de_passe) >= 12:
|
||
|
|
score += 1
|
||
|
|
if len(mot_de_passe) >= 16:
|
||
|
|
score += 1
|
||
|
|
|
||
|
|
# Variété de caractères
|
||
|
|
if any(c.islower() for c in mot_de_passe):
|
||
|
|
score += 1
|
||
|
|
if any(c.isupper() for c in mot_de_passe):
|
||
|
|
score += 1
|
||
|
|
if any(c.isdigit() for c in mot_de_passe):
|
||
|
|
score += 1
|
||
|
|
if any(c in "!@#$%^&*()_+-=[]{}|;:,.<>?" for c in mot_de_passe):
|
||
|
|
score += 1
|
||
|
|
|
||
|
|
# À compléter : retourner le niveau selon le score
|
||
|
|
pass
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Partie 5 : Interface utilisateur
|
||
|
|
|
||
|
|
### 5.1. Menu interactif
|
||
|
|
|
||
|
|
Créer une interface en ligne de commande :
|
||
|
|
|
||
|
|
```python
|
||
|
|
def menu_principal():
|
||
|
|
"""Affiche le menu principal."""
|
||
|
|
print("\n" + "=" * 40)
|
||
|
|
print(" GESTIONNAIRE DE MOTS DE PASSE")
|
||
|
|
print("=" * 40)
|
||
|
|
print("1. Ajouter un mot de passe")
|
||
|
|
print("2. Rechercher un mot de passe")
|
||
|
|
print("3. Générer un mot de passe")
|
||
|
|
print("4. Lister les sites")
|
||
|
|
print("5. Supprimer une entrée")
|
||
|
|
print("6. Sauvegarder")
|
||
|
|
print("7. Quitter")
|
||
|
|
print("=" * 40)
|
||
|
|
return input("Votre choix : ")
|
||
|
|
|
||
|
|
|
||
|
|
def application():
|
||
|
|
"""Lance l'application."""
|
||
|
|
print("Bienvenue dans le gestionnaire de mots de passe !")
|
||
|
|
mdp_maitre = input("Entrez votre mot de passe maître : ")
|
||
|
|
|
||
|
|
gestionnaire = GestionnaireMDP(mdp_maitre)
|
||
|
|
|
||
|
|
# Tenter de charger un coffre existant
|
||
|
|
try:
|
||
|
|
gestionnaire.charger("coffre.dat")
|
||
|
|
print("Coffre chargé avec succès.")
|
||
|
|
except FileNotFoundError:
|
||
|
|
print("Nouveau coffre créé.")
|
||
|
|
|
||
|
|
while True:
|
||
|
|
choix = menu_principal()
|
||
|
|
|
||
|
|
if choix == "1":
|
||
|
|
# Ajouter
|
||
|
|
site = input("Site : ")
|
||
|
|
identifiant = input("Identifiant : ")
|
||
|
|
mdp = input("Mot de passe (laisser vide pour générer) : ")
|
||
|
|
if mdp == "":
|
||
|
|
mdp = generer_mot_de_passe()
|
||
|
|
print(f"Mot de passe généré : {mdp}")
|
||
|
|
gestionnaire.ajouter(site, identifiant, mdp)
|
||
|
|
print("Ajouté !")
|
||
|
|
|
||
|
|
elif choix == "2":
|
||
|
|
# Rechercher
|
||
|
|
site = input("Site à rechercher : ")
|
||
|
|
resultat = gestionnaire.rechercher(site)
|
||
|
|
if resultat:
|
||
|
|
print(f"Site : {resultat.site}")
|
||
|
|
print(f"Identifiant : {resultat.identifiant}")
|
||
|
|
print(f"Mot de passe : {resultat.mot_de_passe}")
|
||
|
|
else:
|
||
|
|
print("Site non trouvé.")
|
||
|
|
|
||
|
|
elif choix == "3":
|
||
|
|
# Générer
|
||
|
|
longueur = int(input("Longueur (défaut 16) : ") or "16")
|
||
|
|
mdp = generer_mot_de_passe(longueur)
|
||
|
|
print(f"Mot de passe généré : {mdp}")
|
||
|
|
print(f"Robustesse : {evaluer_robustesse(mdp)}")
|
||
|
|
|
||
|
|
elif choix == "4":
|
||
|
|
# Lister
|
||
|
|
sites = gestionnaire.lister_sites()
|
||
|
|
print("Sites enregistrés :")
|
||
|
|
for s in sites:
|
||
|
|
print(f" - {s}")
|
||
|
|
|
||
|
|
elif choix == "5":
|
||
|
|
# Supprimer
|
||
|
|
site = input("Site à supprimer : ")
|
||
|
|
if gestionnaire.supprimer(site):
|
||
|
|
print("Supprimé !")
|
||
|
|
else:
|
||
|
|
print("Site non trouvé.")
|
||
|
|
|
||
|
|
elif choix == "6":
|
||
|
|
# Sauvegarder
|
||
|
|
gestionnaire.sauvegarder("coffre.dat")
|
||
|
|
print("Coffre sauvegardé.")
|
||
|
|
|
||
|
|
elif choix == "7":
|
||
|
|
gestionnaire.sauvegarder("coffre.dat")
|
||
|
|
print("Au revoir !")
|
||
|
|
break
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Partie 6 : Améliorations (Bonus)
|
||
|
|
|
||
|
|
### 6.1. Hashage du mot de passe maître
|
||
|
|
|
||
|
|
Au lieu de stocker le mot de passe maître directement, utiliser un hash :
|
||
|
|
|
||
|
|
```python
|
||
|
|
import hashlib
|
||
|
|
|
||
|
|
def hasher(mot_de_passe):
|
||
|
|
"""Retourne le hash SHA-256 du mot de passe."""
|
||
|
|
return hashlib.sha256(mot_de_passe.encode()).hexdigest()
|
||
|
|
```
|
||
|
|
|
||
|
|
### 6.2. Dérivation de clé
|
||
|
|
|
||
|
|
Utiliser une fonction de dérivation de clé (PBKDF2) pour renforcer la sécurité.
|
||
|
|
|
||
|
|
### 6.3. Chiffrement AES
|
||
|
|
|
||
|
|
Remplacer XOR par AES pour un chiffrement professionnel (nécessite la bibliothèque `cryptography`).
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Questions de synthèse
|
||
|
|
|
||
|
|
1. Pourquoi ne faut-il **jamais** stocker un mot de passe en clair ?
|
||
|
|
|
||
|
|
2. Quelle est la différence entre **chiffrement** et **hashage** ?
|
||
|
|
|
||
|
|
3. Pourquoi le mot de passe maître doit-il être robuste ?
|
||
|
|
|
||
|
|
4. Quels sont les risques si quelqu'un obtient le fichier `coffre.dat` ?
|
||
|
|
|
||
|
|
5. Comment les vrais gestionnaires de mots de passe (Bitwarden, 1Password) améliorent-ils la sécurité ?
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Barème indicatif
|
||
|
|
|
||
|
|
| Partie | Points |
|
||
|
|
|--------|--------|
|
||
|
|
| Partie 1 : Chiffrement XOR | 3 |
|
||
|
|
| Partie 2 : Classe MotDePasse | 2 |
|
||
|
|
| Partie 3 : Gestionnaire | 5 |
|
||
|
|
| Partie 4 : Génération | 4 |
|
||
|
|
| Partie 5 : Interface | 4 |
|
||
|
|
| Partie 6 : Bonus | 2 |
|
||
|
|
| **Total** | **20** |
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
Auteur : Florian Mathieu
|
||
|
|
|
||
|
|
Licence CC BY NC
|
||
|
|
|
||
|
|
<a rel="license" href="http://creativecommons.org/licenses/by-nc-sa/4.0/"><img alt="Licence Creative Commons" style="border-width:0" src="https://i.creativecommons.org/l/by-nc-sa/4.0/88x31.png" /></a> <br />Ce cours est mis à disposition selon les termes de la <a rel="license" href="http://creativecommons.org/licenses/by-nc-sa/4.0/">Licence Creative Commons Attribution - Pas d'Utilisation Commerciale - Partage dans les Mêmes Conditions 4.0 International</a>.
|