pymobiledevice3 4.14.6__py3-none-any.whl → 7.0.6__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.
Files changed (164) hide show
  1. misc/plist_sniffer.py +15 -15
  2. misc/remotexpc_sniffer.py +29 -28
  3. misc/understanding_idevice_protocol_layers.md +15 -10
  4. pymobiledevice3/__main__.py +317 -127
  5. pymobiledevice3/_version.py +22 -4
  6. pymobiledevice3/bonjour.py +358 -113
  7. pymobiledevice3/ca.py +253 -16
  8. pymobiledevice3/cli/activation.py +31 -23
  9. pymobiledevice3/cli/afc.py +49 -40
  10. pymobiledevice3/cli/amfi.py +16 -21
  11. pymobiledevice3/cli/apps.py +87 -42
  12. pymobiledevice3/cli/backup.py +160 -90
  13. pymobiledevice3/cli/bonjour.py +44 -40
  14. pymobiledevice3/cli/cli_common.py +204 -198
  15. pymobiledevice3/cli/companion_proxy.py +14 -14
  16. pymobiledevice3/cli/crash.py +105 -56
  17. pymobiledevice3/cli/developer/__init__.py +62 -0
  18. pymobiledevice3/cli/developer/accessibility/__init__.py +65 -0
  19. pymobiledevice3/cli/developer/accessibility/settings.py +43 -0
  20. pymobiledevice3/cli/developer/arbitration.py +50 -0
  21. pymobiledevice3/cli/developer/condition.py +33 -0
  22. pymobiledevice3/cli/developer/core_device.py +294 -0
  23. pymobiledevice3/cli/developer/debugserver.py +244 -0
  24. pymobiledevice3/cli/developer/dvt/__init__.py +438 -0
  25. pymobiledevice3/cli/developer/dvt/core_profile_session.py +295 -0
  26. pymobiledevice3/cli/developer/dvt/simulate_location.py +56 -0
  27. pymobiledevice3/cli/developer/dvt/sysmon/__init__.py +69 -0
  28. pymobiledevice3/cli/developer/dvt/sysmon/process.py +188 -0
  29. pymobiledevice3/cli/developer/fetch_symbols.py +108 -0
  30. pymobiledevice3/cli/developer/simulate_location.py +51 -0
  31. pymobiledevice3/cli/diagnostics/__init__.py +75 -0
  32. pymobiledevice3/cli/diagnostics/battery.py +47 -0
  33. pymobiledevice3/cli/idam.py +42 -0
  34. pymobiledevice3/cli/lockdown.py +108 -103
  35. pymobiledevice3/cli/mounter.py +158 -99
  36. pymobiledevice3/cli/notification.py +38 -26
  37. pymobiledevice3/cli/pcap.py +45 -24
  38. pymobiledevice3/cli/power_assertion.py +18 -17
  39. pymobiledevice3/cli/processes.py +17 -23
  40. pymobiledevice3/cli/profile.py +165 -109
  41. pymobiledevice3/cli/provision.py +35 -34
  42. pymobiledevice3/cli/remote.py +217 -129
  43. pymobiledevice3/cli/restore.py +159 -143
  44. pymobiledevice3/cli/springboard.py +63 -53
  45. pymobiledevice3/cli/syslog.py +193 -86
  46. pymobiledevice3/cli/usbmux.py +73 -33
  47. pymobiledevice3/cli/version.py +5 -7
  48. pymobiledevice3/cli/webinspector.py +376 -214
  49. pymobiledevice3/common.py +3 -1
  50. pymobiledevice3/exceptions.py +182 -58
  51. pymobiledevice3/irecv.py +52 -53
  52. pymobiledevice3/irecv_devices.py +1489 -464
  53. pymobiledevice3/lockdown.py +473 -275
  54. pymobiledevice3/lockdown_service_provider.py +15 -8
  55. pymobiledevice3/osu/os_utils.py +27 -9
  56. pymobiledevice3/osu/posix_util.py +34 -15
  57. pymobiledevice3/osu/win_util.py +14 -8
  58. pymobiledevice3/pair_records.py +102 -21
  59. pymobiledevice3/remote/common.py +8 -4
  60. pymobiledevice3/remote/core_device/app_service.py +94 -67
  61. pymobiledevice3/remote/core_device/core_device_service.py +17 -14
  62. pymobiledevice3/remote/core_device/device_info.py +5 -5
  63. pymobiledevice3/remote/core_device/diagnostics_service.py +19 -4
  64. pymobiledevice3/remote/core_device/file_service.py +53 -23
  65. pymobiledevice3/remote/remote_service_discovery.py +79 -45
  66. pymobiledevice3/remote/remotexpc.py +73 -44
  67. pymobiledevice3/remote/tunnel_service.py +442 -317
  68. pymobiledevice3/remote/utils.py +14 -13
  69. pymobiledevice3/remote/xpc_message.py +145 -125
  70. pymobiledevice3/resources/dsc_uuid_map.py +19 -19
  71. pymobiledevice3/resources/firmware_notifications.py +20 -16
  72. pymobiledevice3/resources/notifications.txt +144 -0
  73. pymobiledevice3/restore/asr.py +27 -27
  74. pymobiledevice3/restore/base_restore.py +110 -21
  75. pymobiledevice3/restore/consts.py +87 -66
  76. pymobiledevice3/restore/device.py +59 -12
  77. pymobiledevice3/restore/fdr.py +46 -48
  78. pymobiledevice3/restore/ftab.py +19 -19
  79. pymobiledevice3/restore/img4.py +163 -0
  80. pymobiledevice3/restore/mbn.py +587 -0
  81. pymobiledevice3/restore/recovery.py +151 -151
  82. pymobiledevice3/restore/restore.py +562 -544
  83. pymobiledevice3/restore/restore_options.py +131 -110
  84. pymobiledevice3/restore/restored_client.py +51 -31
  85. pymobiledevice3/restore/tss.py +385 -267
  86. pymobiledevice3/service_connection.py +252 -59
  87. pymobiledevice3/services/accessibilityaudit.py +202 -120
  88. pymobiledevice3/services/afc.py +962 -365
  89. pymobiledevice3/services/amfi.py +24 -30
  90. pymobiledevice3/services/companion.py +23 -19
  91. pymobiledevice3/services/crash_reports.py +71 -47
  92. pymobiledevice3/services/debugserver_applist.py +3 -3
  93. pymobiledevice3/services/device_arbitration.py +8 -8
  94. pymobiledevice3/services/device_link.py +101 -79
  95. pymobiledevice3/services/diagnostics.py +973 -967
  96. pymobiledevice3/services/dtfetchsymbols.py +8 -8
  97. pymobiledevice3/services/dvt/dvt_secure_socket_proxy.py +4 -4
  98. pymobiledevice3/services/dvt/dvt_testmanaged_proxy.py +4 -4
  99. pymobiledevice3/services/dvt/instruments/activity_trace_tap.py +85 -74
  100. pymobiledevice3/services/dvt/instruments/application_listing.py +2 -3
  101. pymobiledevice3/services/dvt/instruments/condition_inducer.py +7 -6
  102. pymobiledevice3/services/dvt/instruments/core_profile_session_tap.py +466 -384
  103. pymobiledevice3/services/dvt/instruments/device_info.py +20 -11
  104. pymobiledevice3/services/dvt/instruments/energy_monitor.py +1 -1
  105. pymobiledevice3/services/dvt/instruments/graphics.py +1 -1
  106. pymobiledevice3/services/dvt/instruments/location_simulation.py +1 -1
  107. pymobiledevice3/services/dvt/instruments/location_simulation_base.py +10 -10
  108. pymobiledevice3/services/dvt/instruments/network_monitor.py +17 -17
  109. pymobiledevice3/services/dvt/instruments/notifications.py +1 -1
  110. pymobiledevice3/services/dvt/instruments/process_control.py +35 -10
  111. pymobiledevice3/services/dvt/instruments/screenshot.py +2 -2
  112. pymobiledevice3/services/dvt/instruments/sysmontap.py +15 -15
  113. pymobiledevice3/services/dvt/testmanaged/xcuitest.py +42 -52
  114. pymobiledevice3/services/file_relay.py +10 -10
  115. pymobiledevice3/services/heartbeat.py +9 -8
  116. pymobiledevice3/services/house_arrest.py +16 -15
  117. pymobiledevice3/services/idam.py +20 -0
  118. pymobiledevice3/services/installation_proxy.py +173 -81
  119. pymobiledevice3/services/lockdown_service.py +20 -10
  120. pymobiledevice3/services/misagent.py +22 -19
  121. pymobiledevice3/services/mobile_activation.py +147 -64
  122. pymobiledevice3/services/mobile_config.py +331 -294
  123. pymobiledevice3/services/mobile_image_mounter.py +141 -113
  124. pymobiledevice3/services/mobilebackup2.py +203 -145
  125. pymobiledevice3/services/notification_proxy.py +11 -11
  126. pymobiledevice3/services/os_trace.py +134 -74
  127. pymobiledevice3/services/pcapd.py +314 -302
  128. pymobiledevice3/services/power_assertion.py +10 -9
  129. pymobiledevice3/services/preboard.py +4 -4
  130. pymobiledevice3/services/remote_fetch_symbols.py +21 -14
  131. pymobiledevice3/services/remote_server.py +176 -146
  132. pymobiledevice3/services/restore_service.py +16 -16
  133. pymobiledevice3/services/screenshot.py +15 -12
  134. pymobiledevice3/services/simulate_location.py +7 -7
  135. pymobiledevice3/services/springboard.py +15 -15
  136. pymobiledevice3/services/syslog.py +5 -5
  137. pymobiledevice3/services/web_protocol/alert.py +11 -11
  138. pymobiledevice3/services/web_protocol/automation_session.py +251 -239
  139. pymobiledevice3/services/web_protocol/cdp_screencast.py +46 -37
  140. pymobiledevice3/services/web_protocol/cdp_server.py +19 -19
  141. pymobiledevice3/services/web_protocol/cdp_target.py +411 -373
  142. pymobiledevice3/services/web_protocol/driver.py +114 -111
  143. pymobiledevice3/services/web_protocol/element.py +124 -111
  144. pymobiledevice3/services/web_protocol/inspector_session.py +106 -102
  145. pymobiledevice3/services/web_protocol/selenium_api.py +49 -49
  146. pymobiledevice3/services/web_protocol/session_protocol.py +18 -12
  147. pymobiledevice3/services/web_protocol/switch_to.py +30 -27
  148. pymobiledevice3/services/webinspector.py +189 -155
  149. pymobiledevice3/tcp_forwarder.py +87 -69
  150. pymobiledevice3/tunneld/__init__.py +0 -0
  151. pymobiledevice3/tunneld/api.py +63 -0
  152. pymobiledevice3/tunneld/server.py +603 -0
  153. pymobiledevice3/usbmux.py +198 -147
  154. pymobiledevice3/utils.py +14 -11
  155. {pymobiledevice3-4.14.6.dist-info → pymobiledevice3-7.0.6.dist-info}/METADATA +55 -28
  156. pymobiledevice3-7.0.6.dist-info/RECORD +188 -0
  157. {pymobiledevice3-4.14.6.dist-info → pymobiledevice3-7.0.6.dist-info}/WHEEL +1 -1
  158. pymobiledevice3/cli/developer.py +0 -1215
  159. pymobiledevice3/cli/diagnostics.py +0 -99
  160. pymobiledevice3/tunneld.py +0 -524
  161. pymobiledevice3-4.14.6.dist-info/RECORD +0 -168
  162. {pymobiledevice3-4.14.6.dist-info → pymobiledevice3-7.0.6.dist-info}/entry_points.txt +0 -0
  163. {pymobiledevice3-4.14.6.dist-info → pymobiledevice3-7.0.6.dist-info/licenses}/LICENSE +0 -0
  164. {pymobiledevice3-4.14.6.dist-info → pymobiledevice3-7.0.6.dist-info}/top_level.txt +0 -0
