Skip to main content

ios_core/
pairing_transport.rs

1//! SRP pairing XPC transport layer.
2//!
3//! Connects to the "untrusted" tunnel service on a new device and performs
4//! the full SRP pairing handshake over XPC/H2.
5//!
6//! # Connection path
7//! New device (not yet paired):
8//!   1. Device's USB-Ethernet IPv6 address → RSD handshake (port 58783)
9//!   2. RSD discovers "com.apple.internal.dt.coredevice.untrusted.tunnelservice"
10//!   3. XPC/H2 connection to that port
11//!   4. Send handshake, run SRP, receive pair record
12//!
13//! # Reference
14//! go-ios/ios/tunnel/tunnel.go (ManualPairAndConnectToTunnel)
15//! go-ios/ios/tunnel/untrusted.go (ManualPair, setupManualPairing, etc.)
16
17use std::collections::HashMap;
18use std::net::{Ipv6Addr, SocketAddr};
19
20use crate::lockdown::pairing::{
21    build_device_info_tlv, build_setup_tlv, build_srp_proof_tlv, derive_cipher_keys,
22    verify_device_info_response, HostIdentity, SrpSession,
23};
24use crate::proto::tlv::TlvBuffer;
25use bytes::{Bytes, BytesMut};
26use chacha20poly1305::{aead::Aead, ChaCha20Poly1305, KeyInit};
27use indexmap::IndexMap;
28use tokio::net::TcpStream;
29
30pub const UNTRUSTED_SERVICE_NAME: &str = "com.apple.internal.dt.coredevice.untrusted.tunnelservice";
31const CONTROL_CHANNEL_ENVELOPE_TYPE: &str = "RemotePairing.ControlChannelMessageEnvelope";
32const CONTROL_CHANNEL_ORIGIN: &str = "host";
33const MAX_XPC_BODY_SIZE: usize = 1024 * 1024;
34
35// TLV type codes
36const TYPE_PUBLIC_KEY: u8 = 0x03;
37const TYPE_PROOF: u8 = 0x04;
38const TYPE_ENCRYPTED_DATA: u8 = 0x05;
39const TYPE_SALT: u8 = 0x02;
40
41/// Error type for pairing transport.
42#[derive(Debug, thiserror::Error)]
43pub enum PairingTransportError {
44    #[error("IO error: {0}")]
45    Io(#[from] std::io::Error),
46    #[error("XPC error: {0}")]
47    Xpc(String),
48    #[error("RSD error: {0}")]
49    Rsd(String),
50    #[error("SRP crypto error: {0}")]
51    Crypto(String),
52    #[error("pairing failed: {0}")]
53    Failed(String),
54    #[error("pairing rejected: {0}")]
55    Rejected(String),
56    #[error("missing required field: {0}")]
57    MissingField(String),
58    #[error("unexpected field type: {0}")]
59    UnexpectedType(String),
60    #[error("no untrusted tunnel service found in RSD")]
61    ServiceNotFound,
62}
63
64/// Stored credentials after successful pairing.
65#[derive(Debug, Clone)]
66pub struct PairedCredentials {
67    pub remote_identifier: String,
68    pub host_identifier: String,
69    pub host_public_key: Vec<u8>,
70    pub host_private_key: Vec<u8>,
71    pub remote_unlock_host_key: Option<String>,
72    /// Cipher keys for subsequent sessions (client_key, server_key)
73    pub session_keys: Option<([u8; 32], [u8; 32])>,
74}
75
76/// Perform the full SRP pairing handshake with a new (untrusted) device.
77///
78/// `device_addr` – device's USB-Ethernet or Wi-Fi IPv6 address
79///
80/// Returns `PairedCredentials` that should be persisted for future connections.
81///
82/// The user must press "Trust" on the device when prompted.
83pub async fn pair_new_device(
84    device_addr: Ipv6Addr,
85) -> Result<PairedCredentials, PairingTransportError> {
86    // 1. RSD handshake to find the untrusted service port
87    let rsd = crate::xpc::rsd::handshake(device_addr, crate::xpc::rsd::RSD_PORT)
88        .await
89        .map_err(|e| PairingTransportError::Rsd(e.to_string()))?;
90
91    let port = rsd
92        .get_port(UNTRUSTED_SERVICE_NAME)
93        .ok_or(PairingTransportError::ServiceNotFound)?;
94
95    // 2. XPC connection to the untrusted service
96    let sock_addr = SocketAddr::new(device_addr.into(), port);
97    let stream = TcpStream::connect(sock_addr).await?;
98
99    let mut framer = crate::xpc::h2_raw::H2Framer::connect(stream)
100        .await
101        .map_err(|e| PairingTransportError::Xpc(format!("H2: {e}")))?;
102
103    // 3. RemoteXPC bootstrap + handshake request
104    bootstrap_remote_xpc(&mut framer).await?;
105    let mut sequence_number = 1;
106    let handshake_body = build_handshake_request(next_sequence_number(&mut sequence_number));
107    send_xpc(&mut framer, &handshake_body, 1).await?;
108    let handshake = recv_handshake_response(&mut framer).await?;
109    let remote_identifier = extract_remote_identifier(&handshake)?;
110
111    // 4. Generate host identity
112    let identity = HostIdentity::generate();
113
114    // 5. setupManualPairing – send State 1
115    let setup_tlv = build_setup_tlv();
116    let pairing_event = build_pairing_event(
117        &setup_tlv,
118        "setupManualPairing",
119        true,
120        None,
121        next_sequence_number(&mut sequence_number),
122    );
123    send_xpc(&mut framer, &pairing_event, 2).await?;
124    recv_control_plain_message(&mut framer).await?; // ack
125
126    // 6. Read device's SRP public key + salt
127    let device_data = recv_xpc_pairing_data(&mut framer).await?;
128    let device_tlv = parse_tlv(&device_data);
129
130    let device_pub = device_tlv
131        .get(&TYPE_PUBLIC_KEY)
132        .ok_or_else(|| PairingTransportError::Failed("no public key from device".into()))?
133        .to_vec();
134    let salt = device_tlv
135        .get(&TYPE_SALT)
136        .ok_or_else(|| PairingTransportError::Failed("no salt from device".into()))?
137        .to_vec();
138
139    // 7. SRP computation
140    let srp = SrpSession::new(&salt, &device_pub)
141        .map_err(|e| PairingTransportError::Crypto(e.to_string()))?;
142
143    // 8. Send SRP proof (State 3)
144    let proof_tlv = build_srp_proof_tlv(&srp);
145    let proof_event = build_pairing_event(
146        &proof_tlv,
147        "setupManualPairing",
148        false,
149        None,
150        next_sequence_number(&mut sequence_number),
151    );
152    send_xpc(&mut framer, &proof_event, 3).await?;
153
154    // 9. Read device server proof
155    let server_data = recv_xpc_pairing_data(&mut framer).await?;
156    let server_tlv = parse_tlv(&server_data);
157
158    let server_proof = server_tlv
159        .get(&TYPE_PROOF)
160        .ok_or_else(|| PairingTransportError::Failed("no server proof".into()))?
161        .to_vec();
162
163    if !srp.verify_server_proof(&server_proof) {
164        return Err(PairingTransportError::Failed(
165            "server proof verification failed".into(),
166        ));
167    }
168
169    // 10. Exchange device info (State 5)
170    let (info_tlv, setup_key) = build_device_info_tlv(&srp.session_key, &identity)
171        .map_err(|e| PairingTransportError::Crypto(e.to_string()))?;
172
173    let info_event = build_pairing_event(
174        &info_tlv,
175        "setupManualPairing",
176        false,
177        Some("ios-rs-host"),
178        next_sequence_number(&mut sequence_number),
179    );
180    send_xpc(&mut framer, &info_event, 4).await?;
181
182    // 11. Read encrypted device info response (State 6)
183    let enc_data = recv_xpc_pairing_data(&mut framer).await?;
184    let enc_tlv = parse_tlv(&enc_data);
185    let enc_payload = enc_tlv
186        .get(&TYPE_ENCRYPTED_DATA)
187        .ok_or_else(|| PairingTransportError::Failed("no encrypted data in response".into()))?;
188
189    verify_device_info_response(&setup_key, enc_payload)
190        .map_err(|e| PairingTransportError::Crypto(e.to_string()))?;
191
192    // 12. Derive session cipher keys
193    let (client_key, server_key) = derive_cipher_keys(&srp.session_key)
194        .map_err(|e| PairingTransportError::Crypto(e.to_string()))?;
195    let remote_unlock_host_key =
196        create_remote_unlock_key(&mut framer, &client_key, &server_key, &mut sequence_number)
197            .await?;
198
199    Ok(PairedCredentials {
200        remote_identifier,
201        host_identifier: identity.identifier.clone(),
202        host_public_key: identity.public_key_bytes(),
203        host_private_key: identity.private_key_bytes(),
204        remote_unlock_host_key,
205        session_keys: Some((client_key, server_key)),
206    })
207}
208
209// ── XPC message helpers ───────────────────────────────────────────────────────
210
211fn xpc_dict(pairs: &[(&str, crate::xpc::message::XpcValue)]) -> crate::xpc::message::XpcValue {
212    let mut map = IndexMap::new();
213    for (k, v) in pairs {
214        map.insert(k.to_string(), v.clone());
215    }
216    crate::xpc::message::XpcValue::Dictionary(map)
217}
218
219fn xpc_bool(b: bool) -> crate::xpc::message::XpcValue {
220    crate::xpc::message::XpcValue::Bool(b)
221}
222
223fn xpc_int(n: i64) -> crate::xpc::message::XpcValue {
224    crate::xpc::message::XpcValue::Int64(n)
225}
226
227fn xpc_uint(n: u64) -> crate::xpc::message::XpcValue {
228    crate::xpc::message::XpcValue::Uint64(n)
229}
230
231fn xpc_data(d: &[u8]) -> crate::xpc::message::XpcValue {
232    crate::xpc::message::XpcValue::Data(Bytes::copy_from_slice(d))
233}
234
235fn xpc_string(s: &str) -> crate::xpc::message::XpcValue {
236    crate::xpc::message::XpcValue::String(s.to_string())
237}
238
239fn next_sequence_number(sequence_number: &mut u64) -> u64 {
240    let current = *sequence_number;
241    *sequence_number += 1;
242    current
243}
244
245fn build_handshake_request(sequence_number: u64) -> crate::xpc::message::XpcValue {
246    let request = xpc_dict(&[(
247        "handshake",
248        xpc_dict(&[(
249            "_0",
250            xpc_dict(&[
251                (
252                    "hostOptions",
253                    xpc_dict(&[("attemptPairVerify", xpc_bool(true))]),
254                ),
255                ("wireProtocolVersion", xpc_int(19)),
256            ]),
257        )]),
258    )]);
259    build_plain_request(request, sequence_number)
260}
261
262fn build_plain_request(
263    request: crate::xpc::message::XpcValue,
264    sequence_number: u64,
265) -> crate::xpc::message::XpcValue {
266    build_control_channel_envelope(
267        xpc_dict(&[(
268            "plain",
269            xpc_dict(&[("_0", xpc_dict(&[("request", xpc_dict(&[("_0", request)]))]))]),
270        )]),
271        sequence_number,
272    )
273}
274
275fn build_control_channel_envelope(
276    message: crate::xpc::message::XpcValue,
277    sequence_number: u64,
278) -> crate::xpc::message::XpcValue {
279    xpc_dict(&[
280        ("mangledTypeName", xpc_string(CONTROL_CHANNEL_ENVELOPE_TYPE)),
281        (
282            "value",
283            xpc_dict(&[
284                ("message", message),
285                ("originatedBy", xpc_string(CONTROL_CHANNEL_ORIGIN)),
286                ("sequenceNumber", xpc_uint(sequence_number)),
287            ]),
288        ),
289    ])
290}
291
292fn build_encrypted_request(
293    encrypted_payload: &[u8],
294    sequence_number: u64,
295) -> crate::xpc::message::XpcValue {
296    build_control_channel_envelope(
297        xpc_dict(&[(
298            "streamEncrypted",
299            xpc_dict(&[("_0", xpc_data(encrypted_payload))]),
300        )]),
301        sequence_number,
302    )
303}
304
305fn build_pairing_event(
306    tlv_data: &[u8],
307    kind: &str,
308    start_new_session: bool,
309    sending_host: Option<&str>,
310    sequence_number: u64,
311) -> crate::xpc::message::XpcValue {
312    let mut pairs = vec![
313        ("data", xpc_data(tlv_data)),
314        ("kind", xpc_string(kind)),
315        ("startNewSession", xpc_bool(start_new_session)),
316    ];
317    if let Some(h) = sending_host {
318        pairs.push(("sendingHost", xpc_string(h)));
319    }
320    build_control_channel_envelope(
321        xpc_dict(&[(
322            "plain",
323            xpc_dict(&[(
324                "_0",
325                xpc_dict(&[(
326                    "event",
327                    xpc_dict(&[(
328                        "_0",
329                        xpc_dict(&[("pairingData", xpc_dict(&[("_0", xpc_dict(&pairs))]))]),
330                    )]),
331                )]),
332            )]),
333        )]),
334        sequence_number,
335    )
336}
337
338async fn bootstrap_remote_xpc<S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin>(
339    framer: &mut crate::xpc::h2_raw::H2Framer<S>,
340) -> Result<(), PairingTransportError> {
341    crate::xpc::rsd::initialize_xpc_connection_on_framer(framer)
342        .await
343        .map_err(|e| PairingTransportError::Xpc(format!("RemoteXPC bootstrap: {e}")))
344}
345
346async fn send_xpc<S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin>(
347    framer: &mut crate::xpc::h2_raw::H2Framer<S>,
348    body: &crate::xpc::message::XpcValue,
349    msg_id: u64,
350) -> Result<(), PairingTransportError> {
351    use crate::xpc::message::{encode_message, flags, XpcMessage};
352    let msg = XpcMessage {
353        flags: flags::ALWAYS_SET | flags::DATA,
354        msg_id,
355        body: Some(body.clone()),
356    };
357    let bytes = encode_message(&msg).map_err(|e| PairingTransportError::Xpc(e.to_string()))?;
358    framer
359        .write_client_server(&bytes)
360        .await
361        .map_err(|e| PairingTransportError::Xpc(e.to_string()))
362}
363
364fn take_required_field(
365    dict: &mut IndexMap<String, crate::xpc::message::XpcValue>,
366    key: &str,
367    path: &str,
368) -> Result<crate::xpc::message::XpcValue, PairingTransportError> {
369    dict.swap_remove(key)
370        .ok_or_else(|| PairingTransportError::MissingField(path.to_string()))
371}
372
373fn take_required_dict(
374    dict: &mut IndexMap<String, crate::xpc::message::XpcValue>,
375    key: &str,
376    path: &str,
377) -> Result<IndexMap<String, crate::xpc::message::XpcValue>, PairingTransportError> {
378    match take_required_field(dict, key, path)? {
379        crate::xpc::message::XpcValue::Dictionary(value) => Ok(value),
380        _ => Err(PairingTransportError::UnexpectedType(format!(
381            "{path} must be a dictionary"
382        ))),
383    }
384}
385
386fn take_required_data(
387    dict: &mut IndexMap<String, crate::xpc::message::XpcValue>,
388    key: &str,
389    path: &str,
390) -> Result<Vec<u8>, PairingTransportError> {
391    match take_required_field(dict, key, path)? {
392        crate::xpc::message::XpcValue::Data(value) => Ok(value.to_vec()),
393        _ => Err(PairingTransportError::UnexpectedType(format!(
394            "{path} must be a data blob"
395        ))),
396    }
397}
398
399fn take_required_string(
400    dict: &mut IndexMap<String, crate::xpc::message::XpcValue>,
401    key: &str,
402    path: &str,
403) -> Result<String, PairingTransportError> {
404    match take_required_field(dict, key, path)? {
405        crate::xpc::message::XpcValue::String(value) => Ok(value),
406        _ => Err(PairingTransportError::UnexpectedType(format!(
407            "{path} must be a string"
408        ))),
409    }
410}
411
412fn decode_control_plain_message(
413    body: crate::xpc::message::XpcValue,
414) -> Result<IndexMap<String, crate::xpc::message::XpcValue>, PairingTransportError> {
415    let mut envelope = match body {
416        crate::xpc::message::XpcValue::Dictionary(value) => value,
417        _ => {
418            return Err(PairingTransportError::UnexpectedType(
419                "control channel body must be a dictionary".into(),
420            ));
421        }
422    };
423
424    let mangled_type = take_required_string(&mut envelope, "mangledTypeName", "mangledTypeName")?;
425    if mangled_type != CONTROL_CHANNEL_ENVELOPE_TYPE {
426        return Err(PairingTransportError::Failed(format!(
427            "unexpected control channel type: {mangled_type}"
428        )));
429    }
430
431    let mut value = take_required_dict(&mut envelope, "value", "value")?;
432    let mut message = take_required_dict(&mut value, "message", "value.message")?;
433    let mut plain = take_required_dict(&mut message, "plain", "value.message.plain")?;
434    take_required_dict(&mut plain, "_0", "value.message.plain._0")
435}
436
437fn decode_pairing_data_event(
438    mut plain: IndexMap<String, crate::xpc::message::XpcValue>,
439) -> Result<Vec<u8>, PairingTransportError> {
440    let mut event = take_required_dict(&mut plain, "event", "value.message.plain._0.event")?;
441    let mut event_body = take_required_dict(&mut event, "_0", "value.message.plain._0.event._0")?;
442
443    if let Some(rejection) = event_body.get("pairingRejectedWithError") {
444        return Err(PairingTransportError::Rejected(
445            extract_pairing_rejection_message(rejection),
446        ));
447    }
448
449    let mut pairing_data = take_required_dict(
450        &mut event_body,
451        "pairingData",
452        "value.message.plain._0.event._0.pairingData",
453    )?;
454    let mut pairing_data_body = take_required_dict(
455        &mut pairing_data,
456        "_0",
457        "value.message.plain._0.event._0.pairingData._0",
458    )?;
459    take_required_data(
460        &mut pairing_data_body,
461        "data",
462        "value.message.plain._0.event._0.pairingData._0.data",
463    )
464}
465
466fn extract_pairing_rejection_message(value: &crate::xpc::message::XpcValue) -> String {
467    value
468        .as_dict()
469        .and_then(|wrapped| wrapped.get("wrappedError"))
470        .and_then(crate::xpc::message::XpcValue::as_dict)
471        .and_then(|user_info| user_info.get("userInfo"))
472        .and_then(crate::xpc::message::XpcValue::as_dict)
473        .and_then(|user_info| user_info.get("NSLocalizedDescription"))
474        .and_then(crate::xpc::message::XpcValue::as_str)
475        .unwrap_or("pairing rejected by device")
476        .to_string()
477}
478
479async fn recv_xpc<S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin>(
480    framer: &mut crate::xpc::h2_raw::H2Framer<S>,
481) -> Result<crate::xpc::message::XpcValue, PairingTransportError> {
482    use crate::xpc::message::decode_message;
483    let header = framer
484        .read_client_server(24)
485        .await
486        .map_err(|e| PairingTransportError::Xpc(e.to_string()))?;
487    let body_len = xpc_body_len(&header)?;
488    let body = if body_len > 0 {
489        framer
490            .read_client_server(body_len)
491            .await
492            .map_err(|e| PairingTransportError::Xpc(e.to_string()))?
493    } else {
494        Bytes::new()
495    };
496    let mut full = BytesMut::new();
497    full.extend_from_slice(&header);
498    full.extend_from_slice(&body);
499    let msg =
500        decode_message(full.freeze()).map_err(|e| PairingTransportError::Xpc(e.to_string()))?;
501    msg.body
502        .ok_or_else(|| PairingTransportError::MissingField("xpc message body".into()))
503}
504
505fn xpc_body_len(header: &[u8]) -> Result<usize, PairingTransportError> {
506    let len = u64::from_le_bytes(
507        header[8..16]
508            .try_into()
509            .map_err(|_| PairingTransportError::Xpc("bad header length field".into()))?,
510    );
511    let len = usize::try_from(len)
512        .map_err(|_| PairingTransportError::Xpc("xpc body length exceeds usize".into()))?;
513    if len > MAX_XPC_BODY_SIZE {
514        return Err(PairingTransportError::Xpc(format!(
515            "body too large: {len} bytes exceeds {MAX_XPC_BODY_SIZE}"
516        )));
517    }
518    Ok(len)
519}
520
521async fn recv_control_plain_message<S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin>(
522    framer: &mut crate::xpc::h2_raw::H2Framer<S>,
523) -> Result<IndexMap<String, crate::xpc::message::XpcValue>, PairingTransportError> {
524    decode_control_plain_message(recv_xpc(framer).await?)
525}
526
527async fn recv_handshake_response<S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin>(
528    framer: &mut crate::xpc::h2_raw::H2Framer<S>,
529) -> Result<IndexMap<String, crate::xpc::message::XpcValue>, PairingTransportError> {
530    let mut plain = recv_control_plain_message(framer).await?;
531    let mut response =
532        take_required_dict(&mut plain, "response", "value.message.plain._0.response")?;
533    let mut response_body =
534        take_required_dict(&mut response, "_1", "value.message.plain._0.response._1")?;
535    let mut handshake = take_required_dict(
536        &mut response_body,
537        "handshake",
538        "value.message.plain._0.response._1.handshake",
539    )?;
540    take_required_dict(
541        &mut handshake,
542        "_0",
543        "value.message.plain._0.response._1.handshake._0",
544    )
545}
546
547async fn recv_xpc_pairing_data<S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin>(
548    framer: &mut crate::xpc::h2_raw::H2Framer<S>,
549) -> Result<Vec<u8>, PairingTransportError> {
550    decode_pairing_data_event(recv_control_plain_message(framer).await?)
551}
552
553fn extract_remote_identifier(
554    handshake: &IndexMap<String, crate::xpc::message::XpcValue>,
555) -> Result<String, PairingTransportError> {
556    handshake
557        .get("peerDeviceInfo")
558        .and_then(crate::xpc::message::XpcValue::as_dict)
559        .and_then(|peer| peer.get("identifier"))
560        .and_then(crate::xpc::message::XpcValue::as_str)
561        .map(ToOwned::to_owned)
562        .ok_or_else(|| {
563            PairingTransportError::MissingField(
564                "value.message.plain._0.response._1.handshake._0.peerDeviceInfo.identifier".into(),
565            )
566        })
567}
568
569fn parse_tlv(data: &[u8]) -> HashMap<u8, Vec<u8>> {
570    let map = TlvBuffer::decode(data);
571    map.into_iter().map(|(k, v)| (k, v.to_vec())).collect()
572}
573
574fn make_encrypted_nonce(sequence_number: u64) -> [u8; 12] {
575    let mut nonce = [0u8; 12];
576    nonce[..8].copy_from_slice(&sequence_number.to_le_bytes());
577    nonce
578}
579
580fn decode_encrypted_response(
581    body: crate::xpc::message::XpcValue,
582) -> Result<Vec<u8>, PairingTransportError> {
583    let mut envelope = match body {
584        crate::xpc::message::XpcValue::Dictionary(value) => value,
585        _ => {
586            return Err(PairingTransportError::UnexpectedType(
587                "encrypted control channel body must be a dictionary".into(),
588            ));
589        }
590    };
591
592    let mangled_type = take_required_string(&mut envelope, "mangledTypeName", "mangledTypeName")?;
593    if mangled_type != CONTROL_CHANNEL_ENVELOPE_TYPE {
594        return Err(PairingTransportError::Failed(format!(
595            "unexpected control channel type: {mangled_type}"
596        )));
597    }
598
599    let mut value = take_required_dict(&mut envelope, "value", "value")?;
600    let mut message = take_required_dict(&mut value, "message", "value.message")?;
601    take_required_data(&mut message, "_0", "value.message.streamEncrypted._0").or_else(|_| {
602        let mut stream_encrypted = take_required_dict(
603            &mut message,
604            "streamEncrypted",
605            "value.message.streamEncrypted",
606        )?;
607        take_required_data(
608            &mut stream_encrypted,
609            "_0",
610            "value.message.streamEncrypted._0",
611        )
612    })
613}
614
615fn extract_remote_unlock_host_key(
616    response_body: &serde_json::Value,
617) -> Result<Option<String>, PairingTransportError> {
618    let Some(create_remote_unlock_key) = response_body.get("createRemoteUnlockKey") else {
619        return Err(PairingTransportError::MissingField(
620            "encrypted response.response._1.createRemoteUnlockKey".into(),
621        ));
622    };
623
624    Ok(create_remote_unlock_key
625        .get("hostKey")
626        .and_then(serde_json::Value::as_str)
627        .map(ToOwned::to_owned)
628        .filter(|value| !value.is_empty()))
629}
630
631async fn create_remote_unlock_key<S>(
632    framer: &mut crate::xpc::h2_raw::H2Framer<S>,
633    client_key: &[u8; 32],
634    server_key: &[u8; 32],
635    sequence_number: &mut u64,
636) -> Result<Option<String>, PairingTransportError>
637where
638    S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin,
639{
640    let client_cipher = ChaCha20Poly1305::new(client_key.into());
641    let server_cipher = ChaCha20Poly1305::new(server_key.into());
642    let nonce = make_encrypted_nonce(0);
643    let request = serde_json::json!({
644        "request": {
645            "_0": {
646                "createRemoteUnlockKey": {}
647            }
648        }
649    });
650    let encrypted_request = client_cipher
651        .encrypt(&nonce.into(), request.to_string().as_bytes())
652        .map_err(|e| PairingTransportError::Crypto(e.to_string()))?;
653    let body = build_encrypted_request(&encrypted_request, next_sequence_number(sequence_number));
654    send_xpc(framer, &body, 5).await?;
655
656    let encrypted_response = decode_encrypted_response(recv_xpc(framer).await?)?;
657    let plaintext = server_cipher
658        .decrypt(&nonce.into(), encrypted_response.as_ref())
659        .map_err(|e| PairingTransportError::Crypto(e.to_string()))?;
660    let response: serde_json::Value = serde_json::from_slice(&plaintext)
661        .map_err(|e| PairingTransportError::Xpc(format!("invalid encrypted JSON: {e}")))?;
662    let response_body = response
663        .get("response")
664        .and_then(|value| value.get("_1"))
665        .ok_or_else(|| {
666            PairingTransportError::MissingField("encrypted response.response._1".into())
667        })?;
668    if let Some(error) = response_body.get("errorExtended") {
669        return Err(PairingTransportError::Failed(format!(
670            "createRemoteUnlockKey failed: {error:?}"
671        )));
672    }
673    extract_remote_unlock_host_key(response_body)
674}
675
676#[cfg(test)]
677mod tests {
678    use tokio::io::{duplex, AsyncReadExt, AsyncWriteExt};
679    use tokio::time::{timeout, Duration};
680
681    use super::*;
682
683    const FRAME_DATA: u8 = 0x00;
684    const FRAME_SETTINGS: u8 = 0x04;
685    const FLAG_SETTINGS_ACK: u8 = 0x01;
686
687    #[test]
688    fn handshake_envelope_contains_required_control_fields() {
689        let envelope = build_handshake_request(7);
690        let outer = match envelope {
691            crate::xpc::message::XpcValue::Dictionary(value) => value,
692            other => panic!("expected envelope dictionary, got {other:?}"),
693        };
694
695        assert_eq!(
696            outer
697                .get("mangledTypeName")
698                .and_then(crate::xpc::message::XpcValue::as_str),
699            Some(CONTROL_CHANNEL_ENVELOPE_TYPE)
700        );
701
702        let value = outer
703            .get("value")
704            .and_then(crate::xpc::message::XpcValue::as_dict)
705            .expect("value dict");
706        assert_eq!(
707            value
708                .get("originatedBy")
709                .and_then(crate::xpc::message::XpcValue::as_str),
710            Some(CONTROL_CHANNEL_ORIGIN)
711        );
712        assert_eq!(
713            value
714                .get("sequenceNumber")
715                .and_then(crate::xpc::message::XpcValue::as_uint64),
716            Some(7)
717        );
718
719        let handshake = value
720            .get("message")
721            .and_then(crate::xpc::message::XpcValue::as_dict)
722            .and_then(|message| message.get("plain"))
723            .and_then(crate::xpc::message::XpcValue::as_dict)
724            .and_then(|plain| plain.get("_0"))
725            .and_then(crate::xpc::message::XpcValue::as_dict)
726            .and_then(|plain| plain.get("request"))
727            .and_then(crate::xpc::message::XpcValue::as_dict)
728            .and_then(|request| request.get("_0"))
729            .and_then(crate::xpc::message::XpcValue::as_dict)
730            .and_then(|request| request.get("handshake"))
731            .and_then(crate::xpc::message::XpcValue::as_dict)
732            .and_then(|handshake| handshake.get("_0"))
733            .and_then(crate::xpc::message::XpcValue::as_dict)
734            .expect("handshake payload");
735
736        assert_eq!(
737            handshake
738                .get("hostOptions")
739                .and_then(crate::xpc::message::XpcValue::as_dict)
740                .and_then(|options| options.get("attemptPairVerify")),
741            Some(&crate::xpc::message::XpcValue::Bool(true))
742        );
743        assert_eq!(
744            handshake.get("wireProtocolVersion"),
745            Some(&crate::xpc::message::XpcValue::Int64(19))
746        );
747    }
748
749    #[tokio::test]
750    async fn recv_xpc_reads_control_messages_from_client_server_stream() {
751        let (client, mut server) = duplex(4096);
752
753        let server_task = tokio::spawn(async move {
754            let mut preface = [0u8; 24];
755            server.read_exact(&mut preface).await.unwrap();
756            assert_eq!(&preface, crate::xpc::h2_raw::H2_PREFACE);
757
758            let mut settings = [0u8; 21];
759            server.read_exact(&mut settings).await.unwrap();
760            assert_eq!(settings[3], FRAME_SETTINGS);
761
762            let mut window_update = [0u8; 13];
763            server.read_exact(&mut window_update).await.unwrap();
764
765            server.write_all(&settings_frame()).await.unwrap();
766            server.flush().await.unwrap();
767
768            let mut ack = [0u8; 9];
769            server.read_exact(&mut ack).await.unwrap();
770            assert_eq!(ack, settings_ack_frame().as_slice());
771
772            let payload = crate::xpc::message::encode_message(&crate::xpc::message::XpcMessage {
773                flags: crate::xpc::message::flags::ALWAYS_SET | crate::xpc::message::flags::DATA,
774                msg_id: 1,
775                body: Some(build_control_channel_envelope(
776                    xpc_dict(&[("plain", xpc_dict(&[("_0", xpc_dict(&[]))]))]),
777                    1,
778                )),
779            })
780            .unwrap();
781
782            server
783                .write_all(&data_frame(
784                    crate::xpc::h2_raw::STREAM_CLIENT_SERVER,
785                    &payload,
786                ))
787                .await
788                .unwrap();
789            server.flush().await.unwrap();
790        });
791
792        let mut framer = crate::xpc::h2_raw::H2Framer::connect(client).await.unwrap();
793        let plain = timeout(
794            Duration::from_secs(1),
795            recv_control_plain_message(&mut framer),
796        )
797        .await
798        .expect("recv timed out")
799        .unwrap();
800        assert!(plain.is_empty());
801
802        server_task.await.unwrap();
803    }
804
805    #[test]
806    fn decode_pairing_data_event_extracts_inner_data() {
807        let plain = dict_value(&[(
808            "event",
809            dict_value(&[(
810                "_0",
811                dict_value(&[(
812                    "pairingData",
813                    dict_value(&[(
814                        "_0",
815                        dict_value(&[
816                            (
817                                "data",
818                                crate::xpc::message::XpcValue::Data(Bytes::from_static(
819                                    b"\x01\x02",
820                                )),
821                            ),
822                            ("kind", xpc_string("setupManualPairing")),
823                            ("startNewSession", xpc_bool(false)),
824                        ]),
825                    )]),
826                )]),
827            )]),
828        )]);
829
830        let data = decode_pairing_data_event(unwrap_dict(plain)).unwrap();
831        assert_eq!(data, vec![1, 2]);
832    }
833
834    #[test]
835    fn decode_pairing_data_event_surfaces_rejection_reason() {
836        let plain = dict_value(&[(
837            "event",
838            dict_value(&[(
839                "_0",
840                dict_value(&[(
841                    "pairingRejectedWithError",
842                    dict_value(&[(
843                        "wrappedError",
844                        dict_value(&[(
845                            "userInfo",
846                            dict_value(&[(
847                                "NSLocalizedDescription",
848                                xpc_string("Trust dialog denied"),
849                            )]),
850                        )]),
851                    )]),
852                )]),
853            )]),
854        )]);
855
856        let err = decode_pairing_data_event(unwrap_dict(plain)).unwrap_err();
857        assert!(
858            matches!(err, PairingTransportError::Rejected(message) if message == "Trust dialog denied")
859        );
860    }
861
862    #[test]
863    fn extract_remote_identifier_reads_peer_device_info() {
864        let handshake = unwrap_dict(dict_value(&[(
865            "peerDeviceInfo",
866            dict_value(&[("identifier", xpc_string("00008150-000D6D6A1122401C"))]),
867        )]));
868
869        let remote_identifier = extract_remote_identifier(&handshake).unwrap();
870        assert_eq!(remote_identifier, "00008150-000D6D6A1122401C");
871    }
872
873    #[test]
874    fn xpc_body_len_rejects_oversized_body_before_allocation() {
875        let mut header = [0u8; 24];
876        header[8..16].copy_from_slice(&((MAX_XPC_BODY_SIZE as u64) + 1).to_le_bytes());
877
878        let err = xpc_body_len(&header).unwrap_err();
879        assert!(
880            matches!(err, PairingTransportError::Xpc(message) if message.contains("body too large"))
881        );
882    }
883
884    #[test]
885    fn extract_remote_unlock_host_key_reads_host_key() {
886        let response_body = serde_json::json!({
887            "createRemoteUnlockKey": {
888                "hostKey": "PcV5xhyuJBL7Qq9HOGeGVwtU4sJLe1jtl/vRy1tRKcI="
889            }
890        });
891
892        let host_key = extract_remote_unlock_host_key(&response_body).unwrap();
893        assert_eq!(
894            host_key.as_deref(),
895            Some("PcV5xhyuJBL7Qq9HOGeGVwtU4sJLe1jtl/vRy1tRKcI=")
896        );
897    }
898
899    #[test]
900    fn extract_remote_unlock_host_key_allows_missing_host_key() {
901        let response_body = serde_json::json!({
902            "createRemoteUnlockKey": {}
903        });
904
905        let host_key = extract_remote_unlock_host_key(&response_body).unwrap();
906        assert!(host_key.is_none());
907    }
908
909    fn unwrap_dict(
910        value: crate::xpc::message::XpcValue,
911    ) -> IndexMap<String, crate::xpc::message::XpcValue> {
912        match value {
913            crate::xpc::message::XpcValue::Dictionary(dict) => dict,
914            other => panic!("expected dictionary, got {other:?}"),
915        }
916    }
917
918    fn dict_value(
919        pairs: &[(&str, crate::xpc::message::XpcValue)],
920    ) -> crate::xpc::message::XpcValue {
921        xpc_dict(pairs)
922    }
923
924    fn settings_frame() -> Vec<u8> {
925        let mut payload = Vec::new();
926        payload.extend_from_slice(&0x03u16.to_be_bytes());
927        payload.extend_from_slice(&100u32.to_be_bytes());
928        payload.extend_from_slice(&0x04u16.to_be_bytes());
929        payload.extend_from_slice(&1_048_576u32.to_be_bytes());
930        frame(FRAME_SETTINGS, 0, 0, &payload)
931    }
932
933    fn settings_ack_frame() -> Vec<u8> {
934        frame(FRAME_SETTINGS, FLAG_SETTINGS_ACK, 0, &[])
935    }
936
937    fn data_frame(stream_id: u32, payload: &[u8]) -> Vec<u8> {
938        frame(FRAME_DATA, 0, stream_id, payload)
939    }
940
941    fn frame(frame_type: u8, flags: u8, stream_id: u32, payload: &[u8]) -> Vec<u8> {
942        let len = payload.len();
943        let mut out = Vec::with_capacity(9 + len);
944        out.push(((len >> 16) & 0xFF) as u8);
945        out.push(((len >> 8) & 0xFF) as u8);
946        out.push((len & 0xFF) as u8);
947        out.push(frame_type);
948        out.push(flags);
949        out.extend_from_slice(&(stream_id & 0x7fff_ffff).to_be_bytes());
950        out.extend_from_slice(payload);
951        out
952    }
953}