Using Discourse as an account provider and PBKDF2 problems

Hi. This is probably a stupid question, but we have been migrating our forum from XenForo over to Discourse. We have a backend server for authorization which involves connecting to the database and verifying credentials against the users table.

XenForo’s bcrypt algorithm worked as expected, and without any hassle. When we migrated over to Discourse however, the PBKDF2 algorithm did not seem to match my expectations. Same exact password, same exact salt, same exact number of iterations and length, but the output hash is different.

I tried various different implementations of PBKDF2 algorithm, but they all output the exact same (different from Discourse’s) hash. Including my own implementation.
I would rather avoid mechanisms like OAuth2 or SSO due to additional overhead and additional work that it imposes upon us.

Has anyone used Discourse for such use cases, and if you have, how did you solve this problem?

Are you using the migrate password plugin?

No, at least not that I am aware of. are you talking about this?

1 Like

Have you tried openssl’s implementation? That’s what we use (you can see it in discourse/lib/pbkdf2.rb).

As an example, after setting a user’s password to swordfish#:

discourse_development=# select password_hash, salt, password_algorithm from users where id=2;
-[ RECORD 1 ]------+-----------------------------------------------------------------
password_hash      | 67650523776bdc87ebcd2fc11719553c87b11e6c4da49806d9d5232460d2adc9
salt               | 712ef44dd6fe6d6f0f1b6f702bb78459
password_algorithm | $pbkdf2-sha256$i=600000,l=32$
$ openssl kdf \
  -kdfopt pass:'swordfish#' \
  -kdfopt salt:712ef44dd6fe6d6f0f1b6f702bb78459  \
  -kdfopt digest:SHA2-256 \
  -kdfopt iter:600000 \
  -keylen 32 \
  PBKDF2 \
  | tr -d : | tr '[:upper:]' '[:lower:]'
67650523776bdc87ebcd2fc11719553c87b11e6c4da49806d9d5232460d2adc9
1 Like

We primarily used Go’s crypto/bcrypt implementation for Xenforo. The same hashes from various pbkdf2 algorithm implementations suggests me that Go possibly stores strings or casts strings to bytes in a somewhat different way.

I’ll have to try that tomorrow (it’s late over here). If OpenSSL gives me the desired result, then I would have to seek OpenSSL bindings for Go, or I would have to switch to an entirely different language (that has OpenSSL bindings) for the backend.

Do you have a short example test case?

E.g. if you use the info above, what do you get?

1 Like

Sorry, I am currently not in a position to tell you as timezones are annoying. It’s very late out here and I could only do so next day.

I did as you asked. The password is swordfish#98765.

Database entry:

discourse=> SELECT password_hash, salt, password_algorithm FROM users WHERE id=1;
                          password_hash                           |               salt               |      password_algorithm       
------------------------------------------------------------------+----------------------------------+-------------------------------
 db3f0829e66336323e81110a1792a76000b9c60605e1fa6964797ea1b07c33c6 | 0d079078e220158011afaf497794166d | $pbkdf2-sha256$i=600000,l=32$
(1 row)

OpenSSL:

/var/discourse# openssl kdf \
> -kdfopt pass:'swordfish#98765' \
> -kdfopt salt:0d079078e220158011afaf497794166d \
> -kdfopt digest:SHA2-256 \
> -kdfopt iter:600000 \
> -keylen 32 \
> PBKDF2 \
> | tr -d : | tr '[:upper:]' '[:lower:]'
db3f0829e66336323e81110a1792a76000b9c60605e1fa6964797ea1b07c33c6

Go code:

var userId int
var hash string
var salt string
var active bool

row := s.Database.QueryRow(`
	SELECT u.id, u.password_hash, u.salt, u.active
	FROM users AS u
	INNER JOIN user_emails AS ue ON u.id = ue.user_id
	WHERE ue.email = $1;`,
	email,
)

if err := row.Scan(&userId, &hash, &salt, &active); err != nil {
	// error handling...
}

hashBytes, err := hex.DecodeString(hash)
if err != nil {
	// error handling...
}

saltBytes, err := hex.DecodeString(salt)
if err != nil {
	// error handling...
}

key := pbkdf2.Key([]byte(password), saltBytes, 600000, 32, sha256.New)

fmt.Printf("salt: %v\n", salt)
fmt.Printf("hash: %v\n", hash)
fmt.Printf("hex.EncodeToString(key): %v\n", hex.EncodeToString(key))

Output of the above code:

salt: 0d079078e220158011afaf497794166d
hash: db3f0829e66336323e81110a1792a76000b9c60605e1fa6964797ea1b07c33c6
hex.EncodeToString(key): b378c12d96ac62a6099fc674d334f0793e6294f7927da0badc811e794a960802

Nevermind. I had to use the hex representation of the salt as the argument, not the decoded salt like I was doing in the post above. Now the hashes are equal.

5 Likes

This was also my theory! I made the same mistake at first.

1 Like

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.