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)