@@ -7,49 +7,74 @@ import sys
7
7
  import tempfile
8
8
  import time
9
9
  from abc import ABC, abstractmethod
10
- from collections.abc import Generator
10
+ from collections.abc import AsyncIterable
11
11
  from contextlib import contextmanager, suppress
12
12
  from enum import Enum
13
13
  from functools import wraps
14
14
  from pathlib import Path
15
- from ssl import SSLZeroReturnError
15
+ from ssl import SSLError, SSLZeroReturnError, TLSVersion
16
16
  from typing import Optional
17
17
 
18
+ import construct
18
19
  from cryptography import x509
19
20
  from cryptography.hazmat.primitives import hashes, serialization
20
21
  from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey
21
22
  from cryptography.hazmat.primitives.serialization import Encoding
22
- from cryptography.hazmat.primitives.serialization.pkcs7 import PKCS7SignatureBuilder
23
+ from cryptography.hazmat.primitives.serialization.pkcs7 import PKCS7Options, PKCS7SignatureBuilder
23
24
  from packaging.version import Version
24
25
 
25
26
  from pymobiledevice3 import usbmux
26
27
  from pymobiledevice3.bonjour import DEFAULT_BONJOUR_TIMEOUT, browse_mobdev2
27
- from pymobiledevice3.ca import ca_do_everything
28
+ from pymobiledevice3.ca import generate_pairing_cert_chain
28
29
  from pymobiledevice3.common import get_home_folder
29
- from pymobiledevice3.exceptions import CannotStopSessionError, ConnectionTerminatedError, FatalPairingError, \
30
- GetProhibitedError, IncorrectModeError, InvalidConnectionError, InvalidHostIDError, InvalidServiceError, \
31
- LockdownError, MissingValueError, NotPairedError, PairingDialogResponsePendingError, PairingError, \
32
- PasswordRequiredError, SetProhibitedError, StartServiceError, UserDeniedPairingError
30
+ from pymobiledevice3.exceptions import (
31
+ BadDevError,
32
+ CannotStopSessionError,
33
+ ConnectionFailedError,
34
+ ConnectionTerminatedError,
35
+ DeviceNotFoundError,
36
+ FatalPairingError,
37
+ GetProhibitedError,
38
+ IncorrectModeError,
39
+ InvalidConnectionError,
40
+ InvalidHostIDError,
41
+ InvalidServiceError,
42
+ LockdownError,
43
+ MissingValueError,
44
+ NoDeviceConnectedError,
45
+ NotPairedError,
46
+ PairingDialogResponsePendingError,
47
+ PairingError,
48
+ PasswordRequiredError,
49
+ PyMobileDevice3Exception,
50
+ SetProhibitedError,
51
+ StartServiceError,
52
+ UserDeniedPairingError,
53
+ )
33
54
  from pymobiledevice3.irecv_devices import IRECV_DEVICES
34
55
  from pymobiledevice3.lockdown_service_provider import LockdownServiceProvider
35
- from pymobiledevice3.pair_records import create_pairing_records_cache_folder, generate_host_id, \
36
- get_preferred_pair_record
56
+ from pymobiledevice3.pair_records import (
57
+ create_pairing_records_cache_folder,
58
+ generate_host_id,
59
+ get_preferred_pair_record,
60
+ )
37
61
  from pymobiledevice3.service_connection import ServiceConnection
38
62
  from pymobiledevice3.usbmux import PlistMuxConnection
39
63
 
40
- SYSTEM_BUID = '30142955-444094379208051516'
64
+ SYSTEM_BUID = "30142955-444094379208051516"
65
+ RESTORED_SERVICE_TYPE = "com.apple.mobile.restored"
41
66
 
42
- DEFAULT_LABEL = 'pymobiledevice3'
67
+ DEFAULT_LABEL = "pymobiledevice3"
43
68
  SERVICE_PORT = 62078
44
69
 
45
70
 
46
71
  class DeviceClass(Enum):
47
- IPHONE = 'iPhone'
48
- IPAD = 'iPad'
49
- IPOD = 'iPod'
50
- WATCH = 'Watch'
51
- APPLE_TV = 'AppleTV'
52
- UNKNOWN = 'Unknown'
72
+ IPHONE = "iPhone"
73
+ IPAD = "iPad"
74
+ IPOD = "iPod"
75
+ WATCH = "Watch"
76
+ APPLE_TV = "AppleTV"
77
+ UNKNOWN = "Unknown"
53
78
 
54
79
 
55
80
  def _reconnect_on_remote_close(f):
@@ -58,7 +83,7 @@ def _reconnect_on_remote_close(f):
58
83
  transmitted). When this happens, we'll attempt to reconnect.
59
84
  """
60
85
 
61
- def _reconnect(self: 'LockdownClient'):
86
+ def _reconnect(self: "LockdownClient"):
62
87
  self._reestablish_connection()
63
88
  self.validate_pairing()
64
89
 
@@ -66,11 +91,11 @@ def _reconnect_on_remote_close(f):
66
91
  def _inner_reconnect_on_remote_close(*args, **kwargs):
67
92
  try:
68
93
  return f(*args, **kwargs)
69
- except (BrokenPipeError, ConnectionTerminatedError):
94
+ except (BrokenPipeError, ConnectionTerminatedError, SSLError):
70
95
  _reconnect(args[0])
71
96
  return f(*args, **kwargs)
72
97
  except ConnectionAbortedError:
73
- if sys.platform != 'win32':
98
+ if sys.platform != "win32":
74
99
  raise
75
100
  _reconnect(args[0])
76
101
  return f(*args, **kwargs)
@@ -79,9 +104,17 @@ def _reconnect_on_remote_close(f):
79
104
 
80
105
 
81
106
  class LockdownClient(ABC, LockdownServiceProvider):
82
- def __init__(self, service: ServiceConnection, host_id: str, identifier: str = None,
83
- label: str = DEFAULT_LABEL, system_buid: str = SYSTEM_BUID, pair_record: Optional[dict] = None,
84
- pairing_records_cache_folder: Path = None, port: int = SERVICE_PORT):
107
+ def __init__(
108
+ self,
109
+ service: ServiceConnection,
110
+ host_id: str,
111
+ identifier: Optional[str] = None,
112
+ label: str = DEFAULT_LABEL,
113
+ system_buid: str = SYSTEM_BUID,
114
+ pair_record: Optional[dict] = None,
115
+ pairing_records_cache_folder: Optional[Path] = None,
116
+ port: int = SERVICE_PORT,
117
+ ):
85
118
  """
