Wachtwoorden veilig opslaan · eindproject
Eindproject: Project: nergens een leesbaar wachtwoord
De passwords.py zijn af en getest. De server gebruikt ze nog niet: sample_state() bevat nog twee gewone wachtwoorden, en login_user vergelijkt ze nog met !=. Dit project verbindt de twee, en sluit de eerste bevindingen van de cursus.
De eisen:
sample_state()slaatANA_RECORDenBEN_RECORDop onder"users", en geen wachtwoord.login_userbeslist viapasswords.check_login.Na een geslaagde login wordt een record dat met lagere kosten dan
passwords.COST_Ngemaakt is, vervangen door een nieuw record met de kosten van vandaag.Een verkeerd wachtwoord en een onbekende gebruiker krijgen dezelfde 401 na hetzelfde aantal scrypt-aanroepen, en met dezelfde kosten bij records met de kosten van vandaag.
Geen wachtwoord en geen record verschijnt in een antwoord, in een
of in afgedrukte uitvoer. Eén nieuwe aanvalstest, en de rijen F1 en F2 van
SECURITY.mdgesloten.
“Nergens een leesbaar wachtwoord” gaat over wat de dienst opslaat en verstuurt. De twee voorbeeldwachtwoorden staan nog in je testbestand, in pl_sec.py en in het commentaar boven ANA_RECORD in reading_server.py. Het zijn testwaarden, en een test moet ze kennen. Een echte dienst heeft zo’n commentaar niet: niemand behalve de gebruiker kent het wachtwoord achter een record.
Mijlpaal 1: records in de state
Verander één regel van sample_state() in reading_server.py, zodat "users" ana op ANA_RECORD afbeeldt en ben op BEN_RECORD. Beide constanten staan bovenaan het bestand. Het zijn records voor de twee voorbeeldwachtwoorden, gemaakt met jouw recordformaat.
Voer python -m pytest -q uit. De test uit hoofdstuk 1 faalt nog, en hij faalt anders: bij zijn eerste assert, met assert 401 == 200. De login vergelijkt het wachtwoord dat ana stuurt nog steeds met wat er is opgeslagen, en wat er is opgeslagen, is nu een record. Niemand kan inloggen.
Die mislukking kunnen lezen is de reden dat de test eerst de status controleert. Zonder die assert zou de test nu slagen, op een server waar het wachtwoord weg is omdat de login kapot is.
Mijlpaal 2: de login
Herschrijf login_user(state, credentials):
als
credentialsNoneis, geefNoneterug;haal de naam en het wachtwoord uit
credentials;als
passwords.check_login(state["users"], name, password)Falseis, geefNoneterug;geef anders de naam terug.
Voer de tests uit. test_attack_backup_reveals_no_password slaagt, voor het eerst sinds hoofdstuk 1. Het hele bestand is groen.
Mijlpaal 3: ben upgraden
BEN_RECORD is gemaakt bij n=4096, een kwart van de kosten van vandaag. Het staat voor een record van voordat de dienst zijn instellingen verhoogde.
Kijk na een geslaagde login naar het opgeslagen record. Als passwords.needs_rehash(record, n=passwords.COST_N) True is, vervang het in state["users"] door passwords.make_record(password, n=passwords.COST_N, r=passwords.COST_R, p=passwords.COST_P).
Doe dit alleen nadat check_login True teruggaf. Op dat moment heeft de server het echte wachtwoord van de gebruiker. Een record dat vóór de controle vervangen wordt, zou iedereen het wachtwoord van ben laten overschrijven met een verkeerde gok.
Tot ben inlogt, kost een verkeerd wachtwoord voor ben een kwart van wat de dummycontrole kost, dus een klok zou zijn naam kunnen onderscheiden van een onbekende. Dat is een tweede reden om oude records te upgraden.
Waar hoort de upgrade van een zwak record?
Mijlpaal 4: F2
F2 zei dat de login een geheim vergelijkt met !=. Kijk naar je nieuwe login_user: hij vergelijkt niets. De enige vergelijking van een geheim gebeurt nu in verify_password, met hmac.compare_digest.
Schrijf de test die dat zegt, via de server. Schrijf in tests/test_attacks.py test_attack_login_compares_with_compare_digest: bouw reading_server.sample_state(), start helpers.count_compare_digest(), stuur POST /login voor ana met een verkeerd wachtwoord via reading_server.handle(...), en controleer dat de status 401 is en dat counter["calls"] minstens 1 is.
“Minstens 1” is met opzet. Een login die met == vergelijkt, doet helemaal geen aanroep, dus de test vangt hem. En een later hoofdstuk kan op deze route nog een zorgvuldige vergelijking toevoegen zonder je test te breken.
Mijlpaal 5: weer de dief
Voer het scenario uit hoofdstuk 1 uit:
python pl_sec.py stolen-backup
De kopie van de state laat nu twee records zien, en de volgende stap van de dief mislukt. Elk record is hier na de eerste tekens van zijn hash afgekapt:
users 'ana': 'scrypt$16384$8$1$Iiddrf4ZE7Bp5ctR/ENjYg==$SedCTnOCi...
users 'ben': 'scrypt$4096$8$1$QKGmY5EvluaM3abQzz8CUQ==$OhHmwNl+T...
log in as ana with the value stored for ana -> 401 refused
log in as ben with the value stored for ben -> 401 refused
De resultaatregel luidt 0 of 2 stored values worked as a password. python pl_sec.py login-cost laat op alle drie de regels nog steeds één scrypt-aanroep bij n=16384 zien.
Open nu SECURITY.md. Zet de Status van F1 en F2 op closed in 3.6, en zet test_attack_login_compares_with_compare_digest in de cel Test van F2. Een rij is pas gesloten als haar test slaagt, en dat doen ze allebei.
Een dief kopieert de state na deze les. Wat kan hij met het record van ana?
Wat nog open staat
Wees precies over wat je gesloten hebt. Een dief met deze kopie kan nog steeds raden, met één scrypt-aanroep per gok per gebruiker. Daarom doen de kosten ertoe, en daarom gebruikt een echte dienst hogere kosten dan deze cursus. Een wachtwoord als correct horse zou nog steeds vallen voor een
De login staat ook een onbeperkt aantal pogingen toe. Niets beperkt gokken die als verzoeken binnenkomen, tot hoofdstuk 6.
En de kopie bevat meer dan wachtwoorden: twee tokens die nooit verlopen, en een levende sessie-id. De server heeft nog geen log, dus de eis over afgedrukte uitvoer is voorlopig alles wat er over logs te zeggen is. Hoofdstuk 6 komt daar ook op terug.
Voer python -m pytest -q een laatste keer uit. Het eindigt met 8 passed. Klik daarna op Check my work. Hoofdstuk 4 begint met de sessie die nooit eindigt.
Opdracht
Zet de wachtwoordfuncties achter de login, en sluit F1 en F2.
In reading_server.py:
Laat
sample_state()ANA_RECORDenBEN_RECORDopslaan onder"users".Herschrijf
login_user(state, credentials). GeefNoneterug alscredentialsNoneis of alspasswords.check_login(state["users"], name, password)Falseis. Als na een geslaagde loginpasswords.needs_rehash(record, n=passwords.COST_N)Trueis voor het opgeslagen record, vervang het dan doorpasswords.make_record(password, n=passwords.COST_N, r=passwords.COST_R, p=passwords.COST_P). Geef de gebruikersnaam terug.
Schrijf in tests/test_attacks.py test_attack_login_compares_with_compare_digest: bouw reading_server.sample_state(), start counter = helpers.count_compare_digest(), stuur POST /login voor ana met een verkeerd wachtwoord via reading_server.handle(...), en controleer status 401 en dat counter["calls"] minstens 1 is.
Zet in SECURITY.md de Status van F1 en F2 op closed in 3.6 en noem deze test in de cel Test van F2.
Voer python -m pytest -q (dat moet eindigen met 8 passed) en python pl_sec.py stolen-backup uit in de
Klik daarna op Check my work. Die roept handle() aan met states waarvan je de gebruikers en records niet hebt gezien, waarvan er één gemaakt is bij n=4096, en legt elke aanroep van hashlib.scrypt en hmac.compare_digest vast. Een juist wachtwoord moet 200 antwoorden, één n, r en p. De beslissing moet via hmac.compare_digest gaan. Het record bij n=4096 moet na een juist wachtwoord vervangen worden en na een verkeerd wachtwoord blijven staan, en een record dat al bij COST_N is, moet blijven. Daarna voert hij vier van je tests op naam uit (die uit hoofdstuk 1, de twee uit les 4 en 5, en de nieuwe) tegen een correcte dienst, waar ze allemaal moeten slagen, en tegen zes kapotte, waar de bijbehorende test moet falen. Als laatste leest hij de rijen F1 en F2 van SECURITY.md. Er wordt niets getimed, behalve dat een test van jou die langer dan 3 seconden draait, wordt gestopt, en niets uit je terminal wordt beoordeeld.