Source code for gd.utils.crypto.coders

from base64 import urlsafe_b64decode, urlsafe_b64encode
import gzip
import hashlib
import random
import string
import zlib

# absolute import because we are deep
from gd.logging import get_logger
from gd.typing import List, Union

from gd.utils.crypto.xor_cipher import XORCipher as XOR

log = get_logger(__name__)

Z_GZIP_HEADER = 0x10
Z_AUTO_HEADER = 0x20

try:
    from Crypto.Cipher import AES
except ImportError:
    log.warning("Failed to import pycryptodome module. MacOS save coding will not be supported.")


[docs]class Coder: keys = { "message": "14251", "levelpass": "26364", "accountpass": "37526", "levelscore": "39673", "level": "41274", "comment": "29481", "challenges": "19847", "rewards": "59182", "like_rate": "58281", "userscore": "85271", } salts = { "level": "xI25fpAapCQg", "comment": "xPT6iUrtws0J", "like_rate": "ysg6pUrtjn0J", "userscore": "xI35fsAapCRg", "levelscore": "yPg6pUrtWn0J", } mac_key = ( b"\x69\x70\x75\x39\x54\x55\x76\x35\x34\x79\x76\x5d\x69\x73\x46\x4d" b"\x68\x35\x40\x3b\x74\x2e\x35\x77\x33\x34\x45\x32\x52\x79\x40\x7b" ) try: cipher = AES.new(mac_key, AES.MODE_ECB) except NameError: # AES not imported pass @staticmethod def byte_xor(stream: bytes, key: int) -> str: return bytes(byte ^ key for byte in stream).decode(errors="ignore") @classmethod def decode_save(cls, save: Union[bytes, str], needs_xor: bool = True) -> str: if isinstance(save, str): save = save.encode() if needs_xor: save = cls.byte_xor(save, 11) else: save = save.decode() remain = len(save) % 4 if remain: save += "=" * (4 - remain) return inflate(urlsafe_b64decode(save.encode())).decode(errors="ignore") @classmethod def decode_mac_save(cls, save: Union[bytes, str], *args, **kwargs) -> str: if isinstance(save, str): save = save.encode() data = cls.cipher.decrypt(save) last = data[-1] if last < 16: data = data[:-last] return data.decode(errors="ignore") @classmethod def encode_mac_save(cls, save: Union[bytes, str], *args, **kwargs) -> bytes: if isinstance(save, str): save = save.encode() remain = len(save) % 16 if remain: to_add = 16 - remain save += bytes([to_add] * to_add) return cls.cipher.encrypt(save) @classmethod def encode_save(cls, save: Union[bytes, str], needs_xor: bool = True) -> str: if isinstance(save, str): save = save.encode() final = urlsafe_b64encode(deflate(save)) if needs_xor: final = cls.byte_xor(final, 11) else: final = final.decode() return final @classmethod def do_base64( cls, data: str, encode: bool = True, errors: str = "strict", safe: bool = True ) -> str: try: if encode: return urlsafe_b64encode(data.encode(errors=errors)).decode(errors=errors) else: remain = len(data) % 4 if remain: data += "=" * (4 - remain) return urlsafe_b64decode(data.encode(errors=errors)).decode(errors=errors) except Exception: if safe: return data raise
[docs] @classmethod def gen_rs(cls, length: int = 10, charset: str = string.ascii_letters + string.digits) -> str: """Generates a random string of required length. Parameters ---------- length: :class:`int` Amount of characters for a string to have. charset: :class:`str` Character set to use. ``[A-Za-z0-9]`` by default. Returns ------- :class:`str` Generated string. """ return "".join(random.choices(charset, k=length))
[docs] @classmethod def encode(cls, type: str, string: str) -> str: """Encodes a string, combining XOR and Base64 encode methods. Used in different aspects of gd.py. Parameters ---------- type: :class:`str` String representation of type, e.g. ``'levelpass'``. Used to define a XOR key. string: :class:`str` String to encode. Returns ------- :class:`str` Encoded string. """ ciphered = XOR.cipher(key=cls.keys[type], string=string) encoded = cls.do_base64(ciphered, encode=True) return encoded
[docs] @classmethod def decode(cls, type: str, string: str, use_bytes: bool = False) -> str: """Decodes a XOR -> Base64 ciphered string. .. note:: Due to the fact that decode and encode work almost the same, the following is true: .. code-block:: python3 Coder.decode('message', Coder.encode('message', 'NeKit')) == 'NeKit' # True Parameters ---------- type: :class:`str` String representation of a type, e.g. ``'level'``. Used to define a XOR key. string: :class:`str` String to decode. Returns ------- :class:`str` Decoded string. """ string += "=" * (4 - len(string) % 4) # add padding try: cipher_stream = urlsafe_b64decode(string.encode()) except Exception: return string if use_bytes: return XOR.cipher_bytes(key=cls.keys[type], stream=cipher_stream) else: return XOR.cipher(key=cls.keys[type], string=cipher_stream.decode(errors="ignore"))
[docs] @classmethod def gen_chk(cls, type: str, values: List[Union[int, str]]) -> str: """Generates a "chk" value, used in different requests to GD servers. The method is: combine_values -> add salt -> sha1 hash -> XOR -> Base64 encode -> return Parameters ---------- type: :class:`str` String representation of type, e.g. ``'comment'``. Used to define salt and XOR key. values: List[Union[:class:`int`, :class:`str`]] List of values to generate a chk with. Returns ------- :class:`str` Generated ``'chk'``, represented as string. """ salt = cls.salts.get(type, "") # get salt values.append(salt) string = "".join(map(str, values)) # sha1 hash hashed = hashlib.sha1(string.encode()).hexdigest() # XOR xored = XOR.cipher(key=cls.keys[type], string=hashed) # Base64 final = cls.do_base64(xored, encode=True) return final
[docs] @classmethod def unzip(cls, string: Union[bytes, str]) -> Union[bytes, str]: """Decompresses a level string. Used to unzip level data. Parameters ---------- string: Union[:class:`bytes`, :class:`str`] String to unzip, encoded in Base64. Returns ------- Union[:class:`bytes`, :class:`str`] Unzipped level data, as a stream. """ if isinstance(string, str): string = string.encode() unzipped = inflate(urlsafe_b64decode(string)) try: final = unzipped.decode() except UnicodeDecodeError: final = unzipped return final
@classmethod def zip(cls, string: Union[bytes, str], append_semicolon: bool = True) -> str: if isinstance(string, bytes): string = string.decode(errors="ignore") if append_semicolon and not string.endswith(";"): string += ";" return cls.encode_save(string, needs_xor=False) @classmethod def gen_level_upload_seed(cls, data_string: str, chars_required: int = 50) -> str: if len(data_string) < chars_required: return data_string space = len(data_string) // chars_required return data_string[::space][:chars_required] @classmethod def gen_level_lb_seed( cls, jumps: int = 0, percentage: int = 0, seconds: int = 0, played: bool = True ) -> int: return ( 1482 * (played + 1) + (jumps + 3991) * (percentage + 8354) + ((seconds + 4085) ** 2) - 50028039 )
def deflate(data: bytes) -> bytes: compressor = zlib.compressobj(wbits=zlib.MAX_WBITS | Z_GZIP_HEADER) data = compressor.compress(data) + compressor.flush() return data def inflate(data: bytes) -> bytes: try: return gzip.decompress(data) except (gzip.BadGzipFile, zlib.error): pass # fallback and do some other attempts for wbits in (zlib.MAX_WBITS | Z_AUTO_HEADER, zlib.MAX_WBITS | Z_GZIP_HEADER, zlib.MAX_WBITS): try: decompressor = zlib.decompressobj(wbits=wbits) data = decompressor.decompress(data) + decompressor.flush() return data except zlib.error: pass raise RuntimeError("Failed to decompress data.")