Files
DndGamePlayer/docs/licensing-spec.md
T
Ivan Fontosh 32a5479086 fix(license): bind deviceId to physical machine, not OS user
Use OS machine identifiers (Windows MachineGuid, macOS IOPlatformUUID, Linux machine-id) hashed as deviceId so all accounts on one PC share one license slot. Keep legacy userData/device.id for migration and retire it on re-activation.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-23 10:39:46 +08:00

6.7 KiB
Raw Blame History

Спецификация лицензирования TTRPG Player (этап D1)

Документ фиксирует модель D1: онлайн-активация, срок, число устройств, отзыв, и как это согласуется с клиентом и сервером лицензий.

Исходный код сервера вынесен в отдельный репозиторий: DndGamePlayerLicenseServer.

Модель

  1. Продуктовый ключ — секрет покупателя, известен только ему и серверу. Обменивается на лицензионный токен через POST /v1/activate (онлайн-активация). В поле «Указать ключ» приложение принимает продуктовый ключ TTRPG-… или устаревший DND-… (клиент сам вызывает POST /v1/activate с deviceId) или уже готовый токен (две части base64url через одну точку). Новые ключи на сервере — только TTRPG-… (см. репозиторий лицензий).
  2. Лицензионный токен — публичная полезная нагрузка (sub, pid, iat, exp, did) + подпись Ed25519. Клиент хранит только токен и публичный ключ (вшит в приложение); подделать валидный токен без приватного ключа сервера невозможно.
  3. Срок — поле exp (unix секунды) в выданном токене. Клиент отклоняет истёкший токен без сети. На сервере два формата продуктового ключа:
    • fixed — в записи ключа задан expiresAtSec; при активации exp копируется из него (старый формат).
    • period — задан validDays; при первой активации лицензии (sub) сервер фиксирует activatedAtSec и выставляет exp = activatedAtSec + validDays×86400 для всех устройств. Клиенту тип ключа неизвестен — только итоговый exp в токене.
  4. Устройства — поле did в токене: при активации сервер привязывает токен к deviceId клиента и ведёт учёт списка устройств на sub в data.json (maxDevices). deviceIdотпечаток физической машины (не пользователь ОС): Windows MachineGuid, macOS IOPlatformUUID, Linux /etc/machine-id; клиент хеширует значение (SHA-256) и шлёт opaque-строку. На одном ПК разные учётки Windows/macOS/Linux получают один и тот же deviceId. Опционально retireDeviceId в POST /v1/activate снимает старый слот (миграция с per-user UUID).
  5. Отзыв — сервер помечает sub в revokedSubs. Клиент при наличии DND_LICENSE_STATUS_URL запрашивает GET /v1/status?sub=…; при revoked: true лицензия считается недействительной без обновления приложения. Офлайн до истечения exp отозванный токен формально криптографически валиден — это осознанный компромисс; при необходимости сокращайте срок жизни токена или добавляйте принудительную онлайн-проверку перед критичными действиями.

Продакшен-сборка

Скрипт npm run build / node scripts/build.mjs --production подставляет в main-бандл базовый URL https://license.ttrpgplayer.ru/ как process.env.DND_LICENSE_STATUS_URL (если при сборке переменная не задана). Другой хост: DND_LICENSE_STATUS_URL=https://example.com npm run build. В dev по-прежнему можно выставить переменную окружения при запуске Electron без пересборки.

Хранение на клиенте

Токен не хранится открытым текстом в JSON userData: используется Electron safeStorage (на macOS — связка с Keychain, на Windows — DPAPI). Идентификатор устройства вычисляется из ID машины ОС (см. выше); файл device.id в userData — устаревший per-user UUID, читается только для совместимости до повторной активации. Принятие EULA — preferences.json (версия текста).

Для тестов/отладки можно задать DND_LICENSE_DEVICE_ID (готовая строка deviceId, без fingerprint).

Linux / WSL без keyring

На Linux safeStorage обычно требует Secret Service (например gnome-keyring + D-Bus). В WSL без keyring safeStorage.isEncryptionAvailable() часто false, и сохранить токен нельзя.

Явный обход (только если осознанно нужен запуск без OS-хранилища): переменная окружения DND_LICENSE_INSECURE_FILE_STORAGE=1. Тогда токен пишется в файл license.sealed.fallback (AES-256-GCM, ключ от deviceId + константа приложения). Это слабее, чем связка с ОС: при копировании userData + знании формата теоретически проще атаковать офлайн. Для обычного десктопа Linux с рабочим сеансом переменную не задавайте.

Пример запуска AppImage:

DND_LICENSE_INSECURE_FILE_STORAGE=1 ./TTRPGPlayer-1.0.12-x64.AppImage --no-sandbox --appimage-extract-and-run

Юридическое (D9)

Текст EULA в приложении (app/renderer/legal/eulaRu.ts) и формулировки про активацию/отзыв/устройства. Перед первым вводом ключа пользователь принимает EULA (версия EULA_CURRENT_VERSION в app/shared/license/eulaVersion.ts).