2025, Nov 10 09:02
Аутентификация Flask с паролями WordPress 6.8+: проверка хэшей $wp через HMAC‑SHA384 и bcrypt 2y
После обновления WordPress 6.8+ во Flask ломается проверка паролей. Даем код: хэши $wp с HMAC‑SHA384 и bcrypt 2y, ветвление со старым phpass для смешанных баз.
Аутентификация Flask по паролям из WordPress 6.8+ может внезапно перестать работать после обновления. Причина проста: WordPress изменил способ хранения хэшей паролей. Если раньше создавались хэши с префиксом $P$ (phpass), то в WordPress 6.8+ записываются строки, начинающиеся с $wp и содержащие внутри хэш bcrypt с идентификатором 2y. Если ваш Python‑код проверял только старый формат, он будет отвергать новый до тех пор, пока вы не адаптируете проверку.
Постановка задачи
Здесь — минимальный пример старого подхода, который работал с хэшами $P$ (phpass). Логика простая: загрузить сохранённый хэш, проверить открытый пароль и вернуть результат. Имена переменных отличаются, но поведение полностью соответствует исходному.
from passlib.hash import phpass
class Gatekeeper:
def __init__(self, app):
self.app = app
def check_login(self, user_email, plain_secret):
stored_hash = self.fetch_hash(user_email)
# stored_hash выглядел так: $P$...
# plain_secret был в открытом виде, например "something"
if not phpass.verify(plain_secret, stored_hash):
return False, Reply.invalid_auth(self.app.site_url)
return True, None
После обновления попытка перейти на универсальную проверку через, например, flask_bcrypt, приводит к ошибкам вроде "Invalid salt", если подать строку вида $wp2y$10$... Новые хэши WordPress — это не обычные строки bcrypt; у них есть специальный префикс и этап предварительного хеширования, который нужно воспроизвести при проверке.
Что изменилось в WordPress 6.8+
В механизме паролей WordPress 6.8.1 добавлено предварительное хеширование перед bcrypt. Вычисляется HMAC‑SHA384 по обрезанному паролю с фиксированным ключом "wp-sha384". Затем запускается bcrypt с идентификатором 2y, а результат дополняется префиксом $wp, чтобы отличать его от «чистого» bcrypt. Этот префикс — явный маркер того, что пароль использует новую схему.
Решение: проверять по схеме WordPress 6.8+
Чтобы авторизовывать из Flask, воспроизведите конвейер WordPress 6.8+ на стороне Python. Сначала посчитайте HMAC‑SHA384 от пароля‑кандидата с ключом "wp-sha384", затем проверьте получившийся прехэш с частью bcrypt в сохранённой строке, предварительно убрав префикс $wp. Это повторяет логику WordPress и снимает ограничение bcrypt на 72 байта входа, не нарушая совместимость с их форматом.
import hmac
import hashlib
from typing import Union
from passlib.hash import bcrypt
def wp_pre_digest(secret: bytes) -> bytes:
keyed = hmac.new('wp-sha384'.encode('utf-8'), digestmod=hashlib.sha384)
keyed.update(secret)
return keyed.digest()
def wp_hash_build(secret: Union[bytes, str]) -> str:
if isinstance(secret, str):
secret = secret.encode('utf-8')
pre = wp_pre_digest(secret)
baked = bcrypt.using(rounds=10, ident='2y').hash(pre)
return '$wp' + baked
def wp_check_secret(secret: Union[bytes, str], combined_hash: bytes) -> bool:
if combined_hash[:3] != '$wp':
raise ValueError('not WordPress >= 6.8 password hash')
peeled = combined_hash[3:]
if isinstance(secret, str):
secret = secret.encode('utf-8')
pre = wp_pre_digest(secret)
return bcrypt.verify(pre, peeled)
Если в базе смешаны старые и новые хэши, ветвитесь по префиксу. Сохраняйте текущую проверку phpass для $P$, а для $wp используйте новую схему. Так вы сможете аутентифицировать пользователей из обеих «эпох» без принудительной миграции.
from passlib.hash import phpass
class Gatekeeper:
def __init__(self, app):
self.app = app
def check_login(self, user_email, plain_secret):
stored_hash = self.fetch_hash(user_email)
try:
if stored_hash.startswith('$wp'):
ok = wp_check_secret(plain_secret, stored_hash)
else:
ok = phpass.verify(plain_secret, stored_hash)
except Exception:
ok = False
if not ok:
return False, Reply.invalid_auth(self.app.site_url)
return True, None
Почему это важно
Проверка пароля — это не только выбор функции хеширования; важно воспроизвести точный формат и последовательность шагов системы, которая породила хэш. В WordPress 6.8+ осознанно добавили прехэш и собственный префикс $wp, чтобы лучше обрабатывать длину пароля и однозначно отличать получающиеся строки. Игнорирование этих деталей приводит к ложным ошибкам вроде "Invalid salt" или, что хуже, к отказам в приёме пароля, которые выглядят как неверные учётные данные.
Выводы
Когда вашему сервису Flask нужно проверять пароли из WordPress 6.8+, смотрите на сохранённую строку и выбирайте подходящий проверяющий. Для наследуемых хэшей $P продолжайте использовать проверку phpass. Для нового формата $wp...2y$10$ сначала сделайте прехэш HMAC‑SHA384 с ключом "wp-sha384", а затем проверьте его через bcrypt 2y. Такое согласование с внутренней логикой WordPress возвращает работоспособность входа без изменений в модели данных и позволяет безболезненно обслуживать «смешанные» таблицы пользователей во время обновлений.
Статья основана на вопросе на StackOverflow от Pythonic007 и ответе от President James K. Polk.