├── .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 |
--------------------------------------------------------------------------------