# Контракт авторизации MoonWell через лаунчер Статус документа: обязательный контракт для Release-сборок. Ключевые слова **ДОЛЖЕН**, **НЕЛЬЗЯ**, **СЛЕДУЕТ** и **МОЖЕТ** описывают обязательность требований. ## 1. Цель и границы доверия Игровой клиент World of Warcraft 3.3.5a остаётся стандартным SRP6-клиентом, но вместо постоянного пароля получает короткоживущий одноразовый ticket от MoonWell Launcher. Наличие или отсутствие полей логина в GlueXML не является границей безопасности. Решение о допустимости входа всегда принимает `authserver`. В поток входят четыре компонента: 1. backend аутентифицирует пользователя и выпускает game ticket; 2. launcher передаёт game account name и ticket только через environment block нового `Wow.exe`; 3. MoonWell.dll забирает значения до загрузки GlueXML и инициирует обычный SRP; 4. `authserver` использует временные salt/verifier и атомарно погашает ticket после успешного `CMD_AUTH_LOGON_PROOF`. ## 2. Термины - **backend account** — учётная запись пользователя в сервисах MoonWell; - **game account** — имя учётной записи AzerothCore, передаваемое в поле `I` `CMD_AUTH_LOGON_CHALLENGE`; - **launcher session** — аутентифицированная сессия launcher с backend; - **ticket** — одноразовый временный пароль для SRP; - **ticket generation** — уникальный внутренний идентификатор выпуска ticket, используемый для отзыва и атомарного погашения; - **dev account** — явно разрешённая сервером учётная запись с постоянным SRP verifier для локальной разработки. ## 3. Формат launcher credentials Launcher передаёт ровно две обязательные переменные: ```text MOONWELL_LAUNCH_ACCOUNT= MOONWELL_LAUNCH_TICKET= ``` ### 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, но операция выпуска имеет следующую семантику: ```json { "launcher_session_id": "opaque launcher session identifier", "client_build": 12340 } ``` Успешный ответ: ```json { "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 после формирования ответа. Минимальная логическая запись: ```text 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 ``` Активна только запись, для которой одновременно выполнено: ```text consumed_at IS NULL AND revoked_at IS NULL AND expires_at > server_now ``` Если архитектура временно требует хранения открытого ticket, он должен быть зашифрован отдельным ротируемым ключом, недоступным read-only потребителям БД, и удалён при погашении/истечении. Хеш ticket сам по себе недостаточен для построения SRP verifier после выпуска. ## 6. Запуск `Wow.exe` Launcher **ДОЛЖЕН**: 1. получить ticket непосредственно перед запуском; 2. построить отдельный Unicode environment block для дочернего процесса, сохранив необходимые штатные переменные и добавив две MoonWell-переменные; 3. вызвать `CreateProcessW` с `CREATE_UNICODE_ENVIRONMENT`; 4. не добавлять credentials в command line; 5. сразу после возврата `CreateProcessW` затереть все собственные изменяемые буферы с ticket через гарантированно не оптимизируемую операцию, например `SecureZeroMemory`; 6. уничтожить созданный environment block независимо от результата запуска. Launcher **НЕ ДОЛЖЕН** менять собственное глобальное окружение через `SetEnvironmentVariable`, поскольку это создаёт окно утечки и гонки при параллельных запусках. Переменные должны существовать только в явно собранном environment block конкретного дочернего процесса. Ticket нельзя помещать в аргументы, файлы, реестр, crash metadata или логи. Дамп памяти процесса остаётся привилегированной атакой; MoonWell.dll должна минимизировать время жизни plaintext и очищать временные буферы. ## 7. Контракт MoonWell.dll и GlueXML MoonWell.dll до загрузки GlueXML: 1. читает обе launcher-переменные в ограниченные изменяемые буферы; 2. немедленно удаляет их из окружения `Wow.exe`; 3. валидирует обе переменные целиком; 4. передаёт валидную пару GlueXML только в памяти процесса; 5. очищает исходные и промежуточные буферы после начала 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 Кнопка «Авторизоваться»: 1. ищет `MoonWellLauncher.exe` в каталоге фактически запущенного `Wow.exe`; 2. если файл найден, запускает его без credentials; 3. иначе открывает `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 сервер: 1. загружает account, блокировки и режим авторизации; 2. для обычного account ищет активную launcher ticket generation; 3. проверяет expiry, `client_build` и дополнительные привязки; 4. если активной generation нет, возвращает общий отказ до обращения к постоянным account salt/verifier; 5. сохраняет `generation_id` в объекте конкретной `AuthSession`; 6. строит 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 (если применяется) и версии клиента сервер **ДОЛЖЕН** в одной транзакции: 1. выполнить условное погашение именно generation из текущей `AuthSession`: ```sql 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); ``` 2. убедиться, что изменена ровно одна строка; 3. сохранить новую game session key и штатные login metadata; 4. зафиксировать транзакцию; 5. только после 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 и гонки Допустимые переходы: ```text 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. Критерии приёмки Реализация считается соответствующей контракту, если автоматические или интеграционные тесты подтверждают: 1. валидный свежий ticket даёт ровно один успешный вход; 2. повторный proof и второй процесс с тем же ticket получают отказ; 3. новый ticket отзывает предыдущий, включая уже полученный старый challenge; 4. ticket старше 60 секунд отклоняется по серверному времени; 5. прямой Release-запуск не показывает и не использует ручные credentials; 6. одна отсутствующая/невалидная environment-переменная не запускает SRP; 7. переменные удалены из окружения до GlueXML и отсутствуют в дочерних процессах, созданных позже; 8. Release игнорирует `MOONWELL_DEV_LOGIN=1`; 9. Debug dev-login работает только для server-side allowlisted account и разрешённого источника; 10. обычный account с корректным старым постоянным паролем получает отказ; 11. ticket, URL и command line отсутствуют во всех проверяемых логах; 12. параллельные proof одной generation дают ровно один success; 13. запрещённый client build и несоответствующая launcher-session binding дают общий отказ без fallback на постоянный verifier; 14. legacy session keys не позволяют обойти launcher-only политику reconnect. 15. launcher-only challenge не требует интерактивного TOTP после запуска `DefaultServerLogin`. ## 13. Локальный запуск разработчика Поддерживаемая команда: ```powershell .\run.ps1 -Env local -DeveloperLogin ``` Скрипт собирает Debug runtime, формирует отдельный environment block и добавляет `MOONWELL_DEV_LOGIN=1` только запускаемому процессу. Он не изменяет глобальное окружение пользователя и не делает server-side account dev-разрешённым. Debug DLL не включается в пользовательские Release-дистрибутивы.