Files
moonwell-core/doc/LauncherAuthorizationContract.md
2026-08-17 20:24:17 +04:00

22 KiB
Raw Permalink Blame History

Контракт авторизации 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 передаёт ровно две обязательные переменные:

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 ДОЛЖЕН:

  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:

    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 и гонки

Допустимые переходы:

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. Локальный запуск разработчика

Поддерживаемая команда:

.\run.ps1 -Env local -DeveloperLogin

Скрипт собирает Debug runtime, формирует отдельный environment block и добавляет MOONWELL_DEV_LOGIN=1 только запускаемому процессу. Он не изменяет глобальное окружение пользователя и не делает server-side account dev-разрешённым. Debug DLL не включается в пользовательские Release-дистрибутивы.