en:authors_main:task_editor:advanced:api_main

Encrypted Game Information (Task API)

If you need to pass some information about the game during the game to third-party services (Telegram bots, third-party scripts, custom auth setups etc.), it is recommended to use the generation of an encrypted string. In this case, even tech-savvy players will not be able to see what information is being passed or modify it themselves (which is especially relevant for long formats).

You need to create a key in the game. To do this, click the “Generate” button in the game editor under the API for external websites/Telegram bots” section. This will create a key (e.g. XgHxzCvUSXwPfR7wBvdY4xHC6tSFLewx).

In tasks and game descriptions, you can use the construction:

!api:data!

The data fields needed by the external server are specified separated by commas:

  • user_id — user ID. In the decrypted JSON, the field is u.
  • user_name — User login. In JSON — un.
  • team_id — Team ID. In JSON — tm.
  • team_name — Team name. In JSON — tmn.
  • task_n — Level number. In JSON — t.
  • task_id — Level ID. In JSON — tid. Unique within the game rather than site-wide: the same number in two games means two different levels. A key in an external database has to be game_id + task_id.
  • game_id — Game ID. In JSON — gid.

To get an encrypted string in the game:

!api:user_id,task_id!

This will generate the encrypted string:

v2.vyJ42f20mXmdKS5bAr6GDoqqUP2pw4f4vbM7mX1t3SvY1LMZ_Jr80XjEaXkB6iB2l3eN3g

which can be decrypted on your server using the game key (see the examples below) into:

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

Example task in game

  • v2. — the format version. Check this prefix: if it ever changes, your script should report an error rather than silently return garbage.
  • The rest is base64url (the -_ alphabet, no = padding) of three parts joined together: the IV (12 bytes) + the authentication tag (16 bytes) + the ciphertext itself.
  • The cipher is aes-256-gcm. The encryption key is sha256 of the game key (32 raw bytes, not hex).

Because the IV is random, the same data yields a different string every time — that is intended. Do not compare the strings to each other and do not use one as a cache key: decrypt first, compare the JSON.

The authentication tag means a string edited in the link will fail to decrypt, so a player cannot forge the data.

<note important>The string used to be hex-encoded Blowfish (ECB). That format is no longer generated: Blowfish is gone from current OpenSSL builds and the encryption was breaking. If you have a decryption script already, replace it with one of the examples below.</note>

$data = 'v2.vyJ42f20mXmdKS5bAr6GDoqqUP2pw4f4vbM7mX1t3SvY1LMZ_Jr80XjEaXkB6iB2l3eN3g';
$key  = 'XgHxzCvUSXwPfR7wBvdY4xHC6tSFLewx';
 
$raw = base64_decode(strtr(substr($data, 3), '-_', '+/'));
$decoded = json_decode(
  openssl_decrypt(
    substr($raw, 28),           // the ciphertext
    'aes-256-gcm',
    hash('sha256', $key, true), // the game key -> 32 bytes
    OPENSSL_RAW_DATA,
    substr($raw, 0, 12),        // IV
    substr($raw, 12, 16)        // authentication tag
  ),
  true
);
// $decoded => array('u' => 162862, 'tid' => 42719)

*(Requires the cryptography library: 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 comes unpadded
    raw = base64.urlsafe_b64decode(body)
    iv, tag, encrypted = raw[:12], raw[12:28], raw[28:]
    aes_key = hashlib.sha256(key.encode()).digest()
    # cryptography expects the tag at the end of the ciphertext
    return json.loads(AESGCM(aes_key).decrypt(iv, encrypted + tag, None))
 
# Usage
data = 'v2.vyJ42f20mXmdKS5bAr6GDoqqUP2pw4f4vbM7mX1t3SvY1LMZ_Jr80XjEaXkB6iB2l3eN3g'
key = 'XgHxzCvUSXwPfR7wBvdY4xHC6tSFLewx'
print(decrypt_qeng_data(data, key)) # => {'u': 162862, 'tid': 42719}
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) # the game key -> 32 bytes
  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
 
# Usage
data = 'v2.vyJ42f20mXmdKS5bAr6GDoqqUP2pw4f4vbM7mX1t3SvY1LMZ_Jr80XjEaXkB6iB2l3eN3g'
key = 'XgHxzCvUSXwPfR7wBvdY4xHC6tSFLewx'
puts qeng_data_decrypt(data, key) # => {:u=>162862, :tid=>42719}
  • en/authors_main/task_editor/advanced/api_main.txt
  • Last modified: 2026/08/16 23:04
  • (external edit)