22 KiB
Контракт авторизации MoonWell через лаунчер
Статус документа: обязательный контракт для Release-сборок.
Ключевые слова ДОЛЖЕН, НЕЛЬЗЯ, СЛЕДУЕТ и МОЖЕТ описывают обязательность требований.
1. Цель и границы доверия
Игровой клиент World of Warcraft 3.3.5a остаётся стандартным SRP6-клиентом,
но вместо постоянного пароля получает короткоживущий одноразовый ticket от
MoonWell Launcher. Наличие или отсутствие полей логина в GlueXML не является
границей безопасности. Решение о допустимости входа всегда принимает
authserver.
В поток входят четыре компонента:
- backend аутентифицирует пользователя и выпускает game ticket;
- launcher передаёт game account name и ticket только через environment block
нового
Wow.exe; - MoonWell.dll забирает значения до загрузки GlueXML и инициирует обычный SRP;
authserverиспользует временные salt/verifier и атомарно погашает ticket после успешногоCMD_AUTH_LOGON_PROOF.
2. Термины
- backend account — учётная запись пользователя в сервисах MoonWell;
- game account — имя учётной записи AzerothCore, передаваемое в поле
ICMD_AUTH_LOGON_CHALLENGE; - launcher session — аутентифицированная сессия launcher с backend;
- ticket — одноразовый временный пароль для SRP;
- ticket generation — уникальный внутренний идентификатор выпуска ticket, используемый для отзыва и атомарного погашения;
- dev account — явно разрешённая сервером учётная запись с постоянным SRP verifier для локальной разработки.
3. Формат launcher credentials
Launcher передаёт ровно две обязательные переменные:
MOONWELL_LAUNCH_ACCOUNT=<game account name>
MOONWELL_LAUNCH_TICKET=<single-use SRP password>
3.1 MOONWELL_LAUNCH_ACCOUNT
- непустая строка печатного ASCII (
0x21..0x7E), без пробельных символов; - не содержит
NUL,=и управляющих символов; - верхний предел безопасного чтения из environment — 320 байт;
- перед SRP приводится к той же канонической форме, что и имя в AzerothCore, то есть к ASCII uppercase;
- backend и
authserverсравнивают каноническое имя, а не display name.
Ограничение 320 байт является только пределом входного буфера MoonWell.dll.
Текущий протокол и этот репозиторий не поддерживают game account такой длины:
I_len имеет размер uint8, AuthSession.cpp принимает challenge лишь с
добавкой до 16 байт, а AccountMgr использует MAX_ACCOUNT_STR = 17.
Поэтому backend ДОЛЖЕН отображать длинную backend account на отдельное
короткое game account name, совместимое с текущим сервером (не более 17 байт).
Расширение этого лимита является отдельным изменением протокола/сервера и не
входит в настоящий контракт.
3.2 MOONWELL_LAUNCH_TICKET
- ровно 16 ASCII-символов;
- допустимый алфавит:
A-Zи0-9; - генерируется CSPRNG с равномерным rejection sampling;
- энтропия полного пространства
36^16— около 82,7 бит; - срок действия: не более 60 секунд от момента выпуска;
- одноразовый;
- новый выпуск атомарно отзывает предыдущий активный ticket game account.
Ни один компонент НЕ ДОЛЖЕН нормализовать, менять регистр или обрезать ticket. Для SRP ticket используется в точности как получен.
4. Контракт backend
Конкретный HTTP route может определяться backend, но операция выпуска имеет следующую семантику:
{
"launcher_session_id": "opaque launcher session identifier",
"client_build": 12340
}
Успешный ответ:
{
"account": "GAMEACCOUNT",
"ticket": "7Q9AV4R2M8ZK3W1P",
"expires_at": "2026-08-16T12:34:56Z"
}
Требования к операции:
- запрос разрешён только из действующей аутентифицированной launcher session;
- требуемая пользователю MFA завершается до выпуска ticket;
- backend проверяет право launcher session входить в указанный game account;
client_buildдолжен входить в разрешённый список;- выпуск и отзыв предыдущей generation выполняются одной транзакцией;
- ответ передаётся только по TLS и содержит
Cache-Control: no-store; - ticket не возвращается повторно и не попадает в URL, telemetry, analytics, tracing, audit payload, exception text или access log;
- rate limit применяется как минимум к пользователю, game account, launcher session и IP;
- время жизни вычисляется по серверному времени. Часы клиента не являются источником истины.
Рекомендуемые машинные ошибки: UNAUTHENTICATED, ACCOUNT_NOT_ALLOWED,
CLIENT_BUILD_NOT_ALLOWED, RATE_LIMITED, GAME_ACCOUNT_NOT_COMPATIBLE и
INTERNAL_ERROR. Ошибка не должна содержать ticket или verifier.
5. Хранение ticket
Предпочтительный вариант — не хранить открытый ticket. Backend вычисляет
временные SRP salt и verifier тем же кодом/алгоритмом, что
Acore::Crypto::SRP6::MakeRegistrationData(canonicalAccount, ticket), сохраняет
их вместе с generation и уничтожает открытый ticket после формирования ответа.
Минимальная логическая запись:
account_id UNIQUE
generation_id UNIQUE, unpredictable
srp_salt BINARY(32)
srp_verifier BINARY(32)
launcher_session_hash nullable/optional binding
client_build required binding
issued_at server timestamp
expires_at server timestamp, <= issued_at + 60 seconds
consumed_at nullable
revoked_at nullable
Активна только запись, для которой одновременно выполнено:
consumed_at IS NULL
AND revoked_at IS NULL
AND expires_at > server_now
Если архитектура временно требует хранения открытого ticket, он должен быть зашифрован отдельным ротируемым ключом, недоступным read-only потребителям БД, и удалён при погашении/истечении. Хеш ticket сам по себе недостаточен для построения SRP verifier после выпуска.
6. Запуск Wow.exe
Launcher ДОЛЖЕН:
- получить ticket непосредственно перед запуском;
- построить отдельный Unicode environment block для дочернего процесса, сохранив необходимые штатные переменные и добавив две MoonWell-переменные;
- вызвать
CreateProcessWсCREATE_UNICODE_ENVIRONMENT; - не добавлять credentials в command line;
- сразу после возврата
CreateProcessWзатереть все собственные изменяемые буферы с ticket через гарантированно не оптимизируемую операцию, напримерSecureZeroMemory; - уничтожить созданный environment block независимо от результата запуска.
Launcher НЕ ДОЛЖЕН менять собственное глобальное окружение через
SetEnvironmentVariable, поскольку это создаёт окно утечки и гонки при
параллельных запусках. Переменные должны существовать только в явно собранном
environment block конкретного дочернего процесса.
Ticket нельзя помещать в аргументы, файлы, реестр, crash metadata или логи. Дамп памяти процесса остаётся привилегированной атакой; MoonWell.dll должна минимизировать время жизни plaintext и очищать временные буферы.
7. Контракт MoonWell.dll и GlueXML
MoonWell.dll до загрузки GlueXML:
- читает обе launcher-переменные в ограниченные изменяемые буферы;
- немедленно удаляет их из окружения
Wow.exe; - валидирует обе переменные целиком;
- передаёт валидную пару GlueXML только в памяти процесса;
- очищает исходные и промежуточные буферы после начала SRP-входа.
Пара считается валидной только если присутствуют и корректны обе переменные. Частичная, пустая, слишком длинная или синтаксически неверная пара полностью отбрасывается. Нельзя переходить к ручному логину из-за ошибки credentials в Release.
Состояния интерфейса:
| Сборка/вход | AccountLoginUI |
Основное действие |
|---|---|---|
| Release, валидная пара | скрыт | один вызов DefaultServerLogin(account, ticket) |
| Release, пары нет/она невалидна | поля, password и save options скрыты | открыть launcher |
Debug и MOONWELL_DEV_LOGIN=1 |
прежняя форма видима | штатный ручной SRP-вход |
| Debug без dev-флага | как Release | открыть launcher |
При каждом не-dev запуске клиент удаляет ранее сохранённые account/password и
отключает параметры их сохранения до отображения UI. Повторный автоматический
вызов DefaultServerLogin в рамках одного процесса запрещён.
MOONWELL_DEV_LOGIN читается и удаляется тем же ранним нативным кодом. Он
учитывается только Debug-сборкой MoonWell.dll; Release-сборка всегда его
игнорирует.
8. Открытие launcher
Кнопка «Авторизоваться»:
- ищет
MoonWellLauncher.exeв каталоге фактически запущенногоWow.exe; - если файл найден, запускает его без credentials;
- иначе открывает
moonwell://authorize?source=clientсистемным обработчиком.
Путь нельзя брать из current working directory. Перед запуском следует проверять издателя Authenticode/ожидаемую подпись launcher.
Установщик регистрирует URI scheme только для текущего пользователя в
HKCU\Software\Classes\moonwell. Команда обработчика должна корректно заключать
путь executable и аргумент URI в кавычки. Launcher рассматривает URI как
недоверенный ввод, разрешает только известные host/action/query keys и никогда
не принимает credentials через URI.
9. Обязательный контракт authserver
9.1 CMD_AUTH_LOGON_CHALLENGE
После обычного разбора и канонизации game account сервер:
- загружает account, блокировки и режим авторизации;
- для обычного account ищет активную launcher ticket generation;
- проверяет expiry,
client_buildи дополнительные привязки; - если активной generation нет, возвращает общий отказ до обращения к постоянным account salt/verifier;
- сохраняет
generation_idв объекте конкретнойAuthSession; - строит SRP challenge только из временных salt/verifier этой generation.
Для launcher-only account SRP challenge не должен запрашивать интерактивный TOTP/security token: MFA является свойством launcher/backend-сессии и должна быть завершена до выпуска ticket. Иначе скрытый GlueXML не сможет закончить автоматический вход. Штатный AzerothCore TOTP может оставаться только в явно описанном dev-потоке с видимой формой.
Для отсутствующего, истёкшего, отозванного или уже использованного ticket
следует возвращать одинаковый протокольный результат, например
WOW_FAIL_UNKNOWN_ACCOUNT, чтобы не создавать oracle состояния ticket.
9.2 CMD_AUTH_LOGON_PROOF
После успешной криптографической проверки proof, TOTP (если применяется) и версии клиента сервер ДОЛЖЕН в одной транзакции:
-
выполнить условное погашение именно generation из текущей
AuthSession:UPDATE launcher_ticket SET consumed_at = CURRENT_TIMESTAMP(6) WHERE account_id = ? AND generation_id = ? AND consumed_at IS NULL AND revoked_at IS NULL AND expires_at > CURRENT_TIMESTAMP(6); -
убедиться, что изменена ровно одна строка;
-
сохранить новую game session key и штатные login metadata;
-
зафиксировать транзакцию;
-
только после commit отправить клиенту успешный
AUTH_LOGON_PROOF.
Если условное погашение изменило ноль строк или commit не удался, сервер возвращает общий отказ и не создаёт авторизованную game session. Это обеспечивает отказ повторному proof даже если два challenge успели получить одну generation.
Неуспешный proof не погашает ticket, но существующие механизмы rate limit и защиты от перебора продолжают действовать. Ticket всё равно истекает не позднее 60 секунд после выпуска.
9.3 Постоянный verifier и dev-доступ
Для обычных accounts authserver никогда не выбирает и не проверяет постоянные
account.salt/account.verifier. При миграции их следует сделать nullable и
удалить у launcher-only accounts либо заменить отдельным режимом хранения,
который невозможно случайно использовать в launcher-ветке.
Ручной SRP разрешён только если серверная политика независимо подтверждает все условия:
- account присутствует в явном dev allowlist;
- account имеет режим
DEV_SRP; - источник соответствует VPN/IP allowlist;
- production-конфигурация не разрешает dev-режим по умолчанию.
Клиентский Debug-флаг не передаётся authserver и не участвует в решении.
9.4 Reconnect
CMD_AUTH_RECONNECT_* может использовать session key только от ранее успешно
погашенного ticket или разрешённого dev-входа. При включённом reconnect сервер
должен хранить происхождение/время session key и отклонить legacy session keys,
созданные до внедрения этой политики. Reconnect не оживляет ticket и не позволяет
новому процессу выполнить новый logon без нового ticket.
10. Состояния ticket и гонки
Допустимые переходы:
ISSUED -> CONSUMED
ISSUED -> REVOKED (выпущена новая generation)
ISSUED -> EXPIRED (server_now >= expires_at)
CONSUMED, REVOKED и EXPIRED — терминальные состояния. Cleanup может
физически удалить терминальные записи позже, но не должен превращать их обратно
в активные.
Выдача новой generation не обязана завершать уже созданную игровую сессию, но
обязана сделать proof старой незавершённой generation невозможным. Это
обеспечивается сравнением generation_id при погашении.
11. Логи и наблюдаемость
Разрешено логировать только:
- внутренний account id либо необратимо псевдонимизированный идентификатор;
- generation id в хешированном/усечённом виде;
- тип события: issued, rejected, consumed, revoked, expired;
- код результата, build и тайминги без секретов.
Запрещено логировать ticket, SRP verifier, environment block, полный launcher session token и HTTP response body. Фильтрация секретов должна применяться также к debug-логам, SQL tracing, crash reports и APM.
12. Критерии приёмки
Реализация считается соответствующей контракту, если автоматические или интеграционные тесты подтверждают:
- валидный свежий ticket даёт ровно один успешный вход;
- повторный proof и второй процесс с тем же ticket получают отказ;
- новый ticket отзывает предыдущий, включая уже полученный старый challenge;
- ticket старше 60 секунд отклоняется по серверному времени;
- прямой Release-запуск не показывает и не использует ручные credentials;
- одна отсутствующая/невалидная environment-переменная не запускает SRP;
- переменные удалены из окружения до GlueXML и отсутствуют в дочерних процессах, созданных позже;
- Release игнорирует
MOONWELL_DEV_LOGIN=1; - Debug dev-login работает только для server-side allowlisted account и разрешённого источника;
- обычный account с корректным старым постоянным паролем получает отказ;
- ticket, URL и command line отсутствуют во всех проверяемых логах;
- параллельные proof одной generation дают ровно один success;
- запрещённый client build и несоответствующая launcher-session binding дают общий отказ без fallback на постоянный verifier;
- legacy session keys не позволяют обойти launcher-only политику reconnect.
- launcher-only challenge не требует интерактивного TOTP после запуска
DefaultServerLogin.
13. Локальный запуск разработчика
Поддерживаемая команда:
.\run.ps1 -Env local -DeveloperLogin
Скрипт собирает Debug runtime, формирует отдельный environment block и добавляет
MOONWELL_DEV_LOGIN=1 только запускаемому процессу. Он не изменяет глобальное
окружение пользователя и не делает server-side account dev-разрешённым.
Debug DLL не включается в пользовательские Release-дистрибутивы.