diff --git a/content/blog/converting-werkzeug-hashes-to-passlib.rst b/content/blog/converting-werkzeug-hashes-to-passlib.rst
new file mode 100644
index 0000000..9686a43
--- /dev/null
+++ b/content/blog/converting-werkzeug-hashes-to-passlib.rst
@@ -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 `_’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)