{{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}