├── .gitignore ├── LICENSE ├── README.md ├── main.py ├── requirements.txt └── tools ├── __init__.py ├── app_data.py ├── bundle_management.py ├── ios_app.py ├── ssh_connection.py └── utils.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.DS_Store 2 | *.pyc 3 | .DS_Store 4 | .idea 5 | cache/* 6 | config/* 7 | error_logs/* 8 | IPAs/* 9 | Clutch_troll* 10 | *.png 11 | *.deb 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 cdelaof26 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ssh_decrypt_automation tool 2 | 3 | ### What is this? 4 | 5 | This tool automates the process of dumping multiple iOS apps at once in jailbroken devices 6 | 7 | ### Disclaimer 8 | 9 | - Check out [LICENSE](LICENSE) before using this software. 10 | - **Do not use `Clutch` or `bfdecrypt` for piracy!** 11 | - Please, do not spam NyaMisty with logs of this software 12 | - **iOS 15 implemented many jailbreak mitigation techniques, dumping apps became 13 | harder than it used to be, so do not ask for fixes** 14 | - I'm not responsible for any damage or data lost that could happen for using this software 15 | 16 | **You've been warned** 17 | 18 | 19 | ### Compatibility 20 | 21 | Tested with: 22 | 23 | | Device | iOS version | Jailbreak | 24 | |-------------------|-------------|-----------------| 25 | | iPhone XR (A12) | 15.1 | XinaA15 1.1.6.2 | 26 | | iPad Mini 5 (A12) | 15.1 | XinaA15 1.1.6.2 | 27 | 28 | Unfortunately I don't own a checkm8 compatible device on iOS 15 29 | 30 | This tool should work with root and rootless jailbreaks (bfdecrypt) 31 | 32 | | Operating system | Python version | 33 | |-----------------------------|-----------------| 34 | | macOS Monterey 12.5 (ARM64) | 3.9.6 | 35 | | Debian 11 (ARM64) | 3.10.4 | 36 | | Windows 11 (ARM64) | 3.11.2 (x86-64) | 37 | 38 | **Note**: At the moment, paramiko is not installable in 39 | Python3 for ARM in Windows 40 | 41 | ### Dependencies 42 | 43 | 1. [Python](https://www.python.org/downloads/) >= 3.9 44 | - If you're on Windows, you'll need to add python to PATH under installer options 45 | 2. [paramiko](https://pypi.org/project/paramiko/) 46 | 3. [Clutch](https://github.com/NyaMisty/Clutch/) 47 | 4. [bfdecrypt](https://github.com/fenfenS/bfdecrypt) 48 | - bfdecrypt requires [libSparkAppList](https://havoc.app/package/libsparkapplist), repo: https://havoc.app/ 49 | 5. SSH client for your PC and SSH server for your iOS device 50 | - If you're using XinaA15, make sure to activate 51 | `open SSH server` under `Option` tab 52 | - Most Linux distros have an SSH client 53 | - Windows (10, 11) and macOS includes by default an SSH client 54 | 55 | **Notes:** 56 | - You'll need `Clutch` or `bfdecrypt` (both if you want). 57 | - To get `bfdecrypt` to work with iOS 15 (Tested with XinaA15), 58 | you'll need `libSparkAppList` and [fenfenS' bfdecrypt](https://github.com/fenfenS/bfdecrypt/releases/tag/test) 59 | - If you already have bfdecrypt working (with AppList), it should work just fine 60 | 61 | ### Usage 62 | 63 | - Open your preferred terminal 64 | 65 | - Clone this repo 66 | 67 |
 68 | $ git clone https://github.com/cdelaof26/ssh_decrypt_automation_tool.git
 69 | 
 70 | # If you don't have git, click "Code" -> "Download ZIP"
 71 | 
72 | 73 | - Provide a copy of Clutch or bfdecrypt (or both) 74 | 75 |
 76 | #   Clutch:
 77 | #  * You can skip this if you don't want to use Clutch
 78 | #
 79 | # Download the latest version from: 
 80 | #     https://github.com/NyaMisty/Clutch/releases
 81 | # Copy "Clutch_troll" to /path/to/ssh_decrypt_automation_tool
 82 | 
 83 | #   bfdecrypt:
 84 | #  * You can skip this if you already have bfdecrypt working
 85 | #    in your device or you don't want to use bfdecrypt
 86 | #
 87 | # Download the latest version from: 
 88 | #     https://github.com/fenfenS/bfdecrypt/releases
 89 | # Copy "com.level3tjg.bfdecrypt.deb" to /path/to/ssh_decrypt_automation_tool
 90 | # Rename "com.level3tjg.bfdecrypt.deb" as "bfdecrypt.deb"
 91 | 
92 | 93 | - Move into project directory 94 | 95 |
 96 | $ cd ssh_decrypt_automation_tool
 97 | 
98 | 99 | - Install dependencies 100 |
101 | # You might need elevated privileges!
102 | 
103 | $ pip install -r requirements.txt
104 | # or
105 | $ pip3 install -r requirements.txt
106 | 
107 | 108 | Run using python 109 | 110 |
111 | # If you're on Linux or macOS
112 | $ python3 main.py
113 | 
114 | # If you're on Windows
115 | $ python main.py
116 | 
117 | 118 | 119 | ### FAQ 120 | 121 | - **How do I find my iOS device IP?** 122 | 1. Open settings 123 | 2. Go to Wi-FI section 124 | 3. Click on the `i` icon of your Wi-Fi network 125 | 4. Under `IPV4 ADDRESS` section, copy `IP ADDRESS` field 126 | 5. Done 127 | 128 | **Note: Your PC must be connected to the same network** 129 | 130 | 131 | - **I keep getting "Please, consider changing ssh default password!" message, 132 | how do I change root password?** 133 | 1. Open your preferred terminal 134 | 2. Run: `ssh root@` 135 | - e.g: `ssh root@10.0.1.5` 136 | 3. Enter `alpine` as password 137 | 4. Run: `passwd` 138 | 5. Enter your password 139 | - You won't see anything, it's normal 140 | - Write your new password and then press enter 141 | 6. Confirm your password 142 | 7. Done 143 | 144 | 145 | - **I can't find the option to dump apps, where is it?** 146 | 147 | 1. Connect your Apple device 148 | 2. Select `3. Select decrypt utility (needed to decrypt apps)` 149 | in the main menu 150 | 3. Select your preferred option 151 | - `Fallback` means that if first option fails, 152 | the second one will be used immediately to retry app 153 | decryption 154 | 4. Done 155 | 156 | - **I don't want to enter the IP address, username or password 157 | each time I use this software, is there any solution?** 158 | 159 | Yes: 160 | 1. Run the project 161 | 2. Connect your idevice 162 | 3. Select `S. Setting` on the main menu 163 | 4. Enable / disable features as you wish 164 | - **username and password are saved as plain text!** 165 | 5. Done 166 | 167 | 168 | - **Shall I use `Clutch` or `bfdecrypt`?** 169 | 170 | Depends on which works better for you, for me: `bfdecrypt` 171 | 172 | | App name | Clutch | bfdecrypt | 173 | |-----------|---------|-----------| 174 | | Terraria | Success | Success | 175 | | Apollo | Failed | Failed | 176 | | RedditApp | Failed | Success | 177 | | WhatsApp | Failed | Success | 178 | | Telegram | Failed | Success | 179 | | Discord | Failed | Success | 180 | | ... | ... | ... | 181 | 182 | 183 | - **My app keeps failing when dumping, what can I do?** 184 | 185 | Unfortunately, there isn't a workaround for those applications, 186 | so maybe ask anyone else if they can dump that app for you 187 | 188 | * Alternatively, you can use `bfdecrypt` if `Clutch` fails 189 | (this might not work as well) 190 | 191 | ### Changelog 192 | 193 | ### v0.0.4_1 194 | - Fixed bug where `decrypt method` won't be saved if the option 195 | is selected from the main menu 196 | 197 | ### v0.0.4 198 | - Added support for bfdecrypt 199 | - Fixed _Windows experience_ 200 | 201 | ### v0.0.3 202 | - Minor bug fixes 203 | - Fixed bug where the script couldn't connect (Time out!) 204 | and it keeps trying until "too many attempts" error is raised 205 | - Fixed bug where the script would crash when attempting 206 | to delete temporary data but there isn't a cache directory 207 | 208 | ### v0.0.2 209 | - Improved app detection 210 | 211 | ### v0.0.1 212 | - Initial project 213 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import tools.ssh_connection as ssh_connection 2 | import tools.app_data as settings 3 | import tools.utils as utils 4 | 5 | 6 | client, username, password, ip = None, None, None, None 7 | listed_applications = None 8 | 9 | 10 | def connect(setup_new_device): 11 | global client, username, password, ip, listed_applications 12 | client, username, password, ip = ssh_connection.setup_connection(setup_new_device) 13 | if client is not None: 14 | listed_applications = ssh_connection.list_apps(client) 15 | 16 | 17 | if settings.AUTHENTICATE_ON_STARTUP.exists(): 18 | connect(False) 19 | utils.clear_terminal() 20 | 21 | interrupted = False 22 | 23 | settings.read_decrypt_method_config() 24 | 25 | while True: 26 | try: 27 | print(" Welcome to ssh decrypt automation tool") 28 | print(" By WholesomeThoughts26\n") 29 | print(" Main menu") 30 | options = ["1", "E"] 31 | 32 | try: 33 | # Check if client is alive 34 | if client is not None: 35 | client.exec_command("ls") 36 | except AttributeError: 37 | client, username, password, ip = None, None, None, None 38 | listed_applications = None 39 | 40 | if client is None: 41 | print("1. Connect to iOS device") 42 | else: 43 | options.append("2") 44 | options.append("3") 45 | 46 | print(f"1. Disconnect from '{ip}'") 47 | if listed_applications is None: 48 | print("2. List apps") 49 | else: 50 | print("2. Re-list apps") 51 | 52 | if not settings.decrypt_method: 53 | print("3. Select decrypt utility (needed to decrypt apps)") 54 | else: 55 | print("3. Dump app") 56 | print("4. Dump multiple apps") 57 | options.append("4") 58 | 59 | print("S. Settings") 60 | options.append("S") 61 | 62 | print("E. Exit") 63 | print("Select an option") 64 | 65 | option = utils.choose(options) 66 | 67 | if option == "1": 68 | if client is None: 69 | connect(True) 70 | else: 71 | ssh_connection.disconnect(client) 72 | client, username, password, ip = None, None, None, None 73 | 74 | if option == "2": 75 | utils.clear_terminal() 76 | listed_applications = ssh_connection.list_apps(client) 77 | 78 | if option == "3": 79 | if not settings.decrypt_method: 80 | settings.select_decrypt_utility() 81 | elif ssh_connection.is_idevice_ready(client): 82 | app = utils.select_apps(listed_applications, False) 83 | if app is not None: 84 | ssh_connection.dump_app(client, app, False) 85 | 86 | if option == "4": 87 | if ssh_connection.is_idevice_ready(client): 88 | ssh_connection.dump_multiple_apps(client, utils.select_apps(listed_applications, True)) 89 | 90 | if option == "S": 91 | settings.show_settings_menu(username, password, ip) 92 | 93 | if option == "E": 94 | break 95 | 96 | input("\nPress enter to continue... ") 97 | utils.clear_terminal() 98 | except KeyboardInterrupt: 99 | interrupted = client is not None 100 | break 101 | 102 | if client is not None: 103 | if interrupted: 104 | print(" Please don't interrupt disconnection process!") 105 | ssh_connection.disconnect(client) 106 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | paramiko 2 | -------------------------------------------------------------------------------- /tools/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cdelaof26/ssh_decrypt_automation_tool/2c588760b61c8c91133b81b83fa804ec167ccf62/tools/__init__.py -------------------------------------------------------------------------------- /tools/app_data.py: -------------------------------------------------------------------------------- 1 | import tools.utils as utils 2 | from pathlib import Path 3 | from enum import Enum 4 | import shutil 5 | 6 | 7 | class DumpUtility(Enum): 8 | CLUTCH = 0 9 | BFDECRYPT = 1 10 | CLUTCH_BFDECRYPT = 2 11 | BFDECRYPT_CLUTCH = 3 12 | 13 | 14 | CWD_PATH = Path().cwd() 15 | 16 | CONFIG_DIR = CWD_PATH.joinpath("config") 17 | 18 | 19 | # If this file exist, this program will try to authenticate as soon as it starts 20 | AUTHENTICATE_ON_STARTUP = CONFIG_DIR.joinpath("auth") 21 | 22 | # This directory contains logs for failed dumps 23 | FAILED_LOGS = CWD_PATH.joinpath("error_logs") 24 | 25 | 26 | # This file just holds the last IP entered, 27 | # If file is found, contents are read to skip having to 28 | # enter the IP again 29 | IP_FILE = CONFIG_DIR.joinpath("ip.txt") 30 | 31 | # This file holds username and password if user decides to save them as plain text 32 | # (not recommend) 33 | UP_FILE = CONFIG_DIR.joinpath("up.txt") 34 | 35 | DECRYPT_METHOD_CONFIG = CONFIG_DIR.joinpath("d_method") 36 | decrypt_method = "" 37 | 38 | CLUTCH_EXECUTABLE = CWD_PATH.joinpath("Clutch_troll") 39 | BFDECRYPT_DEB = CWD_PATH.joinpath("bfdecrypt.deb") 40 | DOWNLOADED_APPS = CWD_PATH.joinpath("IPAs") 41 | 42 | 43 | def read_ip_file() -> str: 44 | global IP_FILE 45 | if not IP_FILE.exists(): 46 | return "" 47 | 48 | return utils.read_file(IP_FILE) 49 | 50 | 51 | def save_ip_file(ip: str): 52 | global IP_FILE 53 | utils.write_file(IP_FILE, ip) 54 | 55 | 56 | def read_up_file() -> list: 57 | global UP_FILE 58 | if not UP_FILE.exists(): 59 | return [] 60 | 61 | return utils.read_file(UP_FILE).split("\n") 62 | 63 | 64 | def save_up_file(usr: str, pwd: str): 65 | global UP_FILE 66 | utils.write_file(UP_FILE, usr + "\n" + pwd) 67 | 68 | 69 | def does_clutch_exist() -> bool: 70 | global CLUTCH_EXECUTABLE, DECRYPT_METHOD_CONFIG 71 | if not CLUTCH_EXECUTABLE.exists(): 72 | print(f"\"{CLUTCH_EXECUTABLE.resolve()}\" doesn't exist") 73 | return False 74 | 75 | if not DECRYPT_METHOD_CONFIG.exists(): 76 | select_decrypt_utility("1") 77 | 78 | return True 79 | 80 | 81 | def does_bfdecrypt_exist() -> bool: 82 | global BFDECRYPT_DEB 83 | if not BFDECRYPT_DEB.exists(): 84 | print(f"\"{BFDECRYPT_DEB.resolve()}\" doesn't exist") 85 | return False 86 | 87 | return True 88 | 89 | 90 | def get_file_copy(copy_as_clutch: bool): 91 | global CLUTCH_EXECUTABLE, BFDECRYPT_DEB 92 | 93 | utils.clear_terminal() 94 | print("Drag and drop the file") 95 | 96 | if not utils.IS_SYSTEM_NT: 97 | file = Path(input("> ").replace("\\", "").strip()) 98 | else: 99 | file = Path(input("> ").strip()) 100 | 101 | if file.is_dir(): 102 | return False 103 | 104 | if file.exists(): 105 | if copy_as_clutch: 106 | shutil.copy(str(file), str(CLUTCH_EXECUTABLE)) 107 | else: 108 | shutil.copy(str(file), str(BFDECRYPT_DEB)) 109 | 110 | if copy_as_clutch: 111 | return CLUTCH_EXECUTABLE.exists() 112 | return BFDECRYPT_DEB.exists() 113 | 114 | 115 | def write_log(log: str): 116 | global FAILED_LOGS 117 | if not FAILED_LOGS.exists(): 118 | FAILED_LOGS.mkdir() 119 | 120 | log_file = FAILED_LOGS.joinpath("dump_log.txt") 121 | i = 0 122 | while log_file.exists(): 123 | log_file = FAILED_LOGS.joinpath(f"dump_log ({i}).txt") 124 | i += 1 125 | 126 | utils.write_file(log_file, log) 127 | 128 | 129 | def read_decrypt_method_config(): 130 | global DECRYPT_METHOD_CONFIG, decrypt_method 131 | if DECRYPT_METHOD_CONFIG.exists(): 132 | decrypt_method = utils.read_file(DECRYPT_METHOD_CONFIG) 133 | try: 134 | DumpUtility[decrypt_method] 135 | except KeyError: 136 | print(f" Invalid decrypt mode! ({decrypt_method})") 137 | print("Please, reconfigure decrypt method in settings") 138 | DECRYPT_METHOD_CONFIG.unlink() 139 | 140 | 141 | def select_decrypt_utility(option=None): 142 | global CONFIG_DIR, DECRYPT_METHOD_CONFIG, decrypt_method 143 | if not CONFIG_DIR.exists(): 144 | CONFIG_DIR.mkdir() 145 | 146 | utils.clear_terminal() 147 | 148 | if option is None: 149 | print(" Select the utility to use") 150 | print("1. Clutch") 151 | print("2. bfdecrypt") 152 | print("3. Clutch -> bfdecrypt as fallback") 153 | print("4. bfdecrypt -> Clutch as fallback") 154 | print("5. Cancel") 155 | print("Select an option") 156 | option = utils.choose(["1", "2", "3", "4", "5"]) 157 | 158 | if option == "5": 159 | return 160 | 161 | if option == "1": 162 | decrypt_method = DumpUtility.CLUTCH.name 163 | elif option == "2": 164 | decrypt_method = DumpUtility.BFDECRYPT.name 165 | elif option == "3": 166 | decrypt_method = DumpUtility.CLUTCH_BFDECRYPT.name 167 | else: 168 | decrypt_method = DumpUtility.BFDECRYPT_CLUTCH.name 169 | 170 | utils.write_file(DECRYPT_METHOD_CONFIG, decrypt_method) 171 | 172 | 173 | def show_settings_menu(usr: str, pwd: str, ip: str): 174 | global CONFIG_DIR, AUTHENTICATE_ON_STARTUP, IP_FILE, UP_FILE, decrypt_method 175 | if not CONFIG_DIR.exists(): 176 | CONFIG_DIR.mkdir() 177 | 178 | while True: 179 | utils.clear_terminal() 180 | print(" Settings") 181 | if not IP_FILE.exists(): 182 | print("1. Save IP for future uses") 183 | else: 184 | print("1. Delete saved IP") 185 | 186 | if not UP_FILE.exists(): 187 | print("2. Save user and password for future uses") 188 | else: 189 | print("2. Delete saved user and password") 190 | 191 | if not AUTHENTICATE_ON_STARTUP.exists(): 192 | print("3. Enable auto authentication when app starts") 193 | else: 194 | print("3. Disable auto authentication when app starts") 195 | 196 | if decrypt_method: 197 | print(f"4. Select decrypt utility [Selected: {decrypt_method}]") 198 | else: 199 | print(f"4. Select decrypt utility") 200 | 201 | print("E. Go back") 202 | print("Select an option") 203 | option = utils.choose(["1", "2", "3", "4", "E"]) 204 | 205 | if option == "1": 206 | if not IP_FILE.exists(): 207 | save_ip_file(ip) 208 | else: 209 | IP_FILE.unlink() 210 | elif option == "2": 211 | if not UP_FILE.exists(): 212 | save_up_file(usr, pwd) 213 | else: 214 | UP_FILE.unlink() 215 | elif option == "3": 216 | if not AUTHENTICATE_ON_STARTUP.exists(): 217 | utils.write_file(AUTHENTICATE_ON_STARTUP, "") 218 | else: 219 | AUTHENTICATE_ON_STARTUP.unlink() 220 | elif option == "4": 221 | select_decrypt_utility() 222 | else: 223 | break 224 | -------------------------------------------------------------------------------- /tools/bundle_management.py: -------------------------------------------------------------------------------- 1 | from tools.ios_app import APPLICATION_BUNDLES, APPLICATION_DOCUMENTS, AppInfo 2 | 3 | # Utilities for processing applications info 4 | 5 | 6 | def find_plists(applications_ids: str, find_in_documents: bool) -> list: 7 | bundle_ids = applications_ids.split("\n") 8 | if not bundle_ids: 9 | return [] 10 | 11 | plists = list() 12 | for bundle_id in bundle_ids: 13 | if not bundle_id: 14 | continue 15 | 16 | if find_in_documents: 17 | plist_path = f"{APPLICATION_DOCUMENTS + bundle_id}/.com.apple.mobile_container_manager.metadata.plist" 18 | plists.append(plist_path) 19 | else: 20 | plist_path = f"{APPLICATION_BUNDLES + bundle_id}/iTunesMetadata.plist" 21 | plists.append(AppInfo(bundle_id, plist_path)) 22 | 23 | # find_in_documents if true: 24 | # will return a list of strings, if false 25 | # it'll return a list of AppInfo objects 26 | 27 | return plists 28 | -------------------------------------------------------------------------------- /tools/ios_app.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | # Utilities for apps management 4 | 5 | LOCAL_CACHE_DIR = Path().cwd().joinpath("cache") 6 | APPLICATION_BUNDLES = "/var/containers/Bundle/Application/" 7 | APPLICATION_DOCUMENTS = "/var/mobile/Containers/Data/Application/" 8 | MOBILE_DOCUMENTS = "/var/mobile/Documents/" 9 | 10 | 11 | MOBILE_SUBSTRATE_PATH_ROOT = "/Library/MobileSubstrate/DynamicLibraries/" 12 | MOBILE_SUBSTRATE_PATH_ROOTLESS = "/var/Library/MobileSubstrate/DynamicLibraries/" 13 | 14 | MS_BFDECRYPT_SETTINGS = "bfdecrypt.plist" 15 | MS_SETTINGS_ROOT_F = MOBILE_SUBSTRATE_PATH_ROOT + MS_BFDECRYPT_SETTINGS 16 | MS_SETTINGS_ROOTLESS_F = MOBILE_SUBSTRATE_PATH_ROOTLESS + MS_BFDECRYPT_SETTINGS 17 | 18 | BFDECRYPT_SETTINGS_PATH = "/var/mobile/Library/Preferences/" 19 | BFDECRYPT_SETTINGS = "com.level3tjg.bfdecrypt.plist" 20 | BFDECRYPT_SETTINGS_F = BFDECRYPT_SETTINGS_PATH + BFDECRYPT_SETTINGS 21 | 22 | 23 | def is_there_any_cache() -> int: 24 | global LOCAL_CACHE_DIR 25 | if LOCAL_CACHE_DIR.exists(): 26 | return len(list(LOCAL_CACHE_DIR.iterdir())) 27 | 28 | return 0 29 | 30 | 31 | def clear_cache(): 32 | global LOCAL_CACHE_DIR 33 | if not LOCAL_CACHE_DIR.exists(): 34 | return 35 | 36 | for cache in LOCAL_CACHE_DIR.iterdir(): 37 | cache.unlink() 38 | 39 | 40 | # This class holds various information about 41 | # installed apps 42 | 43 | class AppInfo: 44 | def __init__(self, bundle_id: str, ios_device_plist_path: str): 45 | self.bundle_id: str = bundle_id 46 | self.bundle_path: str = ios_device_plist_path 47 | self.docs_bundle_id: str = "" 48 | 49 | self.ios_device_plist_path: str = ios_device_plist_path 50 | self.local_plist_path: Path 51 | self.app_executable: str = "" 52 | self.app_name: str = "" 53 | self.app_bundle: str = "" 54 | self.app_version: str = "" 55 | -------------------------------------------------------------------------------- /tools/ssh_connection.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | 3 | import tools.bundle_management as bm 4 | import tools.app_data as app_data 5 | import tools.ios_app as ios_apps 6 | import paramiko 7 | import socket 8 | import tools.utils as utils 9 | from paramiko.channel import ChannelFile 10 | from socket import gaierror 11 | from time import sleep 12 | from re import findall, sub 13 | 14 | 15 | # Utilities for connection 16 | 17 | 18 | def connect(ip: str, username: str, password: str) -> paramiko.SSHClient: 19 | client = paramiko.SSHClient() 20 | client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) 21 | 22 | client.connect(ip, username=username, password=password, timeout=30) 23 | 24 | return client 25 | 26 | 27 | def disconnect(client: paramiko.SSHClient): 28 | print("Closing connection... ", end="", flush=True) 29 | if client: 30 | sleep(5) 31 | client.close() 32 | print("Done") 33 | else: 34 | print("No connection was opened") 35 | 36 | 37 | def is_client_darwin(client: paramiko.SSHClient) -> bool: 38 | ssh_stdin, ssh_stdout, ssh_stderr = client.exec_command("uname") 39 | return findall("Darwin", read_output(ssh_stdout)) != [] 40 | 41 | 42 | def disconnect_sftp(sftp_client: paramiko.SFTPClient): 43 | if sftp_client: 44 | sleep(5) 45 | sftp_client.close() 46 | 47 | 48 | def setup_connection(ask_to_setup_new_idevice=True) -> list: 49 | utils.clear_terminal() 50 | print(" Connection setup") 51 | tmp_ip = app_data.read_ip_file() 52 | if not tmp_ip: 53 | ip = None 54 | else: 55 | ip = tmp_ip 56 | 57 | tmp_up = app_data.read_up_file() 58 | if not tmp_up or len(tmp_up) != 2: 59 | username = None 60 | password = None 61 | else: 62 | username = tmp_up[0] 63 | password = tmp_up[1] 64 | 65 | client = None 66 | 67 | if tmp_ip and tmp_up and ask_to_setup_new_idevice: 68 | print("\nDo you want to setup a new device?") 69 | print("1. Yes") 70 | print(f"2. No, connect to {ip}") 71 | if utils.choose(["1", "2"], [True, False]): 72 | ip, username, password = None, None, None 73 | ios_apps.clear_cache() 74 | 75 | attempts = 0 76 | 77 | while client is None and attempts < 3: 78 | if ip is None: 79 | print("\nEnter the iOS device IP:") 80 | ip = input("> ") 81 | if not ip: 82 | ip = None 83 | continue 84 | 85 | if username is None or password is None: 86 | print("\nEnter your credentials") 87 | print(" Do you want to login with root/alpine?") 88 | print("1. Yes") 89 | print("2. No, let me enter my username and password") 90 | 91 | if utils.choose(["1", "2"], [True, False]): 92 | username, password = "root", "alpine" 93 | else: 94 | print("\nEnter your username") 95 | username = input("> ") 96 | if not username: 97 | username = None 98 | continue 99 | 100 | print("\nEnter your password") 101 | password = input("> ") 102 | 103 | try: 104 | print("\nTrying connection... ", end="", flush=True) 105 | client = connect(ip, username, password) 106 | except socket.timeout: 107 | print("Time out!") 108 | client = None 109 | ip = None 110 | attempts += 1 111 | continue 112 | except (paramiko.ssh_exception.NoValidConnectionsError, gaierror): 113 | print(f"Failed to find host '{ip}'") 114 | client = None 115 | ip = None 116 | attempts += 1 117 | continue 118 | except paramiko.ssh_exception.AuthenticationException: 119 | print("Failed to authenticate!") 120 | client = None 121 | username = None 122 | password = None 123 | attempts += 1 124 | continue 125 | 126 | if not client: 127 | print("\n Please check your internet connection, username and password") 128 | print(" If you need more assistance, check README.md file\n") 129 | raise InterruptedError("Too many attempts") 130 | 131 | print("Connection success!") 132 | 133 | if password == "alpine": 134 | print("\n Please, consider changing ssh default password!") 135 | 136 | if not is_client_darwin(client): 137 | print("\n Looks like you're not connected to an iOS device...") 138 | disconnect(client) 139 | return [None, None, None, None] 140 | 141 | return [client, username, password, ip] 142 | 143 | 144 | def read_output(ssh_stdout: ChannelFile) -> str: 145 | data = ssh_stdout.read() 146 | if data: 147 | return data.decode("utf-8") 148 | 149 | return "" 150 | 151 | 152 | def put_clutch_troll(client: paramiko.SSHClient) -> bool: 153 | if not app_data.does_clutch_exist(): 154 | utils.clear_terminal() 155 | print(" Clutch_troll not found...") 156 | print("1. Provide a copy") 157 | print("2. Abort installation") 158 | print("Select an option") 159 | if utils.choose(["1", "2"], [True, False]): 160 | app_data.get_file_copy(True) 161 | return put_clutch_troll(client) 162 | else: 163 | return False 164 | 165 | ssh_stdin, ssh_stdout, ssh_stderr = client.exec_command(f"cd {ios_apps.MOBILE_DOCUMENTS}; ls") 166 | output = read_output(ssh_stdout) 167 | if "Clutch_troll" not in output: 168 | try: 169 | sftp_client = client.open_sftp() 170 | sftp_client.put(app_data.CLUTCH_EXECUTABLE, ios_apps.MOBILE_DOCUMENTS + "Clutch_troll") 171 | disconnect_sftp(sftp_client) 172 | client.exec_command(f"cd {ios_apps.MOBILE_DOCUMENTS}; chmod +x Clutch_troll") 173 | return True 174 | except FileNotFoundError: 175 | print("It's not possible copy Clutch_troll!") 176 | return False 177 | 178 | client.exec_command(f"cd {ios_apps.MOBILE_DOCUMENTS}; chmod +x Clutch_troll") 179 | 180 | return True 181 | 182 | 183 | def install_bfdecrypt(client: paramiko.SSHClient) -> bool: 184 | utils.clear_terminal() 185 | if not app_data.does_bfdecrypt_exist(): 186 | utils.clear_terminal() 187 | print(" bfdecrypt deb not found...") 188 | print("1. Provide a copy") 189 | print("2. Abort installation") 190 | print("Select an option") 191 | if utils.choose(["1", "2"], [True, False]): 192 | app_data.get_file_copy(False) 193 | return install_bfdecrypt(client) 194 | else: 195 | return False 196 | 197 | print("Installing... ", end="", flush=True) 198 | 199 | sftp_client = client.open_sftp() 200 | try: 201 | sftp_client.put(app_data.BFDECRYPT_DEB, ios_apps.MOBILE_DOCUMENTS + app_data.BFDECRYPT_DEB.name) 202 | except FileNotFoundError: 203 | if app_data.BFDECRYPT_DEB.exists(): 204 | print(f" w h a t?\n\"{ios_apps.MOBILE_DOCUMENTS}\" doesn't exist, this might be a Mac") 205 | else: 206 | print(f"{app_data.BFDECRYPT_DEB} doesn't exist") 207 | return False 208 | 209 | disconnect_sftp(sftp_client) 210 | 211 | ssh_stdin, ssh_stdout, ssh_stderr = client.exec_command( 212 | f"cd {ios_apps.MOBILE_DOCUMENTS}; dpkg -i {app_data.BFDECRYPT_DEB.name}" 213 | ) 214 | 215 | output = read_output(ssh_stderr) 216 | if "com.spark.libsparkapplist" in output: 217 | print("Failed!") 218 | print(" \"libsparkapplist\" is required!") 219 | print(" If you need assistance, please check README.md") 220 | client.exec_command("dpkg --remove com.level3tjg.bfdecrypt") 221 | return False 222 | 223 | print("Success") 224 | 225 | return True 226 | 227 | 228 | def is_bfdecrypt_installed(client: paramiko.SSHClient) -> bool: 229 | while True: 230 | try: 231 | ssh_stdin, ssh_stdout, ssh_stderr = client.exec_command( 232 | f"cd {ios_apps.MOBILE_SUBSTRATE_PATH_ROOT}; ls" 233 | ) 234 | 235 | error = read_output(ssh_stderr) 236 | if "No such file or directory" in error: 237 | raise FileNotFoundError("Not root access") 238 | except FileNotFoundError: # Rootless jailbreak 239 | ssh_stdin, ssh_stdout, ssh_stderr = client.exec_command( 240 | f"cd {ios_apps.MOBILE_SUBSTRATE_PATH_ROOTLESS}; ls" 241 | ) 242 | 243 | output = read_output(ssh_stdout) 244 | if ios_apps.MS_BFDECRYPT_SETTINGS in output: 245 | return True 246 | 247 | utils.clear_terminal() 248 | print(" bfdecrypt is not installed...") 249 | print("1. Look up for bfdecrypt again") 250 | print("2. Install bfdecrypt") 251 | print("3. Cancel") 252 | print("Select an option") 253 | option = utils.choose(["1", "2", "3"]) 254 | 255 | if option == "1": 256 | continue 257 | elif option == "2": 258 | if not install_bfdecrypt(client): 259 | input("\nPress enter to continue... ") 260 | else: 261 | return False 262 | 263 | 264 | def is_idevice_ready(client: paramiko.SSHClient) -> bool: 265 | check_clutch = "CLUTCH" in app_data.decrypt_method 266 | check_bfdecrypt = "BFDECRYPT" in app_data.decrypt_method 267 | 268 | ready = True 269 | if check_clutch: 270 | ready = ready and put_clutch_troll(client) 271 | 272 | if check_bfdecrypt: 273 | ready = ready and is_bfdecrypt_installed(client) 274 | 275 | return ready 276 | 277 | 278 | def list_bundle_ids(client: paramiko.SSHClient, find_documents: bool) -> list: 279 | if find_documents: 280 | ssh_stdin, ssh_stdout, ssh_stderr = client.exec_command(f"cd {ios_apps.APPLICATION_DOCUMENTS}; ls") 281 | else: 282 | ssh_stdin, ssh_stdout, ssh_stderr = client.exec_command(f"cd {ios_apps.APPLICATION_BUNDLES}; ls") 283 | 284 | output = read_output(ssh_stdout) 285 | 286 | return bm.find_plists(output, find_documents) 287 | 288 | 289 | def find_app_executables(client: paramiko.SSHClient, listed_apps: list): 290 | for app in listed_apps: 291 | app_path = app.ios_device_plist_path.replace("/iTunesMetadata.plist", "") 292 | ssh_stdin, ssh_stdout, ssh_stderr = client.exec_command(f"cd {app_path}; ls") 293 | output = read_output(ssh_stdout) 294 | executable_name = findall(r".+\.app", output) 295 | if executable_name: 296 | app.app_executable = executable_name[0].replace(".app", "") 297 | 298 | 299 | def document_plist_to_local_path(plist: str) -> pathlib.Path: 300 | local_plist_name = plist.replace("/.com.apple.mobile_container_manager.metadata.plist", "") 301 | local_plist_name = "d_" + local_plist_name.replace(ios_apps.APPLICATION_DOCUMENTS, "") + ".plist" 302 | return ios_apps.LOCAL_CACHE_DIR.joinpath(local_plist_name) 303 | 304 | 305 | def retrieve_apps_plists(client: paramiko.SSHClient, listed_applications: list, documents_plists: list): 306 | if not ios_apps.LOCAL_CACHE_DIR.exists(): 307 | ios_apps.LOCAL_CACHE_DIR.mkdir() 308 | 309 | sftp_client = client.open_sftp() 310 | 311 | for app in listed_applications: 312 | app.local_plist_path = ios_apps.LOCAL_CACHE_DIR.joinpath(app.bundle_id + ".plist") 313 | if app.local_plist_path.exists(): 314 | continue 315 | 316 | try: 317 | remote_file = sftp_client.open(app.ios_device_plist_path, "rb") 318 | utils.write_binary_file(app.local_plist_path, remote_file.read()) 319 | except FileNotFoundError: 320 | pass 321 | 322 | for plist in documents_plists: 323 | local_plist_path = document_plist_to_local_path(plist) 324 | if local_plist_path.exists(): 325 | continue 326 | 327 | try: 328 | remote_file = sftp_client.open(plist, "rb") 329 | utils.write_binary_file(local_plist_path, remote_file.read()) 330 | except FileNotFoundError: 331 | pass 332 | 333 | disconnect_sftp(sftp_client) 334 | 335 | 336 | def link_documents_plist(listed_applications: list, documents_plists: list): 337 | for plist in documents_plists: 338 | if isinstance(plist, pathlib.Path): 339 | local_plist_path = plist 340 | else: 341 | local_plist_path = document_plist_to_local_path(plist) 342 | 343 | contents = utils.read_plist_file(local_plist_path) 344 | 345 | for app in listed_applications: 346 | if contents["MCMMetadataIdentifier"] == app.app_bundle: 347 | app.docs_bundle_id = local_plist_path.name.replace(".plist", "").replace("d_", "") 348 | 349 | 350 | def retrieve_apps_names(client: paramiko.SSHClient, listed_applications: list, documents_plists: list): 351 | files_in_cache = ios_apps.is_there_any_cache() 352 | download_plist = files_in_cache == 0 or files_in_cache != (len(listed_applications) + len(documents_plists)) 353 | 354 | if download_plist: 355 | retrieve_apps_plists(client, listed_applications, documents_plists) 356 | else: 357 | for app in listed_applications: 358 | app.local_plist_path = ios_apps.LOCAL_CACHE_DIR.joinpath(app.bundle_id + ".plist") 359 | 360 | for app in listed_applications: 361 | plist_data = utils.read_plist_file(app.local_plist_path) 362 | if not plist_data: 363 | continue 364 | 365 | try: 366 | app.app_name = plist_data["itemName"] 367 | app.app_bundle = plist_data["softwareVersionBundleId"] 368 | app.app_version = plist_data["bundleShortVersionString"] 369 | except KeyError: 370 | continue 371 | 372 | link_documents_plist(listed_applications, documents_plists) 373 | 374 | 375 | def list_apps(client: paramiko.SSHClient) -> list: 376 | print("\nListing apps... ", end="", flush=True) 377 | listed_applications = list_bundle_ids(client, False) 378 | plist_documents = list_bundle_ids(client, True) 379 | retrieve_apps_names(client, listed_applications, plist_documents) 380 | find_app_executables(client, listed_applications) 381 | print("Done") 382 | 383 | return listed_applications 384 | 385 | 386 | def decrypt_app_with_clutch(client: paramiko.SSHClient, app: ios_apps.AppInfo) -> str: 387 | cmd = f"cd {ios_apps.MOBILE_DOCUMENTS} && ./Clutch_troll -d {app.app_bundle}" 388 | ssh_stdin, ssh_stdout, ssh_stderr = client.exec_command(cmd) 389 | 390 | output = read_output(ssh_stdout) + "\n" + read_output(ssh_stderr) # Somehow output is getting into stderr 391 | 392 | if "FAILED" not in output: # FAILED was not found, meaning that app was decrypted 393 | decrypted = findall(f"DONE: /private/var/mobile/Documents/Dumped/{app.app_bundle}.*", output) 394 | if decrypted: 395 | return str(decrypted[0]).replace("DONE: ", "") 396 | 397 | output = f"Command: {cmd}\n" \ 398 | f"App: {app.app_name}\n" \ 399 | f"AppExec: {app.app_executable}\n" \ 400 | f"AppVersion: {app.app_version}\n" \ 401 | f"AppBundle: {app.app_bundle}\n" \ 402 | f"AppPath: {app.bundle_id}\n" \ 403 | f"Clutch output:\n" + output 404 | app_data.write_log(output) 405 | return "" 406 | 407 | 408 | def modify_bfdecrypt_plist(client: paramiko.SSHClient, apps: list): 409 | sftp_client = client.open_sftp() 410 | 411 | bfdecrypt_settings = ios_apps.LOCAL_CACHE_DIR.joinpath(ios_apps.BFDECRYPT_SETTINGS) 412 | 413 | settings_file = sftp_client.open(ios_apps.BFDECRYPT_SETTINGS_F, "rb") 414 | utils.write_binary_file(bfdecrypt_settings, settings_file.read()) 415 | 416 | file_content = utils.read_file(bfdecrypt_settings) 417 | bfdecrypt_bundle = "\n" 418 | for app in apps: 419 | bfdecrypt_bundle += f"\t\t{app.app_bundle}\n" 420 | 421 | if not apps: 422 | bfdecrypt_bundle += "\n" 423 | 424 | bfdecrypt_bundle += "\t" 425 | 426 | file_content = sub(r"[\w\W]+", bfdecrypt_bundle, file_content) 427 | utils.write_file(bfdecrypt_settings, file_content) 428 | sftp_client.put(bfdecrypt_settings, ios_apps.BFDECRYPT_SETTINGS_F) 429 | try: 430 | sftp_client.put(bfdecrypt_settings, ios_apps.MS_SETTINGS_ROOT_F) 431 | except FileNotFoundError: # Jailbreak is rootless 432 | sftp_client.put(bfdecrypt_settings, ios_apps.MS_SETTINGS_ROOTLESS_F) 433 | 434 | disconnect_sftp(sftp_client) 435 | 436 | 437 | def revert_plist(client: paramiko.SSHClient): # Revert to avoid re-decrypting if user opens normally 438 | client.exec_command(f"killall Preferences") 439 | modify_bfdecrypt_plist(client, list()) 440 | client.exec_command(f"uiopen 'prefs:root=bfdecrypt'") 441 | sleep(3) 442 | client.exec_command(f"killall Preferences") 443 | 444 | 445 | def decrypt_app_with_bfdecrypt(client: paramiko.SSHClient, app: ios_apps.AppInfo) -> str: 446 | client.exec_command(f"killall \"{app.app_executable}\"") 447 | client.exec_command(f"killall Preferences") 448 | sleep(1) 449 | client.exec_command(f"uiopen 'prefs:root=bfdecrypt'") 450 | 451 | documents_path = f"{ios_apps.APPLICATION_DOCUMENTS + app.docs_bundle_id}/Documents/" 452 | 453 | timeout = 0 454 | last_directory_size = 0 455 | directory_size = 0 456 | decrypted = False 457 | 458 | sleep(3) 459 | client.exec_command(f"uiopen --bundleid {app.app_bundle}") 460 | 461 | while timeout < 30: 462 | ssh_stdin, ssh_stdout, ssh_stderr = client.exec_command(f"cd {documents_path}; ls") 463 | output = read_output(ssh_stdout) 464 | if "decrypted-app.ipa" in output: 465 | decrypted = True 466 | break 467 | if "ipa" in output and "decrypted-app-temp.ipa" not in output: 468 | ssh_stdin, ssh_stdout, ssh_stderr = client.exec_command(f"du -hs {documents_path}/ipa") 469 | s_output = read_output(ssh_stdout) 470 | new_dir_size = findall(r"\d+", s_output) 471 | if new_dir_size: 472 | directory_size = int(new_dir_size[0]) 473 | 474 | if directory_size == last_directory_size: 475 | timeout += 1 476 | else: 477 | last_directory_size = directory_size 478 | 479 | sleep(1) 480 | 481 | client.exec_command(f"rm {documents_path}/decrypted-app-temp.ipa") 482 | client.exec_command(f"rm -fr {documents_path}/ipa") 483 | 484 | if decrypted: 485 | client.exec_command(f"killall \"{app.app_executable}\"") 486 | return documents_path + "/decrypted-app.ipa" 487 | 488 | return "" 489 | 490 | 491 | def download_app(client: paramiko.SSHClient, app: ios_apps.AppInfo, ipa_path: str) -> str: 492 | if not app_data.DOWNLOADED_APPS.exists(): 493 | app_data.DOWNLOADED_APPS.mkdir() 494 | 495 | local_file_name = app_data.DOWNLOADED_APPS.joinpath(app.app_name.replace(" ", "_") + "_" + app.app_version + ".ipa") 496 | 497 | sftp_client = client.open_sftp() 498 | try: 499 | sftp_client.get(ipa_path, local_file_name) 500 | client.exec_command(f"rm \"{ipa_path}\"") 501 | 502 | return local_file_name.name 503 | except FileNotFoundError: 504 | return "" 505 | finally: 506 | disconnect_sftp(sftp_client) 507 | 508 | 509 | def dump_app(client: paramiko.SSHClient, app: ios_apps.AppInfo, multiple_apps: bool): 510 | utils.clear_terminal() 511 | print(f"Preparing to dump {app.app_name} [{app.app_version}]... ") 512 | 513 | if not multiple_apps and "BFDECRYPT" in app_data.decrypt_method: 514 | modify_bfdecrypt_plist(client, [app]) 515 | 516 | ipa_path = "" 517 | attempt_clutch_first = app_data.decrypt_method.startswith("CLUTCH") 518 | clutch_attempted = False 519 | bfdecrypt_attempted = False 520 | 521 | attempts = 0 522 | if "CLUTCH" in app_data.decrypt_method: 523 | attempts += 1 524 | 525 | if "BFDECRYPT" in app_data.decrypt_method: 526 | attempts += 1 527 | 528 | for _ in range(attempts): # Two attempts to decrypt as maximum 529 | if not clutch_attempted and "CLUTCH" in app_data.decrypt_method and attempt_clutch_first: 530 | print(f"Dumping {app.app_bundle} (CLUTCH)... ", end="", flush=True) 531 | 532 | clutch_attempted = True 533 | ipa_path = decrypt_app_with_clutch(client, app) 534 | elif not bfdecrypt_attempted and "BFDECRYPT" in app_data.decrypt_method: 535 | print(f"Dumping {app.app_bundle} (BFDECRYPT)... ", end="", flush=True) 536 | 537 | attempt_clutch_first = True # Just in case if clutch is secondly tried 538 | bfdecrypt_attempted = True 539 | ipa_path = decrypt_app_with_bfdecrypt(client, app) 540 | 541 | if ipa_path: 542 | print("Success") 543 | if not multiple_apps and "BFDECRYPT" in app_data.decrypt_method: # Revert plist 544 | revert_plist(client) 545 | 546 | print("Downloading... ", end="", flush=True) 547 | downloaded = download_app(client, app, ipa_path) 548 | if downloaded: 549 | print("File saved as", downloaded) 550 | break 551 | else: 552 | print("Download failed!") 553 | else: 554 | print(f"Error while decrypting ipa!") 555 | 556 | 557 | def dump_multiple_apps(client: paramiko.SSHClient, apps: list): 558 | if "BFDECRYPT" in app_data.decrypt_method: 559 | modify_bfdecrypt_plist(client, apps) 560 | 561 | for app in apps: 562 | dump_app(client, app, True) 563 | 564 | if "BFDECRYPT" in app_data.decrypt_method: 565 | revert_plist(client) 566 | -------------------------------------------------------------------------------- /tools/utils.py: -------------------------------------------------------------------------------- 1 | import plistlib 2 | from pathlib import Path 3 | from subprocess import call 4 | 5 | # General utilities 6 | 7 | CLEAR_CMD = "cls" 8 | IS_SYSTEM_NT = True 9 | 10 | try: 11 | from os import uname 12 | CLEAR_CMD = "clear" 13 | IS_SYSTEM_NT = False 14 | except ImportError: # uname doesn't exist in Windows 15 | pass 16 | 17 | 18 | def clear_terminal(): 19 | global CLEAR_CMD 20 | call(CLEAR_CMD, shell=True) 21 | 22 | 23 | # "options" and "options_values" must have the same size 24 | def choose(options: list, options_values=None): 25 | selection = "" 26 | 27 | while not selection: 28 | selection = input("> ").upper() 29 | if selection not in options: 30 | print(" Option not found!") 31 | print(" Please select an option in list") 32 | selection = "" 33 | 34 | if options_values is not None: 35 | return options_values[options.index(selection)] 36 | 37 | return selection 38 | 39 | 40 | def write_file(file_path: Path, data: str) -> bool: 41 | try: 42 | with open(file_path, "w") as file: 43 | file.write(data) 44 | return True 45 | except (UnicodeDecodeError, FileNotFoundError, IsADirectoryError): 46 | return False 47 | 48 | 49 | def write_binary_file(file_path: Path, data: bytes) -> bool: 50 | try: 51 | with open(file_path, "wb") as file: 52 | file.write(data) 53 | return True 54 | except IsADirectoryError: 55 | return False 56 | 57 | 58 | def read_file(file_path: Path) -> str: 59 | try: 60 | with open(file_path, "r") as file: 61 | return file.read() 62 | except (UnicodeDecodeError, FileNotFoundError, IsADirectoryError): 63 | # print(" Cannot read file ", file_path) 64 | return "" 65 | 66 | 67 | def read_plist_file(file_path: Path) -> dict: 68 | try: 69 | with open(file_path, "rb") as file: 70 | return plistlib.load(file) 71 | except (FileNotFoundError, IsADirectoryError): 72 | return dict() 73 | 74 | 75 | def enumerate_app_list(apps: list) -> list: 76 | enumerated_apps = "" 77 | enumerated_apps_as_options = list() 78 | apps_copy = apps.copy() 79 | 80 | i = 0 81 | while i < len(apps_copy): 82 | if apps_copy[i].app_name: 83 | enumerated_apps += f"{i + 1}.\t" \ 84 | f"{apps_copy[i].app_name}\n" 85 | enumerated_apps_as_options.append(f"{i + 1}") 86 | i += 1 87 | else: 88 | apps_copy.pop(i) 89 | 90 | return [enumerated_apps[:-1], enumerated_apps_as_options, apps_copy] 91 | 92 | 93 | def select_apps(listed_applications: list, select_multiple_apps): 94 | clear_terminal() 95 | input("Make sure your device is unlocked, press enter to proceed") 96 | 97 | selected_apps = list() 98 | valid_bundles = listed_applications.copy() 99 | while True: 100 | listed_apps, options, valid_bundles = enumerate_app_list(valid_bundles) 101 | if select_multiple_apps: 102 | listed_apps += "\nA. Select all" 103 | options += ["A"] 104 | valid_bundles += "A" 105 | 106 | listed_apps += "\nE. End" 107 | options += ["E"] 108 | valid_bundles += "E" 109 | 110 | print(listed_apps) 111 | print("Select an app") 112 | app = choose(options, valid_bundles) 113 | 114 | if select_multiple_apps: 115 | valid_bundles = valid_bundles[:-2] 116 | else: 117 | valid_bundles = valid_bundles[:-1] 118 | 119 | if isinstance(app, str): 120 | if app == "E": 121 | break 122 | 123 | print("\nThis might take a while!") 124 | selected_apps = valid_bundles 125 | break 126 | 127 | selected_apps.append(app) 128 | valid_bundles.remove(app) 129 | 130 | clear_terminal() 131 | if not select_multiple_apps: 132 | break 133 | 134 | if selected_apps and not select_multiple_apps: 135 | return selected_apps[0] 136 | elif not select_multiple_apps: 137 | return None 138 | 139 | return selected_apps 140 | --------------------------------------------------------------------------------