3 Commits

Author SHA1 Message Date
Jackson Coxson
aefda7e6f1 Merge branch 'master' into rppairing 2025-10-02 09:07:56 -06:00
Jackson Coxson
b4caa3b271 Merge branch 'master' into rppairing
a
2025-09-05 12:00:25 -06:00
Jackson Coxson
81a644170e Initial scafolding for RPPairing 2025-07-21 07:57:49 -06:00
9 changed files with 1026 additions and 218 deletions

682
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -53,6 +53,10 @@ x509-cert = { version = "0.2", optional = true, features = [
"builder",
"pem",
], default-features = false }
x25519-dalek = { version = "2", optional = true }
ed25519-dalek = { version = "2", features = ["rand_core"], optional = true }
hkdf = { version = "0.12", optional = true }
chacha20poly1305 = { version = "0.10", optional = true }
obfstr = { version = "0.4", optional = true }
@@ -98,6 +102,14 @@ pcapd = []
preboard_service = []
obfuscate = ["dep:obfstr"]
restore_service = []
remote_pairing = [
"dep:json",
"dep:x25519-dalek",
"dep:ed25519-dalek",
"dep:hkdf",
"dep:chacha20poly1305",
"dep:uuid",
]
rsd = ["xpc"]
screenshotr = []
syslog_relay = ["dep:bytes"]
@@ -136,6 +148,10 @@ full = [
"pcapd",
"preboard_service",
"restore_service",
"usbmuxd",
"xpc",
"location_simulation",
"remote_pairing",
"rsd",
"screenshotr",
"springboardservices",

View File

@@ -723,6 +723,25 @@ pub enum IdeviceError {
#[error("Developer mode is not enabled")]
DeveloperModeNotEnabled = -68,
#[cfg(feature = "remote_pairing")]
#[error("could not parse as JSON")]
JsonParseFailed(#[from] json::Error) = -69,
#[cfg(feature = "remote_pairing")]
#[error("unknown TLV type: {0}")]
UnknownTlv(u8) = -70,
#[cfg(feature = "remote_pairing")]
#[error("malformed TLV")]
MalformedTlv = -71,
#[error("failed to decode base64 string")]
Base64Decode(#[from] base64::DecodeError) = -72,
#[cfg(feature = "remote_pairing")]
#[error("pair verify failed")]
PairVerifyFailed = -73,
}
impl IdeviceError {
@@ -887,6 +906,15 @@ impl IdeviceError {
#[cfg(feature = "installation_proxy")]
IdeviceError::MalformedPackageArchive(_) => -67,
IdeviceError::DeveloperModeNotEnabled => -68,
#[cfg(feature = "remote_pairing")]
IdeviceError::JsonParseFailed(_) => -69,
#[cfg(feature = "remote_pairing")]
IdeviceError::UnknownTlv(_) => -70,
#[cfg(feature = "remote_pairing")]
IdeviceError::MalformedTlv => -71,
IdeviceError::Base64Decode(_) => -72,
#[cfg(feature = "remote_pairing")]
IdeviceError::PairVerifyFailed => -73,
}
}
}

View File

@@ -39,6 +39,8 @@ pub mod os_trace_relay;
pub mod pcapd;
#[cfg(feature = "preboard_service")]
pub mod preboard_service;
#[cfg(feature = "remote_pairing")]
pub mod remote_pairing;
#[cfg(feature = "restore_service")]
pub mod restore_service;
#[cfg(feature = "rsd")]

View File

@@ -0,0 +1,319 @@
// Jackson Coxson
use base64::{engine::general_purpose::STANDARD as B64, Engine as _};
use chacha20poly1305::{
aead::{Aead, Payload},
ChaCha20Poly1305, KeyInit as _, Nonce,
};
use ed25519_dalek::{Signature, SigningKey};
use hkdf::Hkdf;
use json::{object, JsonValue};
use log::{debug, warn};
use rp_pairing_file::RpPairingFile;
use rsa::signature::SignerMut;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use crate::{IdeviceError, ReadWrite};
pub mod rp_pairing_file;
mod tlv;
const RP_MAGIC: &str = "RPPairing";
pub struct RPPairingClient<R: ReadWrite> {
socket: R,
sequence_number: usize,
}
impl<R: ReadWrite> RPPairingClient<R> {
pub fn new(socket: R) -> Self {
Self {
socket,
sequence_number: 0,
}
}
pub async fn handshake(&mut self) -> Result<(), IdeviceError> {
let req = object! {
"request": {
"_0": {
"handshake": {
"_0": {
"hostOptions": {
"attemptPairVerify": true
},
"wireProtocolVersion": 24
}
}
}
}
};
self.send_plain(req).await?;
let res = self.read_json().await?;
debug!("Handshake response: {res:#}");
Ok(())
}
pub async fn pair(&mut self) -> Result<RpPairingFile, IdeviceError> {
let pairing = RpPairingFile::generate();
// M1 for a NEW pairing
let t = vec![
tlv::TLV8Entry {
tlv_type: tlv::PairingDataComponentType::Method,
data: vec![0x00],
},
tlv::TLV8Entry {
tlv_type: tlv::PairingDataComponentType::State,
data: vec![0x01],
},
];
let t = B64.encode(tlv::serialize_tlv8(&t));
self.send_pairing_data(object! {
"data": t,
"kind": "setupManualPairing",
"sendingHost": "Mac",
"startNewSession": true,
})
.await?;
let res = self.read_event_data().await?;
debug!("Pair (M1) res: {res:#?}");
// M2: Now you handle the SRP steps...
todo!("Implement SRP steps using the device's public key and salt from the response");
}
pub async fn validate_pairing(&mut self, pairing: RpPairingFile) -> Result<(), IdeviceError> {
let pairing_data = tlv::serialize_tlv8(&[
tlv::TLV8Entry {
tlv_type: tlv::PairingDataComponentType::State,
data: vec![0x01],
},
tlv::TLV8Entry {
tlv_type: tlv::PairingDataComponentType::PublicKey,
data: pairing.x_public_key.to_bytes().to_vec(),
},
]);
let pairing_data = B64.encode(pairing_data);
let req = object! {
"event": {
"_0": {
"pairingData": {
"_0": {
"data": pairing_data,
"kind": "verifyManualPairing",
"startNewSession": true
}
}
}
}
};
self.send_plain(req).await?;
let res = self.read_json().await?;
debug!("Public key response: {res:#}");
let data =
&res["message"]["plain"]["_0"]["event"]["_0"]["pairingData"]["_0"]["data"].as_str();
let data = match data {
Some(d) => d,
None => {
warn!("RPPairing validate pair message didn't contain pairingData -> _0 -> data");
return Err(IdeviceError::UnexpectedResponse);
}
};
let data = B64.decode(data)?;
let data = tlv::deserialize_tlv8(&data)?;
println!("{data:#?}");
let device_public_key = match data
.iter()
.find(|x| x.tlv_type == tlv::PairingDataComponentType::PublicKey)
{
Some(d) => d,
None => {
warn!("No public key in TLV data");
return Err(IdeviceError::UnexpectedResponse);
}
};
let peer_pub_bytes: [u8; 32] = match device_public_key.data.as_slice().try_into() {
Ok(d) => d,
Err(_) => {
warn!("Device public key isn't the expected size");
return Err(IdeviceError::NotEnoughBytes(
32,
device_public_key.data.len(),
));
}
};
let device_public_key = x25519_dalek::PublicKey::from(peer_pub_bytes);
let shared_secret = pairing.x_private_key.diffie_hellman(&device_public_key);
// Derive encryption key with HKDF-SHA512
let hk =
Hkdf::<sha2::Sha512>::new(Some(b"Pair-Verify-Encrypt-Salt"), shared_secret.as_bytes());
let mut okm = [0u8; 32];
hk.expand(b"Pair-Verify-Encrypt-Info", &mut okm).unwrap();
// ChaCha20Poly1305 AEAD cipher
let cipher = ChaCha20Poly1305::new(chacha20poly1305::Key::from_slice(&okm));
let mut ed25519_signing_key = pairing.e_private_key;
let mut signbuf = Vec::with_capacity(32 + pairing.identifier.len() + 32);
signbuf.extend_from_slice(pairing.x_public_key.as_bytes()); // 32 bytes
signbuf.extend_from_slice(pairing.identifier.as_bytes()); // variable
signbuf.extend_from_slice(device_public_key.as_bytes()); // 32 bytes
let signature: Signature = ed25519_signing_key.sign(&signbuf);
let plaintext = vec![
tlv::TLV8Entry {
tlv_type: tlv::PairingDataComponentType::Identifier,
data: pairing.identifier.as_bytes().to_vec(),
},
tlv::TLV8Entry {
tlv_type: tlv::PairingDataComponentType::Signature,
data: signature.to_vec(),
},
];
let plaintext = tlv::serialize_tlv8(&plaintext);
let nonce = Nonce::from_slice(b"\x00\x00\x00\x00PV-Msg03"); // 12-byte nonce
let ciphertext = cipher
.encrypt(
nonce,
Payload {
msg: &plaintext,
aad: &[],
},
)
.expect("encryption should not fail");
let msg = vec![
tlv::TLV8Entry {
tlv_type: tlv::PairingDataComponentType::State,
data: [0x03].to_vec(),
},
tlv::TLV8Entry {
tlv_type: tlv::PairingDataComponentType::EncryptedData,
data: ciphertext,
},
];
let msg = object! {"event": {"_0": {"pairingData": {"_0": {
"data": B64.encode(tlv::serialize_tlv8(&msg)),
"kind": "verifyManualPairing",
"startNewSession": false}}}}};
self.send_plain(msg).await?;
let res = self.read_json().await?;
debug!("Verify response: {res:#}");
let data =
&res["message"]["plain"]["_0"]["event"]["_0"]["pairingData"]["_0"]["data"].as_str();
let data = match data {
Some(d) => d,
None => {
warn!("RPPairing validate pair message didn't contain pairingData -> _0 -> data");
return Err(IdeviceError::UnexpectedResponse);
}
};
let data = B64.decode(data)?;
let data = tlv::deserialize_tlv8(&data)?;
println!("{data:#?}");
// Check if the device responded with an error (which is expected for a new pairing)
if data
.iter()
.any(|x| x.tlv_type == tlv::PairingDataComponentType::ErrorResponse)
{
debug!("Verification failed, device reported an error. This is expected for a new pairing.");
tokio::time::sleep(std::time::Duration::from_secs(3)).await;
// Tell the device we are aborting the verification attempt.
let msg = object! {"event": {"_0": {"pairVerifyFailed": {}}}};
self.send_plain(msg).await?;
tokio::time::sleep(std::time::Duration::from_secs(3)).await;
self.pair().await?;
// Return a specific error to the caller.
return Err(IdeviceError::PairVerifyFailed);
}
Ok(())
}
async fn send_pairing_data(&mut self, data: JsonValue) -> Result<(), IdeviceError> {
self.send_event(object! {
"pairingData": {
"_0": data
}
})
.await
}
async fn send_event(&mut self, data: JsonValue) -> Result<(), IdeviceError> {
let req = object! {
"event": {
"_0": data
}
};
self.send_plain(req).await
}
async fn read_event_data(&mut self) -> Result<Vec<tlv::TLV8Entry>, IdeviceError> {
let res = self.read_json().await?;
match &res["message"]["plain"]["_0"]["event"]["_0"]["pairingData"]["_0"]["data"].as_str() {
Some(r) => Ok(tlv::deserialize_tlv8(&B64.decode(r)?)?),
None => Err(IdeviceError::UnexpectedResponse),
}
}
async fn send_plain(&mut self, data: JsonValue) -> Result<(), IdeviceError> {
let req = object! {
sequenceNumber: self.sequence_number,
originatedBy: "host",
message: {
plain: {
_0: data
}
}
};
debug!("Sending {req:#}");
self.sequence_number += 1;
self.send_json(req).await?;
Ok(())
}
async fn send_json(&mut self, data: JsonValue) -> Result<(), IdeviceError> {
// Send the magic
self.socket.write_all(RP_MAGIC.as_bytes()).await?;
// Packet length
let data = data.to_string().into_bytes();
self.socket.write_u16(data.len() as u16).await?; // big endian
self.socket.write_all(&data).await?;
Ok(())
}
async fn read_json(&mut self) -> Result<JsonValue, IdeviceError> {
// Read the magic
let mut magic_buf = [0u8; RP_MAGIC.len()];
self.socket.read_exact(&mut magic_buf).await?;
// Read JSON length
let len = self.socket.read_u16().await?;
let mut buf = vec![0u8; len as usize];
self.socket.read_exact(&mut buf).await?;
let data = String::from_utf8_lossy(&buf);
Ok(json::parse(&data)?)
}
}

View File

@@ -0,0 +1,35 @@
// Jackson Coxson
use ed25519_dalek::{SigningKey, VerifyingKey};
use rsa::rand_core::OsRng;
use x25519_dalek::{EphemeralSecret, PublicKey as X25519PublicKey};
pub struct RpPairingFile {
pub(crate) x_private_key: EphemeralSecret,
pub(crate) x_public_key: X25519PublicKey,
pub(crate) e_private_key: SigningKey,
pub(crate) e_public_key: VerifyingKey,
pub(crate) identifier: String,
}
impl RpPairingFile {
pub fn generate() -> Self {
// X25519 private key (ephemeral)
let x25519_private_key = EphemeralSecret::random_from_rng(OsRng);
let x25519_public_key = X25519PublicKey::from(&x25519_private_key);
// Ed25519 private key (persistent signing key)
let ed25519_private_key = SigningKey::generate(&mut OsRng);
let ed25519_public_key = VerifyingKey::from(&ed25519_private_key);
let identifier = uuid::Uuid::new_v4().to_string().to_uppercase();
Self {
x_private_key: x25519_private_key,
x_public_key: x25519_public_key,
e_private_key: ed25519_private_key,
e_public_key: ed25519_public_key,
identifier,
}
}
}

View File

@@ -0,0 +1,133 @@
// Jackson Coxson
use crate::IdeviceError;
// from pym3
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum PairingDataComponentType {
Method = 0x00,
Identifier = 0x01,
Salt = 0x02,
PublicKey = 0x03,
Proof = 0x04,
EncryptedData = 0x05,
State = 0x06,
ErrorResponse = 0x07,
RetryDelay = 0x08,
Certificate = 0x09,
Signature = 0x0a,
Permissions = 0x0b,
FragmentData = 0x0c,
FragmentLast = 0x0d,
SessionId = 0x0e,
Ttl = 0x0f,
ExtraData = 0x10,
Info = 0x11,
Acl = 0x12,
Flags = 0x13,
ValidationData = 0x14,
MfiAuthToken = 0x15,
MfiProductType = 0x16,
SerialNumber = 0x17,
MfiAuthTokenUuid = 0x18,
AppFlags = 0x19,
OwnershipProof = 0x1a,
SetupCodeType = 0x1b,
ProductionData = 0x1c,
AppInfo = 0x1d,
Separator = 0xff,
}
#[derive(Debug, Clone)]
pub struct TLV8Entry {
pub tlv_type: PairingDataComponentType,
pub data: Vec<u8>,
}
impl TLV8Entry {
/// SRP stage
pub fn m(stage: u8) -> Self {
Self {
tlv_type: PairingDataComponentType::State,
data: [stage].to_vec(),
}
}
}
pub fn serialize_tlv8(entries: &[TLV8Entry]) -> Vec<u8> {
let mut out = Vec::new();
for entry in entries {
out.push(entry.tlv_type as u8);
out.push(entry.data.len() as u8);
out.extend(&entry.data);
}
out
}
pub fn deserialize_tlv8(input: &[u8]) -> Result<Vec<TLV8Entry>, IdeviceError> {
let mut index = 0;
let mut result = Vec::new();
while index + 2 <= input.len() {
let type_byte = input[index];
let length = input[index + 1] as usize;
index += 2;
if index + length > input.len() {
return Err(IdeviceError::MalformedTlv);
}
let data = input[index..index + length].to_vec();
index += length;
let tlv_type = PairingDataComponentType::try_from(type_byte)
.map_err(|_| IdeviceError::UnknownTlv(type_byte))?;
result.push(TLV8Entry { tlv_type, data });
}
Ok(result)
}
impl TryFrom<u8> for PairingDataComponentType {
type Error = u8;
fn try_from(value: u8) -> Result<Self, Self::Error> {
use PairingDataComponentType::*;
Ok(match value {
0x00 => Method,
0x01 => Identifier,
0x02 => Salt,
0x03 => PublicKey,
0x04 => Proof,
0x05 => EncryptedData,
0x06 => State,
0x07 => ErrorResponse,
0x08 => RetryDelay,
0x09 => Certificate,
0x0a => Signature,
0x0b => Permissions,
0x0c => FragmentData,
0x0d => FragmentLast,
0x0e => SessionId,
0x0f => Ttl,
0x10 => ExtraData,
0x11 => Info,
0x12 => Acl,
0x13 => Flags,
0x14 => ValidationData,
0x15 => MfiAuthToken,
0x16 => MfiProductType,
0x17 => SerialNumber,
0x18 => MfiAuthTokenUuid,
0x19 => AppFlags,
0x1a => OwnershipProof,
0x1b => SetupCodeType,
0x1c => ProductionData,
0x1d => AppInfo,
0xff => Separator,
other => return Err(other),
})
}
}

View File

@@ -97,6 +97,10 @@ path = "src/lockdown.rs"
name = "restore_service"
path = "src/restore_service.rs"
[[bin]]
name = "remote_pairing"
path = "src/remote_pairing.rs"
[[bin]]
name = "companion_proxy"
path = "src/companion_proxy.rs"

View File

@@ -0,0 +1,25 @@
// Jackson Coxson
use idevice::{
remote_pairing::{rp_pairing_file::RpPairingFile, RPPairingClient},
IdeviceError,
};
#[tokio::main]
async fn main() -> Result<(), IdeviceError> {
env_logger::init();
let conn = tokio::net::TcpStream::connect("192.168.50.247:49152")
.await
.unwrap();
let mut client = RPPairingClient::new(conn);
client.handshake().await?;
let pairing = RpPairingFile::generate();
client
.validate_pairing(pairing)
.await
.expect("No validate?");
client.pair().await?;
Ok(())
}