Failure to first truncate passwords to 72 characters can cause 500s on
error registration for any Django application using bcrypt v5.
This tweaks the hasher docstring to better document how different bcrypt
versions handle truncation.
Users of `BCryptPasswordHasher` should likely truncate passwords
*before* invoking `check_password()`.
bcrypt v5
=========
The Python library bcrypt released a new major version on Sept 25:
d8a36672bf/CHANGELOG.rst
As part of the v5 release, hashing longer passwords now raises a
`ValueError` instead of silently truncating to 72 bytes:
> Passing `hashpw` a password longer than 72 bytes now raises a
> `ValueError.` Previously the password was silently truncated,
> following the behavior of the original OpenBSD `bcrypt` implementation.
Remove the recommendation of pre-hashing
========================================
I'm not a cryptographer, but it's perhaps best to remove the advice
suggesting users to instead favor `BCryptSHA256PasswordHasher`. Passing
passwords through SHA-256 before bycrpt is vulnerable to password shucking:
https://www.scottbrady.io/authentication/beware-of-password-shucking
Instead, we can merely remove the advice & document differences.
Demonstration of v4 versus v5 behavior
======================================
```python
from django.contrib.auth.models import AbstractBaseUser
class User(AbstractBaseUser): # Normally just settings.AUTH_USER_MODEL
pass
class DemoTest(TestCase):
def test_truncation():
user = User()
with self.settings(PASSWORD_HASHERS=["django.contrib.auth.hashers.BCryptPasswordHasher"]):
user.check_password(73 * 'x')
```
On newer bcrypt, we get:
```
ValueError: password cannot be longer than 72 bytes, truncate manually if necessary (e.g. my_password[:72])
```
bcrypt version 4 or older simply truncated silently to 72 bytes.
Previous documentation efforts
==============================
This docstring was originally written in 2013:
https://github.com/django/django/pull/958/
A followup PR then documented the truncation behavior:
https://github.com/django/django/pull/962/
IN 2018, `BCryptPasswordHasher` was removed from `PASSWORD_HASHERS`,
removing the documentation about password truncation:
https://github.com/django/django/pull/9728
This is a follow up for the fix of CVE-2024-39329
(5d86458579) where the timing of
verify_password() was standardized when checking unusable passwords.
Argon2 encodes the salt as base64 for representation in the final hash
output. To be able to accurately return the used salt from decode(),
add padding, b64decode, and decode from latin1 (for the remote
possibility that someone supplied a custom hash consisting solely of
bytes -- this would require a manual construction of the hash though,
Django's interface does not allow for that).
By convention a hasher which does not use a salt should populate the
decode dict with `None` rather than omit the dict key.
Co-Authored-By: Florian Apolloner <apollo13@users.noreply.github.com>