0

I have the following hashed entry in a password file:

$pbkdf2-sha512$25000$K0XonfPe29vbW0up9X5vDQ$3scRqpOxF29.tqPWpKJmcFvpb8/SFtbAiI2UlrM473B3tD.Mw8xzamNaE0KpZApTc7N1stlK/vvdUl9xna6wIA

Now, I know that the password used to generate this entry was "foobar". Per this URL, I also know the following:

  • pbkdf2-sha512 identifies the cryptographic hash used to generate the hash.
  • 25000 identifies the iterations performed.
  • K0XonfPe29vbW0up9X5vDQ is the adapted base64 encoding of the raw salt bytes passed into the PBKDF2 function
  • 3scRqpOxF29.tqPWpKJmcFvpb8/SFtbAiI2UlrM473B3tD.Mw8xzamNaE0KpZApTc7N1stlK/vvdUl9xna6wIA is the raw derived key bytes returned from the PBKDF2 function

So my code proceeds from this as follows:

from passlib.utils.binary import ab64_decode
from passlib.hash import pbkdf2_sha512

salt = 'K0XonfPe29vbW0up9X5vDQ'
salt_decoded = ab64_decode(salt)
hashed_string = pbkdf2_sha512.hash('foobar', rounds=25000, salt=salt_decoded)

My problem: I expect hashed_string to always return the same value and to match the password file entry. But instead, I always get the following value for hashed_string:

$pbkdf2-sha512$25000$K0XonfPe29vbW0up9X5vDQ$ekYyFII.tyf0kWX1BBkICleOTCWIjDbKtGQ4iAU/qvGSmWQf.SAcFcJu6ZGwFFQMe4Kws2ngw.pgGaVe7F/I2g

Where am I going wrong?

I've tried various combinations of encoding and decoding values, without success. I would guess that if I knew to derive the salt correctly from the entry in the password file and/or knew how to call pbkdf2_sha512.hash() correctly, my problem would be solved.

Barmar
  • 741,623
  • 53
  • 500
  • 612
Alex
  • 1
  • 2
  • 1
    "Now, I know that the password used to generate this entry was "foobar"." - perhaps you're wrong about that. – user2357112 Jun 18 '23 at 02:53
  • No, that part is right, because this password file entry is being used to successfully authenticate a user with that password. – Alex Jun 18 '23 at 02:56
  • 2
    You might want to take another look at the code that authenticates them, then. Maybe there's a pepper you weren't aware of, or an encoding difference or something. – user2357112 Jun 18 '23 at 03:00

2 Answers2

0

Just reading the docs, but it appears that:

Passing PasswordHash.setting_kwds such as rounds and salt_size directly into the hash() method is deprecated. Callers should instead use handler.using(**settings).hash(secret). Support for the old method is is tentatively scheduled for removal in Passlib 2.0.

So, you should probably switch to use the 'using()' paradigm. Still, your code should work.

I can see two obvious problem sources. The encoding the string password (Unicode/UTF-8(/ASCII)), the documentation is not entirely clear on that point and it depends on how it was done on the source system. The second possibility I can see is that the original system used big-endian encoding.

Xecrets
  • 79
  • 5
  • I've played around a little with your code. Since the salt actually comes out as expected, there does not appear to be any problem with with the encoding/decoding of that. The rounds are also correct. I can only conclude that either the password used is not in fact "foobar", or some extra processing is performed by the source between password entry and calling the passlib code. Neither of the suspects with encoding or big-endian seems to apply. Recheck the original code or the original password. – Xecrets Jun 19 '23 at 16:38
  • Thanks for taking the time to look at this closely; I'm too new to the site so I can't vote you up. I'm going to continue to look at the problem. While the password is correct, I can't exclude that the encoded password includes some other content. I haven't found a smoking gun in the code anywhere yet, though. Plus, pbkdf2-sha512 is only supposed to take the password, not the user ID as I understand is the case with other algorithms. – Alex Jun 19 '23 at 19:51
  • It's not clear if you have access to the source code producing the sample hash for the sample password, but if you do, I'd start there and also triple check that you really got the correct corresponding hash. As a side note, I did a little experiment and ran hashcat against a large dictionary file of passwords, and easily "cracked" the "foobar" password with the hash produced by your code, but the sample hash was not cracked. So it's not "foobar", or any simple password - or there's additional preprocessing occurring. – Xecrets Jun 20 '23 at 06:35
  • Thanks again for the continuing comments. I see you have a background in cryptography, so I suppose this type of issue gets your attention, but I still appreciate the extra effort. Looking at the source code, the actual hashing logic is buried well inside Flask in a nearly inscrutable fashion. But underneath that is Python's passlib. Looking through the docs, I realized that, as you suggest, there is added processing occurring. I'm giving details in the answer to follow. Thanks again! – Alex Jun 22 '23 at 04:26
  • There you go! Congratulations, good job! – Xecrets Jun 22 '23 at 06:10
0

This issue relates to my attempt to manage pgAdmin users programmatically. To do that, I had to understand how pgAdmin hashes user passwords.

Under the hood, pgAdmin uses Flask, which in turn uses passlib. The step I was missing became clear when I went through passlib documentation and looked again at the SQlite database that pgAdmin uses to store user information. That database has a keys table that stores a value for SECURITY_PASSWORD_SALT. It is this value, together with the salt, that passlib uses to create the pbkdf2-sha512 hash string that pgAdmin then stores in the password column for each user in SQlite.

Here's Python3 code to generate a usable password entry:

import hashlib
import hmac
import os

salt = os.urandom(16)
h = hmac.new(str.encode(security_password_salt),
             password.encode("utf-8"),
             hashlib.sha512)
user_hash = pbkdf2_sha512.hash(b64encode(h.digest()),
                               rounds=25000,
                               salt=salt)

To verify a password, use the salt stored in the password string and SECURITY_PASSWORD_SALT. Apply the algorithm shown here and the resulting user_hash string will match that stored in SQlite.

Following Xecrets' suggestion, the last line could probably be updated to something like the following when hashing the password:

user_hash = pbkdf2_sha512.using(rounds=25000,salt_size=16).hash(b64encode(h.digest()))

But I haven't tried this yet.

Alex
  • 1
  • 2