use std::hash::Hasher;
use base64::Engine;
use siphasher::sip::SipHasher;
use crate::IdeviceError;
use super::{opack, tlv};
#[derive(Debug, Clone)]
pub struct PeerDevice {
pub account_id: String,
pub alt_irk: Vec<u8>,
pub model: String,
pub name: String,
pub remotepairing_udid: String,
}
impl PeerDevice {
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<Self, IdeviceError> {
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<String, IdeviceError> {
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<Vec<u8>, 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<plist::Dictionary, IdeviceError> {
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<PeerDevice, IdeviceError> {
let info = parse_info_dictionary_from_tlv(entries)?;
PeerDevice::try_from_info_dictionary(&info)
}
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
));
}
}