diff --git a/Cargo.lock b/Cargo.lock index 6631e96..3e658e6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1302,6 +1302,7 @@ dependencies = [ "serde_json", "sha1", "sha2", + "siphasher", "thiserror", "tokio", "tokio-openssl", @@ -2483,6 +2484,12 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" +[[package]] +name = "siphasher" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" + [[package]] name = "slab" version = "0.4.12" diff --git a/idevice/Cargo.toml b/idevice/Cargo.toml index f63292e..3c8f557 100644 --- a/idevice/Cargo.toml +++ b/idevice/Cargo.toml @@ -62,6 +62,7 @@ aes = { version = "0.8", optional = true } cbc = { version = "0.1", features = ["alloc"], optional = true } hmac = { version = "0.12", optional = true } sha1 = { version = "0.10", optional = true } +siphasher = { version = "1", optional = true } idevice-srp = { version = "0.6", optional = true } rand = { version = "0.10" } @@ -146,6 +147,7 @@ remote_pairing = [ "dep:hkdf", "dep:chacha20poly1305", "dep:idevice-srp", + "dep:siphasher", "dep:uuid", "dep:aes", "dep:cbc", diff --git a/idevice/src/remote_pairing/mod.rs b/idevice/src/remote_pairing/mod.rs index 7f9d2cb..87dc5e9 100644 --- a/idevice/src/remote_pairing/mod.rs +++ b/idevice/src/remote_pairing/mod.rs @@ -22,6 +22,7 @@ use x25519_dalek::{EphemeralSecret, PublicKey as X25519PublicKey}; pub mod errors; mod opack; +mod peer_device; mod rp_pairing_file; mod socket; pub mod tls_psk; @@ -29,6 +30,7 @@ mod tlv; pub mod tunnel; // export +pub use peer_device::PeerDevice; pub use rp_pairing_file::RpPairingFile; pub use socket::{RpPairingSocket, RpPairingSocketProvider}; #[cfg(feature = "openssl")] @@ -51,6 +53,8 @@ pub struct RemotePairingClient<'a, R: RpPairingSocketProvider> { client_cipher: ChaCha20Poly1305, server_cipher: ChaCha20Poly1305, + + paired_peer_device: Option, } impl<'a, R: RpPairingSocketProvider> RemotePairingClient<'a, R> { @@ -69,6 +73,7 @@ impl<'a, R: RpPairingSocketProvider> RemotePairingClient<'a, R> { encryption_key: initial_key, client_cipher, server_cipher, + paired_peer_device: None, } } @@ -108,6 +113,15 @@ impl<'a, R: RpPairingSocketProvider> RemotePairingClient<'a, R> { Ok(()) } + /// Returns peer device info captured during this client's successful `pair()` flow. + pub fn paired_peer_device(&self) -> Result<&PeerDevice, IdeviceError> { + self.paired_peer_device + .as_ref() + .ok_or(IdeviceError::UnexpectedResponse( + "paired peer device info is only available after a successful pair() call".into(), + )) + } + pub async fn validate_pairing(&mut self) -> Result<(), IdeviceError> { let x_private_key = EphemeralSecret::random_from_rng(OsRng); let x_public_key = X25519PublicKey::from(&x_private_key); @@ -344,7 +358,11 @@ impl<'a, R: RpPairingSocketProvider> RemotePairingClient<'a, R> { { let (salt, public_key, pin) = self.request_pair_consent(pin_callback, state).await?; let key = self.init_srp_context(&salt, &public_key, &pin).await?; - self.save_pair_record_on_peer(&key).await?; + let tlv = self.save_pair_record_on_peer(&key).await?; + let peer_device = peer_device::parse_peer_device_from_tlv(&tlv)?; + + self.pairing_file.alt_irk = Some(peer_device.alt_irk.clone()); + self.paired_peer_device = Some(peer_device); Ok(()) } diff --git a/idevice/src/remote_pairing/opack.rs b/idevice/src/remote_pairing/opack.rs index a7220b0..b1955d5 100644 --- a/idevice/src/remote_pairing/opack.rs +++ b/idevice/src/remote_pairing/opack.rs @@ -2,6 +2,18 @@ use plist::Value; +pub fn opack_to_plist(bytes: &[u8]) -> Result { + let mut offset = 0; + let value = opack_to_plist_inner(bytes, &mut offset)?; + if offset != bytes.len() { + return Err(format!( + "unexpected trailing bytes after OPACK payload: {}", + bytes.len() - offset + )); + } + Ok(value) +} + pub fn plist_to_opack(value: &Value) -> Vec { let mut buf = Vec::new(); plist_to_opack_inner(value, &mut buf); @@ -129,6 +141,194 @@ fn plist_to_opack_inner(node: &Value, buf: &mut Vec) { } } +fn opack_to_plist_inner(bytes: &[u8], offset: &mut usize) -> Result { + let tag = read_u8(bytes, offset)?; + match tag { + 0x01 => Ok(Value::Boolean(true)), + 0x02 => Ok(Value::Boolean(false)), + 0x08..=0x2F => Ok(Value::Integer((tag as u64 - 8).into())), + 0x30 => Ok(Value::Integer((read_u8(bytes, offset)? as u64).into())), + 0x32 => Ok(Value::Integer( + (u32::from_le_bytes(read_exact::<4>(bytes, offset)?) as u64).into(), + )), + 0x33 => Ok(Value::Integer( + u64::from_le_bytes(read_exact::<8>(bytes, offset)?).into(), + )), + 0x35 => { + let n = u32::from_ne_bytes(read_exact::<4>(bytes, offset)?).swap_bytes(); + Ok(Value::Real(f32::from_bits(n) as f64)) + } + 0x36 => { + let n = u64::from_ne_bytes(read_exact::<8>(bytes, offset)?).swap_bytes(); + Ok(Value::Real(f64::from_bits(n))) + } + 0x40..=0x64 => parse_string_value(tag, bytes, offset), + 0x70..=0x94 => parse_data_value(tag, bytes, offset), + 0xD0..=0xDE => parse_array(bytes, offset, Some((tag - 0xD0) as usize)), + 0xDF => parse_array(bytes, offset, None), + 0xE0..=0xEE => parse_dictionary(bytes, offset, Some((tag - 0xE0) as usize)), + 0xEF => parse_dictionary(bytes, offset, None), + 0x03 => Err("unexpected OPACK terminator".into()), + _ => Err(format!("unsupported OPACK tag: 0x{tag:02x}")), + } +} + +fn parse_string_value(bytes_tag: u8, bytes: &[u8], offset: &mut usize) -> Result { + let len = read_sized_len( + bytes_tag, + bytes, + offset, + SizedLenTags { + inline_base: 0x40, + u8_tag: 0x61, + u16_tag: 0x62, + u32_tag: 0x63, + u64_tag: 0x64, + kind: "string", + }, + )?; + Ok(Value::String(read_string(bytes, offset, len)?)) +} + +fn parse_data_value(bytes_tag: u8, bytes: &[u8], offset: &mut usize) -> Result { + let len = read_sized_len( + bytes_tag, + bytes, + offset, + SizedLenTags { + inline_base: 0x70, + u8_tag: 0x91, + u16_tag: 0x92, + u32_tag: 0x93, + u64_tag: 0x94, + kind: "data", + }, + )?; + Ok(Value::Data(read_vec(bytes, offset, len)?)) +} + +#[derive(Copy, Clone)] +struct SizedLenTags { + inline_base: u8, + u8_tag: u8, + u16_tag: u8, + u32_tag: u8, + u64_tag: u8, + kind: &'static str, +} + +fn read_sized_len( + tag: u8, + bytes: &[u8], + offset: &mut usize, + tags: SizedLenTags, +) -> Result { + match tag { + t if (tags.inline_base..tags.u8_tag).contains(&t) => Ok((tag - tags.inline_base) as usize), + t if t == tags.u8_tag => Ok(read_u8(bytes, offset)? as usize), + t if t == tags.u16_tag => Ok(u16::from_le_bytes(read_exact::<2>(bytes, offset)?) as usize), + t if t == tags.u32_tag => Ok(u32::from_le_bytes(read_exact::<4>(bytes, offset)?) as usize), + t if t == tags.u64_tag => { + let len_u64 = u64::from_le_bytes(read_exact::<8>(bytes, offset)?); + usize::try_from(len_u64) + .map_err(|_| format!("{} too large for this platform: {len_u64}", tags.kind)) + } + _ => Err(format!("unsupported OPACK {} tag: 0x{tag:02x}", tags.kind)), + } +} + +fn parse_array(bytes: &[u8], offset: &mut usize, count: Option) -> Result { + let mut items = Vec::with_capacity(count.unwrap_or(0)); + + match count { + Some(count) => { + for _ in 0..count { + items.push(opack_to_plist_inner(bytes, offset)?); + } + } + None => { + while !peek_is_terminator(bytes, *offset) { + items.push(opack_to_plist_inner(bytes, offset)?); + } + *offset += 1; + } + } + + Ok(Value::Array(items)) +} + +fn parse_dictionary( + bytes: &[u8], + offset: &mut usize, + count: Option, +) -> Result { + let mut dict = plist::Dictionary::new(); + + match count { + Some(count) => { + for _ in 0..count { + let key = read_dictionary_key(bytes, offset)?; + let value = opack_to_plist_inner(bytes, offset)?; + dict.insert(key, value); + } + } + None => { + while !peek_is_terminator(bytes, *offset) { + let key = read_dictionary_key(bytes, offset)?; + let value = opack_to_plist_inner(bytes, offset)?; + dict.insert(key, value); + } + *offset += 1; + } + } + + Ok(Value::Dictionary(dict)) +} + +fn read_dictionary_key(bytes: &[u8], offset: &mut usize) -> Result { + opack_to_plist_inner(bytes, offset)? + .into_string() + .ok_or_else(|| "dictionary key is not a string".to_string()) +} + +fn peek_is_terminator(bytes: &[u8], offset: usize) -> bool { + bytes.get(offset).copied() == Some(0x03) +} + +fn read_u8(bytes: &[u8], offset: &mut usize) -> Result { + let b = bytes + .get(*offset) + .copied() + .ok_or_else(|| "unexpected EOF while reading OPACK tag".to_string())?; + *offset += 1; + Ok(b) +} + +fn read_exact(bytes: &[u8], offset: &mut usize) -> Result<[u8; N], String> { + let end = offset.saturating_add(N); + let slice = bytes + .get(*offset..end) + .ok_or_else(|| format!("unexpected EOF while reading {N} bytes"))?; + let mut out = [0u8; N]; + out.copy_from_slice(slice); + *offset = end; + Ok(out) +} + +fn read_vec(bytes: &[u8], offset: &mut usize, len: usize) -> Result, String> { + let end = offset.saturating_add(len); + let slice = bytes + .get(*offset..end) + .ok_or_else(|| format!("unexpected EOF while reading {len} bytes"))?; + *offset = end; + Ok(slice.to_vec()) +} + +fn read_string(bytes: &[u8], offset: &mut usize, len: usize) -> Result { + let data = read_vec(bytes, offset, len)?; + String::from_utf8(data).map_err(|e| format!("invalid UTF-8 string in OPACK payload: {e}")) +} + #[cfg(test)] mod tests { #[test] @@ -162,4 +362,36 @@ mod tests { println!("{res:02X?}"); assert_eq!(res, expected); } + + #[test] + fn t2() { + let v = [ + 0xe7, 0x46, 0x61, 0x6c, 0x74, 0x49, 0x52, 0x4b, 0x80, 0xe9, 0xe8, 0x2d, 0xc0, 0x6a, + 0x49, 0x79, 0x6b, 0x56, 0x6f, 0x54, 0x00, 0x19, 0xb1, 0xc7, 0x7b, 0x46, 0x62, 0x74, + 0x41, 0x64, 0x64, 0x72, 0x51, 0x31, 0x31, 0x3a, 0x32, 0x32, 0x3a, 0x33, 0x33, 0x3a, + 0x34, 0x34, 0x3a, 0x35, 0x35, 0x3a, 0x36, 0x36, 0x43, 0x6d, 0x61, 0x63, 0x76, 0x11, + 0x22, 0x33, 0x44, 0x55, 0x66, 0x5b, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x70, 0x61, + 0x69, 0x72, 0x69, 0x6e, 0x67, 0x5f, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x5f, 0x6e, + 0x75, 0x6d, 0x62, 0x65, 0x72, 0x4c, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, + 0x41, 0x41, 0x41, 0x41, 0x49, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x49, 0x44, + 0x48, 0x6c, 0x6f, 0x6c, 0x73, 0x73, 0x73, 0x73, 0x73, 0x45, 0x6d, 0x6f, 0x64, 0x65, + 0x6c, 0x4e, 0x63, 0x6f, 0x6d, 0x70, 0x75, 0x74, 0x65, 0x72, 0x2d, 0x6d, 0x6f, 0x64, + 0x65, 0x6c, 0x44, 0x6e, 0x61, 0x6d, 0x65, 0x46, 0x72, 0x65, 0x65, 0x65, 0x65, 0x65, + ]; + + let expected = crate::plist!({ + "altIRK": b"\xe9\xe8-\xc0jIykVoT\x00\x19\xb1\xc7{".to_vec(), + "btAddr": "11:22:33:44:55:66", + "mac": b"\x11\x22\x33\x44\x55\x66".to_vec(), + "remotepairing_serial_number": "AAAAAAAAAAAA", + "accountID": "lolsssss", + "model": "computer-model", + "name": "reeeee", + }); + + let res = super::opack_to_plist(&v).unwrap(); + + println!("{res:02X?}"); + assert_eq!(res, expected); + } } diff --git a/idevice/src/remote_pairing/peer_device.rs b/idevice/src/remote_pairing/peer_device.rs new file mode 100644 index 0000000..eb61b97 --- /dev/null +++ b/idevice/src/remote_pairing/peer_device.rs @@ -0,0 +1,211 @@ +use std::hash::Hasher; + +use base64::Engine; +use siphasher::sip::SipHasher; + +use crate::IdeviceError; + +use super::{opack, tlv}; + +#[derive(Debug, Clone)] +pub struct PeerDevice { + /// peer identifier, same as the identifier returned in the `verifyManualPairing` response + pub account_id: String, + /// altIRK: 16-byte + pub alt_irk: Vec, + /// Device's model identifier, e.g. "iPhone14,4" + pub model: String, + /// Device's name + pub name: String, + /// Device's Unique Device Identifier + pub remotepairing_udid: String, +} + +impl PeerDevice { + /// Validates a `_remotepairing._tcp` mDNS `authTag`. + /// + /// - `alt_irk`: 16-byte mDNS identity key (`alt_irk`) stored in the pairing file + /// - `service_identifier`: the service identifier from the mDNS TXT record + /// - `auth_tag`: 6-byte auth tag from the mDNS TXT record + /// + /// Computes `SipHash-2-4(key=alt_irk, msg=service_identifier)` and compares the + /// first 6 bytes of the 8-byte LE output (reversed) against `auth_tag`. + pub fn validate_auth_tag(alt_irk: &[u8], service_identifier: &str, auth_tag: &str) -> bool { + let bytes = match base64::engine::general_purpose::STANDARD.decode(auth_tag) { + Ok(b) => b, + Err(_) => return false, + }; + if bytes.len() != 6 { + return false; + } + let Ok(alt_irk) = <&[u8; 16]>::try_from(alt_irk) else { + return false; + }; + compute_auth_tag(alt_irk, service_identifier) == bytes.as_slice() + } + + pub fn try_from_info_dictionary(dict: &plist::Dictionary) -> Result { + let alt_irk = required_data_field(dict, "altIRK")?; + if alt_irk.len() != 16 { + return Err(IdeviceError::UnexpectedResponse(format!( + "invalid altIRK length in peer device info: expected 16 bytes, got {}", + alt_irk.len() + ))); + } + + Ok(Self { + account_id: required_string_field(dict, "accountID")?, + alt_irk, + model: required_string_field(dict, "model")?, + name: required_string_field(dict, "name")?, + remotepairing_udid: required_string_field(dict, "remotepairing_udid")?, + }) + } +} + +fn required_string_field(dict: &plist::Dictionary, key: &str) -> Result { + dict.get(key) + .and_then(|value| value.as_string()) + .map(str::to_string) + .ok_or(IdeviceError::UnexpectedResponse(format!( + "missing string field `{key}` in peer device info" + ))) +} + +fn required_data_field(dict: &plist::Dictionary, key: &str) -> Result, IdeviceError> { + dict.get(key) + .and_then(|value| value.as_data()) + .map(|value| value.to_vec()) + .ok_or(IdeviceError::UnexpectedResponse(format!( + "missing data field `{key}` in peer device info" + ))) +} + +fn parse_info_dictionary_from_tlv( + entries: &[tlv::TLV8Entry], +) -> Result { + if tlv::contains_component(entries, tlv::PairingDataComponentType::ErrorResponse) { + return Err(IdeviceError::UnexpectedResponse( + "TLV error response in pair record save".into(), + )); + } + + let info = tlv::collect_component_data(entries, tlv::PairingDataComponentType::Info); + if info.is_empty() { + return Err(IdeviceError::UnexpectedResponse( + "missing info payload in pair record response".into(), + )); + } + + let info_plist = opack::opack_to_plist(&info).map_err(|e| { + IdeviceError::UnexpectedResponse(format!( + "failed to parse OPACK info payload from pair record response: {e}" + )) + })?; + + let info_dict = info_plist + .as_dictionary() + .ok_or(IdeviceError::UnexpectedResponse( + "info OPACK payload is not a dictionary".into(), + ))?; + + Ok(info_dict.to_owned()) +} + +pub(super) fn parse_peer_device_from_tlv( + entries: &[tlv::TLV8Entry], +) -> Result { + let info = parse_info_dictionary_from_tlv(entries)?; + PeerDevice::try_from_info_dictionary(&info) +} + +/// Computes the 6-byte mDNS `authTag` for the given `alt_irk` and `service_identifier`. +/// +/// Algorithm: `SipHash-2-4(key=alt_irk, msg=service_identifier)` → take 8-byte LE output, +/// return the first 6 bytes in **reverse** order. +pub(super) fn compute_auth_tag(alt_irk: &[u8; 16], service_identifier: &str) -> [u8; 6] { + let k0 = u64::from_le_bytes(alt_irk[..8].try_into().unwrap()); + let k1 = u64::from_le_bytes(alt_irk[8..16].try_into().unwrap()); + let mut sip = SipHasher::new_with_keys(k0, k1); + sip.write(service_identifier.as_bytes()); + let output = sip.finish().to_le_bytes(); + let mut tag = [0u8; 6]; + for i in 0..6 { + tag[i] = output[5 - i]; + } + tag +} + +#[cfg(test)] +mod tests { + use super::*; + + fn sample_peer_device_dictionary() -> plist::Dictionary { + let mut dict = plist::Dictionary::new(); + dict.insert( + "accountID".into(), + plist::Value::String("test-account".into()), + ); + dict.insert("altIRK".into(), plist::Value::Data(vec![0xAB; 16])); + dict.insert("model".into(), plist::Value::String("AppleTV11,1".into())); + dict.insert("name".into(), plist::Value::String("Living Room".into())); + dict.insert( + "remotepairing_udid".into(), + plist::Value::String("00008110-001A2B3C00000000".into()), + ); + dict + } + + #[test] + fn peer_device_requires_all_fields() { + let mut dict = sample_peer_device_dictionary(); + dict.remove("model"); + + let err = PeerDevice::try_from_info_dictionary(&dict).unwrap_err(); + + assert!( + matches!(err, IdeviceError::UnexpectedResponse(message) if message.contains("model")) + ); + } + + #[test] + fn peer_device_parses_required_fields() { + let dict = sample_peer_device_dictionary(); + + let peer_device = PeerDevice::try_from_info_dictionary(&dict).unwrap(); + + assert_eq!(peer_device.account_id, "test-account"); + assert_eq!(peer_device.alt_irk, vec![0xAB; 16]); + assert_eq!(peer_device.model, "AppleTV11,1"); + assert_eq!(peer_device.name, "Living Room"); + assert_eq!(peer_device.remotepairing_udid, "00008110-001A2B3C00000000"); + } + + #[test] + fn parse_peer_device_from_tlv_reads_info_payload() { + let info = plist::Value::Dictionary(sample_peer_device_dictionary()); + let entries = vec![tlv::TLV8Entry { + tlv_type: tlv::PairingDataComponentType::Info, + data: opack::plist_to_opack(&info), + }]; + + let peer_device = parse_peer_device_from_tlv(&entries).unwrap(); + + assert_eq!(peer_device.name, "Living Room"); + assert_eq!(peer_device.alt_irk.len(), 16); + } + + #[test] + fn validate_auth_tag_returns_true_for_correct_tag() { + let alt_irk = base64::engine::general_purpose::STANDARD + .decode("Mgp6ZGPzXM2ku9br46vsiw==") + .unwrap(); + let service_identifier = "2BE6E510-0325-4365-923E-B14C6F57DB3A"; + let auth_tag = "kXjlTr2l"; + assert!(PeerDevice::validate_auth_tag( + &alt_irk, + service_identifier, + auth_tag + )); + } +} diff --git a/idevice/src/remote_pairing/rp_pairing_file.rs b/idevice/src/remote_pairing/rp_pairing_file.rs index e345885..ed5d2c6 100644 --- a/idevice/src/remote_pairing/rp_pairing_file.rs +++ b/idevice/src/remote_pairing/rp_pairing_file.rs @@ -16,6 +16,7 @@ pub struct RpPairingFile { pub e_private_key: SigningKey, pub e_public_key: VerifyingKey, pub identifier: String, + pub alt_irk: Option>, } impl RpPairingFile { @@ -34,6 +35,11 @@ impl RpPairingFile { &self.identifier } + /// Returns the `alt_irk` bytes (16 bytes). + pub fn alt_irk(&self) -> Option<&[u8]> { + self.alt_irk.as_deref() + } + pub fn generate(sending_host: &str) -> Self { // Ed25519 private key (persistent signing key) let ed25519_private_key = SigningKey::generate(&mut OsRng); @@ -46,6 +52,7 @@ impl RpPairingFile { e_private_key: ed25519_private_key, e_public_key: ed25519_public_key, identifier, + alt_irk: None, } } @@ -54,16 +61,28 @@ impl RpPairingFile { let ed25519_public_key = VerifyingKey::from(&ed25519_private_key); self.e_public_key = ed25519_public_key; self.e_private_key = ed25519_private_key; + self.alt_irk = None; } /// Serialize to XML plist bytes. pub fn to_bytes(&self) -> Vec { - let v = crate::plist!(dict { - "public_key": self.e_public_key.to_bytes().to_vec(), - "private_key": self.e_private_key.to_bytes().to_vec(), - "identifier": self.identifier.as_str() - }); - plist_to_xml_bytes(&v) + let mut dict = plist::Dictionary::new(); + dict.insert( + "public_key".into(), + plist::Value::Data(self.e_public_key.to_bytes().to_vec()), + ); + dict.insert( + "private_key".into(), + plist::Value::Data(self.e_private_key.to_bytes().to_vec()), + ); + dict.insert( + "identifier".into(), + plist::Value::String(self.identifier.clone()), + ); + if let Some(irk) = &self.alt_irk { + dict.insert("alt_irk".into(), plist::Value::Data(irk.clone())); + } + plist_to_xml_bytes(&dict) } /// Parse from plist bytes (XML or binary). @@ -110,10 +129,19 @@ impl RpPairingFile { } }; + let alt_irk = match p.remove("alt_irk").and_then(|x| x.into_data()) { + Some(irk) => Some(irk), + None => { + warn!("plist did not contain alt_irk"); + None + } + }; + Ok(Self { e_private_key: private_key, e_public_key: public_key, identifier, + alt_irk, }) } @@ -133,6 +161,7 @@ impl std::fmt::Debug for RpPairingFile { f.debug_struct("RpPairingFile") .field("e_public_key", &self.e_public_key) .field("identifier", &self.identifier) + .field("alt_irk", &self.alt_irk) .finish() } } diff --git a/idevice/src/remote_pairing/tlv.rs b/idevice/src/remote_pairing/tlv.rs index f5f16e0..46412de 100644 --- a/idevice/src/remote_pairing/tlv.rs +++ b/idevice/src/remote_pairing/tlv.rs @@ -45,6 +45,23 @@ pub struct TLV8Entry { pub data: Vec, } +pub fn collect_component_data( + entries: &[TLV8Entry], + component: PairingDataComponentType, +) -> Vec { + let mut out = Vec::new(); + for entry in entries { + if entry.tlv_type == component { + out.extend_from_slice(&entry.data); + } + } + out +} + +pub fn contains_component(entries: &[TLV8Entry], component: PairingDataComponentType) -> bool { + entries.iter().any(|entry| entry.tlv_type == component) +} + pub fn serialize_tlv8(entries: &[TLV8Entry]) -> Vec { let mut out = Vec::new(); for entry in entries {