simonw/datasette-auth-passwords

Pick a password hashing technique

Closed this issue · 4 comments

This plugin will use password hashes. Pick a technique for hashing them.

Python 3.6+ has scrypt in the standard library, but it says it requires OpenSSL. I worry that's a dependency that won't be available everywhere. https://docs.python.org/3/library/hashlib.html#hashlib.scrypt

pbkdf2_hmac is definitely available. Django has been using it for years.

https://nitratine.net/blog/post/how-to-hash-passwords-in-python/#hashing-passwords-with-pbkdf2_hmac is a good tutorial on using it.

Here's the relevant code in Django: https://github.com/django/django/blob/ca6c5e5fc23f2855a7094d195f09975b21a7ec3f/django/utils/crypto.py#L82-L89 and https://github.com/django/django/blob/ca6c5e5fc23f2855a7094d195f09975b21a7ec3f/django/contrib/auth/hashers.py#L247-L280

I'm going to replicate Django's storage format from https://github.com/django/django/blob/ca6c5e5fc23f2855a7094d195f09975b21a7ec3f/django/contrib/auth/hashers.py#L265

"%s$%d$%s$%s" % (self.algorithm, iterations, salt, hash)

e.g. pbkdf2_sha256$180000$blahblah/blah/blah/1j=

I'm pretty much going to lift this Django code:

https://github.com/django/django/blob/ca6c5e5fc23f2855a7094d195f09975b21a7ec3f/django/contrib/auth/hashers.py#L255-L280

    algorithm = "pbkdf2_sha256"
    iterations = 260000
    digest = hashlib.sha256

    def encode(self, password, salt, iterations=None):
        assert password is not None
        assert salt and '$' not in salt
        iterations = iterations or self.iterations
        hash = pbkdf2(password, salt, iterations, digest=self.digest)
        hash = base64.b64encode(hash).decode('ascii').strip()
        return "%s$%d$%s$%s" % (self.algorithm, iterations, salt, hash)

    def decode(self, encoded):
        algorithm, iterations, salt, hash = encoded.split('$', 3)
        assert algorithm == self.algorithm
        return {
            'algorithm': algorithm,
            'hash': hash,
            'iterations': int(iterations),
            'salt': salt,
        }

    def verify(self, password, encoded):
        decoded = self.decode(encoded)
        encoded_2 = self.encode(password, decoded['salt'], decoded['iterations'])
        return constant_time_compare(encoded, encoded_2)

Here's my initial implementation:

import base64
import hashlib
import secrets
ALGORITHM = "pbkdf2_sha256"
def hash_password(password, salt=None, iterations=260000):
if salt is None:
salt = secrets.token_hex(16)
assert salt and isinstance(salt, str) and "$" not in salt
assert password and isinstance(password, str)
pw_hash = hashlib.pbkdf2_hmac(
"sha256", password.encode("utf-8"), salt.encode("utf-8"), iterations
)
b64_hash = base64.b64encode(pw_hash).decode("ascii").strip()
return "{}${}${}${}".format(ALGORITHM, iterations, salt, b64_hash)
def verify_password(password, password_hash):
algorithm, iterations, salt, b64_hash = password_hash.split("$", 3)
iterations = int(iterations)
assert algorithm == ALGORITHM
compare_hash = hash_password(password, salt, iterations)
return secrets.compare_digest(password_hash, compare_hash)