├── .gitignore ├── deleo ├── __init__.py ├── __main__.py ├── recovery.py └── restore.py ├── install.sh ├── .pre-commit-config.yaml ├── .github └── workflows │ └── pypi-build-publish.yml ├── pyproject.toml ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | venv 3 | .DS_Store 4 | pyfuturerestore/.DS_Store 5 | .idea 6 | 7 | -------------------------------------------------------------------------------- /deleo/__init__.py: -------------------------------------------------------------------------------- 1 | from importlib.metadata import version 2 | 3 | __version__ = version(__package__) 4 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | rm -r dist 4 | poetry build 5 | python3 -m pip install $(ls dist/*.tar.gz) 6 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/astral-sh/ruff-pre-commit 3 | rev: v0.4.1 4 | hooks: 5 | - id: ruff 6 | args: [--fix, --unsafe-fixes] 7 | - id: ruff-format 8 | -------------------------------------------------------------------------------- /.github/workflows/pypi-build-publish.yml: -------------------------------------------------------------------------------- 1 | name: Upload Python Package 2 | 3 | on: 4 | release: 5 | types: published 6 | 7 | jobs: 8 | build_pkgs: 9 | name: Build packages 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v4 14 | 15 | - name: Build sdist 16 | run: pipx run build 17 | 18 | - uses: actions/upload-artifact@v4 19 | with: 20 | name: packages 21 | path: dist/* 22 | 23 | pypi-publish: 24 | name: Upload release to PyPI 25 | runs-on: ubuntu-latest 26 | environment: 27 | name: PyPI 28 | url: https://pypi.org/p/deleo 29 | permissions: 30 | id-token: write 31 | 32 | steps: 33 | - uses: actions/download-artifact@v4 34 | with: 35 | name: packages 36 | path: dist 37 | 38 | - name: Publish package distributions to PyPI 39 | uses: pypa/gh-action-pypi-publish@release/v1 -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "deleo" 3 | version = "0.1" 4 | description = "A Python CLI tool for downgrading i(Pad)OS devices." 5 | authors = ["m1stadev "] 6 | license = "MIT" 7 | readme = "README.md" 8 | repository = "https://github.com/m1stadev/deleo" 9 | 10 | [tool.poetry.dependencies] 11 | python = "^3.8" 12 | click = "^8.1.6" 13 | coloredlogs = "^15.0.1" 14 | remotezip = "^0.12.1" 15 | ipsw-parser = "^1.1.5" 16 | tqdm = "^4.65.0" 17 | pyimg4 = "0.8" 18 | # Be careful when changing required pymd3 version, any update that changes any restore-related code will most likely break deleo. 19 | pymobiledevice3 = "4.1.1" 20 | 21 | [tool.poetry.scripts] 22 | deleo = "deleo.__main__:main" 23 | 24 | [tool.poetry.urls] 25 | "Bug Tracker" = "https://github.com/m1stadev/deleo/issues" 26 | 27 | [tool.ruff.lint] 28 | extend-select = ["I"] 29 | 30 | [tool.ruff.format] 31 | quote-style = "single" 32 | 33 | [build-system] 34 | requires = ["poetry-core"] 35 | build-backend = "poetry.core.masonry.api" 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 adam 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 |

2 | deleo 3 |

4 |

By m1sta. 5 | 6 |

7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 |

18 | 19 |

20 | A Python CLI tool for downgrading i(Pad)OS devices. 21 |

22 | 23 | ## Usage 24 | ``` 25 | Usage: deleo [OPTIONS] IPSW LATEST_IPSW 26 | 27 | A Python CLI tool for downgrading i(Pad)OS devices. 28 | 29 | Options: 30 | --ecid INTEGER 31 | -v, --verbose 32 | --version Show the version and exit. 33 | -t, --shsh-blob FILENAME SHSH blob for target restore. [required] 34 | -u, --update Keep user data during restore (not recommended if downgrading). 35 | -o, --ota-manifest FILENAME OTA build manifest for latest IPSW. 36 | --help Show this message and exit. 37 | ``` 38 | ## Requirements 39 | - Python 3.8 or higher 40 | - Valid SHSH blobs 41 | - A Linux or macOS system 42 | - Windows support will be coming in the future 43 | - `usbmuxd` on Linux systems 44 | 45 | ## Notes 46 | - deleo only supports 64-bit devices. 47 | - In most cases, you can only restore using a signed 15.x or below IPSW as latest. 48 | - More info on that here. 49 | - In place of an actual IPSW file in the `IPSW` or `LATEST_IPSW` arguments, you can pass a URL to an IPSW instead. 50 | - This is not recommended for the `IPSW` argument, as downloading the RootFS dmg directly from the ZIP will take quite a while... 51 | - Ensure that whatever version you are restoring to is compatible with the SEP version in the latest IPSW. 52 | - You can find a spreadsheet that will show you what iOS versions are compatible with the latest SEP version here. 53 | - On Linux systems that utilize `udev`, you may need to install proper `udev` rules to have proper access to connected *OS devices 54 | - Typically, you only need to install `libirecovery` from your distribution's package manager. 55 | - Alternatively, you can download a rules file provided here, and place it in `/etc/udev/rules.d` 56 | - Once the rules file is installed, reboot to ensure that the rules file is detected properly. 57 | 58 | 59 | 60 | ## Installation 61 | - Install from [PyPI](https://pypi.org/project/deleo/): 62 | - ```python3 -m pip install deleo``` 63 | - Local installation: 64 | - `./install.sh` 65 | - Requires [Poetry](https://python-poetry.org) 66 | 67 | ## Support 68 | 69 | For any questions/issues you have, [open an issue](https://github.com/m1stadev/deleo/issues). 70 | -------------------------------------------------------------------------------- /deleo/__main__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import plistlib 3 | import traceback 4 | from typing import BinaryIO, Optional 5 | from zipfile import ZipFile 6 | 7 | import click 8 | import coloredlogs 9 | from pymobiledevice3.cli.restore import Command 10 | from pymobiledevice3.irecv import IRecv 11 | from pymobiledevice3.lockdown import LockdownClient 12 | from pymobiledevice3.restore.device import Device 13 | from pymobiledevice3.restore.recovery import Behavior 14 | from remotezip import RemoteZip 15 | 16 | from deleo import __version__ 17 | from deleo.restore import Restore 18 | 19 | coloredlogs.install(level=logging.INFO) 20 | 21 | logging.getLogger('quic').disabled = True 22 | logging.getLogger('asyncio').disabled = True 23 | logging.getLogger('zeroconf').disabled = True 24 | logging.getLogger('parso.cache').disabled = True 25 | logging.getLogger('parso.cache.pickle').disabled = True 26 | logging.getLogger('parso.python.diff').disabled = True 27 | logging.getLogger('humanfriendly.prompts').disabled = True 28 | logging.getLogger('blib2to3.pgen2.driver').disabled = True 29 | logging.getLogger('urllib3.connectionpool').disabled = True 30 | 31 | logger = logging.getLogger(__name__) 32 | 33 | 34 | # TODO: Add rest of arguments 35 | @click.command(cls=Command) 36 | @click.version_option(message=f'deleo {__version__}') 37 | @click.option( 38 | '-t', 39 | '--shsh-blob', 40 | 'shsh_blob', 41 | type=click.File('rb'), 42 | help='SHSH blob for target restore.', 43 | required=True, 44 | ) 45 | @click.option( 46 | '-u', 47 | '--update', 48 | 'update_install', 49 | is_flag=True, 50 | help='Keep user data during restore (not recommended if downgrading).', 51 | ) 52 | @click.option( 53 | '-o', 54 | '--ota-manifest', 55 | 'ota_manifest', 56 | type=click.File('rb'), 57 | help='OTA build manifest for latest IPSW.', 58 | ) 59 | @click.argument('ipsw') 60 | @click.argument('latest_ipsw') 61 | def main( 62 | device, 63 | shsh_blob: BinaryIO, 64 | ota_manifest: Optional[BinaryIO], 65 | ipsw: str, 66 | latest_ipsw: str, 67 | update_install: bool, 68 | ): 69 | """A Python CLI tool for downgrading i(Pad)OS devices.""" 70 | 71 | shsh = plistlib.load(shsh_blob) 72 | 73 | if ipsw.startswith('http://') or ipsw.startswith('https://'): 74 | ipsw = RemoteZip(ipsw) 75 | else: 76 | ipsw = ZipFile(ipsw) 77 | 78 | if latest_ipsw.startswith('http://') or latest_ipsw.startswith('https://'): 79 | latest_ipsw = RemoteZip(latest_ipsw) 80 | else: 81 | latest_ipsw = ZipFile(latest_ipsw) 82 | 83 | lockdown = None 84 | irecv = None 85 | if isinstance(device, LockdownClient): 86 | lockdown = device 87 | elif isinstance(device, IRecv): 88 | irecv = device 89 | device = Device(lockdown=lockdown, irecv=irecv) 90 | 91 | if update_install: 92 | behavior = Behavior.Update 93 | if 'updateInstall' not in shsh.keys(): 94 | raise click.BadParameter( 95 | f'Provided SHSH blob does not support update install: {shsh_blob.name}' 96 | ) 97 | shsh = shsh['updateInstall'] 98 | else: 99 | behavior = Behavior.Erase 100 | 101 | if ota_manifest: 102 | manifest_data = ota_manifest.read() 103 | else: 104 | manifest_data = None 105 | 106 | try: 107 | Restore( 108 | ipsw, latest_ipsw, device, shsh, behavior, ota_manifest=manifest_data 109 | ).update() 110 | except Exception: 111 | # click may "swallow" several exception types so we try to catch them all here 112 | traceback.print_exc() 113 | raise 114 | 115 | 116 | if __name__ == '__main__': 117 | main() 118 | -------------------------------------------------------------------------------- /deleo/recovery.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import Mapping, Optional 3 | from zipfile import ZipFile 4 | 5 | from ipsw_parser.build_manifest import BuildManifest 6 | from ipsw_parser.ipsw import IPSW 7 | from pymobiledevice3.exceptions import PyMobileDevice3Exception 8 | from pymobiledevice3.restore import recovery 9 | from pymobiledevice3.restore.base_restore import BaseRestore, Behavior 10 | from pymobiledevice3.restore.device import Device 11 | from pymobiledevice3.restore.recovery import ( 12 | RESTORE_VARIANT_ERASE_INSTALL, 13 | RESTORE_VARIANT_UPGRADE_INSTALL, 14 | ) 15 | from pymobiledevice3.restore.tss import TSSRequest, TSSResponse 16 | 17 | RESTORE_VARIANT_OTA_UPGRADE = 'Customer Software Update' 18 | 19 | 20 | class Recovery(recovery.Recovery): 21 | def __init__( 22 | self, 23 | ipsw: ZipFile, 24 | latest_ipsw: ZipFile, 25 | device: Device, 26 | shsh: Mapping, 27 | behavior: Behavior, 28 | tss: Optional[Mapping] = None, 29 | ota_manifest: Optional[bytes] = None, 30 | ): 31 | BaseRestore.__init__( 32 | self, ipsw, device, tss, behavior, logger=logging.getLogger(__name__) 33 | ) 34 | self.tss_localpolicy = None 35 | self.tss_recoveryos_root_ticket = None 36 | self.restore_boot_args = None 37 | self.latest_ipsw = IPSW(latest_ipsw) 38 | self.shsh = TSSResponse(shsh) 39 | 40 | self.logger.debug( 41 | 'scanning 2nd BuildManifest.plist for the correct BuildIdentity' 42 | ) 43 | 44 | if ota_manifest: 45 | ota_manifest = BuildManifest(self.latest_ipsw, ota_manifest) 46 | self.latest_build_identity = ota_manifest.get_build_identity( 47 | self.device.hardware_model, 48 | restore_behavior=Behavior.Update.value, 49 | variant=RESTORE_VARIANT_OTA_UPGRADE, 50 | ) 51 | 52 | else: 53 | variant = { 54 | Behavior.Update: RESTORE_VARIANT_UPGRADE_INSTALL, 55 | Behavior.Erase: RESTORE_VARIANT_ERASE_INSTALL, 56 | }[behavior] 57 | 58 | self.latest_build_identity = ( 59 | self.latest_ipsw.build_manifest.get_build_identity( 60 | self.device.hardware_model, 61 | restore_behavior=behavior.value, 62 | variant=variant, 63 | ) 64 | ) 65 | 66 | build_info = self.latest_build_identity.get('Info') 67 | if build_info is None: 68 | raise PyMobileDevice3Exception( 69 | 'build identity does not contain an "Info" element' 70 | ) 71 | 72 | device_class = build_info.get('DeviceClass') 73 | if device_class is None: 74 | raise PyMobileDevice3Exception( 75 | 'build identity does not contain an "DeviceClass" element' 76 | ) 77 | 78 | def get_tss_response(self): 79 | # populate parameters 80 | parameters = dict() 81 | 82 | parameters['ApECID'] = self.device.ecid 83 | if self.device.ap_nonce is not None: 84 | parameters['ApNonce'] = self.device.ap_nonce 85 | 86 | if self.device.sep_nonce is not None: 87 | parameters['ApSepNonce'] = self.device.sep_nonce 88 | 89 | parameters['ApProductionMode'] = True 90 | 91 | if self.device.is_image4_supported: 92 | parameters['ApSecurityMode'] = True 93 | parameters['ApSupportsImg4'] = True 94 | else: 95 | parameters['ApSupportsImg4'] = False 96 | 97 | self.latest_build_identity.populate_tss_request_parameters(parameters) 98 | 99 | tss = TSSRequest() 100 | tss.add_common_tags(parameters) 101 | tss.add_ap_tags(parameters) 102 | 103 | # add personalized parameters 104 | if self.device.is_image4_supported: 105 | tss.add_ap_img4_tags(parameters) 106 | else: 107 | tss.add_ap_img3_tags(parameters) 108 | 109 | # normal mode; request baseband ticket as well 110 | if self.device.lockdown is not None: 111 | pinfo = self.device.preflight_info 112 | if pinfo: 113 | self.logger.debug('adding preflight info') 114 | 115 | node = pinfo.get('Nonce') 116 | if node is not None: 117 | parameters['BbNonce'] = node 118 | 119 | node = pinfo.get('ChipID') 120 | if node is not None: 121 | parameters['BbChipID'] = node 122 | 123 | node = pinfo.get('CertID') 124 | if node is not None: 125 | parameters['BbGoldCertId'] = node 126 | 127 | node = pinfo.get('ChipSerialNo') 128 | if node is not None: 129 | parameters['BbSNUM'] = node 130 | 131 | tss.add_baseband_tags(parameters) 132 | 133 | euiccchipid = pinfo.get('EUICCChipID') 134 | if euiccchipid: 135 | self.logger.debug('adding EUICCChipID info') 136 | parameters['eUICC,ChipID'] = euiccchipid 137 | 138 | if euiccchipid >= 5: 139 | node = pinfo.get('EUICCCSN') 140 | if node is not None: 141 | parameters['eUICC,EID'] = node 142 | 143 | node = pinfo.get('EUICCCertIdentifier') 144 | if node is not None: 145 | parameters['eUICC,RootKeyIdentifier'] = node 146 | 147 | node = pinfo.get('EUICCGoldNonce') 148 | if node is not None: 149 | parameters['EUICCGoldNonce'] = node 150 | 151 | node = pinfo.get('EUICCMainNonce') 152 | if node is not None: 153 | parameters['EUICCMainNonce'] = node 154 | 155 | tss.add_vinyl_tags(parameters) 156 | 157 | # send request and grab response 158 | return tss.send_receive() 159 | 160 | def send_component(self, name: str): 161 | if name == 'RestoreSEP': 162 | data = self.latest_build_identity.get_component( 163 | name, tss=self.tss 164 | ).personalized_data 165 | else: 166 | data = self.build_identity.get_component( 167 | name, tss=self.shsh 168 | ).personalized_data 169 | 170 | self.logger.info(f'Sending {name} ({len(data)} bytes)...') 171 | self.device.irecv.send_buffer(data) 172 | -------------------------------------------------------------------------------- /deleo/restore.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import plistlib 3 | import struct 4 | from typing import Mapping, Optional 5 | from zipfile import ZipFile 6 | 7 | from ipsw_parser.ipsw import IPSW 8 | from pymobiledevice3.exceptions import ( 9 | ConnectionFailedError, 10 | NoDeviceConnectedError, 11 | PyMobileDevice3Exception, 12 | ) 13 | from pymobiledevice3.restore import restore 14 | from pymobiledevice3.restore.base_restore import BaseRestore 15 | from pymobiledevice3.restore.device import Device 16 | from pymobiledevice3.restore.ftab import Ftab 17 | from pymobiledevice3.restore.recovery import Behavior 18 | from pymobiledevice3.restore.restored_client import RestoredClient 19 | from pymobiledevice3.restore.tss import TSSRequest, TSSResponse 20 | from pymobiledevice3.service_connection import ServiceConnection 21 | from pymobiledevice3.utils import plist_access_path 22 | from tqdm import trange 23 | 24 | from deleo.recovery import Recovery 25 | 26 | 27 | class Restore(restore.Restore): 28 | def __init__( 29 | self, 30 | ipsw: ZipFile, 31 | latest_ipsw: ZipFile, 32 | device: Device, 33 | shsh: Mapping, 34 | behavior: Behavior, 35 | tss: Optional[Mapping] = None, 36 | ota_manifest: Optional[bytes] = None, 37 | ): 38 | BaseRestore.__init__( 39 | self, ipsw, device, tss, behavior, logger=logging.getLogger(__name__) 40 | ) 41 | self.recovery = Recovery( 42 | ipsw, 43 | latest_ipsw, 44 | device, 45 | shsh, 46 | behavior, 47 | tss=tss, 48 | ota_manifest=ota_manifest, 49 | ) 50 | self.bbtss: Optional[TSSResponse] = None 51 | self._restored: Optional[RestoredClient] = None 52 | self._restore_finished = False 53 | 54 | # used when ignore_fdr=True, to store an active FDR connection just to make the device believe it can actually 55 | # perform an FDR communication, but without really establishing any 56 | self._fdr: Optional[ServiceConnection] = None 57 | self._ignore_fdr = False 58 | 59 | # query preflight info while device may still be in normal mode 60 | self._preflight_info = self.device.preflight_info 61 | 62 | # prepare progress bar for OS component verify 63 | self._pb_verify_restore = None 64 | self._pb_verify_restore_old_value = None 65 | 66 | self._handlers = { 67 | # data request messages are sent by restored whenever it requires 68 | # files sent to the server by the client. these data requests include 69 | # SystemImageData, RootTicket, KernelCache, NORData and BasebandData requests 70 | 'DataRequestMsg': self.handle_data_request_msg, 71 | # restore logs are available if a previous restore failed 72 | 'PreviousRestoreLogMsg': self.handle_previous_restore_log_msg, 73 | # progress notification messages sent by the restored inform the client 74 | # of it's current operation and sometimes percent of progress is complete 75 | 'ProgressMsg': self.handle_progress_msg, 76 | # status messages usually indicate the current state of the restored 77 | # process or often to signal an error has been encountered 78 | 'StatusMsg': self.handle_status_msg, 79 | # checkpoint notifications 80 | 'CheckpointMsg': self.handle_checkpoint_msg, 81 | # baseband update message 82 | 'BBUpdateStatusMsg': self.handle_bb_update_status_msg, 83 | # baseband updater output data request 84 | 'BasebandUpdaterOutputData': self.handle_baseband_updater_output_data, 85 | } 86 | 87 | self._data_request_handlers = { 88 | # this request is sent when restored is ready to receive the filesystem 89 | 'SystemImageData': self.send_filesystem, 90 | 'BuildIdentityDict': self.send_buildidentity, 91 | 'PersonalizedBootObjectV3': self.send_personalized_boot_object_v3, 92 | 'SourceBootObjectV4': self.send_source_boot_object_v4, 93 | 'RecoveryOSLocalPolicy': self.send_restore_local_policy, 94 | # this request is sent when restored is ready to receive the filesystem 95 | 'RecoveryOSASRImage': self.send_filesystem, 96 | # Send RecoveryOS RTD 97 | 'RecoveryOSRootTicketData': self.send_recovery_os_root_ticket, 98 | # send RootTicket (== APTicket from the TSS request) 99 | 'RootTicket': self.send_root_ticket, 100 | 'NORData': self.send_nor, 101 | 'BasebandData': self.send_baseband_data, 102 | 'FDRTrustData': self.send_fdr_trust_data, 103 | 'FirmwareUpdaterData': self.send_firmware_updater_data, 104 | # TODO: verify 105 | 'FirmwareUpdaterPreflight': self.send_firmware_updater_preflight, 106 | } 107 | 108 | self._data_request_components = { 109 | 'KernelCache': self.send_component, 110 | 'DeviceTree': self.send_component, 111 | } 112 | 113 | self.latest_ipsw = IPSW(latest_ipsw) 114 | self.shsh = TSSResponse(shsh) 115 | 116 | def send_personalized_boot_object_v3(self, message: Mapping): 117 | self.logger.debug('send_personalized_boot_object_v3') 118 | image_name = message['Arguments']['ImageName'] 119 | component_name = image_name 120 | self.logger.info(f'About to send {component_name}...') 121 | 122 | if image_name == '__GlobalManifest__': 123 | data = self.extract_global_manifest() 124 | elif image_name == '__RestoreVersion__': 125 | data = self.ipsw.restore_version 126 | elif image_name == '__SystemVersion__': 127 | data = self.ipsw.system_version 128 | else: 129 | data = self.build_identity.get_component( 130 | component_name, tss=self.recovery.shsh 131 | ).personalized_data 132 | 133 | self.logger.info(f'Sending {component_name} now...') 134 | chunk_size = 8192 135 | for i in trange(0, len(data), chunk_size): 136 | self._restored.send({'FileData': data[i : i + chunk_size]}) 137 | 138 | # Send FileDataDone 139 | self._restored.send({'FileDataDone': True}) 140 | 141 | self.logger.info(f'Done sending {component_name}') 142 | 143 | def send_source_boot_object_v4(self, message: Mapping): 144 | self.logger.debug('send_source_boot_object_v4') 145 | image_name = message['Arguments']['ImageName'] 146 | component_name = image_name 147 | self.logger.info(f'About to send {component_name}...') 148 | 149 | if image_name == '__GlobalManifest__': 150 | data = self.extract_global_manifest() 151 | elif image_name == '__RestoreVersion__': 152 | data = self.ipsw.restore_version 153 | elif image_name == '__SystemVersion__': 154 | data = self.ipsw.system_version 155 | else: 156 | data = ( 157 | self.get_build_identity_from_request(message) 158 | .get_component(component_name, tss=self.recovery.shsh) 159 | .data 160 | ) 161 | 162 | self.logger.info(f'Sending {component_name} now...') 163 | chunk_size = 8192 164 | for i in trange(0, len(data), chunk_size): 165 | self._restored.send({'FileData': data[i : i + chunk_size]}) 166 | 167 | # Send FileDataDone 168 | self._restored.send({'FileDataDone': True}) 169 | 170 | self.logger.info(f'Done sending {component_name}') 171 | 172 | def send_root_ticket(self, message: Mapping): 173 | self.logger.info('About to send RootTicket...') 174 | 175 | if self.recovery.shsh is None: 176 | raise PyMobileDevice3Exception('Cannot send RootTicket without SHSH blob') 177 | 178 | self.logger.info('Sending RootTicket now...') 179 | self._restored.send({'RootTicketData': self.recovery.shsh.ap_img4_ticket}) 180 | 181 | def send_nor(self, message: Mapping): 182 | self.logger.info('About to send NORData...') 183 | flash_version_1 = False 184 | llb_path = self.build_identity.get_component('LLB', tss=self.recovery.shsh).path 185 | llb_filename_offset = llb_path.find('LLB') 186 | 187 | arguments = message.get('Arguments') 188 | if arguments: 189 | flash_version_1 = arguments.get('FlashVersion1', False) 190 | 191 | if llb_filename_offset == -1: 192 | raise PyMobileDevice3Exception( 193 | 'Unable to extract firmware path from LLB filename' 194 | ) 195 | 196 | firmware_path = llb_path[: llb_filename_offset - 1] 197 | self.logger.info(f'Found firmware path: {firmware_path}') 198 | 199 | firmware_files = dict() 200 | try: 201 | firmware = self.ipsw.get_firmware(firmware_path) 202 | firmware_files = firmware.get_files() 203 | except KeyError: 204 | self.logger.info('Getting firmware manifest from build identity') 205 | build_id_manifest = self.build_identity['Manifest'] 206 | for component, manifest_entry in build_id_manifest.items(): 207 | if isinstance(manifest_entry, dict): 208 | is_fw = plist_access_path( 209 | manifest_entry, ('Info', 'IsFirmwarePayload'), bool 210 | ) 211 | loaded_by_iboot = plist_access_path( 212 | manifest_entry, ('Info', 'IsLoadedByiBoot'), bool 213 | ) 214 | is_secondary_fw = plist_access_path( 215 | manifest_entry, ('Info', 'IsSecondaryFirmwarePayload'), bool 216 | ) 217 | 218 | if is_fw or (is_secondary_fw and loaded_by_iboot): 219 | comp_path = plist_access_path(manifest_entry, ('Info', 'Path')) 220 | if comp_path: 221 | firmware_files[component] = comp_path 222 | 223 | if not firmware_files: 224 | raise PyMobileDevice3Exception('Unable to get list of firmware files.') 225 | 226 | component = 'LLB' 227 | llb_data = self.build_identity.get_component( 228 | component, tss=self.recovery.shsh, path=llb_path 229 | ).personalized_data 230 | req = {'LlbImageData': llb_data} 231 | 232 | if flash_version_1: 233 | norimage = {} 234 | else: 235 | norimage = [] 236 | 237 | for component, comppath in firmware_files.items(): 238 | if component in ('LLB', 'RestoreSEP'): 239 | # skip LLB, it's already passed in LlbImageData 240 | # skip RestoreSEP, it's passed in RestoreSEPImageData 241 | continue 242 | 243 | nor_data = self.build_identity.get_component( 244 | component, tss=self.recovery.shsh, path=comppath 245 | ).personalized_data 246 | 247 | if flash_version_1: 248 | norimage[component] = nor_data 249 | else: 250 | # make sure iBoot is the first entry in the array 251 | if component.startswith('iBoot'): 252 | norimage = [nor_data] + norimage 253 | else: 254 | norimage.append(nor_data) 255 | 256 | req['NorImageData'] = norimage 257 | 258 | for component in ('RestoreSEP', 'SEP'): 259 | comp = self.recovery.latest_build_identity.get_component( 260 | component, tss=self.recovery.tss 261 | ) 262 | if comp.path: 263 | req[f'{component}ImageData'] = comp.personalized_data 264 | 265 | self.logger.info('Sending NORData now...') 266 | self._restored.send(req) 267 | 268 | def send_baseband_data(self, message: Mapping): 269 | self.logger.info(f'About to send BasebandData: {message}') 270 | 271 | # NOTE: this function is called 2 or 3 times! 272 | 273 | # setup request data 274 | arguments = message['Arguments'] 275 | bb_chip_id = arguments.get('ChipID') 276 | bb_cert_id = arguments.get('CertID') 277 | bb_snum = arguments.get('ChipSerialNo') 278 | bb_nonce = arguments.get('Nonce') 279 | bbtss = self.bbtss 280 | 281 | if (bb_nonce is None) or (self.bbtss is None): 282 | # populate parameters 283 | parameters = {'ApECID': self.device.ecid} 284 | if bb_nonce: 285 | parameters['BbNonce'] = bb_nonce 286 | parameters['BbChipID'] = bb_chip_id 287 | parameters['BbGoldCertId'] = bb_cert_id 288 | parameters['BbSNUM'] = bb_snum 289 | 290 | self.recovery.latest_build_identity.populate_tss_request_parameters( 291 | parameters 292 | ) 293 | 294 | # create baseband request 295 | request = TSSRequest() 296 | 297 | # add baseband parameters 298 | request.add_common_tags(parameters) 299 | request.add_baseband_tags(parameters) 300 | 301 | fdr_support = self.recovery.latest_build_identity['Info'].get( 302 | 'FDRSupport', False 303 | ) 304 | if fdr_support: 305 | request.update({'ApProductionMode': True, 'ApSecurityMode': True}) 306 | 307 | self.logger.info('Sending Baseband TSS request...') 308 | bbtss = request.send_receive() 309 | 310 | if bb_nonce: 311 | # keep the response for later requests 312 | self.bbtss = bbtss 313 | 314 | # get baseband firmware file path from build identity 315 | bbfwpath = self.recovery.latest_build_identity['Manifest']['BasebandFirmware'][ 316 | 'Info' 317 | ]['Path'] 318 | 319 | # extract baseband firmware to temp file 320 | bbfw = self.latest_ipsw.read(bbfwpath) 321 | 322 | buffer = self.sign_bbfw(bbfw, bbtss, bb_nonce) 323 | 324 | self.logger.info('Sending BasebandData now...') 325 | self._restored.send({'BasebandData': buffer}) 326 | 327 | def send_image_data(self, message, image_list_k, image_type_k, image_data_k): 328 | self.logger.debug(f'send_image_data: {message}') 329 | arguments = message['Arguments'] 330 | want_image_list = arguments.get(image_list_k) 331 | image_name = arguments.get('ImageName') 332 | build_id_manifest = self.build_identity['Manifest'] 333 | 334 | if not want_image_list and image_name is not None: 335 | if image_name not in build_id_manifest: 336 | if image_name.startswith('Ap'): 337 | image_name = image_name.replace('Ap', 'Ap,') 338 | if image_name not in build_id_manifest: 339 | raise PyMobileDevice3Exception( 340 | f'{image_name} not in build_id_manifest' 341 | ) 342 | 343 | if image_type_k is None: 344 | image_type_k = arguments['ImageType'] 345 | 346 | if image_type_k is None: 347 | raise PyMobileDevice3Exception('missing ImageType') 348 | 349 | if want_image_list is None and image_name is None: 350 | self.logger.info(f'About to send {image_data_k}...') 351 | 352 | matched_images = [] 353 | data_dict = dict() 354 | 355 | for component, manifest_entry in build_id_manifest.items(): 356 | if not isinstance(manifest_entry, dict): 357 | continue 358 | 359 | is_image_type = manifest_entry['Info'].get(image_type_k) 360 | if is_image_type: 361 | if want_image_list: 362 | self.logger.info(f'found {component} component') 363 | matched_images.append(component) 364 | elif image_name is None or image_name == component: 365 | if image_name is None: 366 | self.logger.info( 367 | f"found {image_type_k} component '{component}'" 368 | ) 369 | else: 370 | self.logger.info(f"found component '{component}'") 371 | 372 | data_dict[component] = self.build_identity.get_component( 373 | component, tss=self.recovery.shsh 374 | ).personalized_data 375 | 376 | req = dict() 377 | if want_image_list: 378 | req[image_list_k] = matched_images 379 | self.logger.info(f'Sending {image_type_k} image list') 380 | else: 381 | if image_name: 382 | if image_name in data_dict: 383 | req[image_data_k] = data_dict[image_name] 384 | req['ImageName'] = image_name 385 | self.logger.info(f'Sending {image_type_k} for {image_name}...') 386 | else: 387 | req[image_data_k] = data_dict 388 | self.logger.info(f'Sending {image_type_k} now...') 389 | 390 | self._restored.send(req) 391 | 392 | def get_se_firmware_data( 393 | self, updater_name: str, info: Mapping, arguments: Mapping 394 | ): 395 | chip_id = info.get('SE,ChipID') 396 | if chip_id is None: 397 | chip_id = self.recovery.latest_build_identity['Manifest']['SE,ChipID'] 398 | 399 | if chip_id == 0x20211: 400 | comp_name = 'SE,Firmware' 401 | elif chip_id in (0x73, 0x64, 0xC8, 0xD2, 0x2C, 0x36): 402 | comp_name = 'SE,UpdatePayload' 403 | else: 404 | self.logger.warning( 405 | f'Unknown SE,ChipID {chip_id} detected. Restore might fail.' 406 | ) 407 | 408 | if self.recovery.latest_build_identity.has_component('SE,UpdatePayload'): 409 | comp_name = 'SE,UpdatePayload' 410 | elif self.recovery.latest_build_identity.has_component('SE,Firmware'): 411 | comp_name = 'SE,Firmware' 412 | else: 413 | raise NotImplementedError( 414 | "Neither 'SE,Firmware' nor 'SE,UpdatePayload' found in build identity." 415 | ) 416 | 417 | component_data = self.recovery.latest_build_identity.get_component( 418 | comp_name 419 | ).data 420 | 421 | if 'DeviceGeneratedTags' in arguments: 422 | response = self.get_device_generated_firmware_data( 423 | updater_name, info, arguments 424 | ) 425 | else: 426 | # create SE request 427 | request = TSSRequest() 428 | parameters = dict() 429 | 430 | # add manifest for latest build_identity to parameters 431 | self.recovery.latest_build_identity.populate_tss_request_parameters( 432 | parameters 433 | ) 434 | 435 | # add SE,* tags from info dictionary to parameters 436 | parameters.update(info) 437 | 438 | # add required tags for SE TSS request 439 | request.add_se_tags(parameters, None) 440 | 441 | self.logger.info('Sending SE TSS request...') 442 | response = request.send_receive() 443 | 444 | if 'SE,Ticket' in response: 445 | self.logger.info('Received SE ticket') 446 | else: 447 | raise PyMobileDevice3Exception( 448 | "No 'SE,Ticket' in TSS response, this might not work" 449 | ) 450 | 451 | response['FirmwareData'] = component_data 452 | 453 | return response 454 | 455 | def get_yonkers_firmware_data(self, info: Mapping): 456 | # create Yonkers request 457 | request = TSSRequest() 458 | parameters = dict() 459 | 460 | # add manifest for latest build_identity to parameters 461 | self.recovery.latest_build_identity.populate_tss_request_parameters(parameters) 462 | 463 | # add Yonkers,* tags from info dictionary to parameters 464 | parameters.update(info) 465 | 466 | # add required tags for Yonkers TSS request 467 | comp_name = request.add_yonkers_tags(parameters, None) 468 | 469 | if comp_name is None: 470 | raise PyMobileDevice3Exception( 471 | 'Could not determine Yonkers firmware component' 472 | ) 473 | 474 | self.logger.debug(f'restore_get_yonkers_firmware_data: using {comp_name}') 475 | 476 | self.logger.info('Sending SE Yonkers request...') 477 | response = request.send_receive() 478 | 479 | if 'Yonkers,Ticket' in response: 480 | self.logger.info('Received SE ticket') 481 | else: 482 | raise PyMobileDevice3Exception( 483 | "No 'Yonkers,Ticket' in TSS response, this might not work" 484 | ) 485 | 486 | # now get actual component data 487 | component_data = self.recovery.latest_build_identity.get_component( 488 | comp_name 489 | ).data 490 | 491 | firmware_data = { 492 | 'YonkersFirmware': component_data, 493 | } 494 | 495 | response['FirmwareData'] = firmware_data 496 | 497 | return response 498 | 499 | def get_savage_firmware_data(self, info: Mapping): 500 | # create Savage request 501 | request = TSSRequest() 502 | parameters = dict() 503 | 504 | # add manifest for latest build_identity to parameters 505 | self.recovery.latest_build_identity.populate_tss_request_parameters(parameters) 506 | 507 | # add Savage,* tags from info dictionary to parameters 508 | parameters.update(info) 509 | 510 | # add required tags for Savage TSS request 511 | comp_name = request.add_savage_tags(parameters, None) 512 | 513 | if comp_name is None: 514 | raise PyMobileDevice3Exception( 515 | 'Could not determine Savage firmware component' 516 | ) 517 | 518 | self.logger.debug(f'restore_get_savage_firmware_data: using {comp_name}') 519 | 520 | self.logger.info('Sending SE Savage request...') 521 | response = request.send_receive() 522 | 523 | if 'Savage,Ticket' in response: 524 | self.logger.info('Received SE ticket') 525 | else: 526 | raise PyMobileDevice3Exception( 527 | "No 'Savage,Ticket' in TSS response, this might not work" 528 | ) 529 | 530 | # now get actual component data 531 | component_data = self.recovery.latest_build_identity.get_component( 532 | comp_name 533 | ).data 534 | component_data = struct.pack('