iphonetool 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
iphonetool/__init__.py ADDED
File without changes
iphonetool/__main__.py ADDED
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env -S uv run --script
2
+ # /// script
3
+ # requires-python = ">=3.13"
4
+ # dependencies = [
5
+ # "pymobiledevice3>=10.1.0",
6
+ # "pyusb>=1.3.1",
7
+ # ]
8
+ # ///
9
+
10
+ try:
11
+ import usb.core
12
+ except ImportError:
13
+ print("pyusb not installed and is needed for all operations. Please install pyusb")
14
+ raise
15
+ import argparse
16
+ import asyncio
17
+ import sys
18
+ from enum import IntEnum
19
+
20
+ import dfu
21
+ import helpers
22
+ import normal
23
+ import recovery
24
+
25
+
26
+ async def main() -> int:
27
+ if "-w" in sys.argv or "--wait" in sys.argv:
28
+ print("Waiting for device...")
29
+ dev = await helpers.wait_device()
30
+ print("Found device")
31
+ else:
32
+ dev = helpers.get_device()
33
+
34
+ # If this fails, it will fall through to a "normal" device below
35
+ if dev is None and ("-h" not in sys.argv and "--help" not in sys.argv):
36
+ print("No idevice found :(")
37
+ return 1
38
+
39
+ parser = argparse.ArgumentParser()
40
+ # This is only for showing up in help
41
+ parser.add_argument(
42
+ "-w", "--wait", help="Wait for a device to appear", action="store_true"
43
+ )
44
+ subparsers = parser.add_subparsers(dest="action")
45
+ # List is supported in all modes
46
+ subparsers.add_parser("info")
47
+
48
+ if dev is None:
49
+ return await normal.main(None, parser, subparsers)
50
+
51
+ # Figure out what mode the device is in
52
+ match helpers.classify_mode(dev):
53
+ case helpers.DeviceMode.NORMAL:
54
+ return await normal.main(dev, parser, subparsers)
55
+ case helpers.DeviceMode.RECOVERY:
56
+ return await recovery.main(dev, parser, subparsers)
57
+ case helpers.DeviceMode.DFU:
58
+ return await dfu.main(dev, parser, subparsers)
59
+
60
+
61
+ if __name__ == "__main__":
62
+ exit(asyncio.run(main()))
iphonetool/config.py ADDED
@@ -0,0 +1,11 @@
1
+ from enum import IntEnum
2
+
3
+ APPLE_VENDORID = 0x05AC
4
+
5
+
6
+ class AppleProductId(IntEnum):
7
+ RECOVERY = 0x1281
8
+ DFU = 0x1227
9
+
10
+
11
+ USBLITER8_TRANSFER_SIZE = 0x800
iphonetool/dfu.py ADDED
@@ -0,0 +1,243 @@
1
+ import argparse
2
+ import asyncio
3
+ import glob
4
+ import os
5
+ import pathlib
6
+ import shutil
7
+ import subprocess
8
+ import sys
9
+ import tempfile
10
+ from enum import IntEnum
11
+ from typing import Any, Optional
12
+
13
+ import usb.core
14
+
15
+ import config
16
+ import helpers
17
+
18
+
19
+ async def main(
20
+ dev: usb.core.Device,
21
+ parser: argparse.ArgumentParser,
22
+ subparsers: argparse._SubParsersAction,
23
+ ):
24
+ subparsers.add_parser("exit_dfu_helper", help="Help exiting DFU mode")
25
+
26
+ try:
27
+ serial = dev.serial_number
28
+ except ValueError as e:
29
+ raise ValueError(
30
+ "This script must be run as root for iPhones in recovery or dfu mode."
31
+ ) from e
32
+
33
+ if helpers.serial_info_get(serial, "PWND") == "[usbliter8]":
34
+ pwned_device = True
35
+ else:
36
+ pwned_device = False
37
+
38
+ if pwned_device:
39
+ subparsers.add_parser(
40
+ "demote", help="Demote this device to a development device"
41
+ )
42
+
43
+ boot_parser = subparsers.add_parser(
44
+ "boot", help='Low-level device exploit booting. You probably want "linux"'
45
+ )
46
+ boot_subparsers = boot_parser.add_subparsers(dest="boot_action")
47
+
48
+ boot_raw_parser = boot_subparsers.add_parser(
49
+ "raw", help="Boot a raw iBoot file"
50
+ )
51
+ boot_raw_parser.add_argument(
52
+ "iBoot", type=pathlib.Path, help="The raw iBoot to boot"
53
+ )
54
+
55
+ boot_remote_parser = boot_subparsers.add_parser(
56
+ "remote", help="Remote boot m1n1, linux, etc."
57
+ )
58
+ boot_remote_parser.add_argument(
59
+ "-1",
60
+ "--m1n1",
61
+ type=pathlib.Path,
62
+ help="m1n1-idevice.macho + any addons (linux, dtbs, etc)",
63
+ required=True,
64
+ )
65
+ boot_remote_parser.add_argument(
66
+ "-m", "--monitor", type=pathlib.Path, help="m1n1 monitor stub"
67
+ )
68
+
69
+ # High-level linux booter
70
+ linux_parser = subparsers.add_parser("linux", help="Boot linux on device")
71
+ linux_parser.add_argument(
72
+ "-1",
73
+ "--m1n1",
74
+ type=pathlib.Path,
75
+ help="Path to m1n1-idevice.macho",
76
+ required=True,
77
+ )
78
+ linux_parser_dtb_group = linux_parser.add_mutually_exclusive_group(
79
+ required=True
80
+ )
81
+ linux_parser_dtb_group.add_argument(
82
+ "-d", "--dtb", type=pathlib.Path, help="Path to devicetree for device"
83
+ )
84
+ linux_parser_dtb_group.add_argument(
85
+ "-D", "--dtbs", type=pathlib.Path, help="Path to directory of devicetrees"
86
+ )
87
+ linux_parser.add_argument(
88
+ "-k", "--kernel", type=pathlib.Path, help="Path to kernel", required=True
89
+ )
90
+ linux_parser.add_argument(
91
+ "-c", "--commandline", help="Linux commandline to boot"
92
+ )
93
+ linux_parser.add_argument(
94
+ "-i", "--initramfs", type=pathlib.Path, help="Linux initramfs to boot"
95
+ )
96
+ linux_parser.add_argument(
97
+ "-m", "--monitor", type=pathlib.Path, help="m1n1 monitor stub"
98
+ )
99
+
100
+ args = parser.parse_args()
101
+
102
+ return await main_real(dev, pwned_device, serial, args.action, vars(args))
103
+
104
+
105
+ class Usbliter8Command(IntEnum):
106
+ DFU_DNLOAD = 1
107
+ DFU_ABORT = 4
108
+ CUSTOM_DEMOTE = 7
109
+ CUSTOM_BOOT = 8
110
+
111
+
112
+ def send_usbliter8_command(
113
+ dev: usb.core.Device, command: Usbliter8Command, data: Any, timeout: int
114
+ ) -> None:
115
+ dev.ctrl_transfer(0x21, command, 0, 0, data, timeout)
116
+
117
+
118
+ def usbliter8_download(dev: usb.core.Device, data: bytes) -> True:
119
+ offset = 0
120
+ left = len(data)
121
+
122
+ while left:
123
+ current_block_length = min(config.USBLITER8_TRANSFER_SIZE, left)
124
+
125
+ send_usbliter8_command(
126
+ dev,
127
+ Usbliter8Command.DFU_DNLOAD,
128
+ data[offset : offset + current_block_length],
129
+ 1000,
130
+ )
131
+
132
+ offset += current_block_length
133
+ left -= current_block_length
134
+
135
+ print(f"\rUploaded: 0x{offset:x}/0x{len(data):x} bytes.", end="")
136
+
137
+ print()
138
+
139
+ send_usbliter8_command(dev, Usbliter8Command.DFU_DNLOAD, None, 100)
140
+
141
+
142
+ def linux_remote_boot(m1n1_blob: pathlib.Path, monitor_stub: Optional[pathlib.Path]):
143
+ subprocess.check_call(["./remote_boot/remoteboot.sh", "build"])
144
+ if monitor_stub is not None:
145
+ subprocess.check_call(
146
+ [
147
+ helpers.base_directory() / "./remote_boot/remoteboot.sh",
148
+ "boot",
149
+ m1n1_blob,
150
+ monitor_stub,
151
+ ],
152
+ env={
153
+ "USBLITER8CTL": helpers.base_directory() / "usbliter8ctl.py",
154
+ "PYTHON": sys.executable,
155
+ "HOME": os.getenv("HOME"),
156
+ },
157
+ )
158
+ else:
159
+ subprocess.check_call(
160
+ ["./remote_boot/remoteboot.sh", "boot", m1n1_blob],
161
+ env={
162
+ "USBLITER8CTL": helpers.base_directory() / "usbliter8ctl.py",
163
+ "PYTHON": sys.executable,
164
+ "HOME": os.getenv("HOME"),
165
+ },
166
+ )
167
+
168
+
169
+ async def main_real(
170
+ dev: usb.core.Device, pwned: bool, serial: str, action: str, args: dict
171
+ ) -> int:
172
+ ecid = int(helpers.serial_info(serial, "ECID"), 16)
173
+
174
+ irecovery = subprocess.check_output(["irecovery", "-q", "-i", hex(ecid)]).decode()
175
+
176
+ print(args)
177
+
178
+ match action:
179
+ case "info":
180
+ if pwned:
181
+ print(
182
+ f"Detected PWNED DFU mode {helpers.irecovery_info(irecovery, "NAME")}:"
183
+ )
184
+ else:
185
+ print(f"Detected DFU mode {helpers.irecovery_info(irecovery, "NAME")}:")
186
+ print("iPhone ID:", ecid)
187
+ print(
188
+ "iPhone internal version:", helpers.irecovery_info(irecovery, "PRODUCT")
189
+ )
190
+ print("Codename:", helpers.irecovery_info(irecovery, "MODEL"))
191
+ print("CPU:", helpers.serial_info(serial, "CPID"))
192
+ case "exit_dfu_helper":
193
+ # TODO: This
194
+ print("Idk. You figure it out")
195
+ case "demote":
196
+ print(f"Telling device {ecid} to demote to development.")
197
+ send_usbliter8_command(dev, Usbliter8Command.CUSTOM_DEMOTE, None, 100)
198
+ print(f"Device {ecid} has demoted to development.")
199
+ case "boot":
200
+ match args["boot_action"]:
201
+ case "raw":
202
+ iboot_file = args["iBoot"]
203
+ print(f"Uploading {iboot_file} to device...")
204
+ usbliter8_download(dev, iboot_file.read_bytes())
205
+
206
+ send_usbliter8_command(dev, Usbliter8Command.CUSTOM_BOOT, None, 100)
207
+ send_usbliter8_command(dev, Usbliter8Command.DFU_ABORT, None, 100)
208
+ case "remote":
209
+ linux_remote_boot(args["m1n1"], args.get("monitor"))
210
+ case "linux":
211
+ print("Preapring iboot...")
212
+ with tempfile.NamedTemporaryFile(mode="wb") as m1n1_blob_file:
213
+ print("adding m1n1")
214
+ with args["m1n1"].open("rb") as f:
215
+ shutil.copyfileobj(f, m1n1_blob_file)
216
+ if args["commandline"] is not None:
217
+ print("adding commandline")
218
+ m1n1_blob_file.write(
219
+ f'chosen.bootargs={args["commandline"]}\n'.encode()
220
+ )
221
+ if args["dtb"] is not None:
222
+ if not args["dtb"].is_file():
223
+ raise ValueError("Specified DTB is not a file.")
224
+ print("adding dtb")
225
+ with args["dtb"].open("rb") as f:
226
+ shutil.copyfileobj(f, m1n1_blob_file)
227
+ else:
228
+ if not args["dtbs"].is_dir():
229
+ raise ValueError("Specified DTB directory is not a dir.")
230
+ print("adding dtbs")
231
+ for dtb in glob.iglob("./*.dtb", root_dir=args["dtbs"]):
232
+ with (args["dtbs"] / dtb).open("rb") as f:
233
+ shutil.copyfileobj(f, m1n1_blob_file)
234
+ print("adding kernel")
235
+ with args["kernel"].open("rb") as f:
236
+ shutil.copyfileobj(f, m1n1_blob_file)
237
+ if args["initramfs"] is not None:
238
+ print("adding initramfs")
239
+ with args["initramfs"].open("rb") as f:
240
+ shutil.copyfileobj(f, m1n1_blob_file)
241
+ linux_remote_boot(
242
+ pathlib.Path(m1n1_blob_file.name), args.get("monitor")
243
+ )
iphonetool/helpers.py ADDED
@@ -0,0 +1,87 @@
1
+ import asyncio
2
+ import functools
3
+ import pathlib
4
+ import subprocess
5
+ from enum import Enum, auto
6
+ from typing import Optional
7
+
8
+ import usb.core
9
+
10
+ from config import APPLE_VENDORID, AppleProductId
11
+
12
+
13
+ class DeviceMode(Enum):
14
+ NORMAL = auto()
15
+ RECOVERY = auto()
16
+ DFU = auto()
17
+
18
+
19
+ def get_device():
20
+ return usb.core.find(idVendor=APPLE_VENDORID)
21
+
22
+
23
+ async def wait_device():
24
+ while True:
25
+ dev = get_device()
26
+ if dev is not None:
27
+ return dev
28
+
29
+ await asyncio.sleep(0.1)
30
+
31
+
32
+ async def wait_disconnect(dev: usb.core.Device):
33
+ while True:
34
+ try:
35
+ device = usb.core.find(idVendor=dev.idVendor, idProduct=dev.idProduct)
36
+ if device is not None and device.serial_number == dev.serial_number:
37
+ await asyncio.sleep(0.1)
38
+ continue
39
+ except ValueError:
40
+ continue
41
+ break
42
+
43
+
44
+ def classify_mode(dev: usb.core.Device) -> DeviceMode:
45
+ match dev.idProduct:
46
+ case AppleProductId.RECOVERY:
47
+ return DeviceMode.RECOVERY
48
+ case AppleProductId.DFU:
49
+ return DeviceMode.DFU
50
+ case _:
51
+ return DeviceMode.NORMAL
52
+
53
+
54
+ def serial_info_get(serial, key) -> Optional[str]:
55
+ value = dict(
56
+ list(map(functools.partial(str.split, sep=":", maxsplit=1), serial.split(" ")))
57
+ ).get(key.upper())
58
+ if value is None:
59
+ return value
60
+ return value.strip()
61
+
62
+
63
+ def serial_info(serial, key) -> str:
64
+ value = serial_info_get(serial, key)
65
+ if value is None:
66
+ raise ValueError(f"Could not find key {key} in recovery serial {serial}")
67
+ return value
68
+
69
+
70
+ def irecovery_info(irecovery, key) -> str:
71
+ try:
72
+ return dict(list(map(functools.partial(str.split, sep=":", maxsplit=1), irecovery.splitlines())))[key.upper()].strip() # type: ignore
73
+ except IndexError as e:
74
+ raise ValueError(
75
+ f"Could not find key {key} in irecovery data {irecovery}"
76
+ ) from e
77
+
78
+
79
+ def irecovery_command(cmd: str, ecid: Optional[int] = None) -> None:
80
+ if ecid is not None:
81
+ subprocess.run(["irecovery", "-i", hex(ecid), "-c", cmd], check=True)
82
+ else:
83
+ subprocess.run(["irecovery", "-c", cmd], check=True)
84
+
85
+
86
+ def base_directory() -> pathlib.Path:
87
+ return pathlib.Path(__file__).parent
iphonetool/normal.py ADDED
@@ -0,0 +1,89 @@
1
+ import argparse
2
+ import asyncio
3
+
4
+ import usb.core
5
+
6
+ import helpers
7
+ import recovery
8
+
9
+ try:
10
+ import pymobiledevice3.exceptions
11
+ import pymobiledevice3.lockdown
12
+ except ImportError:
13
+ print(
14
+ "pymobiledevice3 not installed and is needed for normal/recovery mode operation. Please install pymobiledevice3"
15
+ )
16
+ raise
17
+
18
+
19
+ async def main(
20
+ dev: usb.core.Device,
21
+ parser: argparse.ArgumentParser,
22
+ subparsers: argparse._SubParsersAction,
23
+ ) -> int:
24
+ subparsers.add_parser("dfu_helper", help="Help put device into DFU mode")
25
+ subparsers.add_parser("enter_recovery", help="Enter recovery mode")
26
+
27
+ args = parser.parse_args()
28
+
29
+ return await main_real(dev, args.action)
30
+
31
+
32
+ # This can be called easily from other python files
33
+ async def main_real(dev: usb.core.Device, action: str) -> int:
34
+ serial = dev.serial_number.rstrip("\x00")
35
+
36
+ while True:
37
+ try:
38
+ connector = await pymobiledevice3.lockdown.create_using_usbmux(
39
+ connection_type="USB", serial=serial
40
+ )
41
+ break
42
+ except (
43
+ pymobiledevice3.exceptions.DeviceNotFoundError,
44
+ pymobiledevice3.exceptions.ConnectionFailedToUsbmuxdError,
45
+ ):
46
+ # usbmuxd needs a little time sometimes
47
+ print("Failed to connect. Retrying...")
48
+ await asyncio.sleep(0.3)
49
+ continue
50
+ except pymobiledevice3.exceptions.MissingValueError as e:
51
+ # THis can occur if the attempt to connect is made while the device is shutting down
52
+ raise ValueError("Failed to connect to device") from e
53
+
54
+ async with connector as lockdown:
55
+ match action:
56
+ case "info":
57
+ print(f"Detected normal mode {lockdown.display_name}:")
58
+ print("iPhone ID:", lockdown.udid)
59
+ print("iPhone internal version:", lockdown.product_type)
60
+ print("Device name:", lockdown.all_values["DeviceName"])
61
+ print("iOS Version:", lockdown.product_version)
62
+ print("Codename:", lockdown.hardware_model.upper())
63
+ print("CPU:", lockdown.all_values["HardwarePlatform"].upper())
64
+ case "enter_recovery":
65
+ print(f"Telling device {lockdown.udid} to enter recovery.")
66
+ await lockdown.enter_recovery()
67
+ print(f"Device {lockdown.udid} has entered recovery.")
68
+ case "dfu_helper":
69
+ # Put device into recovery
70
+ print(f"Telling device {lockdown.udid} to enter recovery.")
71
+ await lockdown.enter_recovery()
72
+ print(f"Device {lockdown.udid} has entered recovery.")
73
+
74
+ # Wait for the device to disconnect
75
+ print("Waiting for device disconnect.")
76
+ await helpers.wait_disconnect(dev)
77
+ print(f"Device {lockdown.udid} disconnected")
78
+
79
+ # Release the old device
80
+ del dev
81
+
82
+ # Get a new device
83
+ print("Waiting for device to reconnect...")
84
+ dev = await helpers.wait_device()
85
+
86
+ # Continue dfu helper there
87
+ return await recovery.main_real(dev, "dfu_helper")
88
+
89
+ return 0
iphonetool/recovery.py ADDED
@@ -0,0 +1,112 @@
1
+ import argparse
2
+ import asyncio
3
+ import subprocess
4
+ from collections.abc import Callable
5
+ from typing import Optional
6
+
7
+ import usb.core
8
+
9
+ import helpers
10
+ import normal
11
+
12
+ try:
13
+ import pymobiledevice3
14
+ except ImportError:
15
+ print(
16
+ "pymobiledevice3 not installed and is needed for normal/recovery mode operation. Please install pymobiledevice3"
17
+ )
18
+ raise
19
+
20
+
21
+ async def print_in(secs: float, message: str) -> None:
22
+ try:
23
+ await asyncio.sleep(secs)
24
+ print(message)
25
+ except asyncio.CancelledError:
26
+ pass
27
+
28
+
29
+ async def main(
30
+ dev: usb.core.Device,
31
+ parser: argparse.ArgumentParser,
32
+ subparsers: argparse._SubParsersAction,
33
+ ):
34
+ subparsers.add_parser("dfu_helper", help="Help put device into DFU mode")
35
+ subparsers.add_parser("exit_recovery", help="Exit recovery mode")
36
+
37
+ args = parser.parse_args()
38
+
39
+ return await main_real(dev, args.action)
40
+
41
+
42
+ # This can be called easily from other python files
43
+ async def main_real(dev: usb.core.Device, action: str) -> int:
44
+ try:
45
+ serial = dev.serial_number
46
+ except ValueError as e:
47
+ raise ValueError(
48
+ "This script must be run as root for iPhones in recovery or dfu mode."
49
+ ) from e
50
+ ecid = int(helpers.serial_info(serial, "ECID"), 16)
51
+
52
+ irecovery = subprocess.check_output(["irecovery", "-q", "-i", hex(ecid)]).decode()
53
+
54
+ match action:
55
+ case "info":
56
+ print(
57
+ f"Detected recovery mode {helpers.irecovery_info(irecovery, "NAME")}:"
58
+ )
59
+ print("iPhone ID:", ecid)
60
+ print(
61
+ "iPhone internal version:", helpers.irecovery_info(irecovery, "PRODUCT")
62
+ )
63
+ print("Codename:", helpers.irecovery_info(irecovery, "MODEL"))
64
+ print("CPU:", helpers.serial_info(serial, "CPID"))
65
+ case "exit_recovery":
66
+ print(f"Telling device {ecid} to exit recovery.", flush=True)
67
+ helpers.irecovery_command("setenv auto-boot true", ecid)
68
+ helpers.irecovery_command("saveenv", ecid)
69
+ helpers.irecovery_command("reboot", ecid)
70
+ print(f"Device {ecid} has exited recovery", flush=True)
71
+ case "dfu_helper":
72
+ helpers.irecovery_command("setenv auto-boot true", ecid)
73
+ helpers.irecovery_command("saveenv", ecid)
74
+
75
+ input(
76
+ "Hold down buttons 1 & 2 on the device (and keep holding until this program says stop) and press enter."
77
+ )
78
+
79
+ helpers.irecovery_command("reset", ecid)
80
+
81
+ await asyncio.sleep(2.5)
82
+
83
+ print("Release button 1. Keep holding button 2")
84
+
85
+ info_task = asyncio.create_task(
86
+ print_in(
87
+ 8, "Whoops. Device did not enter DFU mode. Waiting for re-connect."
88
+ )
89
+ )
90
+ dev = await helpers.wait_device()
91
+ canceled = info_task.cancel()
92
+
93
+ # Now actually check if the device entered DFU
94
+ mode = helpers.classify_mode(dev)
95
+
96
+ match mode:
97
+ case helpers.DeviceMode.NORMAL:
98
+ if not canceled:
99
+ # Print the message now if it wasn't printed before
100
+ print("Whoops. Device did not enter DFU mode.")
101
+ print("Device re-connected in normal mode.")
102
+ return await normal.main_real(dev, "dfu_helper")
103
+ case helpers.DeviceMode.RECOVERY:
104
+ if not canceled:
105
+ # Print the message now if it wasn't printed before
106
+ print("Whoops. Device did not enter DFU mode.")
107
+ print("Device re-connected in recovery mode.")
108
+ return await main_real(dev, "dfu_helper")
109
+ case helpers.DeviceMode.DFU:
110
+ print("Device entered DFU mode successfully!")
111
+
112
+ return 0
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env -S uv run --script
2
+ # /// script
3
+ # requires-python = ">=3.13"
4
+ # dependencies = [
5
+ # "pyusb>=1.3.1",
6
+ # ]
7
+ # ///
8
+
9
+ # This is a clone of usbliter8ctl using the helpers in this module.
10
+ # Used for remote_boot
11
+
12
+ import argparse
13
+ import pathlib
14
+
15
+ import usb
16
+
17
+ from dfu import Usbliter8Command, send_usbliter8_command, usbliter8_download
18
+ from helpers import get_device
19
+
20
+
21
+ def do_boot(args, dev):
22
+ usbliter8_download(dev, args.iboot.read_bytes())
23
+
24
+ send_usbliter8_command(dev, Usbliter8Command.CUSTOM_BOOT, None, 100)
25
+ send_usbliter8_command(dev, Usbliter8Command.DFU_ABORT, None, 100)
26
+
27
+
28
+ def do_demote(args, dev):
29
+ send_usbliter8_command(dev, Usbliter8Command.CUSTOM_DEMOTE, None, 100)
30
+
31
+
32
+ def main():
33
+ parser = argparse.ArgumentParser(description="Love is Control")
34
+
35
+ subparsers = parser.add_subparsers()
36
+
37
+ boot_parser = subparsers.add_parser("boot", help="boot raw iBoot")
38
+ boot_parser.set_defaults(func=do_boot)
39
+ boot_parser.add_argument("iboot", type=pathlib.Path)
40
+
41
+ demote_parser = subparsers.add_parser("demote", help="demote production mode")
42
+ demote_parser.set_defaults(func=do_demote)
43
+
44
+ args = parser.parse_args()
45
+ if not hasattr(args, "func"):
46
+ parser.print_help()
47
+ exit(-1)
48
+
49
+ dev = get_device()
50
+
51
+ srnm = dev.serial_number
52
+
53
+ if "PWND:[" not in srnm:
54
+ raise RuntimeError("this is not Pwned DFU device")
55
+
56
+ args.func(args, dev)
57
+
58
+
59
+ if __name__ == "__main__":
60
+ main()
@@ -0,0 +1,26 @@
1
+ Metadata-Version: 2.4
2
+ Name: iphonetool
3
+ Version: 0.1.0
4
+ Summary: All of the modern iOS tools. Unified
5
+ Author: Jacob Freeman
6
+ Author-email: Jacob Freeman <t00niyf5@duck.com>
7
+ License-Expression: GPL-3.0-or-later
8
+ License-File: LICENSE
9
+ Requires-Dist: pymobiledevice3>=10.1.0
10
+ Requires-Dist: pyusb>=1.3.1
11
+ Requires-Python: >=3.13
12
+ Description-Content-Type: text/markdown
13
+
14
+ # iphonetool
15
+
16
+ All of the modern iOS tools. Unified.
17
+
18
+
19
+ # Usage
20
+
21
+ - Run `iphonetool.py info` to give info on connected devices
22
+ - Run `iphonetool.py -h` to show help for the current device (this help changes per-device)
23
+
24
+ # License
25
+
26
+ `iphonetool` is currently licensed under the GPLv3.