wordpressauthenticationflaskbcrypt

How to Verify WordPress 6.8 hash using Flask


I'm looking for guidance on implementing user authentication in a Flask application that integrates with WordPress 6.8, specifically focusing on how to handle the new password hashing methods used by WordPress.

Cause after updating WordPress to 6.8, it now has different hash than before.

My previous code was working fine before updating WordPress to 6.8 :

# ...
from passlib.hash import phpass

class Authenticator:

    def __init__(self, flask_app):
        self.flask_app = flask_app

    def authenticate(self, email, password):
        user_password = self.get_user_password(email)
        # user password from DB looks like: $P$.....
        # password looks like (Plain text): "something"
        if not phpass.verify(password, user_password):
            return False, Response.wrong_credentials(self.flask_app.site_url)

        return True, None

I tried using flask_bcrypt but I got an error: Invalid salt

# ...
from flask_bcrypt import Bcrypt


class Authenticator:

    def __init__(self, flask_app):
        self.flask_app = flask_app
        self.bcrypt = Bcrypt(flask_app)

    def authenticate(self, email, password):
        user_password = self.get_user_password(email)
        # user password in DB looks like: $WP$2y$10$...
        # password looks like (Plain text): "something"
        try:
            if not self.bcrypt.check_password_hash(user_password, password):
                return False, Response.wrong_credentials(self.flask_app.site_url)
        except Exception as e:
            print(e) # it throw error: Invalid Salt
        return True, None

Solution

  • CAVEAT: I have not tested this.

    The source code for WordPress version 6.8.1's wp_hash_password in WordPress/wp-includes/pluggable.php contains these lines:

    // ...
    // Use SHA-384 to retain entropy from a password that's longer than 72 bytes, and a `wp-sha384` key for domain separation.
    $password_to_hash = base64_encode( hash_hmac( 'sha384', trim( $password ), 'wp-sha384', true ) );
    
    // Add a prefix to facilitate distinguishing vanilla bcrypt hashes.
    return '$wp' . password_hash( $password_to_hash, $algorithm, $options );
    

    This shows that, before bcrypt is applied, a pre-hash is performed using HMAC-SHA384 with a key of wp-sha384. The pre-hash eliminates bcrypt's limit of no more than 72 bytes for the password. This somewhat unique pre-hash helps mitigate a potential security issue of pre-hashing generally called password shucking, as it is unlikely this particular hash was ever used in any other system previously.

    The equivalent python code could look something like:

    import hmac
    import hashlib
    from typing import Union
    
    from passlib.hash import bcrypt
    
    
    def wp_pre_hash(password: bytes) -> bytes:
        hmac_sha384 = hmac.new('wp-sha384'.encode('utf-8'), digestmod=hashlib.sha384)
        hmac_sha384.update(password)
        return hmac_sha384.digest()
    
    def wp_password_hash(password: Union[bytes, str]) -> str:
        if isinstance(password, str):
            password = password.encode('utf-8')
        pre_hash = wp_pre_hash(password)
        final_hash = bcrypt.using(rounds=10, ident='2y').hash(pre_hash)
        return '$wp' + final_hash
    
    def wp_password_verify(password: Union[bytes, str], hashed_password: bytes) -> bool:
        if hashed_password[:3] != '$wp':
            raise ValueError('not WordPress >= 6.8 password hash')
        hashed_password = hashed_password[3:]
        if isinstance(password, str):
            password = password.encode('utf-8')
        pre_hash = wp_pre_hash(password)
        verified = bcrypt.verify(pre_hash, hashed_password)
        return verified