How can I utilize werkzeug.security's check_password_hash function to verify correct password against existing salted sha1 password hashes

Viewed 3663

I'm working on a new python application for work that needs to verify existing user credentials in our database. Many of our relic applications were built on PHP's Symfony framework, which is where these password hashes and salts would have originated from. In the database the algorithm (sha1), the salt & the hashed password for users is retained. I'm trying to figure out how to correctly verify a password using this information in conjuncture the check_password_hash function of the werkzeug.security module. However, so far I've been unsuccessful.

For demonstration purposes I've created a test user and the database contains the following values for the user.

Test user values

algorithm: sha1
salt: e40e1e9addc186828a5554a71527342c
password (hashed): 784517f57fbe61179960739e29d7ae925aa4fd5b

The actual password of the test user is 123456

I've tried the following methods to verify the password.

Notes:
1. Hash format was adapted from the werkzeug.security documentation
2. Comparative Hash method was adapted from this StackOverflow answer

from werkzeug.security import generate_password_hash
from werkzeug.security import check_password_hash

# Attempt 1 - Results in False
check_password_hash('sha1$e40e1e9addc186828a5554a71527342c$784517f57fbe61179960739e29d7ae925aa4fd5b','123456')

# Attempt 2 - Results in False
check_password_hash('pbkdf2:sha1$e40e1e9addc186828a5554a71527342c$784517f57fbe61179960739e29d7ae925aa4fd5b','123456')

# Attempt 3
# Step 1: Append the salt_value to the given password and hash it using the same hash function.
generate_password_hash('123456$e40e1e9addc186828a5554a71527342c','sha1')
# sha1$9lUceSsd$60f7dcb3ff9c22d4613e59fcbfed0c463ee4189e
# Step 2: Compare the hash to the hash in the database
# 60f7dcb3ff9c22d4613e59fcbfed0c463ee4189e != 784517f57fbe61179960739e29d7ae925aa4fd5b

# Attempt 4 - Same steps as Attempt 3 except adding salt_length argument
# Step 1: Append the salt_value to the given password and hash it using the same hash function.  
generate_password_hash('123456$e40e1e9addc186828a5554a71527342c','sha1',len('e40e1e9addc186828a5554a71527342c'))
# sha1$EbPv6DP0wMyu02UpA6ZYazFvvYvZTVI1$b8252583fea027d42af20c0d0f3eac3fbf468bd1
# Step 2: Compare the hash to the hash in the database
# b8252583fea027d42af20c0d0f3eac3fbf468bd1 != 784517f57fbe61179960739e29d7ae925aa4fd5b

Can anyone provide some insight on what I'm doing wrong? Should I try using a different library like passlib? I was hoping that this module would suffice since I'm familiar with it, having created a few of our existing python applications that utilize it for new user sign-ups, but in a separate database.

Edit - To show an alternative method to using werkzeug.security's check_password_hash function

import hashlib
# hashlib.sha1('{salt}{pw}'.encode()).hexdigest() == {pw_hash}
hashlib.sha1('e40e1e9addc186828a5554a71527342c123456'.encode()).hexdigest() == '784517f57fbe61179960739e29d7ae925aa4fd5b'
# True
1 Answers

werkzeug.generate_password_hash wants to generate a salt value. Taking a look at the source we can see _hash_internal called with the generated salt value.

def generate_password_hash(password, method="pbkdf2:sha256", salt_length=8):
    """Hash a password with the given method and salt with a string of
    the given length. The format of the string returned includes the method
    that was used so that :func:`check_password_hash` can check the hash.

    The format for the hashed string looks like this::

        method$salt$hash

    This method can **not** generate unsalted passwords but it is possible
    to set param method='plain' in order to enforce plaintext passwords.
    If a salt is used, hmac is used internally to salt the password.

    If PBKDF2 is wanted it can be enabled by setting the method to
    ``pbkdf2:method:iterations`` where iterations is optional::

        pbkdf2:sha256:80000$salt$hash
        pbkdf2:sha256$salt$hash

    :param password: the password to hash.
    :param method: the hash method to use (one that hashlib supports). Can
                   optionally be in the format ``pbkdf2:<method>[:iterations]``
                   to enable PBKDF2.
    :param salt_length: the length of the salt in letters.
    """
    salt = gen_salt(salt_length) if method != "plain" else ""
    h, actual_method = _hash_internal(method, salt, password)
    return "%s$%s$%s" % (actual_method, salt, h)

If we call _hash_internal with your sha1 method, and provided salt/password, we get a different hash

In [83]: import werkzeug.security                                                                                                                                                     

In [84]: h, method = werkzeug.security._hash_internal('sha1', 'e40e1e9addc186828a5554a71527342c', '123456')                                                                           

In [86]: h == '784517f57fbe61179960739e29d7ae925aa4fd5b'                                                                                                                              
Out[86]: False

In [85]: h                                                                                                                                                                            
Out[85]: 'e8c2de9bdc1ab92479e3e55b608a040dad7bf656'

I think you'll need to revisit your PHP code to see how these values are generated.

EDIT: per your comment

In [138]: werkzeug.security._hash_internal('sha1', '', 'e40e1e9addc186828a5554a71527342c123456')                                                                                      
Out[138]: ('784517f57fbe61179960739e29d7ae925aa4fd5b', 'sha1')

If you want to use check_password_hash you'd have to not set the salt on your hash and instead prepend it to your password:

In [148]: werkzeug.check_password_hash('sha1$$784517f57fbe61179960739e29d7ae925aa4fd5b', 'e40e1e9addc186828a5554a71527342c123456')                                                    
Out[148]: True

If we look at the source we can see that check_password_hash extracts method, salt, and hashval out of the pwhash, then validates the hashed password matches hashval. I'm not familiar with hashing in PHP, but it appears the password was "salted" and then hashed without salt (as far as werkzeug.security is concerned anyway).

def check_password_hash(pwhash, password):
    """check a password against a given salted and hashed password value.
    In order to support unsalted legacy passwords this method supports
    plain text passwords, md5 and sha1 hashes (both salted and unsalted).

    Returns `True` if the password matched, `False` otherwise.

    :param pwhash: a hashed string like returned by
                   :func:`generate_password_hash`.
    :param password: the plaintext password to compare against the hash.
    """
    if pwhash.count("$") < 2:
        return False
    method, salt, hashval = pwhash.split("$", 2)
    return safe_str_cmp(_hash_internal(method, salt, password)[0], hashval)
Related