Background
Passwords are currently hashed with bcrypt (src/powonline/model.py:10, 488–498). Argon2 (specifically argon2-cffi) is the current recommended algorithm by OWASP and the Password Hashing Competition winner. It is memory-hard, which makes GPU/ASIC brute-force significantly more expensive than bcrypt.
Work required
Dependency change
- Remove
bcrypt from pyproject.toml and requirements.txt
- Add
argon2-cffi (provides argon2.PasswordHasher)
src/powonline/model.py
Replace the three bcrypt call sites with argon2.PasswordHasher:
| Current |
Replacement |
hashpw(password.encode("utf8"), gensalt()) |
ph.hash(password) |
checkpw(password.encode("utf8"), self.password) |
ph.verify(self.password, password) |
setpw method — same as above |
same |
Argon2 hashes are stored as strings (str), so the password column type should be changed from BYTEA to TEXT (requires an Alembic migration).
Alembic migration
A new migration is needed to:
ALTER COLUMN password TYPE TEXT USING password::text (or re-hash on first login — see note below)
- Optionally set a
password_algorithm marker column to distinguish legacy bcrypt rows from new argon2 rows, enabling a transparent upgrade path on login (similar to the existing password_is_plaintext flag).
Transparent upgrade path (recommended)
On User.checkpw():
- If the stored hash starts with
$2b$ (bcrypt prefix), verify with bcrypt, then re-hash and store as argon2.
- Otherwise verify with argon2.
This avoids a forced password reset for existing users.
Tests
Update tests/seed.sql and any fixtures that insert raw bcrypt hashes.
References
Background
Passwords are currently hashed with
bcrypt(src/powonline/model.py:10,488–498). Argon2 (specificallyargon2-cffi) is the current recommended algorithm by OWASP and the Password Hashing Competition winner. It is memory-hard, which makes GPU/ASIC brute-force significantly more expensive than bcrypt.Work required
Dependency change
bcryptfrompyproject.tomlandrequirements.txtargon2-cffi(providesargon2.PasswordHasher)src/powonline/model.pyReplace the three bcrypt call sites with
argon2.PasswordHasher:hashpw(password.encode("utf8"), gensalt())ph.hash(password)checkpw(password.encode("utf8"), self.password)ph.verify(self.password, password)setpwmethod — same as aboveArgon2 hashes are stored as strings (
str), so thepasswordcolumn type should be changed fromBYTEAtoTEXT(requires an Alembic migration).Alembic migration
A new migration is needed to:
ALTER COLUMN password TYPE TEXT USING password::text(or re-hash on first login — see note below)password_algorithmmarker column to distinguish legacy bcrypt rows from new argon2 rows, enabling a transparent upgrade path on login (similar to the existingpassword_is_plaintextflag).Transparent upgrade path (recommended)
On
User.checkpw():$2b$(bcrypt prefix), verify with bcrypt, then re-hash and store as argon2.This avoids a forced password reset for existing users.
Tests
Update
tests/seed.sqland any fixtures that insert raw bcrypt hashes.References
password_is_plaintextupgrade-path pattern inmodel.py:491–494