The SSH host key has changed on 8 April, 2022 to this one: SHA256:573uTBSeh74kvOo0HJXi5ijdzRm8me27suzNEDlGyrQ
Sources of the site gergely.polonkai.eu, Jekyll version
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
gergelypolonkai-web-jekyll/content/blog/converting-werkzeug-hashes-...

129 lines
5.6 KiB

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)