bootstrap

This commit is contained in:
Francois Lesueur 2023-01-22 17:36:16 +01:00
parent e8b20b4ac8
commit e08cdc3b61
5 changed files with 632 additions and 2 deletions

View File

@ -1,3 +1,30 @@
# R4.C.08 BUT2 Info - R4.C.08 - Cryptographie et sécurité
===============================================
R4.C.08 - Cryptographie et sécurité _François Lesueur ([francois.lesueur@univ-ubs.fr](mailto:francois.lesueur@univ-ubs.fr))_
L'objectif de cette ressource est d'étudier les protocoles cryptographiques, utilisés en particulier au niveau réseau.
Savoirs de référence étudiés :
* Hachage, signature, stockage des mots de passe
* Cryptographie symétrique (par ex. : DES, AES...)
* Protocole SSL et certificats
* Intégrité des données (par ex. : codes correcteurs…)
La matière se déroule sur la période 3 et est composée de 3 séances de TP de 3h.
Les TP (à partir de la semaine 5) seront réalisés sur la plateforme MI-LXC (https://github.com/flesueur/mi-lxc) pour laquelle il faudra télécharger une VM Virtualbox **avant** : [.ova à télécharger ici](https://flesueur.irisa.fr/mi-lxc/images/milxc-snster-vm-2.1.0.ova), nous utiliserons la version 2.1.0 (la version 2.0.0 précédemment utilisée en réseau fonctionnera également). Il faudra arriver en séance avec Virtualbox installé et le .ova de MI-LXC déjà téléchargé et importé.
Sur ce dépôt, vous trouverez les sujets de TP.
Programme
=========
Le programme prévisionnel est le suivant :
* S4 :
* [TD1](td-jdr.md) : TD crypto asymétrique
* [TD2](td-passwords.md) : TD passwords
* S5 :
* TP1 : TLS/CA
* S6 :
* TP2 : TBA

117
td-jdr.md Normal file
View File

@ -0,0 +1,117 @@
TD2 : Usage de la cryptographie asymétrique
=============================================
_Pas de compte-rendu pour ce TD_
Ce TD présente et applique les notions de cryptographie asymétrique :
* Génération de clés RSA
* Distribution de clés
* Signature et chiffrement RSA
Le cryptosystème que nous allons utiliser ici est basé sur la fonction RSA. Le cryptosystème proposé est simple et présente donc certaines vulnérabilités mais illustre le fonctionnement.
Génération de clés RSA
======================
Nous allons commencer par générer une paire de clés RSA pour chacun. Voici l'algorithme simplifié de génération de clés RSA (en réalité, d'autres tests doivent être réalisés) :
* Choisir deux nombres premiers _p_ et _q_ ([exemples de premiers](https://fr.wikipedia.org/wiki/Liste_de_nombres_premiers))
* Calculer _n = p * q_ (__Attention, pour que la suite du TD fonctionne, n doit être supérieur à 1000 !__)
* Calculer _φ(n) = (p-1)(q-1)_
* Choisir _e_ tel que :
* _1 < e < &phi;(n)_
* _pgcd(e, &phi;(n)) = 1_
* Par exemple, un premier qui ne divise pas &phi;(n)
* Déterminer l'inverse modulaire _d &equiv; e<sup>-1</sup> mod &phi;(n)_. Vous pouvez utiliser [DCODE](https://www.dcode.fr/inverse-modulaire) pour cela (attention, pas le `pow` Python pour ça, sauf si vous êtes *certain* d'avoir une version de python supérieure ou égale à 3.8 !) <!-- Vous pouvez utiliser [Wolfram Alpha](http://www.wolframalpha.com), avec une requête de la forme `7 ^ -1 mod 1147` (attention, pas le `pow` Python pour ça !) -->
* La clé publique est _(e,n)_ et la clé privée est _(d,n)_
Gardez votre clé privée secrète et transmettez votre clé publique avec votre nom à l'enseignant, sur un papier. Elle sera inscrite au tableau (la "PKI").
Les exemples dans la suite du sujet sont réalisés avec p=31, q=37, n=1147, &phi;(n)=1080, e=7, d=463. La clé publique est _(e,n)_, ici _(7,1147)_, et la clé privée est _(d,n)_, ici _(463,1147)_.
<!-- Code Python pour calculer _a<sup>-1</sup> mod b_ : `modinv(a,b)` disponible [ici](modinv.py) -->
Rappel : la propriété utilisée est que pour tout message _m, m<sup>de</sup>[n] = m_.
Chiffrement et déchiffrement
============================
Description
-----------
Nous allons chiffrer des chaînes de caractères. Pour cela, chaque lettre est remplacée par son rang dans l'alphabet, sur 2 chiffres :
|a|b|c|d|e|f|g|h|i|j|k|l|m|n|o|p|q|r|s|t|u|v|w|x|y|z|_|
|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
|01|02|03|04|05|06|07|08|09|10|11|12|13|14|15|16|17|18|19|20|21|22|23|24|25|26|27|
Par exemple, "crypto" devient `03 18 25 16 20 15`
Ensuite, afin de ne pas retomber dans un chiffrement par substitution simple, les chiffres sont assemblés par blocs de 3 (complété éventuellement de 0 à la fin), ainsi `03 18 25 16 20 15` devient `031 825 162 015`.
Enfin, chaque bloc clair de 3 chiffres est chiffré indépendamment par la fonction RSA : bloc<sub>chiffré</sub> = bloc<sub>clair</sub><sup>e</sup>[n]. Attention, _(e,n)_ représente une clé publique, mais celle de qui ? L'utilisation de la clé _(7,1147)_ donne le chiffré `1116 751 245 1108`.
> Pour calculer les exponentiations modulaires, vous pouvez utiliser python (dans l'interpréteur, tapez `pow(a,b,c)` pour obtenir a<sup>b</sup>[c]) ou [DCODE](https://www.dcode.fr/exponentiation-modulaire)<!--[Wolfram Alpha](http://www.wolframalpha.com)-->. Attention, lors des calculs, n'écrivez pas de '0' en début d'entier. Par exemple, pour le bloc clair `031`, tapez `pow(31,7,1147)`. Commencer un entier par '0' le fait interpréter comme un nombre encodé en _octal_ (même principe qu'un nombre commençant par '0x' qui est interprété comme un hexadécimal).
Le déchiffrement est opéré de manière analogue, en utilisant la clé privée au lieu de la clé publique. Chaque bloc clair est réobtenu à partir du bloc chiffré par le calcul : bloc<sub>clair</sub> = bloc<sub>chiffré</sub><sup>d</sup>[n]
Mise en pratique
----------------
Vous allez maintenant transmettre un message chiffré à un étudiant éloigné par un protocole multi-saut : vous le transmettez à un voisin, qui le redonne à un voisin, etc., jusqu'à sa destination. Vous jouerez à la fois les rôles d'émetteur, de routeur et de récepteur. Le chiffrement assure la _confidentialité_ du message transmis.
1. **Envoi de votre message** : Chiffrez un message de votre choix avec le cryptosystème proposé. Inscrivez sur un papier votre identité, le message chiffré et le destinataire. Envoyez-le !
2. **Routage des autres messages** : Que fait un routeur ? Il lit un message, l'analyse, décide où l'envoyer puis le reproduit. De manière analogue, vous allez pour chaque saut retransmettre le message entrant mais vous pouvez le lire avant de le retransmettre. Pouvez-vous en déduire des informations ?
3. **Réception d'un message** : À la réception d'un message, appliquez l'algorithme de déchiffrement. Quelqu'un d'autre sur la route du message pouvait-il obtenir le clair de ce message ?
Signature et vérification
=========================
Description
-----------
Nous allons signer des chaînes de caractères. Pour cela, chaque lettre est remplacée par son rang dans l'alphabet. Pour un message _m = (m<sub>0</sub>, ..., m<sub>i</sub>)_ avec _(m<sub>0</sub>, ..., m<sub>i</sub>)_ les rangs de chaque lettre (attention, on ne fait plus des blocs de 3 chiffres ici), le haché _h(m)_ est calculé par l'algorithme suivant :
h = 2;
for (j=0; j<i; j++) {
h = h * 2;
h = h + m[j];
}
return h%1000;
La valeur de la signature vaut alors _h(m)<sup>d</sup>[n]_. Attention, _(d,n)_ représente une clé privée, mais celle de qui ? Le haché de "crypto" vaut par exemple 831 et la signature par _(463,1147)_ est 335.
Le message est alors envoyé accompagné de sa signature. La vérification d'un message reçu _m_ signé avec _sig_ est opérée de la manière suivante :
* Calculer _h(m)_ par rapport au _m_ reçu
* Calculer _sig<sup>e</sup>[n]_
* Vérifier que _h(m) == sig<sup>e</sup>[n]_ sur le message reçu
Mise en pratique
----------------
Vous allez maintenant transmettre un message clair (non chiffré) signé à un étudiant éloigné par ce même protocole multi-saut. La signature permet de vérifier l'_intégrité_ du message transmis.
1. **Envoi de votre message** : Signez un message de votre choix avec le cryptosystème proposé. Inscrivez sur un papier votre identité, le message clair, la signature et le destinataire. Envoyez-le !
2. **Routage des autres messages** : Utilisez le même protocole multi-saut que précédemment. Pour chaque saut, recopiez le message entrant sur un autre papier puis retransmettez ce second papier.
3. **Réception d'un message** : À la réception d'un message, appliquez l'algorithme de vérification de la signature. Le message reçu est-il intègre ? Si non, quelle attaque avez-vous détectée ? Un attaquant pouvait-il l'altérer en chemin ?
Bonus : Attaques sur le cryptosystème proposé
=====================================
Étudiez et testez quelques attaques sur le système mis en place :
* Modification de message en conservant la validité de la signature
* Attaque de la clé privée (par factorisation de _n_ par exemple)
Toutes ces attaques sont possibles ici. Réfléchissez à leur cause et aux protections mises en place dans les cryptosystèmes réels. Implémentez une (ou plusieurs) attaque dans le langage de votre choix, proposez une contre-mesure et évaluez la complexité rajoutée par votre contre-mesure.
![Don't roll your own crypto](https://image.slidesharecdn.com/signal-publickeycrypto-181018000453/95/signal-practical-cryptography-40-638.jpg?cb=1539821143)

View File

@ -0,0 +1,131 @@
#!/usr/bin/env python3
import time
import sys
from toolbox import *
# You should tweak these values during the work
nblogins = 10 # would be larger in real-life
nbpasswords = 1000000 # would be larger in real-life
nbiterations = 10 # 10000 is currently recommended, should be adapted to the usecase and changed with time (improvement of computation power), like a key size
############################################
# Part of the script to edit #
############################################
# Hint : you can call decrypt(key,data) to decrypt data using key
def crackencrypted(database):
key = readfile("enckey")[0]
crackeddb = []
for i in database:
# i[0] is the login, i[1] is the encrypted password
#...
crackeddb.append((i[0],i[1])) # second argument should contain cleartext password
return crackeddb
# Hint : - genshahashes(passwords) returns all the hashes of passwords dictionary
# - getpassfromshahash(hashes, hash) returns the password which hashed as "hash" in hashes
def cracksha(database):
global nbpasswords
passwords = getPassDict(nbpasswords) # passwords contains a dictionary of passwords
#...
crackeddb = []
for i in database:
# i[0] is the login, i[1] is the hashed password
#...
crackeddb.append((i[0],i[1])) # second argument should contain cleartext password
return crackeddb
# Hint : salthash(password, salt) return the salted hash of password
def cracksaltedsha(database):
global nbpasswords
passwords = getPassDict(nbpasswords)
crackeddb = []
for i in database:
# i[0] is the login, i[1] is the hashed password, i[2] is the salt
#...
crackeddb.append((i[0],i[1])) # second argument should contain cleartext password
return crackeddb
# Hint : pbkdf2(password, salt, nbiterations) returns the pbkdf2 of password using salt and nbiterations
def crackpbkdf2(database):
global nbpasswords
passwords = getPassDict(nbpasswords)
crackeddb = []
for i in database:
# i[0] is the login, i[1] is the hashed password, i[2] is the salt, i[3] is the iteration count
#...
crackeddb.append((i[0],i[1])) # second argument should contain cleartext password
return crackeddb
############################################
# Nothing to change after this line ! #
############################################
if __name__ == '__main__':
# When called with init
if len(sys.argv) > 1 and sys.argv[1] == "init":
initworkspace(nblogins,nbpasswords,nbiterations)
print("Workspace initialized in files/ subdirectory")
exit(0)
# Test whether init has been called before
try :
readfile("plain")
except FileNotFoundError:
initworkspace(nblogins,nbpasswords,nbiterations)
print("Workspace initialized in files/ subdirectory")
# test plain DB
print("\n============\nPlain storage:")
plaindb = readfile("plain")
print("Plain DB is : " + str(plaindb))
print("Authenticating with plain DB : " + str(authplain(plaindb[0][0],plaindb[0][1],plaindb)))
#test encrypted db
print("\n============\nEncrypted storage:")
encdb = readfile("enc")
print("Encrypted DB is " + str(encdb))
print("Authenticating with encrypted DB : " + str(authencrypted(plaindb[1][0],plaindb[1][1],encdb)))
start = time.time()
crackedenc = crackencrypted(encdb)
end = time.time()
print("Time to crack encrypted DB : " + str(end-start) + " seconds")
print("Cracked encrypted DB is " + str(crackedenc))
#test SHA db
print("\n============\nSHA storage:")
shadb = readfile("sha")
print("SHA DB is " + str(shadb))
print("Authenticating with SHA DB : " + str(authsha(plaindb[0][0],plaindb[0][1],shadb)))
start = time.time()
crackedsha = cracksha(shadb)
end = time.time()
print("Time to crack SHA DB : " + str(end-start) + " seconds")
print("Cracked SHA DB is " + str(crackedsha))
#test Salted SHA db
print("\n============\nSalted SHA storage:")
saltedshadb = readfile("saltedsha")
print("Salted SHA DB is " + str(saltedshadb))
print("Authenticating with Salted SHA DB : " + str(authsaltedsha(plaindb[0][0],plaindb[0][1],saltedshadb)))
start = time.time()
crackedsaltedsha = cracksaltedsha(saltedshadb)
end = time.time()
print("Time to crack salted SHA DB : " + str(end-start) + " seconds")
print("Cracked salted SHA DB is " + str(crackedsaltedsha))
# test PBKDF2 DB
print("\n============\nPBKDF2 storage:")
pbkdf2db = readfile("pbkdf2")
print("PBKDF2 DB is " + str(pbkdf2db))
print("Authenticating with PBKDF2 DB : " + str(authpbkdf2(plaindb[0][0],plaindb[0][1],pbkdf2db)))
start = time.time()
crackedpbkdf2 = crackpbkdf2(pbkdf2db)
end = time.time()
print("Time to crack PBKDF2 DB : " + str(end-start) + " seconds")
print("Cracked PBKDF2 DB is " + str(crackedpbkdf2))

View File

@ -0,0 +1,250 @@
#!/usr/bin/env python3
# Avoir une lib avec des briques prêtes
# Avoir un script qui crée les fichiers de password SHA/PB/etc. pour pouvoir les manipuler en texte
# TD : associer les briques pour évaluer les attaques sur un pass / une base
# mettre un « except ImportError » et ressayer avec « Cryptodome » a la place de « Crypto »
import re
import time
import random
import hashlib
# tweak to (try to) handle different crypto lib naming across systems (Linux, Mac, Win)
try:
from Crypto.Cipher import AES
from Crypto import Random
except ImportError:
try:
from crypto.Cipher import AES
from crypto import Random
except ImportError:
from Cryptodome.Cipher import AES
from Cryptodome import Random
import base64
import os
import urllib.request
import string
# returns an array of a dictionary of passwords
def getPassDict(nbpasswords):
try:
f = open("files/passwords.txt")
except FileNotFoundError:
print("Downloading a passwords list...")
urllib.request.urlretrieve("https://github.com/danielmiessler/SecLists/blob/master/Passwords/Common-Credentials/10-million-password-list-top-1000000.txt?raw=true", "files/passwords.txt")
print("Done !")
f = open("files/passwords.txt")
passwords = []
#nbpasswords = 10000
passtogen = nbpasswords
for password in f:
passwords.append(password.strip())
passtogen-=1
if passtogen == 0:
break
return passwords
def genRandomPassword():
length = 6
chars = string.ascii_letters + string.digits
return ''.join(random.choice(chars) for i in range(length))
# reads/writes shadow-style files
def readfile(filename):
f = open("files/"+filename)
res = []
for line in f:
output = line.strip().split(":")
res.append(output)
return res
def writeFile(filename, array):
f = open("files/"+filename,'w')
for line in array:
towrite = ""
for item in line:
towrite+=item + ":"
towrite = towrite[:-1]
f.write(towrite)
f.write('\n')
# Plain storage
def genplain(nblogins,nbpasswords):
passwords = getPassDict(nbpasswords)
logins = []
for i in range(0,nblogins):
login = "user" + str(i)
if (random.randint(0,10) < 4):
logins.append((login,passwords[random.randint(0,len(passwords)-1)]))
else:
logins.append((login,genRandomPassword()))
return logins
def authplain(login, passwd, database):
for i in database:
if i[0] == login:
current = i[1]
return (current == passwd)
# Encrypted storage
def genencrypted(logins):
encdb = []
key = Random.new().read(16)
iv = Random.new().read(AES.block_size)
f = open("files/enckey",'wb')
f.write((base64.b64encode(key)))
f.write(b":")
#f = open("files/enciv",'wb')
f.write((base64.b64encode(iv)))
for i in logins:
cipher = AES.new(key, AES.MODE_CFB, iv)
enc = (base64.b64encode(cipher.encrypt(i[1].encode('utf-8')))).decode("utf-8")
encdb.append((i[0],enc))
#print(enc)
return encdb
def authencrypted(login, passwd, database):
for i in database:
if i[0] == login:
current = i[1]
keyiv = readfile("enckey")[0]
key = base64.b64decode(keyiv[0])
iv = base64.b64decode(keyiv[1])
#key = base64.b64decode(readfile("enckey")[0][0])
#iv = base64.b64decode(readfile("enciv")[0][0])
cipher = AES.new(key, AES.MODE_CFB, iv)
return (passwd == cipher.decrypt(base64.b64decode(current)).decode('utf-8'))
def decrypt(keyiv,data):
key = base64.b64decode(keyiv[0])
iv = base64.b64decode(keyiv[1])
cipher = AES.new(key, AES.MODE_CFB, iv)
return cipher.decrypt(base64.b64decode(data)).decode('utf-8')
# SHA storage
def gensha(logins):
db = []
for i in logins:
csum = hashlib.sha256(i[1].encode('utf-8')).hexdigest()
db.append((i[0],csum))
return db
def authsha(login, passwd, database):
for i in database:
if i[0] == login:
current = i[1]
return (current == hashlib.sha256(passwd.encode('utf-8')).hexdigest())
def genshahashes(passwords):
hashes = []
for passwd in passwords:
hashes.append([hashlib.sha256(passwd.encode('utf-8')).hexdigest(),passwd])
return hashes
def getpassfromshahash(hashes, hash):
for j in hashes:
if j[0] == hash:
return j[1]
return None
# Salted SHA storage
def gensaltedsha(logins):
db = []
for i in logins:
salt = str(random.randint(0,65535))
csum = hashlib.sha256((i[1]+salt).encode('utf-8')).hexdigest()
db.append((i[0],csum,salt))
return db
def authsaltedsha(login, passwd, database):
for i in database:
if i[0] == login:
current = i[1]
salt = i[2]
return (current == hashlib.sha256((passwd+salt).encode('utf-8')).hexdigest())
def salthash(password,salt):
return hashlib.sha256((password+str(salt)).encode('utf-8')).hexdigest()
# PBKDF2 storage
def genpbkdf2(logins,nbiterations):
db = []
for i in logins:
salt = str(random.randint(0,65535))
csum = base64.b64encode(hashlib.pbkdf2_hmac('sha256',i[1].encode('utf-8'),str(salt).encode('utf-8'),nbiterations)).decode('utf-8')
db.append((i[0],csum,salt,str(nbiterations)))
return db
def authpbkdf2(login, passwd, database):
for i in database:
if i[0] == login:
current = i[1]
salt = i[2]
nbiterations = int(i[3])
return (base64.b64decode(current) == hashlib.pbkdf2_hmac('sha256',passwd.encode('utf-8'),str(salt).encode('utf-8'),nbiterations))
def pbkdf2(password,salt,nbiterations):
nbiterations = int(nbiterations)
return base64.b64encode(hashlib.pbkdf2_hmac('sha256',password.encode('utf-8'),str(salt).encode('utf-8'),nbiterations)).decode('utf-8')
# Generate shadow-style files
def initworkspace(nblogins,nbpasswords,nbiterations):
print("Generating " + str(nblogins) + " logins and " + str(nbpasswords) + " passwords")
try :
os.mkdir("files")
except FileExistsError:
pass
plaindb = genplain(nblogins,nbpasswords)
writeFile("plain", plaindb)
encdb = genencrypted(plaindb)
writeFile("enc", encdb)
shadb = gensha(plaindb)
writeFile("sha", shadb)
saltedshadb = gensaltedsha(plaindb)
writeFile("saltedsha", saltedshadb)
pbkdf2db = genpbkdf2(plaindb,nbiterations)
writeFile("pbkdf2", pbkdf2db)
# Unit tests
if __name__ == '__main__':
# create shadow files
initworkspace(10,100,1000)
print("======\nUnit tests of the toolbox, you must work in skeleton.py\n=========")
# test plain DB
print("\n============\nPlain storage:")
plaindb = readfile("plain")
print("Plain DB is : " + str(plaindb))
print("Authenticating with plain DB : " + str(authplain(plaindb[0][0],plaindb[0][1],plaindb)))
#test encrypted db
print("\n============\nEncrypted storage:")
encdb = readfile("enc")
print("Encrypted DB is " + str(encdb))
print("Authenticating with encrypted DB : " + str(authencrypted(plaindb[1][0],plaindb[1][1],encdb)))
#test SHA db
print("\n============\nSHA storage:")
shadb = readfile("sha")
print("SHA DB is " + str(shadb))
print("Authenticating with SHA DB : " + str(authsha(plaindb[0][0],plaindb[0][1],shadb)))
#test Salted SHA db
print("\n============\nSalted SHA storage:")
saltedshadb = readfile("saltedsha")
print("Salted SHA DB is " + str(saltedshadb))
print("Authenticating with Salted SHA DB : " + str(authsaltedsha(plaindb[0][0],plaindb[0][1],saltedshadb)))
# test PBKDF2 DB
print("\n============\nPBKDF2 storage:")
pbkdf2db = readfile("pbkdf2")
print("PBKDF2 DB is " + str(pbkdf2db))
print("Authenticating with PBKDF2 DB : " + str(authpbkdf2(plaindb[0][0],plaindb[0][1],pbkdf2db)))
print("\n======\nUnit tests of the toolbox, you must work in skeleton.py\n=========")

105
td-passwords.md Normal file
View File

@ -0,0 +1,105 @@
# TD : Stockage et authentification par mot de passe
Ce TD présente le stockage et l'authentification par mot de passe côté serveur. Il s'agit typiquement du problème rencontré par une application web qui souhaite authentifier ses utilisateurs, ou de ce qui est mis en œuvre dans une base de mots de passes système (/etc/shadow ou LDAP). Pour cela, l'application va stocker en base les comptes existants ainsi que le moyen de les authentifier. Comme il n'est évidemment pas souhaitable de stocker les mots de passe des utilisateurs en clair, nous allons analyser comment résoudre ce problème.
> Le compte-rendu est à déposer en binôme sur Moodle au format PDF uniquement.
Contexte général
================
Le scénario d'attaque est une exfiltration des fichiers de l'application (dont le fichier de la base de données ou le fichier `/etc/shadow` par exemple). Vous pouvez consulter une liste non exhaustive de bases ayant été dérobées sur le site [HaveIBeenPwned](https://haveibeenpwned.com/PwnedWebsites) pour vous rendre compte de l'étendue du problème. Dans ce cadre, nous posons les points suivants :
* Le serveur est déjà compromis, l'obtention d'un compte valide sur ce serveur n'a pas d'intérêt pour l'attaquant.
* Les victimes potentielles sont les utilisateurs du site qui y ont enregistré un compte. En effet, un attaquant pourrait alors essayer de se connecter en leur nom sur des services tiers grâce aux informations récupérées.
Pour limiter ce risque, deux approches complémentaires doivent être mises en place :
1. Le serveur doit compliquer autant que possible la tâche de l'attaquant qui a volé la base en maximisant le temps nécessaire pour obtenir des informations valides à partir de la base (objet de ce TD).
2. Les utilisateurs, n'ayant pas la possibilité de connaître les contre-mesures mises en place par le serveur, doivent limiter l'impact de cette compromission en utilisant des mots de passes différents, idéalement un pour chaque site (ce qui passe souvent par un gestionnaire de mots de passe, non abordé dans ce TD).
Ces deux mesures sont bien complémentaires car il est du devoir de chaque site de protéger les mots de passes des utilisateurs n'appliquant pas les meilleures pratiques et de chaque utilisateur de protéger au mieux de ses capacités ses mots de passes. Dans le cadre de ce TD, nous analysons la mesure (1), à appliquer côté serveur.
Squelette de code fourni
========================
Vous devez télécharger le squelette de code [ici](td-passwords-files). Vous pouvez récupérer l'intégralité du dépôt en tapant `git clone https://git.kaz.bzh/francois.lesueur/LPDLIS.git`, puis aller dans le dossier `td-passwords-files`. Vous pourriez avoir besoin d'installer la bibliothèque python PyCryptodome (de préférence, et nécessaire avec Python 3.8) ou PyCrypto (dépréciée, mais a priori fonctionnelle jusque Python 3.7). Par exemple avec pip3 pour avoir PyCryptodome uniquement (les deux ne peuvent pas coexister sur le système) :
```
pip3 uninstall PyCrypto
pip3 install -U PyCryptodome
```
* `toolbox.py` est la bibliothèque contenant la boîte à outils, il peut être intéressant d'aller la consulter mais elle n'est pas à modifier ;
* `skeleton.py` contient le programme à écrire.
En début de TD, vous devez lancer `./skeleton.py init` pour créer votre espace de travail. Cette commande, que vous pourrez rappeler plus tard, génère les fichiers suivants (lisibles) dans le sous-dossier `files` :
* `plain` contient la base login/password en clair
* `enc` contient la base chiffrée en AES (la clé, volée également car nécessairement accessible à proximité sauf cas particulier est dans `enckey`)
* `sha` contient la base hashée avec SHA256
* `saltedsha` contient la base hashée/salée avec SHA256
* `pbkdf2` contient la base avec n itérations de hash salé (PBKDF2)
Vous pouvez visualiser tous ces fichiers dans un éditeur de texte classique et analyser les différents champs présents. Vous pouvez ensuite exécuter `./skeleton.py` (sans argument) qui affichera, pour chaque schéma de stockage :
* le contenu de la base stockée
* le résultat d'un test unitaire d'authentification (doit toujours être vrai)
* l'appel (chronométré) à une fonction pour casser la base (fonctions non implémentées dans le squelette fourni)
Analyse des différents schémas
==============================
Pour chaque schéma (clair `plain`, chiffré `enc`, hash `sha`, hash salé `saltedsha`, hash salé coûteux `pbkdf2`), vous devez :
* analyser le processus de l'ajout d'un compte et de la vérification d'un mot de passe
* proposer la procédure de récupération pour un utilisateur qui a perdu son mot de passe
* évaluer l'information révélée directement par la base de mots de passe
* évaluer le coût de cassage d'un mot de passe isolé
* évaluer le coût de cassage de la base entière
* implémenter la fonction pour casser la base
<!--
Notations
=========
* h(m) est le hash du message m
* Si K<sub>A</sub> est une clé symétrique, {m}<sub>K<sub>A</sub></sub> est le chiffré de m avec la clé K<sub>A</sub>, m = { {m}<sub>K<sub>A</sub></sub>}<sub>K<sub>A</sub></sub>
* Si Pub<sub>A</sub> et Priv<sub>A</sub> sont des clés asymétriques complémentaires publique/privée, {m}<sub>Pub<sub>A</sub></sub> est le chiffré de m avec la clé Pub<sub>A</sub> et m = { {m}<sub>Pub<sub>A</sub></sub>}<sub>Priv<sub>A</sub></sub>
* m signé avec la clé Priv<sub>A</sub> est noté m.{h(m)}<sub>Priv<sub>A</sub></sub>
-->
<!--
Côté client
===========
Un gestionnaire de mots de passe conserve une table liant un titre, un login et un mot de passe. Par exemple :
| Titre | Login | Password |
| - | - | - |
| CDiscount | Alice31 | hujk15tr |
| laposte.net | AliceLefur@laposte.net | jku78!io |
| CB | 4785 1547 4554 6657 | 7514 |
Considérons que l'utilisation de ce gestionnaire de mots de passe nécessite la saisie préalable d'un mot de passe maître, lors de l'ouverture.
Proposez la mise en œuvre d'un gestionnaire de mots de passe local puis d'un gestionnaire de mots de passe en ligne. Analysez les risques d'attaques par les différents acteurs (eux-mêmes ou suite à comprommission de leur infrastructure).
-->
Pour approfondir
================
**Les schémas vus dans ce TD, de manière similaire à ce que nous avons vu côté RSA, sont simplifiés pour comprendre le principe (textbook)**. Pour référence plus précise, vous pouvez ensuite consulter (et garder) :
* [SOPHOS : Serious Security: How to store your users passwords safely](https://nakedsecurity.sophos.com/2013/11/20/serious-security-how-to-store-your-users-passwords-safely/)
* [OWASP : Password Storage Cheat Sheet](https://www.owasp.org/index.php/Password_Storage_Cheat_Sheet)
* [Micode : 30 000 MOTS DE PASSE CRACKÉS EN 5 MINUTES ! (vidéo)](https://youtu.be/_1ONcmFUOxE)
* [Des exemples de dictionnaires](https://weakpass.com/wordlist)