Ga naar hoofdinhoud

Wachtwoorden: registreren met Argon2

Argon2 uit les 6 doet alles wat je in de lessen hiervoor nodig had: zijn hash kun je niet terugrekenen, elk wachtwoord krijgt een eigen zout, en hij is expres traag. In je server verandert alleen /registreer.

Argon2 in /registreer​

from argon2 import PasswordHasher
from fastapi import FastAPI, Form, HTTPException
from sqlitedict import SqliteDict

app = FastAPI()
ph = PasswordHasher()

@app.post("/registreer")
async def registreer(naam: str = Form(...), wachtwoord: str = Form(...)):
with SqliteDict("gebruikers.db") as db:
if naam in db:
raise HTTPException(status_code=400, detail="Deze naam is al bezet")
db[naam] = ph.hash(wachtwoord)
db.commit()
return {"bericht": f"Welkom, {naam}"}
  1. Regel 6:

    Eén PasswordHasher voor de hele server, met instellingen die veilig zijn voor nu.

  2. Regel 13:

    ph.hash maakt de hash, met een nieuw zout erin. Die gaat de database in, het wachtwoord zelf niet.

Neem niet het hele blok over: alleen de regels met PasswordHasher en /registreer zijn nieuw. Zet de import en ph = PasswordHasher() bovenaan je main.py, en vervang je /registreer door deze. import hashlib en /inloggen laat je staan: die zet je in les 8 om.

Inloggen werkt tot les 8 niet

/inloggen vergelijkt nog met een sha256-hash, en in de database staan nu Argon2-hashes. Tot je /inloggen in les 8 omzet, lukt inloggen niet, ook niet met het goede wachtwoord.

Gooi gebruikers.db weg: daar staan nog sha256-hashes in uit de lessen hiervoor. Draai daarna registreer.py en lees_database.py opnieuw.

Zo ziet je main.py er nu uit
import hashlib

from argon2 import PasswordHasher
from fastapi import FastAPI, Form, HTTPException
from sqlitedict import SqliteDict

app = FastAPI()
ph = PasswordHasher()

@app.post("/registreer")
async def registreer(naam: str = Form(...), wachtwoord: str = Form(...)):
with SqliteDict("gebruikers.db") as db:
if naam in db:
raise HTTPException(status_code=400, detail="Deze naam is al bezet")
db[naam] = ph.hash(wachtwoord)
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"}

/inloggen rekent hier nog met sha256; dat verandert in les 8.

Wat er in de hash staat​

Een Argon2-hash bestaat uit stukken, gescheiden door een $:

StukBetekent
argon2idwelk algoritme
v=19welke versie van Argon2
m=65536,t=3,p=4hoeveel geheugen en rekenwerk het kost
X9osXpPM7r/H8fRMyPCE+Qhet zout
etV1GLXt…de hash zelf

Het zout staat er dus in, zoals les 5 al zei: het hoeft niet geheim te zijn. En omdat ook de instellingen erin staan, heb je later bij het inloggen aan de opgeslagen code genoeg.

Controlevraag
In les 5 stond het zout naast de hash. Waar laat Argon2 het zout?
Kies wat jij denkt. Bij het goede antwoord zie je de uitleg.

Er gaat iets mis​

Deze naam is al bezet​

registreer.py geeft drie keer {'detail': 'Deze naam is al bezet'}, en lees_database.py toont nog de sha256-codes uit les 4.

Oorzaak: de oude gebruikers.db staat er nog. De namen bestaan daarin al, dus je nieuwe /registreer weigert ze voordat hij een Argon2-hash maakt.

Oplossing: gooi gebruikers.db weg en draai registreer.py opnieuw.

Opdrachten​

Opdracht 1: Investigate - Twee keer hashen​

Draai dit scriptje een paar keer, en leg de twee regels steeds naast de tabel hierboven:

from argon2 import PasswordHasher

ph = PasswordHasher()
print(ph.hash("welkom123"))
print(ph.hash("welkom123"))

Vraag: welke stukken zijn in de twee regels gelijk, en welke verschillen? En waarom?

Tip

Welke stukken hangen af van de instellingen van ph, en welk stuk kiest ph.hash elke keer opnieuw?

Antwoord

argon2id, v=19 en m=65536,t=3,p=4 zijn gelijk: dat zijn het algoritme en de instellingen, en die komen van dezelfde ph. Het zout verschilt, want ph.hash kiest bij elke hash een nieuw. En omdat het zout anders is, is de hash zelf ook anders. Twee hashes van hetzelfde wachtwoord zijn dus nooit gelijk. Hoe je dan controleert of een wachtwoord klopt, zie je in les 8.

Opdracht 2: Run - Een lange wachtzin​

Registreer een vierde gebruiker, Tim, met de wachtzin zonnebloem fiets regen zolder. Voorspel eerst: wordt de code van Tim in lees_database.py langer dan die van Sara, die welkom123 heeft?

Tip

Maak een kopie van registreer.py met in de lijst alleen ("tim", "zonnebloem fiets regen zolder"). Denk bij het voorspellen aan les 3: was de sha256 van een lange naam langer dan die van een korte?

Antwoord

Nee. In lees_database.py staat er een regel bij zoals deze:

tim: $argon2id$v=19$m=65536,t=3,p=4$UJPkc6G27VAZQnKN6K2xkQ$NGys+yqqtBo/7xbmkDKX8H7M8mGYMsLO+0ciV1pZ6qU

Die code is even lang als die van Sara. De hash zelf, het laatste stuk, heeft altijd dezelfde lengte, hoe lang het wachtwoord ook is. Een lange wachtzin kost je database dus geen extra ruimte, en de spaties zijn gewone tekens van het wachtwoord.

Door naar les 8: inloggen met verify.