From 81a644170e84193a5ceae49bbfcd05167a3be5ed Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Mon, 21 Jul 2025 07:57:14 -0600 Subject: [PATCH] Initial scafolding for RPPairing --- Cargo.lock | 190 +++++++++++ idevice/Cargo.toml | 16 + idevice/src/lib.rs | 29 ++ idevice/src/services/mod.rs | 2 + idevice/src/services/remote_pairing/mod.rs | 319 ++++++++++++++++++ .../remote_pairing/rp_pairing_file.rs | 35 ++ idevice/src/services/remote_pairing/tlv.rs | 133 ++++++++ tools/Cargo.toml | 4 + tools/src/remote_pairing.rs | 25 ++ 9 files changed, 753 insertions(+) create mode 100644 idevice/src/services/remote_pairing/mod.rs create mode 100644 idevice/src/services/remote_pairing/rp_pairing_file.rs create mode 100644 idevice/src/services/remote_pairing/tlv.rs create mode 100644 tools/src/remote_pairing.rs diff --git a/Cargo.lock b/Cargo.lock index 015659c..0dcc8d6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,16 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -415,6 +425,30 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20", + "cipher", + "poly1305", + "zeroize", +] + [[package]] name = "chrono" version = "0.4.41" @@ -429,6 +463,17 @@ dependencies = [ "windows-link", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", + "zeroize", +] + [[package]] name = "clang-sys" version = "1.8.1" @@ -562,9 +607,37 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", + "rand_core 0.6.4", "typenum", ] +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + [[package]] name = "der" version = "0.7.10" @@ -607,6 +680,7 @@ dependencies = [ "block-buffer", "const-oid", "crypto-common", + "subtle", ] [[package]] @@ -626,6 +700,31 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" +dependencies = [ + "curve25519-dalek", + "ed25519", + "rand_core 0.6.4", + "serde", + "sha2", + "subtle", + "zeroize", +] + [[package]] name = "either" version = "1.15.0" @@ -720,6 +819,12 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + [[package]] name = "flagset" version = "0.4.7" @@ -955,6 +1060,24 @@ dependencies = [ "libc", ] +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + [[package]] name = "home" version = "0.5.11" @@ -1187,9 +1310,12 @@ dependencies = [ "base64", "byteorder", "bytes", + "chacha20poly1305", "chrono", + "ed25519-dalek", "env_logger 0.11.8", "futures", + "hkdf", "indexmap", "json", "log", @@ -1208,6 +1334,7 @@ dependencies = [ "tokio-rustls", "tun-rs 2.1.4", "uuid", + "x25519-dalek", "x509-cert", ] @@ -1275,6 +1402,15 @@ dependencies = [ "serde", ] +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + [[package]] name = "ipnet" version = "2.11.0" @@ -1708,6 +1844,12 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + [[package]] name = "parking" version = "2.2.1" @@ -1835,6 +1977,17 @@ dependencies = [ "rand 0.8.5", ] +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] + [[package]] name = "portable-atomic" version = "1.11.0" @@ -2178,6 +2331,15 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rustix" version = "0.38.44" @@ -2269,6 +2431,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "semver" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" + [[package]] name = "serde" version = "1.0.219" @@ -2863,6 +3031,16 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + [[package]] name = "untrusted" version = "0.9.0" @@ -3462,6 +3640,18 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +[[package]] +name = "x25519-dalek" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" +dependencies = [ + "curve25519-dalek", + "rand_core 0.6.4", + "serde", + "zeroize", +] + [[package]] name = "x509-cert" version = "0.2.5" diff --git a/idevice/Cargo.toml b/idevice/Cargo.toml index 4549865..01b2ef4 100644 --- a/idevice/Cargo.toml +++ b/idevice/Cargo.toml @@ -47,6 +47,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 } @@ -73,6 +77,14 @@ location_simulation = [] pair = ["chrono/default", "tokio/time", "dep:sha2", "dep:rsa", "dep:x509-cert"] obfuscate = ["dep:obfstr"] restore_service = [] +remote_pairing = [ + "dep:json", + "dep:x25519-dalek", + "dep:ed25519-dalek", + "dep:hkdf", + "dep:chacha20poly1305", + "dep:uuid", +] rsd = ["xpc"] syslog_relay = ["dep:bytes"] tcp = ["tokio/net"] @@ -97,6 +109,10 @@ full = [ "mobile_image_mounter", "pair", "restore_service", + "usbmuxd", + "xpc", + "location_simulation", + "remote_pairing", "rsd", "springboardservices", "syslog_relay", diff --git a/idevice/src/lib.rs b/idevice/src/lib.rs index 749a31f..3c4a6d1 100644 --- a/idevice/src/lib.rs +++ b/idevice/src/lib.rs @@ -598,6 +598,25 @@ pub enum IdeviceError { #[error("unknown error `{0}` returned from device")] UnknownErrorType(String) = -59, + #[cfg(feature = "remote_pairing")] + #[error("could not parse as JSON")] + JsonParseFailed(#[from] json::Error) = -63, + + #[cfg(feature = "remote_pairing")] + #[error("unknown TLV type: {0}")] + UnknownTlv(u8) = -64, + + #[cfg(feature = "remote_pairing")] + #[error("malformed TLV")] + MalformedTlv = -65, + + #[error("failed to decode base64 string")] + Base64Decode(#[from] base64::DecodeError) = -66, + + #[cfg(feature = "remote_pairing")] + #[error("pair verify failed")] + PairVerifyFailed = -67, + #[error("invalid arguments were passed")] FfiInvalidArg = -60, #[error("invalid string was passed")] @@ -750,6 +769,16 @@ impl IdeviceError { IdeviceError::FfiInvalidArg => -60, IdeviceError::FfiInvalidString => -61, IdeviceError::FfiBufferTooSmall(_, _) => -62, + + #[cfg(feature = "remote_pairing")] + IdeviceError::JsonParseFailed(_) => -63, + #[cfg(feature = "remote_pairing")] + IdeviceError::UnknownTlv(_) => -64, + #[cfg(feature = "remote_pairing")] + IdeviceError::MalformedTlv => -65, + IdeviceError::Base64Decode(_) => -66, + #[cfg(feature = "remote_pairing")] + IdeviceError::PairVerifyFailed => -67, } } } diff --git a/idevice/src/services/mod.rs b/idevice/src/services/mod.rs index dea53bf..7a00438 100644 --- a/idevice/src/services/mod.rs +++ b/idevice/src/services/mod.rs @@ -25,6 +25,8 @@ pub mod misagent; pub mod mobile_image_mounter; #[cfg(feature = "syslog_relay")] pub mod os_trace_relay; +#[cfg(feature = "remote_pairing")] +pub mod remote_pairing; #[cfg(feature = "restore_service")] pub mod restore_service; #[cfg(feature = "rsd")] diff --git a/idevice/src/services/remote_pairing/mod.rs b/idevice/src/services/remote_pairing/mod.rs new file mode 100644 index 0000000..916c757 --- /dev/null +++ b/idevice/src/services/remote_pairing/mod.rs @@ -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 { + socket: R, + sequence_number: usize, +} + +impl RPPairingClient { + 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 { + 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::::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, 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 { + // 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)?) + } +} diff --git a/idevice/src/services/remote_pairing/rp_pairing_file.rs b/idevice/src/services/remote_pairing/rp_pairing_file.rs new file mode 100644 index 0000000..5d8663b --- /dev/null +++ b/idevice/src/services/remote_pairing/rp_pairing_file.rs @@ -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, + } + } +} diff --git a/idevice/src/services/remote_pairing/tlv.rs b/idevice/src/services/remote_pairing/tlv.rs new file mode 100644 index 0000000..fcea37f --- /dev/null +++ b/idevice/src/services/remote_pairing/tlv.rs @@ -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, +} + +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 { + 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, 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 for PairingDataComponentType { + type Error = u8; + + fn try_from(value: u8) -> Result { + 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), + }) + } +} diff --git a/tools/Cargo.toml b/tools/Cargo.toml index 8526f46..6636461 100644 --- a/tools/Cargo.toml +++ b/tools/Cargo.toml @@ -93,6 +93,10 @@ path = "src/lockdown.rs" name = "restore_service" path = "src/restore_service.rs" +[[bin]] +name = "remote_pairing" +path = "src/remote_pairing.rs" + [dependencies] idevice = { path = "../idevice", features = ["full"] } tokio = { version = "1.43", features = ["io-util", "macros", "time", "full"] } diff --git a/tools/src/remote_pairing.rs b/tools/src/remote_pairing.rs new file mode 100644 index 0000000..e20d6c1 --- /dev/null +++ b/tools/src/remote_pairing.rs @@ -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(()) +}