{{indexmenu_n>40}} ====== Зашифрована інформація про гру (Task API) ====== ===== Навіщо це потрібно? ===== Якщо вам необхідно безпечно передати дані про поточного гравця або команду під час гри стороннім зовнішнім сервісам (наприклад, Telegram-боту, скрипту авторизації або кастомному сайту з флеш-грою), використовуйте генерацію зашифрованого рядка. У цьому випадку навіть технічно підковані гравці не зможуть побачити, які саме дані передаються в URL або параметрах скрипта, і не зможуть підмінити їх самостійно. Це особливо критично на іграх довгого формату (Long). ===== Як це налаштувати? ===== ==== 1. Генерація секретного ключа ==== * Перейдіть до редактора вашої гри. * Знайдіть розділ **"API для зовнішніх сайтів / телеграм ботів"**. * Натисніть кнопку **"Згенерувати"**. Движок створить унікальний ключ (наприклад, ''XgHxzCvUSXwPfR7wBvdY4xHC6tSFLewx''). ==== 2. Використання плейсхолдера в завданні ==== У тексті завдання, підказки або в параметрах посилання на зовнішній сервіс пропишіть конструкцію виду: !api:поле1,поле2! Доступні поля для шифрування (вказуються через кому): * **''user_id''** — ID гравця. У розшифрованому JSON перетворюється на ключ **''u''**. * **''user_name''** — логін гравця. У JSON — **''un''**. * **''team_id''** — ID команди. У JSON — **''tm''**. * **''team_name''** — назва команди. У JSON — **''tmn''**. * **''task_n''** — порядковий номер поточного рівня. У JSON — **''t''**. * **''task_id''** — ID поточного рівня. У JSON — **''tid''**. Унікальний усередині гри, а не по всьому сайту: те саме число в різних іграх означає різні рівні. Ключем у зовнішній базі має бути пара **''game_id''** + **''task_id''**. * **''game_id''** — ID гри. У JSON — **''gid''**. ==== Приклад генерації ==== Якщо ви пропишете в завданні: !api:user_id,task_id! Движок згенерує зашифрований рядок виду: v2.vyJ42f20mXmdKS5bAr6GDoqqUP2pw4f4vbM7mX1t3SvY1LMZ_Jr80XjEaXkB6iB2l3eN3g Його можна розшифрувати на вашому сервері за допомогою ключа гри (див. приклади нижче). В результаті ви отримаєте JSON: {"u": 162862, "tid": 42719} [[https://qeng.org/game.php?jump_to&gid=3493&task_id=42719|Приклад завдання в грі]] ==== Формат рядка ==== * ''v2.'' — версія формату. Перевіряйте цей префікс: якщо він зміниться, ваш скрипт має повідомити про помилку, а не мовчки видати сміття. * Далі — **base64url** (алфавіт ''-_'', без вирівнювальних ''='') від склейки трьох частин: **IV** (12 байтів) + **тег автентифікації** (16 байтів) + сам шифротекст. * Алгоритм — **aes-256-gcm**. Ключ шифрування — **''sha256''** від ключа гри (сирі 32 байти, не hex). Через випадковий IV **ті самі дані щоразу дають різний рядок** — це нормально і зроблено навмисно. Не порівнюйте рядки між собою і не використовуйте їх як ключ кешу: спочатку розшифруйте, порівнюйте вже JSON. Тег автентифікації означає, що виправлений у посиланні рядок розшифрувати не вдасться — підробити дані гравець не зможе. Раніше рядок був hex-кодованим Blowfish (ECB). Цей формат більше не видається: Blowfish зник із сучасних збірок OpenSSL, і шифрування ламалося. Якщо у вас є готовий скрипт розшифрування, замініть його на один із прикладів нижче. ===== Приклади розшифрування на сервері ===== ==== PHP ==== $data = 'v2.vyJ42f20mXmdKS5bAr6GDoqqUP2pw4f4vbM7mX1t3SvY1LMZ_Jr80XjEaXkB6iB2l3eN3g'; $key = 'XgHxzCvUSXwPfR7wBvdY4xHC6tSFLewx'; $raw = base64_decode(strtr(substr($data, 3), '-_', '+/')); $decoded = json_decode( openssl_decrypt( substr($raw, 28), // шифротекст 'aes-256-gcm', hash('sha256', $key, true), // ключ гри -> 32 байти OPENSSL_RAW_DATA, substr($raw, 0, 12), // IV substr($raw, 12, 16) // тег автентифікації ), true ); // $decoded => array('u' => 162862, 'tid' => 42719) ==== Python ==== *(Потрібна бібліотека cryptography: ''pip install cryptography'')* import base64 import hashlib import json import typing from cryptography.hazmat.primitives.ciphers.aead import AESGCM def decrypt_qeng_data(data: str, key: str) -> typing.Dict[str, typing.Union[str, int]]: if not data.startswith('v2.'): raise ValueError('unknown payload format: ' + data[:3]) body = data[3:] body += '=' * (-len(body) % 4) # base64url надходить без вирівнювання raw = base64.urlsafe_b64decode(body) iv, tag, encrypted = raw[:12], raw[12:28], raw[28:] aes_key = hashlib.sha256(key.encode()).digest() # cryptography чекає тег у кінці шифротексту return json.loads(AESGCM(aes_key).decrypt(iv, encrypted + tag, None)) # Використання data = 'v2.vyJ42f20mXmdKS5bAr6GDoqqUP2pw4f4vbM7mX1t3SvY1LMZ_Jr80XjEaXkB6iB2l3eN3g' key = 'XgHxzCvUSXwPfR7wBvdY4xHC6tSFLewx' print(decrypt_qeng_data(data, key)) # => {'u': 162862, 'tid': 42719} ==== Ruby ==== require 'base64' require 'digest' require 'json' require 'openssl' def qeng_data_decrypt(data, key) raise "unknown payload format: #{data[0, 3]}" unless data.start_with?('v2.') body = data[3..] raw = Base64.urlsafe_decode64(body + '=' * (-body.length % 4)) crypto = OpenSSL::Cipher.new('aes-256-gcm').decrypt crypto.key = Digest::SHA256.digest(key) # ключ гри -> 32 байти crypto.iv = raw[0, 12] crypto.auth_tag = raw[12, 16] crypto.auth_data = '' JSON.parse(crypto.update(raw[28..]) + crypto.final, symbolize_names: true) end # Використання data = 'v2.vyJ42f20mXmdKS5bAr6GDoqqUP2pw4f4vbM7mX1t3SvY1LMZ_Jr80XjEaXkB6iB2l3eN3g' key = 'XgHxzCvUSXwPfR7wBvdY4xHC6tSFLewx' puts qeng_data_decrypt(data, key) # => {:u=>162862, :tid=>42719}