Ga naar hoofdinhoud

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:

Wachtwoordsha256
welkom1232a5c5f2623024ce3…
welkom124b4514acd68f0dc54…
Welkom1237bca025eff4fb877…

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 werkt nu niet meer

/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.