Использование Discourse как провайдера учётных записей и проблемы с PBKDF2

Привет. Возможно, это глупый вопрос, но мы мигрируем наш форум с XenForo на Discourse. У нас есть бэкенд-сервер для авторизации, который подключается к базе данных и проверяет учётные данные в таблице пользователей.

Алгоритм bcrypt в XenForo работал как ожидалось, без каких-либо проблем. Однако при миграции на Discourse алгоритм PBKDF2 не соответствовал нашим ожиданиям. Тот же самый пароль, та же самая соль, то же самое количество итераций и длина, но результирующий хеш отличается.

Я пробовал различные реализации алгоритма PBKDF2, но все они выдавали одинаковый хеш (отличный от хеша Discourse), включая мою собственную реализацию.
Мы бы предпочли избежать таких механизмов, как OAuth2 или SSO, из-за дополнительных накладных расходов и объёма работы, который они на нас возлагают.

Кто-нибудь использовал Discourse для подобных задач? Если да, то как вы решили эту проблему?

Вы используете плагин для миграции паролей?

Нет, по крайней мере, мне об этом не известно. Вы имеете в виду это?

Вы пробовали реализацию openssl? Мы используем именно её (это видно в discourse/lib/pbkdf2.rb).

В качестве примера, после установки пароля пользователя в 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

Для Xenforo мы в основном использовали реализацию Go crypto/bcrypt. То, что хеши, полученные из различных реализаций алгоритма PBKDF2, совпадают, позволяет предположить, что Go, возможно, хранит строки или преобразует их в байты несколько иначе.

Мне придется попробовать это завтра (у нас уже поздно). Если OpenSSL даст нужный результат, то мне придется найти привязки OpenSSL для Go или же полностью переключиться на другой язык (с привязками к OpenSSL) для бэкенда.

У вас есть короткий пример тестового случая?

Например, если вы используете информацию выше, что вы получите?

Извините, я сейчас не могу вам ответить, так как часовые пояса — это неудобно. У меня уже очень поздно, и я смогу сделать это только завтра.

Я сделал, как вы просили. Пароль: swordfish#98765.

Запись в базе данных:

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:

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 {
	// обработка ошибки...
}

hashBytes, err := hex.DecodeString(hash)
if err != nil {
	// обработка ошибки...
}

saltBytes, err := hex.DecodeString(salt)
if err != nil {
	// обработка ошибки...
}

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))

Вывод вышеуказанного кода:

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

Неважно. Мне пришлось использовать шестнадцатеричное представление соли в качестве аргумента, а не декодированную соль, как я делал в посте выше. Теперь хеши совпадают.

Это тоже была моя версия! Сначала я тоже совершил ту же ошибку.