Wachtwoorden: wat een hash is
In les 1 stond elk wachtwoord leesbaar in de database. Het
idee om dat op te lossen: bewaar niet het wachtwoord zelf, maar een hash
ervan. Een hash is een berekening die van tekst een lange code maakt. Python
heeft er een ingebouwd, sha256:
import hashlib
wachtwoord = "welkom123"
print(hashlib.sha256(wachtwoord.encode()).hexdigest())
Uitvoer:
2a5c5f2623024ce3de6fe7dc8f5e13ca55b7aadc13174254b40af574e37018c1
.encode() maakt van de tekst bytes, want daar rekent sha256 mee, en
.hexdigest() schrijft de uitkomst als tekst van cijfers en de letters a tot
en met f.
Drie eigenschappen
Eenrichtingsverkeer. Van een wachtwoord naar een hash is één regel code. Van een hash terug naar het wachtwoord bestaat geen berekening. Daarom kun je een hash bewaren: wie hem ziet, kan hem niet omrekenen.
Hetzelfde erin, hetzelfde eruit. welkom123 geeft elke keer precies
dezelfde hash. Zo kun je bij het inloggen controleren of iemand het goede
wachtwoord typt: bereken de hash van wat hij intypt, en vergelijk die met de
opgeslagen hash.
Een klein verschil, een heel andere hash. Eén letter anders is een compleet andere code:
| Wachtwoord | sha256 |
|---|---|
welkom123 | 2a5c5f2623024ce3… |
welkom124 | b4514acd68f0dc54… |
Welkom123 | 7bca025eff4fb877… |
Aan de hash zie je dus niet of twee wachtwoorden op elkaar lijken.
Een hash in je server
In /registreer uit les 1 bewaar je nu de hash in plaats van het wachtwoord.
Regel 9 wordt:
db[naam] = hashlib.sha256(wachtwoord.encode()).hexdigest()
Zet ook import hashlib bovenaan main.py. Gooi gebruikers.db weg, start je
server en draai registreer.py en lees_database.py opnieuw:
sara: 2a5c5f2623024ce3de6fe7dc8f5e13ca55b7aadc13174254b40af574e37018c1
alex: 9c6514b2297031c14bc3968b0dc45fc0ca22d6795a835f5abc8c08a9eb6d250d
noor: 2a5c5f2623024ce3de6fe7dc8f5e13ca55b7aadc13174254b40af574e37018c1
De wachtwoorden zijn niet meer te lezen. Maar kijk naar Sara en Noor: dezelfde code. Hetzelfde erin, hetzelfde eruit, en dus zie je in de database wie hetzelfde wachtwoord heeft. Daar gaat les 3 over.
/inloggen uit les 1 vergelijkt het ingetypte wachtwoord nog met wat er in de
database staat. Daar staat nu een hash, dus die twee zijn nooit gelijk. In
opdracht 3 hieronder zet je /inloggen om.
Er gaat iets mis
import hashlib
hashlib.sha256("welkom123").hexdigest()
Uitvoer:
TypeError: Strings must be encoded before hashing
Oorzaak: sha256 rekent met bytes, niet met tekst.
Oplossing: zet .encode() achter de tekst: "welkom123".encode().
Opdrachten
Opdracht 1: Predict - Twee keer hetzelfde
Een vierde gebruiker, Tim, registreert met het wachtwoord welkom123.
Vraag: welke code staat er in de database bij Tim?
Tip
Een sha256-hash hangt alleen af van wat je erin stopt.
Antwoord
Precies dezelfde als bij Sara en Noor:
2a5c5f2623024ce3de6fe7dc8f5e13ca55b7aadc13174254b40af574e37018c1.
Opdracht 2: Run - Je eigen hash
Bereken de sha256-hash van je eigen naam. Verander daarna één letter en
bereken hem opnieuw. Hoeveel tekens van de twee hashes zijn gelijk?
Antwoord
Bijna geen: hooguit een paar tekens komen toevallig overeen, op willekeurige plekken. Eén letter verschil in de invoer geeft een heel andere hash.
Opdracht 3: Modify - Inloggen met een hash
Pas /inloggen aan, zodat Sara weer kan inloggen. Test het met een scriptje
inloggen.py dat drie pogingen doet: Sara met welkom123, Sara met
welkom124, en Bob (die geen account heeft) met welkom123.
Tip
In de database staat de hash, dus vergelijk met de hash van wat er ingetypt
wordt: dezelfde berekening als in /registreer.
Antwoord
In /inloggen wordt de regel met de if:
if db.get(naam) == hashlib.sha256(wachtwoord.encode()).hexdigest():
En inloggen.py:
import httpx
adres = "http://127.0.0.1:8000/inloggen"
pogingen = [("sara", "welkom123"), ("sara", "welkom124"), ("bob", "welkom123")]
for naam, wachtwoord in pogingen:
antwoord = httpx.post(adres, data={"naam": naam, "wachtwoord": wachtwoord})
print(f"{naam}, {wachtwoord}: {antwoord.json()['bericht']}")
Uitvoer:
sara, welkom123: Ingelogd
sara, welkom124: Naam of wachtwoord klopt niet
bob, welkom123: Naam of wachtwoord klopt niet
Dit werkt omdat dezelfde invoer altijd dezelfde sha256-hash geeft. In les 6
zie je dat dat met zout anders moet.
Zo ziet je main.py er nu uit
import hashlib
from fastapi import FastAPI, Form
from sqlitedict import SqliteDict
app = FastAPI()
@app.post("/registreer")
async def registreer(naam: str = Form(...), wachtwoord: str = Form(...)):
with SqliteDict("gebruikers.db") as db:
db[naam] = hashlib.sha256(wachtwoord.encode()).hexdigest()
db.commit()
return {"bericht": f"Welkom, {naam}"}
@app.post("/inloggen")
async def inloggen(naam: str = Form(...), wachtwoord: str = Form(...)):
with SqliteDict("gebruikers.db") as db:
if db.get(naam) == hashlib.sha256(wachtwoord.encode()).hexdigest():
return {"bericht": "Ingelogd"}
return {"bericht": "Naam of wachtwoord klopt niet"}
Les 3 en 4 veranderen niets aan je server; in les 5 gaat /registreer weer om.
Door naar les 3: zout.