Add post abourt converting Werkzeug hashes to Passlib format
This commit is contained in:
parent
d3b11a083a
commit
8b7dd42b27
129
content/blog/converting-werkzeug-hashes-to-passlib.rst
Normal file
129
content/blog/converting-werkzeug-hashes-to-passlib.rst
Normal file
@ -0,0 +1,129 @@
|
||||
Converting werkzeug’s hashes to Passlib format
|
||||
##############################################
|
||||
|
||||
:date: 2020-07-19T05:02Z
|
||||
:category: blog
|
||||
:tags: python,security
|
||||
:url: blog/converting-werkzeug-hashes-to-passlib/
|
||||
:save_as: blog/converting-werkzeug-hashes-to-passlib/index.html
|
||||
:status: published
|
||||
:author: Gergely Polonkai
|
||||
|
||||
|
||||
In one of my projects i use `Werkzeug <https://palletsprojects.com/p/werkzeug/>`_’s
|
||||
``generate_password_hash`` to, well, generate password hashes. Werkzeug uses Python’s
|
||||
`hashlib <https://docs.python.org/3.8/library/hashlib.html>`_ module under the hood which,
|
||||
unfortunately to me, doesn’t support Argon2 (which i want to transition to for greater security).
|
||||
|
||||
Enter `Passlib <https://passlib.readthedocs.io/>`_.
|
||||
|
||||
Passlib conveniently supports both `PBKDF2
|
||||
<https://passlib.readthedocs.io/en/stable/lib/passlib.hash.pbkdf2_digest.html>`_ (what i currently
|
||||
use in the project, using SHA256 digests), and also supports `Argon2
|
||||
<https://passlib.readthedocs.io/en/stable/lib/passlib.hash.argon2.html>`_. Here are the
|
||||
differences:
|
||||
|
||||
.. table::
|
||||
:widths: auto
|
||||
|
||||
================== ==================== ============================
|
||||
Feature Werkzeug Passlib
|
||||
================== ==================== ============================
|
||||
Iteration count 150000, hardcoded 29000, changeable
|
||||
Default method PBKDF2 None, must be set explicitly
|
||||
Default digest SHA-256 None, must be set explicitly
|
||||
Default salt size 8, changeable 16, changeable
|
||||
Salt character set ASCII only Binary
|
||||
Salt storage Plain text (shorter) Adapted Base64
|
||||
Hash storage Hex string Adapted Base64 (shorter)
|
||||
================== ==================== ============================
|
||||
|
||||
But even if i force the same settings on Passlib, their format is different and, obviously,
|
||||
Passlib doesn’t understand Werkzeug’s format so i had to convert all my hashes to the new format.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
>>> generate_password_hash('password', method='pbkdf2:sha256', salt_length=8)
|
||||
'pbkdf2:sha256:150000$2dFFA24B$1602ed71733451acaf29abd26b1d1a78aced4442467f9efbab9b1fa21ae39d68'
|
||||
>>> pbkdf2_sha256.using(rounds=150000, salt_size=8).hash('password')
|
||||
'$pbkdf2-sha256$29000$tdZ6731vzTnn/H9vTSnl3A$maWqohBS0ghQEjIoJWnYC1zGF2T/qOwRmHzVzHt3NqU'
|
||||
|
||||
First, let’s split the old hash by the ``$`` characters, and also split the first part by colons:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
full_method, salt, hashed_value = old_hash.split('$')
|
||||
method, digest, rounds = full_method.split(':')
|
||||
|
||||
As it soon turned out, the two libraries even store the actual data in different formats.
|
||||
Werkzeug stores the salt in plain text (it’s always ASCII characters), and the resulting hash in a
|
||||
hex string. Passlib, however, aims for greater security with a binary salt, so it’s Base64
|
||||
encoded. This encoding is, however, a bit different from what you’d expect, as it’s “using
|
||||
shortened base64 format which omits padding & whitespace” and also “uses custom ``./`` altchars”.
|
||||
It even has its own `ab64_encode
|
||||
<https://passlib.readthedocs.io/en/stable/lib/passlib.utils.binary.html?highlight=ab64_encode#passlib.utils.binary.ab64_encode>`_
|
||||
and `ab64_decode
|
||||
<https://passlib.readthedocs.io/en/stable/lib/passlib.utils.binary.html?highlight=ab64_encode#passlib.utils.binary.ab64_decode>`_
|
||||
functions to handle this encoding. So i first need to re-encode both the salt string and the hash
|
||||
value hex string, so i have raw bytes.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
salt_bytes = salt.encode('ascii')
|
||||
hash_bytes = bytes.fromhex(hash)
|
||||
|
||||
Then encode them to the adapted Base64 format (i also convert it back to ``str`` for easier
|
||||
concatenation later):
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
passlib_salt = ab64_encode(salt_bytes).decode('ascii')
|
||||
passlib_hash = ab64_encode(hash_bytes).decode('ascii')
|
||||
|
||||
Now we just need to concatenate all the things with the right separators.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
passlib_final_hash = f'${method}-{digest}${rounds}${passlib_salt}${passlib_hash}'
|
||||
|
||||
Finally, let’s verify everything went right:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
>>> pbkdf2_sha256.verify('password', passlib_final_hash)
|
||||
True
|
||||
|
||||
Here’s the whole series of command, converted to a Python function (with slightly altered variable
|
||||
names) for your copy-pasting convenience (plus, it’s not using f-strings, so you can use it with
|
||||
Python <3.6):
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def werkzeug_to_passlib_hash(old_hash):
|
||||
"""Convert Werkzeug’s password hashes to Passlib format.
|
||||
|
||||
Copied from https://gergely.polonkai.eu/blog/converting-werkzeug-hashes-to-passlib/
|
||||
"""
|
||||
|
||||
from passlib.utils import ab64_encode
|
||||
|
||||
# Werkzeug hashes look like full_method$salt$hash. We handle full_method later; salt is
|
||||
# an ASCII string; hashed value is the hex string representation of the hashed value
|
||||
full_method, salt, hashed_value = old_hash.split('$')
|
||||
|
||||
# Werkzeug’s full_method is a colon delimited list of the method, digest, and rounds
|
||||
method, digest, rounds = full_method.split(':')
|
||||
|
||||
new_parts = [
|
||||
# Passlib expects the hashed value to starts with a $ sign (hence the empty string at the
|
||||
# beginning of this list).
|
||||
'',
|
||||
# Passlib’s method and digest is concatenated together with a hyphen.
|
||||
f'{method}-{digest}',
|
||||
rounds,
|
||||
# Passlib expects the salt and the actual hash to be in the adapted base64 encoding format.
|
||||
ab64_encode(salt.encode('ascii')).decode('ascii'),
|
||||
ab64_encode(bytes.fromhex(hashed_value)).decode('utf-8'),
|
||||
]
|
||||
|
||||
return '$'.join(new_parts)
|
Loading…
Reference in New Issue
Block a user