Свой лицензионный сервер: как я защитил десктоп-продукт от копирования
Как выдавать офлайн-проверяемые лицензии на десктоп-продукт: Ed25519, подписанные токены и один лёгкий сервер вместо постоянных запросов в облако.
Когда я собирал лицензирование для десктопной утилиты, шифрование меня волновало во вторую очередь. Первым делом я ломал голову над другим: как не подвесить продукт на собственный сервер. Расскажу схему, которую собрал и проверил руками.
Зачем городить свой сервер
Простой способ защиты — дёргать сервер при каждом запуске: «этому пользователю можно?». Работает, пока есть интернет и пока сервер жив. Отвалилась сеть — программа не открывается. Умер сервер через два года — продукт умер у всех, кто его купил. Для десктопной утилиты размен так себе.
Мне нужно было другое: чтобы клиент проверял лицензию сам, без обращения к серверу, но подделать её было нельзя. Классическая задача на асимметричную подпись, и решается она чище, чем кажется.
Идея: подпись вместо базы
Ключевой сдвиг в голове — сервер не хранит список «кому можно». Сервер один раз выдаёт подписанный токен, дальше клиент живёт с ним офлайн.
Схема такая:
- сервер держит приватный ключ и никому его не отдаёт;
- в клиент зашит публичный ключ (у меня он лежит прямо в сборке .NET-приложения);
- при покупке сервер собирает данные лицензии, подписывает их и отдаёт токен;
- клиент проверяет подпись публичным ключом локально.
Подделать токен без приватного ключа нельзя. Проверить — можно где угодно и без интернета.
Формат токена
Токен у меня выглядит как payloadB64url.sigB64url — две части через точку. Похоже на JWT, но это не JWT, и разница здесь принципиальная.
В payload лежит обычный JSON: версия, ключ лицензии, имя владельца, тип, дата выпуска, срок. Дальше идёт деталь, на которой легко споткнуться. Подписывать нужно именно байты уже закодированной строки payloadB64url. Собирать JSON заново для проверки нельзя: клиент и сервер соберут его чуть по-разному — порядок полей, пробелы, — и подписи разъедутся, валидные лицензии начнут отваливаться. Когда подписываешь байты строки, проверка идёт байт в байт, и проблема исчезает сама.
Ed25519 и почему он
Взял Ed25519. Причины приземлённые:
- короткие ключи и подписи, токен получается компактным;
- есть прямо в стандартной библиотеке —
node:cryptoна сервере, штатная криптография на стороне .NET; - быстрая проверка и никакой возни с выбором параметров, как у RSA.
Внешних крипто-библиотек тянуть не пришлось. Меньше зависимостей — меньше того, что сломается через год.
Стек, который не жалко поднять
Сервер — один сервис на Hono (TypeScript, запускаю через tsx). База — SQLite через better-sqlite3, файл лежит в Docker-томе. Сверху nginx как reverse-proxy. Всё.
Был соблазн взять «взрослую» СУБД, очередь, отдельный микросервис на подпись. Не понадобилось. Лицензии выдаются редко, нагрузка копеечная, а файловая SQLite в томе бэкапится копированием одного файла. Один контейнер, который спокойно живёт на дешёвом VPS.
Тот же сервис заодно отдаёт лендинг и два веб-кабинета — клиентский и владельца. Городить под это отдельный фронтенд-стек смысла не было.
Чего эта защита не делает
Честно про границы, потому что «защита от копирования» звучит громче, чем оно есть.
Подпись гарантирует, что лицензию нельзя подделать или выписать в обход сервера. Скопировать саму программу вместе с валидным токеном на другую машину она не помешает. Привязка к железу, отзыв лицензий, лимит активаций — это отдельный слой, который навешивается поверх, когда реально нужен. Многим продуктам он не нужен вовсе. Честный покупатель получает рабочий офлайн-продукт. А массовое воровство отсекается само собой: скачать бесплатный кряк проще, чем красть чужие подписанные токены.
Приватный ключ — единственная точка, которую надо беречь. Утёк ключ — вся схема рухнула. Он живёт только на сервере и не попадает ни в сборку клиента, ни в репозиторий.
С чего начать
Если хочется повторить, минимальный путь:
- сгенерировать пару Ed25519, публичный ключ вынести в клиент;
- определить формат payload и зафиксировать правило: подписываем байты строки;
- поднять любой лёгкий сервер с одним эндпоинтом выдачи токена;
- на клиенте написать проверку подписи — это буквально несколько строк.
Базы, кабинеты, привязки к железу наращиваются потом, по мере того как продукт этого запросит. Начинать со строительства полной крепости смысла нет. Начните с подписи, которую нельзя подделать, и офлайн-проверки, которая работает без вас.