{{indexmenu_n>40}}
====== Encrypted Game Information (Task API) ======
===== Why? =====
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).
===== How? =====
==== Key Generation ====
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'').
==== Use in Tasks ====
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''**.
==== Example ====
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}
[[https://qeng.org/game.php?jump_to&gid=3493&task_id=42719|Example task in game]]
==== String Format ====
* ''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.
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.
===== Decryption =====
==== PHP ====
$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)
==== Python ====
*(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}
==== 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) # 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}