86
119
  Create a LockdownClient instance
87
120
 
@@ -107,21 +140,31 @@ class LockdownClient(ABC, LockdownServiceProvider):
107
140
  self.pairing_records_cache_folder = pairing_records_cache_folder
108
141
  self.port = port
109
142
 
110
- if self.query_type() != 'com.apple.mobile.lockdown':
143
+ if self.query_type() != "com.apple.mobile.lockdown":
111
144
  raise IncorrectModeError()
112
145
 
113
146
  self.all_values = self.get_value()
114
- self.udid = self.all_values.get('UniqueDeviceID')
115
- self.unique_chip_id = self.all_values.get('UniqueChipID')
116
- self.device_public_key = self.all_values.get('DevicePublicKey')
117
- self.product_type = self.all_values.get('ProductType')
147
+ self.udid = self.all_values.get("UniqueDeviceID")
148
+ self.unique_chip_id = self.all_values.get("UniqueChipID")
149
+ self.device_public_key = self.all_values.get("DevicePublicKey")
150
+ self.product_type = self.all_values.get("ProductType")
118
151
 
119
152
  @classmethod
120
- def create(cls, service: ServiceConnection, identifier: str = None, system_buid: str = SYSTEM_BUID,
121
- label: str = DEFAULT_LABEL, autopair: bool = True, pair_timeout: float = None,
122
- local_hostname: str = None,
123
- pair_record: Optional[dict] = None, pairing_records_cache_folder: Path = None, port: int = SERVICE_PORT,
124
- private_key: Optional[RSAPrivateKey] = None, **cls_specific_args):
153
+ def create(
154
+ cls,
155
+ service: ServiceConnection,
156
+ identifier: Optional[str] = None,
157
+ system_buid: str = SYSTEM_BUID,
158
+ label: str = DEFAULT_LABEL,
159
+ autopair: bool = True,
160
+ pair_timeout: Optional[float] = None,
161
+ local_hostname: Optional[str] = None,
162
+ pair_record: Optional[dict] = None,
163
+ pairing_records_cache_folder: Optional[Path] = None,
164
+ port: int = SERVICE_PORT,
165
+ private_key: Optional[RSAPrivateKey] = None,
166
+ **cls_specific_args,
167
+ ):
125
168
  """
126
169
  Create a LockdownClient instance
127
170
 
@@ -143,23 +186,32 @@ class LockdownClient(ABC, LockdownServiceProvider):
143
186
  pairing_records_cache_folder = create_pairing_records_cache_folder(pairing_records_cache_folder)
144
187
 
145
188
  lockdown_client = cls(
146
- service, host_id=host_id, identifier=identifier, label=label, system_buid=system_buid,
147
- pair_record=pair_record, pairing_records_cache_folder=pairing_records_cache_folder, port=port,
148
- **cls_specific_args)
189
+ service,
190
+ host_id=host_id,
191
+ identifier=identifier,
192
+ label=label,
193
+ system_buid=system_buid,
194
+ pair_record=pair_record,
195
+ pairing_records_cache_folder=pairing_records_cache_folder,
196
+ port=port,
197
+ **cls_specific_args,
198
+ )
149
199
  lockdown_client._handle_autopair(autopair, pair_timeout, private_key=private_key)
150
200
  return lockdown_client
151
201
 
152
202
  def __repr__(self) -> str:
153
- return f'<{self.__class__.__name__} ID:{self.identifier} VERSION:{self.product_version} ' \
154
- f'TYPE:{self.product_type} PAIRED:{self.paired}>'
203
+ return (
204
+ f"<{self.__class__.__name__} ID:{self.identifier} VERSION:{self.product_version} "
205
+ f"TYPE:{self.product_type} PAIRED:{self.paired}>"
206
+ )
155
207
 
156
- def __enter__(self) -> 'LockdownClient':
208
+ def __enter__(self) -> "LockdownClient":
157
209
  return self
158
210
 
159
211
  def __exit__(self, exc_type, exc_val, exc_tb) -> None:
160
212
  self.close()
161
213
 
162
- async def __aenter__(self) -> 'LockdownClient':
214
+ async def __aenter__(self) -> "LockdownClient":
163
215
  return self
164
216
 
165
217
  async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
@@ -167,24 +219,28 @@ class LockdownClient(ABC, LockdownServiceProvider):
167
219
 
168
220
  @property
169
221
  def product_version(self) -> str:
170
- return self.all_values.get('ProductVersion')
222
+ return self.all_values.get("ProductVersion") or "1.0"
223
+
224
+ @property
225
+ def product_build_version(self) -> str:
226
+ return self.all_values.get("BuildVersion")
171
227
 
172
228
  @property
173
229
  def device_class(self) -> DeviceClass:
174
230
  try:
175
- return DeviceClass(self.all_values.get('DeviceClass'))
231
+ return DeviceClass(self.all_values.get("DeviceClass"))
176
232
  except ValueError:
177
- return DeviceClass('Unknown')
233
+ return DeviceClass("Unknown")
178
234
 
179
235
  @property
180
236
  def wifi_mac_address(self) -> str:
181
- return self.all_values.get('WiFiAddress')
237
+ return self.all_values.get("WiFiAddress")
182
238
 
183
239
  @property
184
240
  def short_info(self) -> dict:
185
- keys_to_copy = ['DeviceClass', 'DeviceName', 'BuildVersion', 'ProductVersion', 'ProductType', 'UniqueDeviceID']
241
+ keys_to_copy = ["DeviceClass", "DeviceName", "BuildVersion", "ProductVersion", "ProductType", "UniqueDeviceID"]
186
242
  result = {
187
- 'Identifier': self.identifier,
243
+ "Identifier": self.identifier,
188
244
  }
189
245
  for key in keys_to_copy:
190
246
  result[key] = self.all_values.get(key)
@@ -192,61 +248,65 @@ class LockdownClient(ABC, LockdownServiceProvider):
192
248
 
193
249
  @property
194
250
  def share_iphone_analytics_enabled(self) -> bool:
195
- return self.get_value('com.apple.MobileDeviceCrashCopy', 'ShouldSubmit')
251
+ return self.get_value("com.apple.MobileDeviceCrashCopy", "ShouldSubmit")
196
252
 
197
253
  @property
198
254
  def assistive_touch(self) -> bool:
199
255
  """AssistiveTouch (the on-screen software home button)"""
200
- return bool(self.get_value('com.apple.Accessibility').get('AssistiveTouchEnabledByiTunes', 0))
256
+ return bool(self.get_value("com.apple.Accessibility").get("AssistiveTouchEnabledByiTunes", 0))
201
257
 
202
258
  @assistive_touch.setter
203
259
  def assistive_touch(self, value: bool) -> None:
204
260
  """AssistiveTouch (the on-screen software home button)"""
205
- self.set_value(int(value), 'com.apple.Accessibility', 'AssistiveTouchEnabledByiTunes')
261
+ self.set_value(int(value), "com.apple.Accessibility", "AssistiveTouchEnabledByiTunes")
206
262
 
207
263
  @property
208
264
  def voice_over(self) -> bool:
209
- return bool(self.get_value('com.apple.Accessibility').get('VoiceOverTouchEnabledByiTunes', 0))
265
+ return bool(self.get_value("com.apple.Accessibility").get("VoiceOverTouchEnabledByiTunes", 0))
210
266
 
211
267
  @voice_over.setter
212
268
  def voice_over(self, value: bool) -> None:
213
- self.set_value(int(value), 'com.apple.Accessibility', 'VoiceOverTouchEnabledByiTunes')
269
+ self.set_value(int(value), "com.apple.Accessibility", "VoiceOverTouchEnabledByiTunes")
214
270
 
215
271
  @property
216
272
  def invert_display(self) -> bool:
217
- return bool(self.get_value('com.apple.Accessibility').get('InvertDisplayEnabledByiTunes', 0))
273
+ return bool(self.get_value("com.apple.Accessibility").get("InvertDisplayEnabledByiTunes", 0))
218
274
 
219
275
  @invert_display.setter
220
276
  def invert_display(self, value: bool) -> None:
221
- self.set_value(int(value), 'com.apple.Accessibility', 'InvertDisplayEnabledByiTunes')
277
+ self.set_value(int(value), "com.apple.Accessibility", "InvertDisplayEnabledByiTunes")
222
278
 
223
279
  @property
224
280
  def enable_wifi_connections(self) -> bool:
225
- return self.get_value('com.apple.mobile.wireless_lockdown').get('EnableWifiConnections', False)
281
+ return self.get_value("com.apple.mobile.wireless_lockdown").get("EnableWifiConnections", False)
226
282
 
227
283
  @enable_wifi_connections.setter
228
284
  def enable_wifi_connections(self, value: bool) -> None:
229
- self.set_value(value, 'com.apple.mobile.wireless_lockdown', 'EnableWifiConnections')
285
+ self.set_value(value, "com.apple.mobile.wireless_lockdown", "EnableWifiConnections")
230
286
 
231
287
  @property
232
288
  def ecid(self) -> int:
233
- return self.all_values['UniqueChipID']
289
+ return self.all_values["UniqueChipID"]
234
290
 
235
291
  @property
236
292
  def date(self) -> datetime.datetime:
237
- return datetime.datetime.fromtimestamp(self.get_value(key='TimeIntervalSince1970'))
293
+ return datetime.datetime.fromtimestamp(self.get_value(key="TimeIntervalSince1970"))
238
294
 
239
295
  @property
240
296
  def language(self) -> str:
241
- return self.get_value(key='Language', domain='com.apple.international')
297
+ return self.get_value(key="Language", domain="com.apple.international")
242
298
 
243
299
  @property
244
300
  def locale(self) -> str:
245
- return self.get_value(key='Locale', domain='com.apple.international')
301
+ return self.get_value(key="Locale", domain="com.apple.international")
246
302
 
247
303
  @property
248
304
  def preflight_info(self) -> dict:
249
- return self.get_value(key='FirmwarePreflightInfo')
305
+ return self.get_value(key="PreflightInfo")
306
+
307
+ @property
308
+ def firmware_preflight_info(self) -> dict:
309
+ return self.get_value(key="FirmwarePreflightInfo")
250
310
 
251
311
  @property
252
312
  def display_name(self) -> str:
@@ -274,34 +334,35 @@ class LockdownClient(ABC, LockdownServiceProvider):
274
334
 
275
335
  @property
276
336
  def developer_mode_status(self) -> bool:
277
- return self.get_value('com.apple.security.mac.amfi', 'DeveloperModeStatus')
337
+ return self.get_value("com.apple.security.mac.amfi", "DeveloperModeStatus")
278
338
 
279
339
  def query_type(self) -> str:
280
- return self._request('QueryType').get('Type')
340
+ return self._request("QueryType").get("Type")
281
341
 
282
342
  def set_language(self, language: str) -> None:
283
- self.set_value(language, key='Language', domain='com.apple.international')
343
+ self.set_value(language, key="Language", domain="com.apple.international")
284
344
 
285
345
  def set_locale(self, locale: str) -> None:
286
- self.set_value(locale, key='Locale', domain='com.apple.international')
346
+ self.set_value(locale, key="Locale", domain="com.apple.international")
287
347
 
288
348
  def set_timezone(self, timezone: str) -> None:
289
- self.set_value(timezone, key='TimeZone')
349
+ self.set_value(timezone, key="TimeZone")
290
350
 
291
351
  def set_uses24hClock(self, value: bool) -> None:
292
- self.set_value(value, key='Uses24HourClock')
352
+ self.set_value(value, key="Uses24HourClock")
293
353
 
294
354
  @_reconnect_on_remote_close
295
355
  def enter_recovery(self):
296
- return self._request('EnterRecovery')
356
+ return self._request("EnterRecovery")
297
357
 
298
358
  def stop_session(self) -> dict:
299
359
  if self.session_id and self.service:
300
- response = self._request('StopSession', {'SessionID': self.session_id})
360
+ response = self._request("StopSession", {"SessionID": self.session_id})
301
361
  self.session_id = None
302
- if not response or response.get('Result') != 'Success':
362
+ if not response or response.get("Result") != "Success":
303
363
  raise CannotStopSessionError()
304
364
  return response
365
+ raise PyMobileDevice3Exception("No active session")
305
366
 
306
367
  def validate_pairing(self) -> bool:
307
368
  if self.pair_record is None:
@@ -310,23 +371,28 @@ class LockdownClient(ABC, LockdownServiceProvider):
310
371
  if self.pair_record is None:
311
372
  return False
312
373
 
313
- if (Version(self.product_version) < Version('7.0')) and (self.device_class != DeviceClass.WATCH):
374
+ if (Version(self.product_version) < Version("7.0")) and (self.device_class != DeviceClass.WATCH):
314
375
  try:
315
- self._request('ValidatePair', {'PairRecord': self.pair_record})
376
+ self._request("ValidatePair", {"PairRecord": self.pair_record})
316
377
  except PairingError:
317
378
  return False
318
379
 
319
- self.host_id = self.pair_record.get('HostID', self.host_id)
320
- self.system_buid = self.pair_record.get('SystemBUID', self.system_buid)
380
+ self.host_id = self.pair_record.get("HostID", self.host_id)
381
+ self.system_buid = self.pair_record.get("SystemBUID", self.system_buid)
321
382
 
322
383
  try:
323
- start_session = self._request('StartSession', {'HostID': self.host_id, 'SystemBUID': self.system_buid})
384
+ start_session = self._request("StartSession", {"HostID": self.host_id, "SystemBUID": self.system_buid})
324
385
  except (InvalidHostIDError, InvalidConnectionError):
325
386
  # no host id means there is no such pairing record
326
387
  return False
327
388
 
328
- self.session_id = start_session.get('SessionID')
329
- if start_session.get('EnableSessionSSL'):
389
+ self.session_id = start_session.get("SessionID")
390
+ if start_session.get("EnableSessionSSL"):
391
+ if (Version(self.product_version) < Version("5.0")) and (self.device_class != DeviceClass.WATCH):
392
+ # TLS v1 is the protocol required for versions prior to iOS 5
393
+ self.service.min_ssl_proto = TLSVersion.SSLv3
394
+ self.service.max_ssl_proto = TLSVersion.TLSv1
395
+
330
396
  with self.ssl_file() as f:
331
397
  try:
332
398
  self.service.ssl_start(f)
@@ -339,41 +405,48 @@ class LockdownClient(ABC, LockdownServiceProvider):
339
405
 
340
406
  # reload data after pairing
341
407
  self.all_values = self.get_value()
342
- self.udid = self.all_values.get('UniqueDeviceID')
408
+ self.udid = self.all_values.get("UniqueDeviceID")
343
409
 
344
410
  return True
345
411
 
346
412
  @_reconnect_on_remote_close
347
- def pair(self, timeout: float = None, private_key: Optional[RSAPrivateKey] = None) -> None:
348
- self.device_public_key = self.get_value('', 'DevicePublicKey')
413
+ def pair(self, timeout: Optional[float] = None, private_key: Optional[RSAPrivateKey] = None) -> None:
414
+ self.device_public_key = self.get_value("", "DevicePublicKey")
349
415
  if not self.device_public_key:
350
- self.logger.error('Unable to retrieve DevicePublicKey')
416
+ self.logger.error("Unable to retrieve DevicePublicKey")
351
417
  self.service.close()
352
418
  raise PairingError()
353
419
 
354
- self.logger.info('Creating host key & certificate')
355
- cert_pem, private_key_pem, device_certificate = ca_do_everything(self.device_public_key,
356
- private_key=private_key)
357
-
358
- pair_record = {'DevicePublicKey': self.device_public_key,
359
- 'DeviceCertificate': device_certificate,
360
- 'HostCertificate': cert_pem,
361
- 'HostID': self.host_id,
362
- 'RootCertificate': cert_pem,
363
- 'RootPrivateKey': private_key_pem,
364
- 'WiFiMACAddress': self.wifi_mac_address,
365
- 'SystemBUID': self.system_buid}
420
+ self.logger.info("Creating host key & certificate")
421
+ host_cert_pem, host_key_pem, device_cert_pem, root_cert_pem, root_key_pem = generate_pairing_cert_chain(
422
+ self.device_public_key,
423
+ private_key=private_key,
424
+ # TODO: consider parsing product_version to support iOS < 4
425
+ )
426
+
427
+ pair_record = {
428
+ "DeviceCertificate": device_cert_pem,
429
+ "HostCertificate": host_cert_pem,
430
+ "HostID": self.host_id,
431
+ "RootCertificate": root_cert_pem,
432
+ "RootPrivateKey": root_key_pem,
433
+ "WiFiMACAddress": self.wifi_mac_address,
434
+ "SystemBUID": self.system_buid,
435
+ }
366
436
 
367
- pair_options = {'PairRecord': pair_record, 'ProtocolVersion': '2',
368
- 'PairingOptions': {'ExtendedPairingErrors': True}}
437
+ pair_options = {
438
+ "PairRecord": pair_record,
439
+ "ProtocolVersion": "2",
440
+ "PairingOptions": {"ExtendedPairingErrors": True},
441
+ }
369
442
 
370
443
  pair = self._request_pair(pair_options, timeout=timeout)
371
444
 
372
- pair_record['HostPrivateKey'] = private_key_pem
373
- escrow_bag = pair.get('EscrowBag')
445
+ pair_record["HostPrivateKey"] = host_key_pem
446
+ escrow_bag = pair.get("EscrowBag")
374
447
 
375
448
  if escrow_bag is not None:
376
- pair_record['EscrowBag'] = pair.get('EscrowBag')
449
+ pair_record["EscrowBag"] = pair.get("EscrowBag")
377
450
 
378
451
  self.pair_record = pair_record
379
452
  self.save_pair_record()
@@ -381,138 +454,150 @@ class LockdownClient(ABC, LockdownServiceProvider):
381
454
 
382
455
  @_reconnect_on_remote_close
383
456
  def pair_supervised(self, keybag_file: Path, timeout: Optional[float] = None) -> None:
384
- with open(keybag_file, 'rb') as keybag_file:
457
+ with open(keybag_file, "rb") as keybag_file:
385
458
  keybag_file = keybag_file.read()
386
459
  private_key = serialization.load_pem_private_key(keybag_file, password=None)
387
460
  cer = x509.load_pem_x509_certificate(keybag_file)
388
461
  public_key = cer.public_bytes(Encoding.DER)
389
462
 
390
- self.device_public_key = self.get_value('', 'DevicePublicKey')
463
+ self.device_public_key = self.get_value("", "DevicePublicKey")
391
464
  if not self.device_public_key:
392
- self.logger.error('Unable to retrieve DevicePublicKey')
465
+ self.logger.error("Unable to retrieve DevicePublicKey")
393
466
  self.service.close()
394
467
  raise PairingError()
395
468
 
396
- self.logger.info('Creating host key & certificate')
397
- cert_pem, private_key_pem, device_certificate = ca_do_everything(self.device_public_key)
398
-
399
- pair_record = {'DevicePublicKey': self.device_public_key,
400
- 'DeviceCertificate': device_certificate,
401
- 'HostCertificate': cert_pem,
402
- 'HostID': self.host_id,
403
- 'RootCertificate': cert_pem,
404
- 'RootPrivateKey': private_key_pem,
405
- 'WiFiMACAddress': self.wifi_mac_address,
406
- 'SystemBUID': self.system_buid}
469
+ self.logger.info("Creating host key & certificate")
470
+ host_cert_pem, host_key_pem, device_cert_pem, root_cert_pem, root_key_pem = generate_pairing_cert_chain(
471
+ self.device_public_key
472
+ # TODO: consider parsing product_version to support iOS < 4
473
+ )
474
+
475
+ pair_record = {
476
+ "DeviceCertificate": device_cert_pem,
477
+ "HostCertificate": host_cert_pem,
478
+ "HostID": self.host_id,
479
+ "RootCertificate": root_cert_pem,
480
+ "RootPrivateKey": root_key_pem,
481
+ "WiFiMACAddress": self.wifi_mac_address,
482
+ "SystemBUID": self.system_buid,
483
+ }
407
484
 
408
- pair_options = {'PairRecord': pair_record, 'ProtocolVersion': '2',
409
- 'PairingOptions': {
410
- 'SupervisorCertificate': public_key,
411
- 'ExtendedPairingErrors': True}}
485
+ pair_options = {
486
+ "PairRecord": pair_record,
487
+ "ProtocolVersion": "2",
488
+ "PairingOptions": {"SupervisorCertificate": public_key, "ExtendedPairingErrors": True},
489
+ }
412
490
 
413
491
  # first pair with SupervisorCertificate as PairingOptions to get PairingChallenge
414
492
  pair = self._request_pair(pair_options, timeout=timeout)
415
- if pair.get('Error') == 'MCChallengeRequired':
416
- extended_response = pair.get('ExtendedResponse')
493
+ if pair.get("Error") == "MCChallengeRequired":
494
+ extended_response = pair.get("ExtendedResponse")
417
495
  if extended_response is not None:
418
- pairing_challenge = extended_response.get('PairingChallenge')
419
- signed_response = PKCS7SignatureBuilder().set_data(pairing_challenge).add_signer(
420
- cer, private_key, hashes.SHA256()).sign(Encoding.DER, [])
421
- pair_options = {'PairRecord': pair_record, 'ProtocolVersion': '2', 'PairingOptions': {
422
- 'ChallengeResponse': signed_response, 'ExtendedPairingErrors': True}}
496
+ pairing_challenge = extended_response.get("PairingChallenge")
497
+ signed_response = (
498
+ PKCS7SignatureBuilder()
499
+ .set_data(pairing_challenge)
500
+ .add_signer(cer, private_key, hashes.SHA256())
501
+ .sign(Encoding.DER, [PKCS7Options.Binary])
502
+ )
503
+ pair_options = {
504
+ "PairRecord": pair_record,
505
+ "ProtocolVersion": "2",
506
+ "PairingOptions": {"ChallengeResponse": signed_response, "ExtendedPairingErrors": True},
507
+ }
423
508
  # second pair with Response to Challenge
424
509
  pair = self._request_pair(pair_options, timeout=timeout)
425
510
 
426
- pair_record['HostPrivateKey'] = private_key_pem
427
- escrow_bag = pair.get('EscrowBag')
511
+ pair_record["HostPrivateKey"] = host_key_pem
512
+ escrow_bag = pair.get("EscrowBag")
428
513
 
429
514
  if escrow_bag is not None:
430
- pair_record['EscrowBag'] = pair.get('EscrowBag')
515
+ pair_record["EscrowBag"] = pair.get("EscrowBag")
431
516
 
432
517
  self.pair_record = pair_record
433
518
  self.save_pair_record()
434
519
  self.paired = True
435
520
 
436
521
  @_reconnect_on_remote_close
437
- def unpair(self, host_id: str = None) -> None:
438
- pair_record = self.pair_record if host_id is None else {'HostID': host_id}
439
- self._request('Unpair', {'PairRecord': pair_record, 'ProtocolVersion': '2'}, verify_request=False)
522
+ def unpair(self, host_id: Optional[str] = None) -> None:
523
+ pair_record = self.pair_record if host_id is None else {"HostID": host_id}
524
+ self._request("Unpair", {"PairRecord": pair_record, "ProtocolVersion": "2"}, verify_request=False)
440
525
 
441
526
  @_reconnect_on_remote_close
442
527
  def reset_pairing(self):
443
- return self._request('ResetPairing', {'FullReset': True})
528
+ return self._request("ResetPairing", {"FullReset": True})
444
529
 
445
530
  @_reconnect_on_remote_close
446
- def get_value(self, domain: str = None, key: str = None):
531
+ def get_value(self, domain: Optional[str] = None, key: Optional[str] = None):
447
532
  options = {}
448
533
 
449
534
  if domain:
450
- options['Domain'] = domain
535
+ options["Domain"] = domain
451
536
  if key:
452
- options['Key'] = key
537
+ options["Key"] = key
453
538
 
454
- res = self._request('GetValue', options)
539
+ res = self._request("GetValue", options)
455
540
  if res:
456
- r = res.get('Value')
457
- if hasattr(r, 'data'):
541
+ r = res.get("Value")
542
+ if hasattr(r, "data"):
458
543
  return r.data
459
544
  return r
460
545
 
461
546
  @_reconnect_on_remote_close
462
- def remove_value(self, domain: str = None, key: str = None) -> dict:
547
+ def remove_value(self, domain: Optional[str] = None, key: Optional[str] = None) -> dict:
463
548
  options = {}
464
549
 
465
550
  if domain:
466
- options['Domain'] = domain
551
+ options["Domain"] = domain
467
552
  if key:
468
- options['Key'] = key
553
+ options["Key"] = key
469
554
 
470
- return self._request('RemoveValue', options)
555
+ return self._request("RemoveValue", options)
471
556
 
472
557
  @_reconnect_on_remote_close
473
- def set_value(self, value, domain: str = None, key: str = None) -> dict:
558
+ def set_value(self, value, domain: Optional[str] = None, key: Optional[str] = None) -> dict:
474
559
  options = {}
475
560
 
476
561
  if domain:
477
- options['Domain'] = domain
562
+ options["Domain"] = domain
478
563
  if key:
479
- options['Key'] = key
564
+ options["Key"] = key
480
565
 
481
- options['Value'] = value
482
- return self._request('SetValue', options)
566
+ options["Value"] = value
567
+ return self._request("SetValue", options)
483
568
 
484
569
  def get_service_connection_attributes(self, name: str, include_escrow_bag: bool = False) -> dict:
485
570
  if not self.paired:
486
571
  raise NotPairedError()
487
572
 
488
- options = {'Service': name}
573
+ options = {"Service": name}
489
574
  if include_escrow_bag:
490
- options['EscrowBag'] = self.pair_record['EscrowBag']
575
+ options["EscrowBag"] = self.pair_record["EscrowBag"]
491
576
 
492
- response = self._request('StartService', options)
493
- if not response or response.get('Error'):
494
- if response.get('Error', '') == 'PasswordProtected':
577
+ response = self._request("StartService", options)
578
+ if not response or response.get("Error"):
579
+ if response.get("Error", "") == "PasswordProtected":
495
580
  raise PasswordRequiredError(
496
- 'your device is protected with password, please enter password in device and try again')
497
- raise StartServiceError(response.get('Error'))
581
+ "your device is protected with password, please enter password in device and try again"
582
+ )
583
+ raise StartServiceError(name, response.get("Error"))
498
584
  return response
499
585
 
500
586
  @_reconnect_on_remote_close
501
587
  def start_lockdown_service(self, name: str, include_escrow_bag: bool = False) -> ServiceConnection:
502
588
  attr = self.get_service_connection_attributes(name, include_escrow_bag=include_escrow_bag)
503
- service_connection = self._create_service_connection(attr['Port'])
589
+ service_connection = self._create_service_connection(attr["Port"])
504
590
 
505
- if attr.get('EnableServiceSSL', False):
591
+ if attr.get("EnableServiceSSL", False):
506
592
  with self.ssl_file() as f:
507
593
  service_connection.ssl_start(f)
508
594
  return service_connection
509
595
 
510
- async def aio_start_lockdown_service(
511
- self, name: str, include_escrow_bag: bool = False) -> ServiceConnection:
596
+ async def aio_start_lockdown_service(self, name: str, include_escrow_bag: bool = False) -> ServiceConnection:
512
597
  attr = self.get_service_connection_attributes(name, include_escrow_bag=include_escrow_bag)
513
- service_connection = self._create_service_connection(attr['Port'])
598
+ service_connection = self._create_service_connection(attr["Port"])
514
599
 
515
- if attr.get('EnableServiceSSL', False):
600
+ if attr.get("EnableServiceSSL", False):
516
601
  with self.ssl_file() as f:
517
602
  await service_connection.aio_ssl_start(f)
518
603
  return service_connection
@@ -522,13 +607,13 @@ class LockdownClient(ABC, LockdownServiceProvider):
522
607
 
523
608
  @contextmanager
524
609
  def ssl_file(self) -> str:
525
- cert_pem = self.pair_record['HostCertificate']
526
- private_key_pem = self.pair_record['HostPrivateKey']
610
+ cert_pem = self.pair_record["HostCertificate"]
611
+ private_key_pem = self.pair_record["HostPrivateKey"]
527
612
 
528
613
  # use delete=False and manage the deletion ourselves because Windows
529
614
  # cannot use in-use files
530
- with tempfile.NamedTemporaryFile('w+b', delete=False) as f:
531
- f.write(cert_pem + b'\n' + private_key_pem)
615
+ with tempfile.NamedTemporaryFile("w+b", delete=False) as f:
616
+ f.write(cert_pem + b"\n" + private_key_pem)
532
617
  filename = f.name
533
618
 
534
619
  try:
@@ -551,52 +636,56 @@ class LockdownClient(ABC, LockdownServiceProvider):
551
636
 
552
637
  @abstractmethod
553
638
  def _create_service_connection(self, port: int) -> ServiceConnection:
554
- """ Used to establish a new ServiceConnection to a given port """
639
+ """Used to establish a new ServiceConnection to a given port"""
555
640
  pass
556
641
 
557
642
  def _request(self, request: str, options: Optional[dict] = None, verify_request: bool = True) -> dict:
558
- message = {'Label': self.label, 'Request': request}
643
+ message = {"Label": self.label, "Request": request}
559
644
  if options:
560
645
  message.update(options)
561
646
  response = self.service.send_recv_plist(message)
562
647
 
563
- if verify_request and response['Request'] != request:
564
- raise LockdownError(f'incorrect response returned. got {response["Request"]} instead of {request}')
648
+ if verify_request and response.get("Request") != request:
649
+ if response.get("Type") == RESTORED_SERVICE_TYPE:
650
+ raise IncorrectModeError(f"Incorrect mode returned. Got: {response}")
651
+ raise LockdownError(f"Incorrect response returned. Got: {response}")
565
652
 
566
- error = response.get('Error')
653
+ error = response.get("Error")
567
654
  if error is not None:
568
655
  # return response if supervisor cert challenge is required, to work with pair_supervisor
569
- if error == 'MCChallengeRequired':
656
+ if error == "MCChallengeRequired":
570
657
  return response
571
- exception_errors = {'PasswordProtected': PasswordRequiredError,
572
- 'PairingDialogResponsePending': PairingDialogResponsePendingError,
573
- 'UserDeniedPairing': UserDeniedPairingError,
574
- 'InvalidHostID': InvalidHostIDError,
575
- 'GetProhibited': GetProhibitedError,
576
- 'SetProhibited': SetProhibitedError,
577
- 'MissingValue': MissingValueError,
578
- 'InvalidService': InvalidServiceError,
579
- 'InvalidConnection': InvalidConnectionError, }
658
+ exception_errors = {
659
+ "PasswordProtected": PasswordRequiredError,
660
+ "PairingDialogResponsePending": PairingDialogResponsePendingError,
661
+ "UserDeniedPairing": UserDeniedPairingError,
662
+ "InvalidHostID": InvalidHostIDError,
663
+ "GetProhibited": GetProhibitedError,
664
+ "SetProhibited": SetProhibitedError,
665
+ "MissingValue": MissingValueError,
666
+ "InvalidService": InvalidServiceError,
667
+ "InvalidConnection": InvalidConnectionError,
668
+ }
580
669
  raise exception_errors.get(error, LockdownError)(error, self.identifier)
581
670
 
582
671
  # iOS < 5: 'Error' is not present, so we need to check the 'Result' instead
583
- if response.get('Result') == 'Failure':
584
- raise LockdownError('', self.identifier)
672
+ if response.get("Result") == "Failure":
673
+ raise LockdownError("", self.identifier)
585
674
 
586
675
  return response
587
676
 
588
677
  def _request_pair(self, pair_options: dict, timeout: Optional[float] = None) -> dict:
589
678
  try:
590
- return self._request('Pair', pair_options)
679
+ return self._request("Pair", pair_options)
591
680
  except PairingDialogResponsePendingError:
592
681
  if timeout == 0:
593
682
  raise
594
683
 
595
- self.logger.info('waiting user pairing dialog...')
684
+ self.logger.info("waiting user pairing dialog...")
596
685
  start = time.time()
597
686
  while timeout is None or time.time() <= start + timeout:
598
687
  with suppress(PairingDialogResponsePendingError):
599
- return self._request('Pair', pair_options)
688
+ return self._request("Pair", pair_options)
600
689
  time.sleep(1)
601
690
  raise PairingDialogResponsePendingError()
602
691
 
@@ -605,7 +694,7 @@ class LockdownClient(ABC, LockdownServiceProvider):
605
694
  self.pair_record = get_preferred_pair_record(self.identifier, self.pairing_records_cache_folder)
606
695
 
607
696
  def save_pair_record(self) -> None:
608
- pair_record_file = self.pairing_records_cache_folder / f'{self.identifier}.plist'
697
+ pair_record_file = self.pairing_records_cache_folder / f"{self.identifier}.plist"
609
698
  pair_record_file.write_bytes(plistlib.dumps(self.pair_record))
610
699
 
611
700
  def _reestablish_connection(self) -> None:
@@ -614,29 +703,39 @@ class LockdownClient(ABC, LockdownServiceProvider):
614
703
 
615
704
 
616
705
  class UsbmuxLockdownClient(LockdownClient):
617
- def __init__(self, service: ServiceConnection, host_id: str, identifier: str = None,
618
- label: str = DEFAULT_LABEL, system_buid: str = SYSTEM_BUID, pair_record: Optional[dict] = None,
619
- pairing_records_cache_folder: Path = None, port: int = SERVICE_PORT,
620
- usbmux_address: Optional[str] = None):
621
- super().__init__(service, host_id, identifier, label, system_buid, pair_record, pairing_records_cache_folder,
622
- port)
706
+ def __init__(
707
+ self,
708
+ service: ServiceConnection,
709
+ host_id: str,
710
+ identifier: Optional[str] = None,
711
+ label: str = DEFAULT_LABEL,
712
+ system_buid: str = SYSTEM_BUID,
713
+ pair_record: Optional[dict] = None,
714
+ pairing_records_cache_folder: Optional[Path] = None,
715
+ port: int = SERVICE_PORT,
716
+ usbmux_address: Optional[str] = None,
717
+ ):
623
718
  self.usbmux_address = usbmux_address
719
+ super().__init__(
720
+ service, host_id, identifier, label, system_buid, pair_record, pairing_records_cache_folder, port
721
+ )
624
722
 
625
723
  @property
626
724
  def short_info(self) -> dict:
627
725
  short_info = super().short_info
628
- short_info['ConnectionType'] = self.service.mux_device.connection_type
726
+ short_info["ConnectionType"] = self.service.mux_device.connection_type
629
727
  return short_info
630
728
 
631
729
  def fetch_pair_record(self) -> None:
632
730
  if self.identifier is not None:
633
- self.pair_record = get_preferred_pair_record(self.identifier, self.pairing_records_cache_folder,
634
- usbmux_address=self.usbmux_address)
731
+ self.pair_record = get_preferred_pair_record(
732
+ self.identifier, self.pairing_records_cache_folder, usbmux_address=self.usbmux_address
733
+ )
635
734
 
636
735
  def _create_service_connection(self, port: int) -> ServiceConnection:
637
- return ServiceConnection.create_using_usbmux(self.identifier, port,
638
- self.service.mux_device.connection_type,
639
- usbmux_address=self.usbmux_address)
736
+ return ServiceConnection.create_using_usbmux(
737
+ self.identifier, port, self.service.mux_device.connection_type, usbmux_address=self.usbmux_address
738
+ )
640
739
 
641
740
 
642
741
  class PlistUsbmuxLockdownClient(UsbmuxLockdownClient):
@@ -648,9 +747,19 @@ class PlistUsbmuxLockdownClient(UsbmuxLockdownClient):
648
747
 
649
748
 
650
749
  class TcpLockdownClient(LockdownClient):
651
- def __init__(self, service: ServiceConnection, host_id: str, hostname: str, identifier: str = None,
652
- label: str = DEFAULT_LABEL, system_buid: str = SYSTEM_BUID, pair_record: Optional[dict] = None,
653
- pairing_records_cache_folder: Path = None, port: int = SERVICE_PORT, keep_alive: bool = True):
750
+ def __init__(
751
+ self,
752
+ service: ServiceConnection,
753
+ host_id: str,
754
+ hostname: str,
755
+ identifier: Optional[str] = None,
756
+ label: str = DEFAULT_LABEL,
757
+ system_buid: str = SYSTEM_BUID,
758
+ pair_record: Optional[dict] = None,
759
+ pairing_records_cache_folder: Optional[Path] = None,
760
+ port: int = SERVICE_PORT,
761
+ keep_alive: bool = True,
762
+ ):
654
763
  """
