1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
//! Restore-mode `restored` client
//!
//! Once a device boots the restore ramdisk it exposes `com.apple.mobile.restored`
//! on port 62078, spoken with the same framing as lockdown (4-byte big-endian
//! length prefix + XML plist). This client wraps an [`crate::Idevice`] connected
//! to that port and drives the request/response and streaming message protocol.
use plist::Value;
use tracing::debug;
use crate::{Idevice, IdeviceError};
/// The label sent with `restored` requests.
const DEFAULT_LABEL: &str = "idevice";
/// A client for the restore-mode `restored` service.
#[derive(Debug)]
pub struct RestoredClient {
/// The underlying connection to port 62078 of a restore-mode device.
pub idevice: Idevice,
/// The `RestoreProtocolVersion` reported by the device (re-sent in `StartRestore`).
pub protocol_version: Value,
/// The label sent with each request.
pub label: String,
/// The usbmux `device_id` this client was found on, when it was discovered
/// over usbmux (via [`connect_by_ecid`](Self::connect_by_ecid)). Data-port and
/// FDR connectors should reuse this so their connections target the *same*
/// physical device rather than whichever USB device usbmux lists first.
pub device_id: Option<u32>,
}
impl RestoredClient {
/// The restore-mode service port (shared with lockdown).
pub const SERVICE_PORT: u16 = 62078;
/// Wraps an [`Idevice`] already connected to port 62078 and performs the
/// `QueryType` handshake, verifying the peer is `com.apple.mobile.restored`.
///
/// # Errors
/// Returns [`IdeviceError::UnexpectedResponse`] if the peer is not the
/// restored service.
pub async fn connect(idevice: Idevice) -> Result<Self, IdeviceError> {
let mut client = Self {
idevice,
protocol_version: Value::Integer(0.into()),
label: DEFAULT_LABEL.to_string(),
device_id: None,
};
client.query_type().await?;
Ok(client)
}
/// Sends `QueryType`, validates the service type and records the protocol version.
async fn query_type(&mut self) -> Result<(), IdeviceError> {
let req = crate::plist!({
"Request": "QueryType",
"Label": self.label.clone(),
});
self.idevice.send_plist(req).await?;
let res = self.idevice.read_plist().await?;
match res.get("Type").and_then(Value::as_string) {
Some("com.apple.mobile.restored") => {}
other => {
return Err(IdeviceError::UnexpectedResponse(format!(
"expected com.apple.mobile.restored, got {other:?}"
)));
}
}
if let Some(v) = res.get("RestoreProtocolVersion") {
self.protocol_version = v.clone();
}
debug!("restored protocol version: {:?}", self.protocol_version);
Ok(())
}
/// Finds a restore-mode device by ECID and connects to `restored`.
#[cfg(feature = "usbmuxd")]
pub async fn connect_by_ecid(
addr: &crate::usbmuxd::UsbmuxdAddr,
ecid: u64,
label: &str,
timeout: std::time::Duration,
) -> Result<Self, IdeviceError> {
use crate::usbmuxd::Connection;
let deadline = crate::time::Instant::now() + timeout;
loop {
let devices = match addr.connect(1).await {
Ok(mut m) => m.get_devices().await.unwrap_or_default(),
Err(_) => Vec::new(),
};
for device in devices {
if device.connection_type != Connection::Usb {
continue;
}
// A fresh mux connection per attempt (connect_to_device consumes it).
let mux = match addr.connect(1).await {
Ok(m) => m,
Err(_) => continue,
};
let idevice = match mux
.connect_to_device(device.device_id, Self::SERVICE_PORT, label)
.await
{
Ok(i) => i,
Err(_) => continue,
};
let mut client = match Self::connect(idevice).await {
Ok(c) => c,
Err(_) => continue, // not a restored device
};
if let Ok(found) = client.ecid().await
&& found == ecid
{
// Remember which device this was so data-port / FDR connectors
// can target the same one rather than "first USB device".
client.device_id = Some(device.device_id);
return Ok(client);
}
}
if crate::time::Instant::now() >= deadline {
return Err(IdeviceError::DeviceNotFound);
}
crate::time::sleep(std::time::Duration::from_secs(1)).await;
}
}
/// Queries a device value by key (`QueryValue`), returning the value stored
/// under that key in the response.
///
/// # Errors
/// Returns [`IdeviceError::UnexpectedResponse`] if the response omits the key.
pub async fn query_value(&mut self, key: &str) -> Result<Value, IdeviceError> {
let req = crate::plist!({
"Request": "QueryValue",
"Label": self.label.clone(),
"QueryKey": key,
});
self.idevice.send_plist(req).await?;
let mut res = self.idevice.read_plist().await?;
res.remove(key).ok_or_else(|| {
IdeviceError::UnexpectedResponse(format!("QueryValue response missing `{key}`"))
})
}
/// Returns the device's `HardwareInfo` dictionary.
pub async fn hardware_info(&mut self) -> Result<plist::Dictionary, IdeviceError> {
match self.query_value("HardwareInfo").await? {
Value::Dictionary(d) => Ok(d),
_ => Err(IdeviceError::UnexpectedResponse(
"HardwareInfo is not a dictionary".into(),
)),
}
}
/// Returns the device's ECID (masked to 64 bits), read from `HardwareInfo`.
pub async fn ecid(&mut self) -> Result<u64, IdeviceError> {
let hw = self.hardware_info().await?;
hw.get("UniqueChipID")
.and_then(Value::as_unsigned_integer)
.ok_or_else(|| {
IdeviceError::UnexpectedResponse("HardwareInfo missing UniqueChipID".into())
})
}
/// Begins the restore by sending `StartRestore` with the given options.
///
/// This is a fire-and-forget request; the device subsequently drives the
/// process by sending messages that the [state machine](super::state_machine)
/// dispatches.
pub async fn start_restore(&mut self, options: plist::Dictionary) -> Result<(), IdeviceError> {
let mut req = plist::Dictionary::new();
req.insert("Request".into(), "StartRestore".into());
req.insert("Label".into(), self.label.clone().into());
req.insert(
"RestoreProtocolVersion".into(),
self.protocol_version.clone(),
);
req.insert("RestoreOptions".into(), Value::Dictionary(options));
self.idevice.send_plist(Value::Dictionary(req)).await
}
/// Requests a reboot, returning the response dictionary.
///
/// From within Restore OS this reboots the device; because the restore entry
/// leaves `auto-boot` set to `false`, iBoot then halts in recovery rather than
/// attempting to boot a possibly half-written OS. This is the graceful bail-out
/// used when a restore is cancelled.
pub async fn reboot(&mut self) -> Result<plist::Dictionary, IdeviceError> {
let req = crate::plist!({
"Request": "Reboot",
"Label": self.label.clone(),
});
self.idevice.send_plist(req).await?;
self.idevice.read_plist().await
}
/// Sends a `Goodbye` request and best-effort reads its acknowledgement.
///
/// Mirrors idevicerestore's teardown: the restore daemon is told the client is
/// leaving so the connection closes cleanly instead of being dropped mid-stream.
pub async fn goodbye(&mut self) -> Result<plist::Dictionary, IdeviceError> {
let req = crate::plist!({
"Request": "Goodbye",
"Label": self.label.clone(),
});
self.idevice.send_plist(req).await?;
self.idevice.read_plist().await
}
/// Sends a raw message to `restored`.
pub async fn send(&mut self, message: Value) -> Result<(), IdeviceError> {
self.idevice.send_plist(message).await
}
/// Receives the next message from `restored`.
///
/// Uses the raw plist read rather than [`Idevice::read_plist`], because
/// restore `StatusMsg`s carry a structured `Error` (a `CFErrorRef` dictionary)
/// that the latter's error handling would choke on.
pub async fn recv(&mut self) -> Result<plist::Dictionary, IdeviceError> {
match self.idevice.read_plist_value().await? {
Value::Dictionary(d) => Ok(d),
other => Err(IdeviceError::UnexpectedResponse(format!(
"restored sent a non-dictionary message: {other:?}"
))),
}
}
}