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 isu.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 begame_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}
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
sha256of 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>
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}