655
764
  Create a LockdownClient instance
656
765
 
@@ -665,10 +774,12 @@ class TcpLockdownClient(LockdownClient):
665
774
  :param port: lockdownd service port
666
775
  :param keep_alive: use keep-alive to get notified when the connection is lost
667
776
  """
668
- super().__init__(service, host_id, identifier, label, system_buid, pair_record, pairing_records_cache_folder,
669
- port)
777
+ super().__init__(
778
+ service, host_id, identifier, label, system_buid, pair_record, pairing_records_cache_folder, port
779
+ )
670
780
  self._keep_alive = keep_alive
671
781
  self.hostname = hostname
782
+ self.identifier = hostname
672
783
 
673
784
  def _create_service_connection(self, port: int) -> ServiceConnection:
674
785
  return ServiceConnection.create_using_tcp(self.hostname, port, keep_alive=self._keep_alive)
@@ -677,21 +788,30 @@ class TcpLockdownClient(LockdownClient):
677
788
  class RemoteLockdownClient(LockdownClient):
678
789
  def _create_service_connection(self, port: int) -> ServiceConnection:
679
790
  raise NotImplementedError(
680
- 'RemoteXPC service connections should only be created using RemoteServiceDiscoveryService')
791
+ "RemoteXPC service connections should only be created using RemoteServiceDiscoveryService"
792
+ )
681
793
 
682
794
  def _handle_autopair(self, *args, **kwargs):
683
795
  # The RemoteXPC version of lockdown doesn't support pairing operations
684
796
  return None
685
797
 
686
798
  def pair(self, *args, **kwargs) -> None:
687
- raise NotImplementedError('RemoteXPC lockdown version does not support pairing operations')
688
-
689
- def unpair(self, timeout: float = None) -> None:
690
- raise NotImplementedError('RemoteXPC lockdown version does not support pairing operations')
691
-
692
- def __init__(self, service: ServiceConnection, host_id: str, identifier: str = None,
693
- label: str = DEFAULT_LABEL, system_buid: str = SYSTEM_BUID, pair_record: Optional[dict] = None,
694
- pairing_records_cache_folder: Path = None, port: int = SERVICE_PORT):
799
+ raise NotImplementedError("RemoteXPC lockdown version does not support pairing operations")
800
+
801
+ def unpair(self, timeout: Optional[float] = None) -> None:
802
+ raise NotImplementedError("RemoteXPC lockdown version does not support pairing operations")
803
+
804
+ def __init__(
805
+ self,
806
+ service: ServiceConnection,
807
+ host_id: str,
808
+ identifier: Optional[str] = None,
809
+ label: str = DEFAULT_LABEL,
810
+ system_buid: str = SYSTEM_BUID,
811
+ pair_record: Optional[dict] = None,
812
+ pairing_records_cache_folder: Optional[Path] = None,
813
+ port: int = SERVICE_PORT,
814
+ ):
695
815
  """
