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}"}
- Regel 6:
Eén
PasswordHashervoor de hele server, met instellingen die veilig zijn voor nu. - Regel 13:
ph.hashmaakt 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 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 $:
| Stuk | Betekent |
|---|---|
argon2id | welk algoritme |
v=19 | welke versie van Argon2 |
m=65536,t=3,p=4 | hoeveel geheugen en rekenwerk het kost |
X9osXpPM7r/H8fRMyPCE+Q | het 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.
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.