├── .gitignore ├── config.json.example ├── README.md └── patcher.py /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.pyc 3 | *.apk 4 | *.keystore 5 | cache.sqlite -------------------------------------------------------------------------------- /config.json.example: -------------------------------------------------------------------------------- 1 | { 2 | "debug": true, 3 | "package": "com.supercell.clashroyale", 4 | "key": "72f1a4a4c48e44da0c42310f800e96624e6dc6a641a9d41c3b5039d8dfadc27e", 5 | "url": "game.clashroyaleapp.com", 6 | "keystore": { 7 | "storepass": "teststorepass", 8 | "key": { 9 | "alias": "testalias", 10 | "keypass": "testkeypass", 11 | "dname": { 12 | "cn": "testcn", 13 | "ou": "testou", 14 | "o": "testo", 15 | "l": "testl", 16 | "s": "tests", 17 | "c": "testc" 18 | } 19 | } 20 | }, 21 | "paths": { 22 | "apktool": "", 23 | "dd": "/bin/dd", 24 | "keytool": "/usr/bin/keytool", 25 | "jarsigner": "/usr/bin/jarsigner", 26 | "zipalign": "" 27 | }, 28 | "versions": { 29 | "1.5.0": { 30 | "key": "bbdba8653396d1df84efaea923ecd150d15eb526a46a6c39b53dac974fff3829", 31 | "arm": { 32 | "md5": "f99a1e9b2693c12d6d14b90f693790cc", 33 | "key-offset": "4145180", 34 | "url-offset": "3381192" 35 | }, 36 | "x86": { 37 | "md5": "95c2c8b6c946459d799fe1037f0fbd0f", 38 | "key-offset": "6029340", 39 | "url-offset": "4551145" 40 | } 41 | }, 42 | "1.6.0": { 43 | "key": "e330c7916ae0a66f3a90eae97a863ee00ac1dcad058877b1eecfc8fe91c93532", 44 | "arm": { 45 | "md5": "af0261082ea75831e817f2154ee3653b", 46 | "key-offset": "3948580", 47 | "url-offset": "3190983" 48 | }, 49 | "x86": { 50 | "md5": "cd77c8f927f24dcc5fe54d12823e1d55", 51 | "key-offset": "5750820", 52 | "url-offset": "4304848" 53 | } 54 | }, 55 | "1.7.0": { 56 | "key": "0f9fff6d583023c5c739c053581c994dbe37789900ffda312fc97edfd091945f", 57 | "arm": { 58 | "md5": "22295cc360c46a9f728701a18535b982", 59 | "key-offset": "4063268", 60 | "url-offset": "3295796" 61 | }, 62 | "x86": { 63 | "md5": "fd18e8531ead172f147939bab481b3c6", 64 | "key-offset": "5931044", 65 | "url-offset": "4452609" 66 | } 67 | }, 68 | "1.8.0": { 69 | "key": "9e6657f2b419c237f6aeef37088690a642010586a7bd9018a15652bab8370f4f", 70 | "arm": { 71 | "md5": "9bd69697a5af0f40dc2d37a4e88e0f35", 72 | "key-offset": "5042212", 73 | "url-offset": "4174681" 74 | }, 75 | "x86": { 76 | "md5": "6b9d520b7d97838582d71861feeace63", 77 | "key-offset": "8093732", 78 | "url-offset": "6313949" 79 | } 80 | }, 81 | "1.8.1": { 82 | "key": "9e6657f2b419c237f6aeef37088690a642010586a7bd9018a15652bab8370f4f", 83 | "arm": { 84 | "md5": "52e301c8af6c52cbdd9747cb7de5f28f", 85 | "key-offset": "5042212", 86 | "url-offset": "4175993" 87 | }, 88 | "x86": { 89 | "md5": "2e6bf97045a051029507917d23d3bec0", 90 | "key-offset": "8097828", 91 | "url-offset": "6316045" 92 | } 93 | }, 94 | "1.9.0": { 95 | "key": "ac30dcbea27e213407519bc05be8e9d930e63f873858479946c144895fa3a26b", 96 | "arm": { 97 | "md5": "0418c92a8515da0643fbb4966b0b777d", 98 | "key-offset": "5324832", 99 | "url-offset": "4412347" 100 | }, 101 | "x86": { 102 | "md5": "45b9d9304b83515563d86b7532f9eaf7", 103 | "key-offset": "8540192", 104 | "url-offset": "6663795" 105 | } 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **Warning:** In April 2016, Supercell has started banning accounts for the use of third party software. We are unsure what, if any, checks are in place that might reveal the use of this tool, so continue at your own risk. See [here](http://supercell.com/en/safe-and-fair-play/) for more info. 2 | 3 | # cr-patcher 4 | 5 | This tool patches the Clash Royale APK. Once applied, your game will connect with the official servers through [cr-proxy](https://github.com/royale-proxy/cr-proxy) instead of directly. This allows you to see every message that is sent to and from the server, decrypted and decoded. 6 | 7 | ## Running 8 | #### Read the [installation](#installation) section before 9 | First, you need an .apk of the game. You can download an official one for example [here](http://www.apkmirror.com/uploads/?q=clash-royale-supercell). Put it to this folder (the one where `patcher.py` is). The name should match the following format: 10 | 11 | -.apk 12 | 13 | If you use the official APK, the package is `com.supercell.clashroyale`, with a file name of `com.supercell.clashroyale-1.8.1.apk` 14 | 15 | Run the script with: 16 | 17 | python3.5 patcher.py [--json] version-number 18 | 19 | For example: 20 | 21 | python3.5 patcher.py 1.8.1 22 | 23 | By default, `cr-patcher` will retrieve the keys, MD5s, and key/URL offsets from the [`cr-proxy` wiki](https://github.com/royale-proxy/cr-proxy/wiki). To provide these values for a new or unknown APK version, enter them in `config.json` and use the `--json` flag. 24 | 25 |
If you need to, enter them with this layout (click to expand)

26 | 27 | ``` 28 | "versions": { 29 | "8.212.9": { 30 | "key": "469b704e7f6009ba8fc72e9b5c864c8e9285a755c5190f03f5c74852f6d9f419", 31 | "arm": { 32 | "md5": "769e2e9e1258b75d15cb7e04b2e49de3", 33 | "key-offset": "4280344", 34 | "url-offset": "3534513" 35 | }, 36 | "x86": { 37 | "md5": "29ca23e48a5e419e83f2a7988c842d3e", 38 | "key-offset": "6189080", 39 | "url-offset": "4768816" 40 | } 41 | } 42 | } 43 | ``` 44 |

45 | 46 | ## Config explained 47 | * `debug` *(true/false)* - when set to true, you can use external tools to debug the app while it's running. If you only want to run the [proxy](https://github.com/royale-proxy/cr-proxy), you probably won't need this, but most likely you also won't have any reason to disable this. 48 | * `package` - if you somehow changed the package in game files before, change it here also (remember to [change the name](#cr-patcher) of .apk). The main reason for changing it is to make it possible to install both the original Clash Royale and your modified version at the same time. This tool doesn't change the package automatically. 49 | * `key` - you shouldn't have any reason to change this. The default key guarantees that after patching the game will be able to connect to `cr-proxy`. If for some reasons you change the key, make sure the proxy is also setup with the same key. 50 | * `url` - the address of server which the game will connect to. The default one, `game.clashroyaleapp.com`, is 23 characters long. Yours also needs to be 23 characters. If you have a domain, you can add a subdomain and redirect it to the proxy. The official server of Clash Royale is running on port 9339, so is the proxy. The game will always look for a server at this port, that's why there is no `port` field in this config. Also, don't try to add the port like `the.ip.here:1337`. 51 | * `keystore` - if the key used to sign the app changes, you won't be able to update it without uninstalling the previous version before. You can learn more about signing Android apps for example [here](https://developer.android.com/studio/publish/app-signing.html). Also note, the `keypass` and `dname` fields are only required to create a new keystore. See [here](http://docs.oracle.com/javase/7/docs/technotes/tools/solaris/keytool.html#DName) for how to fill out the `dname` fields (if you really want to, but that isn't important). 52 | * `paths` - paths of executables of different dependencies. 53 | * `versions` - if you need to change something here - experiment, ask around, or wait for someone else to do it for you, when a new version is out 54 | 55 | ## Installation 56 | 57 | 1. Download the [dependencies](#dependencies) and install if needed. 58 | 2. Copy the `config.json.example` file to `config.json` (so you can do it again when you break something) and fill it in. The changes that you *have* to make are: 59 | * in `paths`, set `apktool` to the path of your Apktool wrapper script. If you followed the instructions on their website, the path is `C:\\Windows\\apktool.bat` on Windows and `/usr/local/bin/apktool` on Linux and Mac. 60 | * in `paths`, set `zipalign` to the path of that program (look at [dependencies](#dependencies)) 61 | **Note:** On Windows, use either `/` to separate folders in path, or use double `\` -> `\\`, because of how json works. 62 | 3. You may want to change a [few more things](#config-explained) in the config 63 | 4. Read the [running](#running) section 64 | 65 | ## Dependencies 66 | - [Python 3.5](https://www.python.org/downloads/release/python-350/) to run this script 67 | - Apktool - [home page](http://ibotpeaches.github.io/Apktool/) - [download & install instructions](http://ibotpeaches.github.io/Apktool/install) 68 | - `keytool` and `jarsigner` from the [Java JDK](http://www.oracle.com/technetwork/java/javase/downloads/index.html), on Windows most likely can be found in `C:\Program Files\Java\\bin\` 69 | - `zipalign` from the [Android SDK](http://developer.android.com/sdk/index.html#Other) 70 | 71 | If you haven't already, install Android Studio. Open it and download SDK for any version of Android (the one that will be chosen by default, lastest stable, should be fine). Then, you can find `zipalign` in `/build-tools//zipalign(.exe on Windows)`. 72 | 73 | For example, on Linux, I found it in `~/Android/Sdk/build-tools/25.0.2/zipalign`, and on Windows in `C:\Program Files (x86)\Android\android-sdk\build-tools\22.0.1\zipalign.exe` (remember to double the `\` or use `/` instead) 74 | 75 | If you don't want to download the entire Android Studio, you can scroll the download page down to *Get just the command line tools*. Download the version for your OS, unzip it, run `sdkmanager(.exe)` (this program has a gui), download one version of SDK, just like you would do in Android Studio. Rest works like above ^ 76 | - `dd` - is built in Linux/MAC. For Windows, you can download it [here](http://www.chrysocome.net/downloads/ddrelease64.exe) 77 | - [`requests`](http://python-requests.org/) and [`requests-cache`](https://github.com/reclosedev/requests-cache) 78 | 79 | Note: `requests` and `requests-cache` can be installed with: 80 | 81 | python3.5 -m pip install requests requests-cache 82 | 83 | To install it this way, you need `pip`. Check if you have it with `pip --version`, if you don't, on Ubuntu you can get it with 84 | 85 | apt-get -y install python-pip 86 | -------------------------------------------------------------------------------- /patcher.py: -------------------------------------------------------------------------------- 1 | from collections import OrderedDict 2 | from xml.etree import ElementTree 3 | from pathlib import Path 4 | import argparse 5 | import os 6 | import sys 7 | import json 8 | import requests 9 | import requests_cache 10 | import subprocess 11 | import shutil 12 | import fileinput 13 | 14 | from hashlib import md5 15 | 16 | parser = argparse.ArgumentParser() 17 | parser.add_argument('version', help='client version') 18 | parser.add_argument('--json', help='use config.json for version info', action='store_true') 19 | args = parser.parse_args() 20 | 21 | BASE_DIR = os.path.dirname(os.path.abspath(__file__)) 22 | RELEASE_NAME = 'com.supercell.clashroyale-{}'.format(args.version) 23 | DECODED_DIR = os.path.join(BASE_DIR, RELEASE_NAME) 24 | MANIFEST_PATH = os.path.join(DECODED_DIR, 'AndroidManifest.xml') 25 | LIBG_ARM = os.path.join(DECODED_DIR, 'lib', 'armeabi-v7a', 'libg.so') 26 | LIBG_X86 = os.path.join(DECODED_DIR, 'lib', 'x86', 'libg.so') 27 | BUILD_DIR = os.path.join(DECODED_DIR, 'build') 28 | APK_FILENAME = '{}.apk'.format(RELEASE_NAME) 29 | APK_PATH = os.path.join(BASE_DIR, APK_FILENAME) 30 | BACKUP_PATH = os.path.join(BASE_DIR, '{}-orig.apk'.format(RELEASE_NAME)) 31 | UNSIGNED_PATH = os.path.join(BUILD_DIR, '{}-unsigned.apk'.format(RELEASE_NAME)) 32 | UNALIGNED_PATH = os.path.join(BUILD_DIR, '{}-unaligned.apk'.format(RELEASE_NAME)) 33 | PATCHED_PATH = os.path.join(BASE_DIR, APK_FILENAME) 34 | KEYSTORE_PATH = os.path.join(BASE_DIR, 'client.keystore') 35 | 36 | def ask(question): 37 | while True: 38 | response = input('{} (y/n): '.format(question)) 39 | if response.lower() in ['yes', 'y']: 40 | return True 41 | elif response.lower() in ['no', 'n']: 42 | return False 43 | 44 | def md5sum(filename): 45 | hash = md5() 46 | with open(filename, "rb") as f: 47 | for chunk in iter(lambda: f.read(128 * hash.block_size), b""): 48 | hash.update(chunk) 49 | return hash.hexdigest() 50 | 51 | print('Getting config ...') 52 | 53 | if not os.path.isfile(os.path.join(BASE_DIR, 'config.json')): 54 | print('ERROR: config.json does not exist.', file=sys.stderr) 55 | sys.exit(1) 56 | 57 | try: 58 | with open(os.path.join(BASE_DIR, 'config.json')) as fp: 59 | config = json.load(fp, object_pairs_hook=OrderedDict) 60 | except json.decoder.JSONDecodeError as e: 61 | print('ERROR: Failed to decode config.json: %s' % e) 62 | sys.exit(1) 63 | else: 64 | if 'debug' not in config: 65 | config['debug'] = False 66 | 67 | print('Checking environment ...') 68 | 69 | if not os.path.isfile(APK_PATH): 70 | print('ERROR: {} does not exist.'.format(APK_FILENAME), file=sys.stderr) 71 | sys.exit(1) 72 | 73 | if 'package' not in config or not config['package']: 74 | print('ERROR: New package ID missing from config.json.', file=sys.stderr) 75 | sys.exit(1) 76 | if 'key' not in config or not config['key']: 77 | print('ERROR: New key missing from config.json.', file=sys.stderr) 78 | sys.exit(1) 79 | else: 80 | try: 81 | bytes.fromhex(config['key']) 82 | except (TypeError, ValueError) as e: 83 | print('ERROR: Failed to decode key.', file=sys.stderr) 84 | sys.exit(1) 85 | else: 86 | if len(bytes.fromhex(config['key'])) != 32: 87 | print('ERROR: New key must be 32 bytes.', file=sys.stderr) 88 | sys.exit(1) 89 | if 'url' not in config or not config['url']: 90 | print('ERROR: New URL missing from config.json.', file=sys.stderr) 91 | sys.exit(1) 92 | elif len(config['url']) != 23: 93 | print('ERROR: New URL must be exactly 23 characters.', file=sys.stderr) 94 | sys.exit(1) 95 | 96 | if 'keystore' not in config or not config['keystore']: 97 | print('ERROR: Keystore info missing from config.json.', file=sys.stderr) 98 | sys.exit(1) 99 | if 'storepass' not in config['keystore'] or not config['keystore']['storepass']: 100 | print('ERROR: Keystore storepass missing from config.json.', file=sys.stderr) 101 | #sys.exit(1) 102 | if 'key' not in config['keystore'] or not config['keystore']['key']: 103 | print('ERROR: Signing key info missing from config.json.', file=sys.stderr) 104 | #sys.exit(1) 105 | if 'alias' not in config['keystore']['key'] or not config['keystore']['key']['alias']: 106 | print('ERROR: Signing key alias missing from config.json.', file=sys.stderr) 107 | #sys.exit(1) 108 | 109 | if not os.path.isfile(KEYSTORE_PATH): 110 | if ask('client.keystore does not exist. Would you like to create it?'): 111 | if 'keypass' not in config['keystore']['key'] or not config['keystore']['key']['keypass']: 112 | print('ERROR: Signing key keypass missing from config.json.', file=sys.stderr) 113 | sys.exit(1) 114 | if 'dname' not in config['keystore']['key'] or not config['keystore']['key']['dname']: 115 | print('ERROR: Signing key dname info missing from config.json.', file=sys.stderr) 116 | sys.exit(1) 117 | if 'cn' not in config['keystore']['key']['dname'] or not config['keystore']['key']['dname']['cn']: 118 | print('ERROR: Signing key dname cn missing from config.json.', file=sys.stderr) 119 | sys.exit(1) 120 | if 'ou' not in config['keystore']['key']['dname'] or not config['keystore']['key']['dname']['ou']: 121 | print('ERROR: Signing key dname ou missing from config.json.', file=sys.stderr) 122 | sys.exit(1) 123 | if 'o' not in config['keystore']['key']['dname'] or not config['keystore']['key']['dname']['o']: 124 | print('ERROR: Signing key dname o missing from config.json.', file=sys.stderr) 125 | sys.exit(1) 126 | if 'l' not in config['keystore']['key']['dname'] or not config['keystore']['key']['dname']['l']: 127 | print('ERROR: Signing key dname l missing from config.json.', file=sys.stderr) 128 | sys.exit(1) 129 | if 's' not in config['keystore']['key']['dname'] or not config['keystore']['key']['dname']['s']: 130 | print('ERROR: Signing key dname s missing from config.json.', file=sys.stderr) 131 | sys.exit(1) 132 | if 'c' not in config['keystore']['key']['dname'] or not config['keystore']['key']['dname']['c']: 133 | print('ERROR: Signing key dname c missing from config.json.', file=sys.stderr) 134 | sys.exit(1) 135 | 136 | dname = ', '.join('{}={}'.format(key, val.translate(str.maketrans({',': '\,'}))) for key, val in config['keystore']['key']['dname'].items()) 137 | result = subprocess.run([config['paths']['keytool'], '-genkey', '-keystore', KEYSTORE_PATH, '-storepass', config['keystore']['storepass'], '-alias', config['keystore']['key']['alias'], '-keypass', config['keystore']['key']['keypass'], '-dname', dname, '-keyalg', 'RSA', '-keysize', '2048', '-validity', '10000'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) 138 | try: 139 | result.check_returncode() 140 | except subprocess.CalledProcessError as e: 141 | print('ERROR: Failed to create keystore.', file=sys.stderr) 142 | sys.exit(1) 143 | else: 144 | print('ERROR: client.keystore is missing.', file=sys.stderr) 145 | sys.exit(1) 146 | 147 | result = subprocess.run([config['paths']['keytool'], '-list', '-keystore', KEYSTORE_PATH, '-storepass', config['keystore']['storepass'], '-alias', config['keystore']['key']['alias']], stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) 148 | try: 149 | result.check_returncode() 150 | except subprocess.CalledProcessError as e: 151 | print('ERROR: Failed to load key from keystore.', file=sys.stderr) 152 | sys.exit(1) 153 | else: 154 | if result.stdout.split('\n')[0].strip(', ').split(', ')[-1] != 'PrivateKeyEntry': 155 | print('ERROR: Key alias must refer to a private key.', file=sys.stderr) 156 | sys.exit(1) 157 | 158 | if 'paths' not in config: 159 | print('ERROR: Paths are missing from config.json.', file=sys.stderr) 160 | sys.exit(1) 161 | dependencies = ['apktool', 'dd', 'keytool', 'jarsigner', 'zipalign'] 162 | for dependency in dependencies: 163 | if dependency not in config['paths'] or not config['paths'][dependency]: 164 | print('ERROR: {} path is missing from config.json.'.format(dependency), file=sys.stderr) 165 | sys.exit(1) 166 | if not os.path.isfile(config['paths'][dependency]): 167 | print('ERROR: {} path does not exist.'.format(dependency), file=sys.stderr) 168 | sys.exit(1) 169 | if not os.access(config['paths'][dependency], os.X_OK): 170 | print('ERROR: {} path is not executable.'.format(dependency), file=sys.stderr) 171 | sys.exit(1) 172 | 173 | print('Getting version info ...') 174 | 175 | if args.json: 176 | if args.version in config['versions']: 177 | info = config['versions'][args.version] 178 | else: 179 | print('ERROR: Version info is missing from config.json.', file=sys.stderr) 180 | sys.exit(1) 181 | else: 182 | class VersionError(Exception): 183 | def __init__(self, value): 184 | self.value = value 185 | def __str__(self): 186 | return repr(self.value) 187 | 188 | def retrieve_version_info(): 189 | pages = OrderedDict([ 190 | ('keys', { 191 | 'url': 'https://github.com/royale-proxy/cr-proxy/wiki/Keys.md', 192 | 'fields' : ['version', 'key'] 193 | }), 194 | ('key-offsets', { 195 | 'url': 'https://github.com/royale-proxy/cr-proxy/wiki/Key-Offsets.md', 196 | 'fields' : ['version', 'arch', 'md5', 'offset'] 197 | }), 198 | ('url-offsets', { 199 | 'url': 'https://github.com/royale-proxy/cr-proxy/wiki/URL-Offsets.md', 200 | 'fields' : ['version', 'arch', 'md5', 'offset'] 201 | }) 202 | ]) 203 | 204 | requests_cache.install_cache('cache') 205 | 206 | def parse_table(url): 207 | return [[cell.strip().strip('`') for cell in line.strip('|').split('|')] for line in requests.get(url).text.split('\n') if args.version in line] 208 | 209 | data = {k: [OrderedDict(zip(v['fields'], line)) for line in parse_table(v['url'])] for k,v in pages.items()} 210 | for k,v in pages.items(): 211 | if not data[k]: 212 | raise VersionError('{} not in {}.'.format(args.version, k)) 213 | info = OrderedDict() 214 | info['key'] = data['keys'][0]['key'] 215 | for line in data['key-offsets']: 216 | info[line['arch']] = OrderedDict() 217 | info[line['arch']]['md5'] = line['md5'].strip() 218 | info[line['arch']]['key-offset'] = line['offset'].strip() 219 | 220 | for line in data['url-offsets']: 221 | 222 | if line['arch'] not in info: 223 | raise VersionError('{} not in key-offsets.'.format(line['arch'])) 224 | if line['md5'].strip() != info[line['arch'].strip()]['md5']: 225 | 226 | raise VersionError('MD5s for {} do not match.'.format(line['arch'])) 227 | info[line['arch']]['url-offset'] = line['offset'] 228 | return info 229 | 230 | try: 231 | info = retrieve_version_info() 232 | except VersionError as e: 233 | if config['debug']: 234 | print('Version missing from cache. Clearing and retrieving again ...') 235 | requests_cache.clear() 236 | try: 237 | info = retrieve_version_info() 238 | except VersionError as e: 239 | if config['debug']: 240 | print('ERROR: Version is missing from wiki ({}).'.format(str(e).rstrip('.')), file=sys.stderr) 241 | else: 242 | print('ERROR: Version is missing from wiki.', file=sys.stderr) 243 | sys.exit(1) 244 | if 'versions' not in config: 245 | config['versions'] = {} 246 | config['versions'][args.version] = info 247 | with open('config.json', 'w') as fp: 248 | json.dump(config, fp, indent=2) 249 | print(json.dumps(info, indent=2)) 250 | 251 | print('Decoding APK ...') 252 | 253 | result = subprocess.run([config['paths']['apktool'], 'd', '-o', DECODED_DIR, '-f', APK_PATH], stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) 254 | try: 255 | result.check_returncode() 256 | except subprocess.CalledProcessError as e: 257 | if config['debug']: 258 | print('ERROR: Failed to decode {} ({}).'.format(APK_FILENAME, result.stderr.strip().rstrip('.').replace('{}/'.format(BASE_DIR), '')), file=sys.stderr) 259 | else: 260 | print('ERROR: Failed to decode {}.'.format(APK_FILENAME), file=sys.stderr) 261 | sys.exit(1) 262 | 263 | print('Checking AndroidManifest.xml ...') 264 | 265 | if not os.path.isfile(MANIFEST_PATH): 266 | print('ERROR: AndroidManifest.xml is missing.', file=sys.stderr) 267 | sys.exit(1) 268 | 269 | print('Checking package ID ...') 270 | 271 | ElementTree.register_namespace('android', 'http://schemas.android.com/apk/res/android') 272 | try: 273 | tree = ElementTree.parse('{}'.format(MANIFEST_PATH)) 274 | except ElementTree.ParseError as e: 275 | print('ERROR: Failed to parse AndroidManifest.xml.', file=sys.stderr) 276 | sys.exit(1) 277 | 278 | root = tree.getroot() 279 | if root.get('package') != 'com.supercell.clashroyale': 280 | print('ERROR: Package {} does not match {}.'.format(root.get('package'), 'com.supercell.clashroyale'), file=sys.stderr) 281 | sys.exit(1) 282 | 283 | print('Patching package ID ...') 284 | 285 | root.set('package', config['package']) 286 | tree.write(MANIFEST_PATH, encoding='utf-8', xml_declaration=True) 287 | 288 | print('Verifying package ID ...') 289 | 290 | try: 291 | tree = ElementTree.parse('{}'.format(MANIFEST_PATH)) 292 | except ElementTree.ParseError as e: 293 | print('ERROR: Failed to parse AndroidManifest.xml.', file=sys.stderr) 294 | sys.exit(1) 295 | 296 | root = tree.getroot() 297 | if root.get('package') != config['package']: 298 | print('ERROR: Package is incorrect.', file=sys.stderr) 299 | sys.exit(1) 300 | 301 | print('Checking libg.so ...') 302 | 303 | if not os.path.isfile(LIBG_X86): 304 | print('ERROR: arm libg.so is missing.', file=sys.stderr) 305 | sys.exit(1) 306 | 307 | if not os.path.isfile(LIBG_X86): 308 | print('ERROR: x86 libg.so is missing.', file=sys.stderr) 309 | sys.exit(1) 310 | 311 | md5arm = md5sum(LIBG_ARM) 312 | if md5arm != info['arm']['md5']: 313 | print('ERROR: arm libg.so MD5 is incorrect {}'.format(md5arm), file=sys.stderr) 314 | sys.exit(1) 315 | 316 | md5x86 = md5sum(LIBG_X86) 317 | if md5x86 != info['x86']['md5']: 318 | print('ERROR: x86 libg.so MD5 is incorrect {}'.format(md5x86), file=sys.stderr) 319 | sys.exit(1) 320 | 321 | print('Checking keys ...') 322 | 323 | result = subprocess.run([config['paths']['dd'], 'if={}'.format(LIBG_ARM), 'skip={}'.format(info['arm']['key-offset']), 'bs=1', 'count=32'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) 324 | try: 325 | result.check_returncode() 326 | except subprocess.CalledProcessError as e: 327 | print('ERROR: Failed to get current arm libg.so key.', file=sys.stderr) 328 | sys.exit(1) 329 | else: 330 | if result.stdout.hex() != info['key']: 331 | print('ERROR: Current arm libg.so key is incorrect.', file=sys.stderr) 332 | sys.exit(1) 333 | 334 | result = subprocess.run([config['paths']['dd'], 'if={}'.format(LIBG_X86), 'skip={}'.format(info['x86']['key-offset']), 'bs=1', 'count=32'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) 335 | try: 336 | result.check_returncode() 337 | except subprocess.CalledProcessError as e: 338 | print('ERROR: Failed to get current x86 libg.so key.', file=sys.stderr) 339 | sys.exit(1) 340 | else: 341 | if result.stdout.hex() != info['key']: 342 | print('ERROR: Current x86 libg.so key is incorrect.', file=sys.stderr) 343 | sys.exit(1) 344 | 345 | print('Checking URLs ...') 346 | 347 | result = subprocess.run([config['paths']['dd'], 'if={}'.format(LIBG_ARM), 'skip={}'.format(info['arm']['url-offset']), 'bs=1', 'count=23'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) 348 | try: 349 | result.check_returncode() 350 | except subprocess.CalledProcessError as e: 351 | print('ERROR: Failed to get current arm libg.so URL.', file=sys.stderr) 352 | sys.exit(1) 353 | else: 354 | try: 355 | result.stdout.decode() 356 | except UnicodeDecodeError as e: 357 | print('ERROR: Failed to get current arm libg.so URL.', file=sys.stderr) 358 | sys.exit(1) 359 | else: 360 | if result.stdout.decode() != 'game.clashroyaleapp.com': 361 | print('ERROR: Current arm libg.so URL is incorrect {}'.format(result.stdout.decode()), file=sys.stderr) 362 | sys.exit(1) 363 | 364 | result = subprocess.run([config['paths']['dd'], 'if={}'.format(LIBG_X86), 'skip={}'.format(info['x86']['url-offset']), 'bs=1', 'count=23'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) 365 | try: 366 | result.check_returncode() 367 | except subprocess.CalledProcessError as e: 368 | print('ERROR: Failed to get current x86 libg.so URL: {}'.format(e), file=sys.stderr) 369 | sys.exit(1) 370 | else: 371 | try: 372 | result.stdout.decode() 373 | except UnicodeDecodeError as e: 374 | print('ERROR: Failed to get current x86 libg.so URL: {}'.format(e), file=sys.stderr) 375 | sys.exit(1) 376 | else: 377 | if result.stdout.decode() != 'game.clashroyaleapp.com': 378 | print('ERROR: Current x86 libg.so URL is incorrect.', file=sys.stderr) 379 | sys.exit(1) 380 | 381 | print('Patching keys ...') 382 | 383 | result = subprocess.run([config['paths']['dd'], 'of={}'.format(LIBG_ARM), 'seek={}'.format(info['arm']['key-offset']), 'bs=1', 'count=32', 'conv=notrunc'], input=bytes.fromhex(config['key']), stdout=subprocess.PIPE, stderr=subprocess.PIPE) 384 | try: 385 | result.check_returncode() 386 | except subprocess.CalledProcessError as e: 387 | print('ERROR: Failed to patch arm libg.so key.', file=sys.stderr) 388 | sys.exit(1) 389 | 390 | result = subprocess.run([config['paths']['dd'], 'of={}'.format(LIBG_X86), 'seek={}'.format(info['x86']['key-offset']), 'bs=1', 'count=32', 'conv=notrunc'], input=bytes.fromhex(config['key']), stdout=subprocess.PIPE, stderr=subprocess.PIPE) 391 | try: 392 | result.check_returncode() 393 | except subprocess.CalledProcessError as e: 394 | print('ERROR: Failed to patch x86 libg.so key.', file=sys.stderr) 395 | sys.exit(1) 396 | 397 | print('Patching URLs ...') 398 | 399 | result = subprocess.run([config['paths']['dd'], 'of={}'.format(LIBG_ARM), 'seek={}'.format(info['arm']['url-offset']), 'bs=1', 'count=23', 'conv=notrunc'], input=config['url'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) 400 | try: 401 | result.check_returncode() 402 | except subprocess.CalledProcessError as e: 403 | print('ERROR: Failed to patch arm libg.so URL.', file=sys.stderr) 404 | sys.exit(1) 405 | 406 | result = subprocess.run([config['paths']['dd'], 'of={}'.format(LIBG_X86), 'seek={}'.format(info['x86']['url-offset']), 'bs=1', 'count=23', 'conv=notrunc'], input=config['url'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) 407 | try: 408 | result.check_returncode() 409 | except subprocess.CalledProcessError as e: 410 | print('ERROR: Failed to patch x86 libg.so URL.', file=sys.stderr) 411 | sys.exit(1) 412 | 413 | print('Verifying keys ...') 414 | 415 | result = subprocess.run([config['paths']['dd'], 'if={}'.format(LIBG_ARM), 'skip={}'.format(info['arm']['key-offset']), 'bs=1', 'count=32'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) 416 | try: 417 | result.check_returncode() 418 | except subprocess.CalledProcessError as e: 419 | print('ERROR: Failed to get new arm libg.so key.', file=sys.stderr) 420 | sys.exit(1) 421 | else: 422 | if result.stdout.hex() != config['key']: 423 | print('ERROR: New arm libg.so key is incorrect.', file=sys.stderr) 424 | sys.exit(1) 425 | 426 | result = subprocess.run([config['paths']['dd'], 'if={}'.format(LIBG_X86), 'skip={}'.format(info['x86']['key-offset']), 'bs=1', 'count=32'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) 427 | try: 428 | result.check_returncode() 429 | except subprocess.CalledProcessError as e: 430 | print('ERROR: Failed to get new x86 libg.so key.', file=sys.stderr) 431 | sys.exit(1) 432 | else: 433 | if result.stdout.hex() != config['key']: 434 | print('ERROR: New x86 libg.so key is incorrect.', file=sys.stderr) 435 | sys.exit(1) 436 | 437 | print('Verifying URLs ...') 438 | 439 | result = subprocess.run([config['paths']['dd'], 'if={}'.format(LIBG_ARM), 'skip={}'.format(info['arm']['url-offset']), 'bs=1', 'count=23'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) 440 | try: 441 | result.check_returncode() 442 | except subprocess.CalledProcessError as e: 443 | print('ERROR: Failed to get new arm libg.so URL.', file=sys.stderr) 444 | sys.exit(1) 445 | else: 446 | try: 447 | result.stdout.decode() 448 | except UnicodeDecodeError as e: 449 | print('ERROR: Failed to get new arm libg.so URL.', file=sys.stderr) 450 | sys.exit(1) 451 | else: 452 | if result.stdout.decode().rstrip() != config['url']: 453 | print('ERROR: New arm libg.so URL is incorrect {}'.format(result.stdout.decode().rstrip()), file=sys.stderr) 454 | sys.exit(1) 455 | 456 | result = subprocess.run([config['paths']['dd'], 'if={}'.format(LIBG_X86), 'skip={}'.format(info['x86']['url-offset']), 'bs=1', 'count=23'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) 457 | try: 458 | result.check_returncode() 459 | except subprocess.CalledProcessError as e: 460 | print('ERROR: Failed to get new x86 libg.so URL.', file=sys.stderr) 461 | sys.exit(1) 462 | else: 463 | try: 464 | result.stdout.decode() 465 | except UnicodeDecodeError as e: 466 | print('ERROR: Failed to get new x86 libg.so URL.', file=sys.stderr) 467 | sys.exit(1) 468 | else: 469 | if result.stdout.decode().rstrip() != config['url']: 470 | print('ERROR: New x86 libg.so URL is incorrect {}'.format(result.stdout.decode().rstrip()), file=sys.stderr) 471 | sys.exit(1) 472 | 473 | print('Backing up original APK ...') 474 | 475 | os.makedirs(BUILD_DIR, exist_ok=True) 476 | shutil.move(APK_PATH, BACKUP_PATH) 477 | 478 | androidManifest = "{}/{}".format(DECODED_DIR, "AndroidManifest.xml") 479 | print('Rewriting android manifest {}'.format(androidManifest)) 480 | if Path(androidManifest).exists(): 481 | with fileinput.FileInput(androidManifest, inplace=True, backup='.bak') as file: 482 | for line in file: 483 | print(line.replace("android:resizeableActivity=\"false\"", ""), end='') 484 | 485 | print('Building APK ...') 486 | 487 | result = subprocess.run([config['paths']['apktool'], 'b', '-o', UNSIGNED_PATH, DECODED_DIR], stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) 488 | try: 489 | result.check_returncode() 490 | except subprocess.CalledProcessError as e: 491 | if config['debug']: 492 | print('ERROR: Failed to build {} ({}).'.format(RELEASE_NAME, result.stderr.strip().rstrip('.').replace('{}/'.format(BASE_DIR), '')), file=sys.stderr) 493 | else: 494 | print('ERROR: Failed to build {}.'.format(RELEASE_NAME), file=sys.stderr) 495 | sys.exit(1) 496 | 497 | print('Signing APK ...') 498 | 499 | signingargs = [config['paths']['jarsigner'], '-sigalg', 'SHA1withRSA', '-digestalg', 'SHA1', '-keystore', KEYSTORE_PATH, '-storepass', config['keystore']['storepass'], '-keypass', config['keystore']['key']['keypass'], UNSIGNED_PATH, config['keystore']['key']['alias']]; 500 | print (" ".join(signingargs)) 501 | result = subprocess.run(signingargs, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) 502 | try: 503 | result.check_returncode() 504 | except subprocess.CalledProcessError as e: 505 | if config['debug']: 506 | print('ERROR: Failed to build {} ({}).'.format(RELEASE_NAME, result.stderr.strip().rstrip('.').replace('{}/'.format(BASE_DIR), '')), file=sys.stderr) 507 | else: 508 | print('ERROR: Failed to build {}.'.format(RELEASE_NAME), file=sys.stderr) 509 | sys.exit(1) 510 | 511 | shutil.move(UNSIGNED_PATH, UNALIGNED_PATH) 512 | 513 | print('Aligning APK ...') 514 | 515 | result = subprocess.run([config['paths']['zipalign'], '4', UNALIGNED_PATH, PATCHED_PATH], stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) 516 | try: 517 | result.check_returncode() 518 | except subprocess.CalledProcessError as e: 519 | if config['debug']: 520 | print('ERROR: Failed to build {} ({}).'.format(RELEASE_NAME, result.stderr.strip().rstrip('.').replace('{}/'.format(BASE_DIR), '')), file=sys.stderr) 521 | else: 522 | print('ERROR: Failed to build {}.'.format(RELEASE_NAME), file=sys.stderr) 523 | sys.exit(1) 524 | 525 | shutil.rmtree(DECODED_DIR) 526 | 527 | print('Done.') 528 | --------------------------------------------------------------------------------