696
816
  Create a LockdownClient instance
697
817
 
@@ -704,14 +824,24 @@ class RemoteLockdownClient(LockdownClient):
704
824
  :param pairing_records_cache_folder: Use the following location to search and save pair records
705
825
  :param port: lockdownd service port
706
826
  """
707
- super().__init__(service, host_id, identifier, label, system_buid, pair_record, pairing_records_cache_folder,
708
- port)
709
-
710
-
711
- def create_using_usbmux(serial: str = None, identifier: str = None, label: str = DEFAULT_LABEL, autopair: bool = True,
712
- connection_type: str = None, pair_timeout: float = None, local_hostname: str = None,
713
- pair_record: Optional[dict] = None, pairing_records_cache_folder: Path = None,
714
- port: int = SERVICE_PORT, usbmux_address: Optional[str] = None) -> UsbmuxLockdownClient:
827
+ super().__init__(
828
+ service, host_id, identifier, label, system_buid, pair_record, pairing_records_cache_folder, port
829
+ )
830
+
831
+
832
+ def create_using_usbmux(
833
+ serial: Optional[str] = None,
834
+ identifier: Optional[str] = None,
835
+ label: str = DEFAULT_LABEL,
836
+ autopair: bool = True,
837
+ connection_type: Optional[str] = None,
838
+ pair_timeout: Optional[float] = None,
839
+ local_hostname: Optional[str] = None,
840
+ pair_record: Optional[dict] = None,
841
+ pairing_records_cache_folder: Optional[Path] = None,
842
+ port: int = SERVICE_PORT,
843
+ usbmux_address: Optional[str] = None,
844
+ ) -> UsbmuxLockdownClient:
715
845
  """
