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 `_’s ``generate_password_hash`` to, well, generate password hashes. Werkzeug uses Python’s `hashlib `_ module under the hood which, unfortunately to me, doesn’t support Argon2 (which i want to transition to for greater security). Enter `Passlib `_. Passlib conveniently supports both `PBKDF2 `_ (what i currently use in the project, using SHA256 digests), and also supports `Argon2 `_. 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 `_ and `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)