Зміст

Зашифрована інформація про гру (Task API)

Навіщо це потрібно?

Якщо вам необхідно безпечно передати дані про поточного гравця або команду під час гри стороннім зовнішнім сервісам (наприклад, Telegram-боту, скрипту авторизації або кастомному сайту з флеш-грою), використовуйте генерацію зашифрованого рядка.

У цьому випадку навіть технічно підковані гравці не зможуть побачити, які саме дані передаються в URL або параметрах скрипта, і не зможуть підмінити їх самостійно. Це особливо критично на іграх довгого формату (Long).

Як це налаштувати?

1. Генерація секретного ключа

2. Використання плейсхолдера в завданні

У тексті завдання, підказки або в параметрах посилання на зовнішній сервіс пропишіть конструкцію виду:

!api:поле1,поле2!

Доступні поля для шифрування (вказуються через кому):

Приклад генерації

Якщо ви пропишете в завданні:

!api:user_id,task_id!

Движок згенерує зашифрований рядок виду:

v2.vyJ42f20mXmdKS5bAr6GDoqqUP2pw4f4vbM7mX1t3SvY1LMZ_Jr80XjEaXkB6iB2l3eN3g

Його можна розшифрувати на вашому сервері за допомогою ключа гри (див. приклади нижче). В результаті ви отримаєте JSON:

{"u": 162862, "tid": 42719}

Приклад завдання в грі

Формат рядка

Через випадковий IV ті самі дані щоразу дають різний рядок — це нормально і зроблено навмисно. Не порівнюйте рядки між собою і не використовуйте їх як ключ кешу: спочатку розшифруйте, порівнюйте вже JSON.

Тег автентифікації означає, що виправлений у посиланні рядок розшифрувати не вдасться — підробити дані гравець не зможе.

<note important>Раніше рядок був hex-кодованим Blowfish (ECB). Цей формат більше не видається: Blowfish зник із сучасних збірок OpenSSL, і шифрування ламалося. Якщо у вас є готовий скрипт розшифрування, замініть його на один із прикладів нижче.</note>

Приклади розшифрування на сервері

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}