716
846
  Create a UsbmuxLockdownClient instance
717
847
 
@@ -728,29 +858,73 @@ def create_using_usbmux(serial: str = None, identifier: str = None, label: str =
728
858
  :param usbmux_address: usbmuxd address
729
859
  :return: UsbmuxLockdownClient instance
730
860
  """
731
- service = ServiceConnection.create_using_usbmux(serial, port, connection_type=connection_type,
732
- usbmux_address=usbmux_address)
733
- cls = UsbmuxLockdownClient
734
- with usbmux.create_mux(usbmux_address=usbmux_address) as client:
735
- if isinstance(client, PlistMuxConnection):
736
- # Only the Plist version of usbmuxd supports this message type
737
- system_buid = client.get_buid()
738
- cls = PlistUsbmuxLockdownClient
739
-
740
- if identifier is None:
741
- # attempt get identifier from mux device serial
742
- identifier = service.mux_device.serial
743
-
744
- return cls.create(
745
- service, identifier=identifier, label=label, system_buid=system_buid, local_hostname=local_hostname,
746
- pair_record=pair_record, pairing_records_cache_folder=pairing_records_cache_folder, pair_timeout=pair_timeout,
747
- autopair=autopair, usbmux_address=usbmux_address)
748
-
749
-
750
- def create_using_tcp(hostname: str, identifier: str = None, label: str = DEFAULT_LABEL, autopair: bool = True,
751
- pair_timeout: float = None, local_hostname: str = None, pair_record: Optional[dict] = None,
752
- pairing_records_cache_folder: Path = None, port: int = SERVICE_PORT,
753
- keep_alive: bool = False) -> TcpLockdownClient:
861
+ service = ServiceConnection.create_using_usbmux(
862
+ serial, port, connection_type=connection_type, usbmux_address=usbmux_address
863
+ )
864
+ try:
865
+ cls = UsbmuxLockdownClient
866
+ with usbmux.create_mux(usbmux_address=usbmux_address) as client:
867
+ if isinstance(client, PlistMuxConnection):
868
+ # Only the Plist version of usbmuxd supports this message type
869
+ system_buid = client.get_buid()
870
+ cls = PlistUsbmuxLockdownClient
871
+
872
+ if identifier is None:
873
+ # attempt get identifier from mux device serial
874
+ identifier = service.mux_device.serial
875
+
876
+ return cls.create(
877
+ service,
878
+ identifier=identifier,
879
+ label=label,
880
+ system_buid=system_buid,
881
+ local_hostname=local_hostname,
882
+ pair_record=pair_record,
883
+ pairing_records_cache_folder=pairing_records_cache_folder,
884
+ pair_timeout=pair_timeout,
885
+ autopair=autopair,
886
+ usbmux_address=usbmux_address,
887
+ )
888
+ except Exception:
889
+ service.close()
890
+ raise
891
+
892
+
893
+ def retry_create_using_usbmux(retry_timeout: Optional[float] = None, **kwargs) -> UsbmuxLockdownClient:
894
+ """
895
+ Repeatedly retry to create a UsbmuxLockdownClient instance while dismissing different errors that might occur
896
+ while device is rebooting
897
+
898
+ :param retry_timeout: Retry timeout in seconds or None for no timeout
899
+ :return: UsbmuxLockdownClient instance
900
+ """
901
+ start = time.time()
902
+ while (retry_timeout is None) or (time.time() - start < retry_timeout):
903
+ try:
904
+ return create_using_usbmux(**kwargs)
905
+ except (
906
+ NoDeviceConnectedError,
907
+ ConnectionFailedError,
908
+ BadDevError,
909
+ OSError,
910
+ construct.core.StreamError,
911
+ DeviceNotFoundError,
912
+ ):
913
+ pass
914
+
915
+
916
+ def create_using_tcp(
917
+ hostname: str,
918
+ identifier: Optional[str] = None,
919
+ label: str = DEFAULT_LABEL,
920
+ autopair: bool = True,
921
+ pair_timeout: Optional[float] = None,
922
+ local_hostname: Optional[str] = None,
923
+ pair_record: Optional[dict] = None,
924
+ pairing_records_cache_folder: Optional[Path] = None,
925
+ port: int = SERVICE_PORT,
926
+ keep_alive: bool = False,
927
+ ) -> TcpLockdownClient:
754
928
  """
755
929
  Create a TcpLockdownClient instance
756
930
 
@@ -767,17 +941,36 @@ def create_using_tcp(hostname: str, identifier: str = None, label: str = DEFAULT
767
941
  :return: TcpLockdownClient instance
768
942
  """
769
943
  service = ServiceConnection.create_using_tcp(hostname, port, keep_alive=keep_alive)
770
- client = TcpLockdownClient.create(
771
- service, identifier=identifier, label=label, local_hostname=local_hostname, pair_record=pair_record,
772
- pairing_records_cache_folder=pairing_records_cache_folder, pair_timeout=pair_timeout, autopair=autopair,
773
- port=port, hostname=hostname, keep_alive=keep_alive)
774
- return client
775
-
776
-
777
- def create_using_remote(service: ServiceConnection, identifier: str = None, label: str = DEFAULT_LABEL,
778
- autopair: bool = True, pair_timeout: float = None, local_hostname: str = None,
779
- pair_record: Optional[dict] = None, pairing_records_cache_folder: Path = None,
780
- port: int = SERVICE_PORT) -> RemoteLockdownClient:
944
+ try:
945
+ return TcpLockdownClient.create(
946
+ service,
947
+ identifier=identifier,
948
+ label=label,
949
+ local_hostname=local_hostname,
950
+ pair_record=pair_record,
951
+ pairing_records_cache_folder=pairing_records_cache_folder,
952
+ pair_timeout=pair_timeout,
953
+ autopair=autopair,
954
+ port=port,
955
+ hostname=hostname,
956
+ keep_alive=keep_alive,
957
+ )
958
+ except Exception:
959
+ service.close()
960
+ raise
961
+
962
+
963
+ def create_using_remote(
964
+ service: ServiceConnection,
965
+ identifier: Optional[str] = None,
966
+ label: str = DEFAULT_LABEL,
967
+ autopair: bool = True,
968
+ pair_timeout: Optional[float] = None,
969
+ local_hostname: Optional[str] = None,
970
+ pair_record: Optional[dict] = None,
971
+ pairing_records_cache_folder: Optional[Path] = None,
972
+ port: int = SERVICE_PORT,
973
+ ) -> RemoteLockdownClient:
781
974
  """
782
975
  Create a TcpLockdownClient instance over RSD
783
976
 
@@ -792,52 +985,57 @@ def create_using_remote(service: ServiceConnection, identifier: str = None, labe
792
985
  :param port: lockdownd service port
793
986
  :return: TcpLockdownClient instance
794
987
  """
795
- client = RemoteLockdownClient.create(
796
- service, identifier=identifier, label=label, local_hostname=local_hostname, pair_record=pair_record,
797
- pairing_records_cache_folder=pairing_records_cache_folder, pair_timeout=pair_timeout, autopair=autopair,
798
- port=port)
799
- return client
988
+ try:
989
+ return RemoteLockdownClient.create(
990
+ service,
991
+ identifier=identifier,
992
+ label=label,
993
+ local_hostname=local_hostname,
994
+ pair_record=pair_record,
995
+ pairing_records_cache_folder=pairing_records_cache_folder,
996
+ pair_timeout=pair_timeout,
997
+ autopair=autopair,
998
+ port=port,
999
+ )
1000
+ except Exception:
1001
+ service.close()
1002
+ raise
800
1003
 
801
1004
 
802
1005
  async def get_mobdev2_lockdowns(
803
- udid: Optional[str] = None, pair_records: Optional[Path] = None, only_paired: bool = False,
804
- timeout: float = DEFAULT_BONJOUR_TIMEOUT) \
805
- -> Generator[tuple[str, TcpLockdownClient], None, None]:
1006
+ udid: Optional[str] = None,
1007
+ pair_records: Optional[Path] = None,
1008
+ only_paired: bool = False,
1009
+ timeout: float = DEFAULT_BONJOUR_TIMEOUT,
1010
+ ) -> AsyncIterable[tuple[str, TcpLockdownClient]]:
806
1011
  records = {}
807
1012
  if pair_records is None:
808
1013
  pair_records = get_home_folder()
809
- for file in pair_records.glob('*.plist'):
810
- if file.name.startswith('remote_'):
1014
+ for file in pair_records.glob("*.plist"):
1015
+ if file.name.startswith("remote_"):
811
1016
  # skip RemotePairing records
812
1017
  continue
813
- record_udid = file.parts[-1].strip('.plist')
1018
+ record_udid = file.parts[-1].strip(".plist")
814
1019
  if udid is not None and record_udid != udid:
815
1020
  continue
816
1021
  record = plistlib.loads(file.read_bytes())
817
- records[record['WiFiMACAddress']] = record
1022
+ records[record["WiFiMACAddress"]] = record
818
1023
 
819
- iterated_ips = set()
820
1024
  for answer in await browse_mobdev2(timeout=timeout):
821
- if '@' not in answer.name:
1025
+ if "@" not in answer.instance:
822
1026
  continue
823
- wifi_mac_address = answer.name.split('@', 1)[0]
1027
+ wifi_mac_address = answer.instance.split("@", 1)[0]
824
1028
  record = records.get(wifi_mac_address)
825
1029
 
826
1030
  if only_paired and record is None:
827
1031
  continue
828
1032
 
829
- for ip in answer.ips:
830
- if ip in iterated_ips:
831
- # skip ips we already iterated over, possibly from previous queries
832
- continue
833
- iterated_ips.add(ip)
1033
+ for address in answer.addresses:
834
1034
  try:
835
- lockdown = create_using_tcp(hostname=ip, autopair=False, pair_record=record)
1035
+ lockdown = create_using_tcp(hostname=address.full_ip, autopair=False, pair_record=record)
836
1036
  except Exception:
837
1037
  continue
838
- if lockdown is None:
839
- continue
840
1038
  if only_paired and not lockdown.paired:
841
1039
  lockdown.close()
842
1040
  continue
843
- yield ip, lockdown
1041
+ yield address.full_ip, lockdown