Start learning free

LESSON 17 OF 20 · SECURITY: INTERVIEW QUESTIONS

Passwords Hashed with MD5

Practice

An interviewer shows you how the shop checks passwords for its oldest accounts and asks: "What does an attacker get if the users table leaks?"

def md5_hash(password):
    return hashlib.md5(password.encode()).hexdigest()

if user["md5_hash"] is not None:
    return hmac.compare_digest(user["md5_hash"], md5_hash(password))

MD5 is fast and has no salt, so the same password always gives the same hash: the MD5 of password is 5f4dcc3b5aa765d61d8327deb882cf99 on every site in the world. An attacker who steals a copy of the users table looks the hashes up in a ready-made table of common passwords and reads ana's and ben's passwords in seconds.

Newer accounts already use slow_hash: pbkdf2 with SHA-256, a salt per user and 600,000 rounds. You can't convert an MD5 hash into one, because that needs the plain password. But the server holds it for one moment, at the user's next successful login: check it the old way once, then store a new salt and a slow hash, and set md5_hash to NULL. The same trick moves API keys from SHA-1 to SHA-256 as each key is used:

if key_row["sha1"] is not None:
    db.execute("UPDATE api_keys SET sha256 = ?, sha1 = NULL WHERE id = ?", (sha256_of(key), key_row["id"]))
    db.commit()

GET /check/stolen plays the thief: it reads the users table from shop.db and looks every MD5 up in a list of common passwords. The check sends:

GET /check/stolen  →  200  [{"cracked": "sunshine", "email": "ana@example.com", "hash": "md5"}, {"cracked": "letmein", "email": "ben@example.com", "hash": "md5"}, {"cracked": null, "email": "chloe@example.com", "hash": "pbkdf2"}]
POST /login  {"email": "ana@example.com", "password": "sunshine"}           →  200  {"email": "ana@example.com", "message": "Logged in"}
POST /login  {"email": "ana@example.com", "password": "sunshine"}           →  200  {"email": "ana@example.com", "message": "Logged in"}
POST /login  {"email": "ana@example.com", "password": "sunshine1"}          →  401  {"error": "Wrong email or password"}
POST /login  {"email": "ben@example.com", "password": "password"}           →  401  {"error": "Wrong email or password"}
POST /login  {"email": "chloe@example.com", "password": "teapot-garden-42"}  →  200  {"email": "chloe@example.com", "message": "Logged in"}
GET /check/stolen  →  200  [{"cracked": null, "email": "ana@example.com", "hash": "pbkdf2"}, {"cracked": "letmein", "email": "ben@example.com", "hash": "md5"}, {"cracked": null, "email": "chloe@example.com", "hash": "pbkdf2"}]
POST /login  {"email": "nobody@example.com", "password": "sunshine"}        →  401  {"error": "Wrong email or password"}

Ana's second login already uses her new slow hash. Ben never logs in successfully, so his MD5 stays until he does.

Your task: in login, after the password check, upgrade a user who still has an MD5 hash: make a new salt with secrets.token_hex(16), compute slow_hash of the password they sent, store both in their row with md5_hash set to NULL, and commit.


Assignment
Follow the Coding Tutorial to replace each old MD5 hash with a slow salted hash at the user's next successful login.


Hint
Put the upgrade above the last return, so it runs only for a password that was just proved right. The password is in data["password"], and the row's id is user["id"].


STEP 1 OF 5
    Ctrl + Enter to check