From 4bea78426031652bd6ff217bf194f732eab83b75 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Thu, 18 Dec 2025 21:21:30 -0700 Subject: [PATCH] Initial rppairing support --- Cargo.lock | 242 +++++- idevice/Cargo.toml | 18 +- idevice/src/lib.rs | 27 + idevice/src/remote_pairing/mod.rs | 718 ++++++++++++++++++ idevice/src/remote_pairing/opack.rs | 165 ++++ idevice/src/remote_pairing/rp_pairing_file.rs | 113 +++ idevice/src/remote_pairing/tlv.rs | 123 +++ tools/Cargo.toml | 4 + tools/src/pair_apple_tv.rs | 89 +++ 9 files changed, 1492 insertions(+), 7 deletions(-) create mode 100644 idevice/src/remote_pairing/mod.rs create mode 100644 idevice/src/remote_pairing/opack.rs create mode 100644 idevice/src/remote_pairing/rp_pairing_file.rs create mode 100644 idevice/src/remote_pairing/tlv.rs create mode 100644 tools/src/pair_apple_tv.rs diff --git a/Cargo.lock b/Cargo.lock index 6b5227b..c8c9e6e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,16 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[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.4" @@ -185,6 +195,12 @@ dependencies = [ "fs_extra", ] +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + [[package]] name = "base64" version = "0.22.1" @@ -335,6 +351,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.42" @@ -349,6 +389,17 @@ dependencies = [ "windows-link 0.2.1", ] +[[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" @@ -510,9 +561,37 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" 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", +] + [[package]] name = "der" version = "0.7.10" @@ -555,6 +634,7 @@ dependencies = [ "block-buffer", "const-oid", "crypto-common", + "subtle", ] [[package]] @@ -574,6 +654,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.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "rand_core 0.6.4", + "serde", + "sha2", + "subtle", + "zeroize", +] + [[package]] name = "either" version = "1.15.0" @@ -632,6 +737,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 = "find-msvc-tools" version = "0.1.5" @@ -868,6 +979,24 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[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 = "http" version = "1.3.1" @@ -935,7 +1064,7 @@ version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52e9a2a24dc5c6821e71a7030e1e14b7b632acac55c40e9d2e082c621261bb56" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "futures-channel", "futures-core", @@ -1064,12 +1193,15 @@ version = "0.1.50" dependencies = [ "async-stream", "async_zip", - "base64", + "base64 0.22.1", "byteorder", "bytes", + "chacha20poly1305", "chrono", "crossfire", + "ed25519-dalek", "futures", + "hkdf", "indexmap", "json", "ns-keyed-archive", @@ -1083,6 +1215,7 @@ dependencies = [ "serde", "serde_json", "sha2", + "srp", "thiserror 2.0.17", "tokio", "tokio-openssl", @@ -1090,6 +1223,7 @@ dependencies = [ "tracing", "tun-rs", "uuid", + "x25519-dalek", "x509-cert", ] @@ -1163,6 +1297,15 @@ dependencies = [ "serde_core", ] +[[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" @@ -1303,6 +1446,16 @@ dependencies = [ "regex-automata", ] +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + [[package]] name = "memchr" version = "2.7.6" @@ -1489,6 +1642,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-bigint-dig" version = "0.8.6" @@ -1559,6 +1722,12 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + [[package]] name = "openssl" version = "0.10.75" @@ -1723,7 +1892,7 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" dependencies = [ - "base64", + "base64 0.22.1", "indexmap", "quick-xml", "serde", @@ -1743,6 +1912,17 @@ dependencies = [ "serde_json", ] +[[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 = "potential_utf" version = "0.1.4" @@ -1912,7 +2092,7 @@ version = "0.12.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "futures-core", "http", @@ -1993,6 +2173,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 = "1.1.2" @@ -2061,6 +2250,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + [[package]] name = "serde" version = "1.0.228" @@ -2228,6 +2423,18 @@ dependencies = [ "der", ] +[[package]] +name = "srp" +version = "0.6.0" +dependencies = [ + "base64 0.21.7", + "digest", + "generic-array", + "lazy_static", + "num-bigint", + "subtle", +] + [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -2676,6 +2883,16 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +[[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" @@ -2688,7 +2905,7 @@ version = "3.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d39cb1dbab692d82a977c0392ffac19e188bd9186a9f32806f0aaa859d75585a" dependencies = [ - "base64", + "base64 0.22.1", "flate2", "log", "percent-encoding", @@ -2705,7 +2922,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b4531c118335662134346048ddb0e54cc86bd7e81866757873055f0e38f5d2" dependencies = [ - "base64", + "base64 0.22.1", "http", "httparse", "log", @@ -2749,6 +2966,7 @@ checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" dependencies = [ "getrandom 0.3.4", "js-sys", + "md-5", "serde", "wasm-bindgen", ] @@ -3241,6 +3459,18 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +[[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 d6104ae..184ffb7 100644 --- a/idevice/Cargo.toml +++ b/idevice/Cargo.toml @@ -30,7 +30,7 @@ tracing = { version = "0.1.41" } base64 = { version = "0.22" } indexmap = { version = "2.11", features = ["serde"], optional = true } -uuid = { version = "1.18", features = ["serde", "v4"], optional = true } +uuid = { version = "1.18", features = ["serde", "v3", "v4"], optional = true } chrono = { version = "0.4", optional = true, default-features = false, features = [ "serde", ] } @@ -54,6 +54,11 @@ 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 } +srp = { path = "../../apple-private-apis/icloud-auth/rustcrypto-srp", optional = true } obfstr = { version = "0.4", optional = true } @@ -101,6 +106,16 @@ pair = ["chrono/default", "tokio/time", "dep:sha2", "dep:rsa", "dep:x509-cert"] pcapd = [] preboard_service = [] obfuscate = ["dep:obfstr"] +remote_pairing = [ + "dep:serde_json", + "dep:json", + "dep:x25519-dalek", + "dep:ed25519-dalek", + "dep:hkdf", + "dep:chacha20poly1305", + "dep:srp", + "dep:uuid", +] restore_service = [] rsd = ["xpc"] screenshotr = [] @@ -140,6 +155,7 @@ full = [ "pair", "pcapd", "preboard_service", + "remote_pairing", "restore_service", "rsd", "screenshotr", diff --git a/idevice/src/lib.rs b/idevice/src/lib.rs index cacd0de..1b629f9 100644 --- a/idevice/src/lib.rs +++ b/idevice/src/lib.rs @@ -8,6 +8,8 @@ mod ca; pub mod pairing_file; pub mod plist_macro; pub mod provider; +#[cfg(feature = "remote_pairing")] +pub mod remote_pairing; #[cfg(feature = "rustls")] mod sni; #[cfg(feature = "tunnel_tcp_stack")] @@ -856,6 +858,23 @@ pub enum IdeviceError { #[error("Developer mode is not enabled")] DeveloperModeNotEnabled = -68, + + #[error("Unknown TLV {0}")] + UnknownTlv(u8) = -69, + #[error("Malformed TLV")] + MalformedTlv = -70, + #[error("Pairing rejected: {0}")] + PairingRejected(String) = -71, + #[cfg(feature = "remote_pairing")] + #[error("Base64 decode error")] + Base64DecodeError(#[from] base64::DecodeError) = -72, + #[error("Pair verified failed")] + PairVerifyFailed = -73, + #[error("SRP auth failed")] + SrpAuthFailed = -74, + #[cfg(feature = "remote_pairing")] + #[error("Chacha encryption error")] + ChachaEncryption(chacha20poly1305::Error) = -75, } impl IdeviceError { @@ -1021,6 +1040,14 @@ impl IdeviceError { #[cfg(feature = "installation_proxy")] IdeviceError::MalformedPackageArchive(_) => -67, IdeviceError::DeveloperModeNotEnabled => -68, + IdeviceError::UnknownTlv(_) => -69, + IdeviceError::MalformedTlv => -70, + IdeviceError::PairingRejected(_) => -71, + #[cfg(feature = "remote_pairing")] + IdeviceError::Base64DecodeError(_) => -72, + IdeviceError::PairVerifyFailed => -73, + IdeviceError::SrpAuthFailed => -74, + IdeviceError::ChachaEncryption(_) => -75, } } } diff --git a/idevice/src/remote_pairing/mod.rs b/idevice/src/remote_pairing/mod.rs new file mode 100644 index 0000000..144b6f5 --- /dev/null +++ b/idevice/src/remote_pairing/mod.rs @@ -0,0 +1,718 @@ +//! Remote Pairing + +use crate::{IdeviceError, ReadWrite}; + +use base64::{Engine as _, engine::general_purpose::STANDARD as B64}; +use chacha20poly1305::{ + ChaCha20Poly1305, Key, KeyInit, Nonce, + aead::{Aead, Payload}, +}; +use ed25519_dalek::Signature; +use hkdf::Hkdf; +use rand::RngCore; +use rsa::{rand_core::OsRng, signature::SignerMut}; +use serde::Serialize; +use serde_json::json; +use sha2::Sha512; +use srp::{client::SrpClient, groups::G_3072}; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tracing::{debug, warn}; +use x25519_dalek::{EphemeralSecret, PublicKey as X25519PublicKey}; + +mod opack; +mod rp_pairing_file; +mod tlv; + +// export +pub use rp_pairing_file::RpPairingFile; + +const RPPAIRING_MAGIC: &[u8] = b"RPPairing"; +const WIRE_PROTOCOL_VERSION: u8 = 19; + +pub struct RemotePairingClient<'a, R: ReadWrite> { + inner: R, + sequence_number: usize, + pairing_file: &'a mut RpPairingFile, + sending_host: String, + + client_cipher: ChaCha20Poly1305, + server_cipher: ChaCha20Poly1305, +} + +impl<'a, R: ReadWrite> RemotePairingClient<'a, R> { + pub fn new(inner: R, sending_host: &str, pairing_file: &'a mut RpPairingFile) -> Self { + let hk = Hkdf::::new(None, pairing_file.e_private_key.as_bytes()); + let mut okm = [0u8; 32]; + hk.expand(b"ClientEncrypt-main", &mut okm).unwrap(); + let client_cipher = ChaCha20Poly1305::new(chacha20poly1305::Key::from_slice(&okm)); + + let hk = Hkdf::::new(None, pairing_file.e_private_key.as_bytes()); + let mut okm = [0u8; 32]; + hk.expand(b"ServerEncrypt-main", &mut okm).unwrap(); + let server_cipher = ChaCha20Poly1305::new(chacha20poly1305::Key::from_slice(&okm)); + + Self { + inner, + sequence_number: 0, + pairing_file, + sending_host: sending_host.to_string(), + + client_cipher, + server_cipher, + } + } + + pub async fn connect( + &mut self, + pin_callback: impl Fn(S) -> Fut, + state: S, + ) -> Result<(), IdeviceError> + where + Fut: std::future::Future, + { + self.attempt_pair_verify().await?; + + if self.validate_pairing().await.is_err() { + self.pair(pin_callback, state).await?; + } + Ok(()) + } + + 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); + + let pairing_data = tlv::serialize_tlv8(&[ + tlv::TLV8Entry { + tlv_type: tlv::PairingDataComponentType::State, + data: vec![0x01], + }, + tlv::TLV8Entry { + tlv_type: tlv::PairingDataComponentType::PublicKey, + data: x_public_key.to_bytes().to_vec(), + }, + ]); + let pairing_data = B64.encode(pairing_data); + self.send_pairing_data(json! {{ + "data": pairing_data, + "kind": "verifyManualPairing", + "startNewSession": true + }}) + .await?; + let pairing_data = self.receive_pairing_data().await?; + let pairing_data = match pairing_data.as_str() { + Some(p) => p, + None => return Err(IdeviceError::UnexpectedResponse), + }; + + let data = B64.decode(pairing_data)?; + let data = tlv::deserialize_tlv8(&data)?; + + if data + .iter() + .any(|x| x.tlv_type == tlv::PairingDataComponentType::ErrorResponse) + { + self.send_pair_verified_failed().await?; + return Err(IdeviceError::PairVerifyFailed); + } + + 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 = 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 ed25519_signing_key = &mut self.pairing_file.e_private_key; + + let mut signbuf = Vec::with_capacity(32 + self.pairing_file.identifier.len() + 32); + signbuf.extend_from_slice(x_public_key.as_bytes()); // 32 bytes + signbuf.extend_from_slice(self.pairing_file.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: self.pairing_file.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, + }, + ]; + + self.send_pairing_data(json! {{ + "data": B64.encode(tlv::serialize_tlv8(&msg)), + "kind": "verifyManualPairing", + "startNewSession": false + }}) + .await?; + let res = self.receive_pairing_data().await?; + let res = match res.as_str() { + Some(r) => r, + None => { + warn!("Pairing data response was not a string"); + return Err(IdeviceError::UnexpectedResponse); + } + }; + debug!("Verify response: {res:#}"); + + let data = B64.decode(res)?; + let data = tlv::deserialize_tlv8(&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." + ); + self.send_pair_verified_failed().await?; + // Return a specific error to the caller. + return Err(IdeviceError::PairVerifyFailed); + } + + Ok(()) + } + + pub async fn send_pair_verified_failed(&mut self) -> Result<(), IdeviceError> { + self.send_plain_request(json! {{"event": {"_0": {"pairVerifyFailed": {}}}}}) + .await + } + + pub async fn attempt_pair_verify(&mut self) -> Result { + self.send_plain_request(json! { + { + "request": { + "_0": { + "handshake": { + "_0": { + "hostOptions": {"attemptPairVerify": true}, + "wireProtocolVersion": WIRE_PROTOCOL_VERSION, + } + } + } + } + } + }) + .await?; + let response = self.receive_plain_request().await?; + + let response = response + .get("response") + .and_then(|x| x.get("_1")) + .and_then(|x| x.get("handshake")) + .and_then(|x| x.get("_0")); + + match response { + Some(v) => Ok(v.to_owned()), + None => Err(IdeviceError::UnexpectedResponse), + } + } + + pub async fn pair( + &mut self, + pin_callback: impl Fn(S) -> Fut, + state: S, + ) -> Result<(), IdeviceError> + where + Fut: std::future::Future, + { + 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?; + + Ok(()) + } + + /// Returns salt and public key and pin + async fn request_pair_consent( + &mut self, + pin_callback: impl Fn(S) -> Fut, + state: S, + ) -> Result<(Vec, Vec, String), IdeviceError> + where + Fut: std::future::Future, + { + let tlv = tlv::serialize_tlv8(&[ + tlv::TLV8Entry { + tlv_type: tlv::PairingDataComponentType::Method, + data: vec![0x00], + }, + tlv::TLV8Entry { + tlv_type: tlv::PairingDataComponentType::State, + data: vec![0x01], + }, + ]); + let tlv = B64.encode(tlv); + self.send_pairing_data(json! {{ + "data": tlv, + "kind": "setupManualPairing", + "sendingHost": self.sending_host, + "startNewSession": true + }}) + .await?; + + let response = self.receive_plain_request().await?; + let response = &response["event"]["_0"]; + let mut pin = None; + + let pairing_data = match if let Some(err) = response.get("pairingRejectedWithError") { + let context = err + .get("wrappedError") + .and_then(|x| x.get("userInfo")) + .and_then(|x| x.get("NSLocalizedDescription")) + .and_then(|x| x.as_str()) + .map(|x| x.to_string()); + return Err(IdeviceError::PairingRejected(context.unwrap_or_default())); + } else if response.get("awaitingUserConsent").is_some() { + pin = Some("000000".to_string()); + self.receive_pairing_data() + .await? + .as_str() + .map(|x| x.to_string()) + } else { + // On Apple TV, we can get the pin now + response["pairingData"]["_0"]["data"] + .as_str() + .map(|x| x.to_string()) + } { + Some(p) => p, + None => { + return Err(IdeviceError::UnexpectedResponse); + } + }; + + let tlv = tlv::deserialize_tlv8(&B64.decode(pairing_data)?)?; + debug!("Received pairingData response: {tlv:#?}"); + + let mut salt = Vec::new(); + let mut public_key = Vec::new(); + for t in tlv { + match t.tlv_type { + tlv::PairingDataComponentType::Salt => { + salt = t.data; + } + tlv::PairingDataComponentType::PublicKey => { + public_key.extend(t.data); + } + tlv::PairingDataComponentType::ErrorResponse => { + warn!("Pairing data contained error response"); + return Err(IdeviceError::UnexpectedResponse); + } + _ => { + continue; + } + } + } + + let pin = match pin { + Some(p) => p, + None => pin_callback(state).await, + }; + + if salt.is_empty() || public_key.is_empty() { + warn!("Pairing data did not contain salt or public key"); + return Err(IdeviceError::UnexpectedResponse); + } + + Ok((salt, public_key, pin)) + } + + /// Returns the encryption key + async fn init_srp_context( + &mut self, + salt: &[u8], + public_key: &[u8], + pin: &str, + ) -> Result, IdeviceError> { + let client = SrpClient::::new( + &G_3072, // PRIME_3072 + generator + ); + + let mut a_private = [0u8; 32]; + rand::rng().fill_bytes(&mut a_private); + + let a_public = client.compute_public_ephemeral(&a_private); + + let verifier = match client.process_reply( + &a_private, + "Pair-Setup".as_bytes(), + &pin.as_bytes()[..6], + salt, + public_key, + false, + ) { + Ok(v) => v, + Err(e) => { + warn!("SRP verifier creation failed: {e:?}"); + return Err(IdeviceError::SrpAuthFailed); + } + }; + + let client_proof = verifier.proof(); + + let tlv = tlv::serialize_tlv8(&[ + tlv::TLV8Entry { + tlv_type: tlv::PairingDataComponentType::State, + data: vec![0x03], + }, + tlv::TLV8Entry { + tlv_type: tlv::PairingDataComponentType::PublicKey, + data: a_public[..254].to_vec(), + }, + tlv::TLV8Entry { + tlv_type: tlv::PairingDataComponentType::PublicKey, + data: a_public[254..].to_vec(), + }, + tlv::TLV8Entry { + tlv_type: tlv::PairingDataComponentType::Proof, + data: client_proof.to_vec(), + }, + ]); + let tlv = B64.encode(tlv); + + self.send_pairing_data(json! {{ + "data": tlv, + "kind": "setupManualPairing", + "sendingHost": self.sending_host, + "startNewSession": false, + + }}) + .await?; + + let response = self.receive_pairing_data().await?; + let response = match response.as_str() { + Some(r) => tlv::deserialize_tlv8(&B64.decode(r)?)?, + None => { + warn!("Pairing data proof response was not a string"); + return Err(IdeviceError::UnexpectedResponse); + } + }; + + debug!("Proof response: {response:#?}"); + + let proof = match response + .iter() + .find(|x| x.tlv_type == tlv::PairingDataComponentType::Proof) + { + Some(p) => &p.data, + None => { + warn!("Proof response did not contain server proof"); + return Err(IdeviceError::UnexpectedResponse); + } + }; + + match verifier.verify_server(proof) { + Ok(_) => Ok(verifier.key().to_vec()), + Err(e) => { + warn!("Server auth failed: {e:?}"); + Err(IdeviceError::SrpAuthFailed) + } + } + } + + async fn save_pair_record_on_peer( + &mut self, + encryption_key: &[u8], + ) -> Result, IdeviceError> { + let salt = b"Pair-Setup-Encrypt-Salt"; + let info = b"Pair-Setup-Encrypt-Info"; + + let hk = Hkdf::::new(Some(salt), encryption_key); + let mut setup_encryption_key = [0u8; 32]; + hk.expand(info, &mut setup_encryption_key) + .expect("HKDF expand failed"); + + self.pairing_file.recreate_signing_keys(); + { + // new scope, update our signing keys + let hk = Hkdf::::new(None, self.pairing_file.e_private_key.as_bytes()); + let mut okm = [0u8; 32]; + hk.expand(b"ClientEncrypt-main", &mut okm).unwrap(); + self.client_cipher = ChaCha20Poly1305::new(chacha20poly1305::Key::from_slice(&okm)); + + let hk = Hkdf::::new(None, self.pairing_file.e_private_key.as_bytes()); + let mut okm = [0u8; 32]; + hk.expand(b"ServerEncrypt-main", &mut okm).unwrap(); + self.server_cipher = ChaCha20Poly1305::new(chacha20poly1305::Key::from_slice(&okm)); + } + + let hk = Hkdf::::new(Some(b"Pair-Setup-Controller-Sign-Salt"), encryption_key); + + let mut signbuf = Vec::with_capacity(32 + self.pairing_file.identifier.len() + 32); + + let mut hkdf_out = [0u8; 32]; + hk.expand(b"Pair-Setup-Controller-Sign-Info", &mut hkdf_out) + .expect("HKDF expand failed"); + + signbuf.extend_from_slice(&hkdf_out); + + signbuf.extend_from_slice(self.pairing_file.identifier.as_bytes()); + signbuf.extend_from_slice(self.pairing_file.e_public_key.as_bytes()); + + let signature = self.pairing_file.e_private_key.sign(&signbuf); + + let device_info = 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": self.pairing_file.identifier.as_str(), + "model": "computer-model", + "name": self.sending_host.as_str() + }); + let device_info = opack::plist_to_opack(&device_info); + + let tlv = tlv::serialize_tlv8(&[ + tlv::TLV8Entry { + tlv_type: tlv::PairingDataComponentType::Identifier, + data: self.pairing_file.identifier.as_bytes().to_vec(), + }, + tlv::TLV8Entry { + tlv_type: tlv::PairingDataComponentType::PublicKey, + data: self.pairing_file.e_public_key.to_bytes().to_vec(), + }, + tlv::TLV8Entry { + tlv_type: tlv::PairingDataComponentType::Signature, + data: signature.to_vec(), + }, + tlv::TLV8Entry { + tlv_type: tlv::PairingDataComponentType::Info, + data: device_info, + }, + ]); + + let key = Key::from_slice(&setup_encryption_key); // 32 bytes + let cipher = ChaCha20Poly1305::new(key); + + let nonce = Nonce::from_slice(b"\x00\x00\x00\x00PS-Msg05"); // 12 bytes + + let plaintext = &tlv; + + let ciphertext = match cipher.encrypt( + nonce, + Payload { + msg: plaintext, + aad: b"", + }, + ) { + Ok(c) => c, + Err(e) => { + warn!("Chacha encryption failed: {e:?}"); + return Err(IdeviceError::ChachaEncryption(e)); + } + }; + debug!("ciphertext len: {}", ciphertext.len()); + + let tlv = tlv::serialize_tlv8(&[ + tlv::TLV8Entry { + tlv_type: tlv::PairingDataComponentType::EncryptedData, + data: ciphertext[..254].to_vec(), + }, + tlv::TLV8Entry { + tlv_type: tlv::PairingDataComponentType::EncryptedData, + data: ciphertext[254..].to_vec(), + }, + tlv::TLV8Entry { + tlv_type: tlv::PairingDataComponentType::State, + data: vec![0x05], + }, + ]); + let tlv = B64.encode(&tlv); + + debug!("Sending encrypted data"); + self.send_pairing_data(json! {{ + "data": tlv, + "kind": "setupManualPairing", + "sendingHost": self.sending_host, + "startNewSession": false, + }}) + .await?; + + debug!("Waiting for encrypted data"); + let response = match self.receive_pairing_data().await?.as_str() { + Some(r) => B64.decode(r)?, + None => { + warn!("Pairing data response was not base64"); + return Err(IdeviceError::UnexpectedResponse); + } + }; + + let tlv = tlv::deserialize_tlv8(&response)?; + + let mut encrypted_data = Vec::new(); + for t in tlv { + match t.tlv_type { + tlv::PairingDataComponentType::EncryptedData => encrypted_data.extend(t.data), + tlv::PairingDataComponentType::ErrorResponse => { + warn!("TLV contained error response"); + return Err(IdeviceError::UnexpectedResponse); + } + _ => {} + } + } + + let nonce = Nonce::from_slice(b"\x00\x00\x00\x00PS-Msg06"); + + let plaintext = cipher + .decrypt( + nonce, + Payload { + msg: &encrypted_data, + aad: b"", + }, + ) + .expect("decryption failure!"); + + let tlv = tlv::deserialize_tlv8(&plaintext)?; + + debug!("Decrypted plaintext TLV: {tlv:?}"); + Ok(tlv) + } + + async fn send_pairing_data( + &mut self, + pairing_data: impl Serialize, + ) -> Result<(), IdeviceError> { + self.send_plain_request(json! {{"event": {"_0": {"pairingData": {"_0": pairing_data}}}}}) + .await + } + + async fn receive_pairing_data(&mut self) -> Result { + let response = self.receive_plain_request().await?; + + let response = match response.get("event").and_then(|x| x.get("_0")) { + Some(r) => r, + None => return Err(IdeviceError::UnexpectedResponse), + }; + + if let Some(data) = response + .get("pairingData") + .and_then(|x| x.get("_0")) + .and_then(|x| x.get("data")) + { + Ok(data.to_owned()) + } else if let Some(err) = response.get("pairingRejectedWithError") { + let context = err + .get("wrappedError") + .and_then(|x| x.get("userInfo")) + .and_then(|x| x.get("NSLocalizedDescription")) + .and_then(|x| x.as_str()) + .map(|x| x.to_string()); + Err(IdeviceError::PairingRejected(context.unwrap_or_default())) + } else { + Err(IdeviceError::UnexpectedResponse) + } + } + + async fn send_plain_request(&mut self, value: impl Serialize) -> Result<(), IdeviceError> { + self.send_rppairing(json!({ + "message": {"plain": {"_0": value}}, + "originatedBy": "host", + "sequenceNumber": self.sequence_number + })) + .await?; + + self.sequence_number += 1; + Ok(()) + } + + async fn receive_plain_request(&mut self) -> Result { + self.inner + .read_exact(&mut vec![0u8; RPPAIRING_MAGIC.len()]) + .await?; + + let mut packet_len_bytes = [0u8; 2]; + self.inner.read_exact(&mut packet_len_bytes).await?; + let packet_len = u16::from_be_bytes(packet_len_bytes); + + let mut value = vec![0u8; packet_len as usize]; + self.inner.read_exact(&mut value).await?; + + let value: serde_json::Value = serde_json::from_slice(&value)?; + let value = value + .get("message") + .and_then(|x| x.get("plain")) + .and_then(|x| x.get("_0")); + + match value { + Some(v) => Ok(v.to_owned()), + None => Err(IdeviceError::UnexpectedResponse), + } + } + + async fn send_rppairing(&mut self, value: impl Serialize) -> Result<(), IdeviceError> { + let value = serde_json::to_string(&value)?; + let x = value.as_bytes(); + + self.inner.write_all(RPPAIRING_MAGIC).await?; + self.inner + .write_all(&(x.len() as u16).to_be_bytes()) + .await?; + self.inner.write_all(x).await?; + Ok(()) + } +} + +impl std::fmt::Debug for RemotePairingClient<'_, R> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("RemotePairingClient") + .field("inner", &self.inner) + .field("sequence_number", &self.sequence_number) + .field("pairing_file", &self.pairing_file) + .field("sending_host", &self.sending_host) + .finish() + } +} diff --git a/idevice/src/remote_pairing/opack.rs b/idevice/src/remote_pairing/opack.rs new file mode 100644 index 0000000..a7220b0 --- /dev/null +++ b/idevice/src/remote_pairing/opack.rs @@ -0,0 +1,165 @@ +// Jackson Coxson + +use plist::Value; + +pub fn plist_to_opack(value: &Value) -> Vec { + let mut buf = Vec::new(); + plist_to_opack_inner(value, &mut buf); + + buf +} + +fn plist_to_opack_inner(node: &Value, buf: &mut Vec) { + match node { + Value::Dictionary(dict) => { + let count = dict.len() as u32; + let blen = if count < 15 { + (count as u8).wrapping_sub(32) + } else { + 0xEF + }; + buf.push(blen); + + for (key, val) in dict { + plist_to_opack_inner(&Value::String(key.clone()), buf); + plist_to_opack_inner(val, buf); + } + + if count > 14 { + buf.push(0x03); + } + } + Value::Array(array) => { + let count = array.len() as u32; + let blen = if count < 15 { + (count as u8).wrapping_sub(48) + } else { + 0xDF + }; + buf.push(blen); + + for val in array { + plist_to_opack_inner(val, buf); + } + + if count > 14 { + buf.push(0x03); // Terminator + } + } + Value::Boolean(b) => { + let bval = if *b { 1u8 } else { 2u8 }; + buf.push(bval); + } + Value::Integer(integer) => { + let u64val = integer.as_unsigned().unwrap_or(0); + + if u64val <= u8::MAX as u64 { + let u8val = u64val as u8; + if u8val > 0x27 { + buf.push(0x30); + buf.push(u8val); + } else { + buf.push(u8val + 8); + } + } else if u64val <= u32::MAX as u64 { + buf.push(0x32); + buf.extend_from_slice(&(u64val as u32).to_le_bytes()); + } else { + buf.push(0x33); + buf.extend_from_slice(&u64val.to_le_bytes()); + } + } + Value::Real(real) => { + let dval = *real; + let fval = dval as f32; + + if fval as f64 == dval { + buf.push(0x35); + buf.extend_from_slice(&fval.to_bits().swap_bytes().to_ne_bytes()); + } else { + buf.push(0x36); + buf.extend_from_slice(&dval.to_bits().swap_bytes().to_ne_bytes()); + } + } + Value::String(s) => { + let bytes = s.as_bytes(); + let len = bytes.len(); + + if len > 0x20 { + if len <= 0xFF { + buf.push(0x61); + buf.push(len as u8); + } else if len <= 0xFFFF { + buf.push(0x62); + buf.extend_from_slice(&(len as u16).to_le_bytes()); + } else if len <= 0xFFFFFFFF { + buf.push(0x63); + buf.extend_from_slice(&(len as u32).to_le_bytes()); + } else { + buf.push(0x64); + buf.extend_from_slice(&(len as u64).to_le_bytes()); + } + } else { + buf.push(0x40 + len as u8); + } + buf.extend_from_slice(bytes); + } + Value::Data(data) => { + let len = data.len(); + if len > 0x20 { + if len <= 0xFF { + buf.push(0x91); + buf.push(len as u8); + } else if len <= 0xFFFF { + buf.push(0x92); + buf.extend_from_slice(&(len as u16).to_le_bytes()); + } else if len <= 0xFFFFFFFF { + buf.push(0x93); + buf.extend_from_slice(&(len as u32).to_le_bytes()); + } else { + buf.push(0x94); + buf.extend_from_slice(&(len as u64).to_le_bytes()); + } + } else { + buf.push(0x70 + len as u8); + } + buf.extend_from_slice(data); + } + _ => {} + } +} + +#[cfg(test)] +mod tests { + #[test] + fn t1() { + let v = 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::plist_to_opack(&v); + + let expected = [ + 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, + ]; + + println!("{res:02X?}"); + assert_eq!(res, expected); + } +} diff --git a/idevice/src/remote_pairing/rp_pairing_file.rs b/idevice/src/remote_pairing/rp_pairing_file.rs new file mode 100644 index 0000000..7052763 --- /dev/null +++ b/idevice/src/remote_pairing/rp_pairing_file.rs @@ -0,0 +1,113 @@ +// Jackson Coxson + +use std::path::Path; + +use ed25519_dalek::{SigningKey, VerifyingKey}; +use plist::Dictionary; +use rsa::rand_core::OsRng; +use serde::de::Error; +use tracing::{debug, warn}; + +use crate::{IdeviceError, util::plist_to_xml_bytes}; + +#[derive(Clone)] +pub struct RpPairingFile { + pub(crate) e_private_key: SigningKey, + pub(crate) e_public_key: VerifyingKey, + pub(crate) identifier: String, +} + +impl RpPairingFile { + pub fn generate(sending_host: &str) -> Self { + // 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_v3(&uuid::Uuid::NAMESPACE_DNS, sending_host.as_bytes()).to_string(); + + Self { + e_private_key: ed25519_private_key, + e_public_key: ed25519_public_key, + identifier, + } + } + + pub(crate) fn recreate_signing_keys(&mut self) { + let ed25519_private_key = SigningKey::generate(&mut OsRng); + let ed25519_public_key = VerifyingKey::from(&ed25519_private_key); + self.e_public_key = ed25519_public_key; + self.e_private_key = ed25519_private_key; + } + + pub async fn write_to_file(&self, path: impl AsRef) -> Result<(), IdeviceError> { + 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() + }); + tokio::fs::write(path, plist_to_xml_bytes(&v)).await?; + + Ok(()) + } + + pub async fn read_from_file(path: impl AsRef) -> Result { + let s = tokio::fs::read_to_string(path).await?; + let mut p: Dictionary = plist::from_bytes(s.as_bytes())?; + debug!("Read dictionary for rppairingfile: {p:#?}"); + + let public_key = match p + .remove("public_key") + .and_then(|x| x.into_data()) + .filter(|x| x.len() == 32) + .and_then(|x| VerifyingKey::from_bytes(&x[..32].try_into().unwrap()).ok()) + { + Some(p) => p, + None => { + warn!("plist did not contain valid public key bytes"); + return Err(IdeviceError::Plist(plist::Error::missing_field( + "public_key", + ))); + } + }; + + let private_key = match p + .remove("private_key") + .and_then(|x| x.into_data()) + .filter(|x| x.len() == 32) + { + Some(p) => SigningKey::from_bytes(&p.try_into().unwrap()), + None => { + warn!("plist did not contain valid private key bytes"); + return Err(IdeviceError::Plist(plist::Error::missing_field( + "private_key", + ))); + } + }; + + let identifier = match p.remove("identifier").and_then(|x| x.into_string()) { + Some(i) => i, + None => { + warn!("plist did not contain identifier"); + return Err(IdeviceError::Plist(plist::Error::missing_field( + "identifier", + ))); + } + }; + + Ok(Self { + e_private_key: private_key, + e_public_key: public_key, + identifier, + }) + } +} + +impl std::fmt::Debug for RpPairingFile { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("RpPairingFile") + .field("e_public_key", &self.e_public_key) + .field("identifier", &self.identifier) + .finish() + } +} diff --git a/idevice/src/remote_pairing/tlv.rs b/idevice/src/remote_pairing/tlv.rs new file mode 100644 index 0000000..35d5d61 --- /dev/null +++ b/idevice/src/remote_pairing/tlv.rs @@ -0,0 +1,123 @@ +// 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, +} + +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 9beaa5d..6fc2f77 100644 --- a/tools/Cargo.toml +++ b/tools/Cargo.toml @@ -77,6 +77,10 @@ path = "src/amfi.rs" name = "pair" path = "src/pair.rs" +[[bin]] +name = "pair_apple_tv" +path = "src/pair_apple_tv.rs" + [[bin]] name = "syslog_relay" path = "src/syslog_relay.rs" diff --git a/tools/src/pair_apple_tv.rs b/tools/src/pair_apple_tv.rs new file mode 100644 index 0000000..5dfb683 --- /dev/null +++ b/tools/src/pair_apple_tv.rs @@ -0,0 +1,89 @@ +// Jackson Coxson +// A PoC to pair by IP +// Ideally you'd browse by mDNS in production + +use std::{io::Write, net::IpAddr, str::FromStr}; + +use clap::{Arg, Command}; +use idevice::remote_pairing::{RemotePairingClient, RpPairingFile}; + +#[tokio::main] +async fn main() { + // tracing_subscriber::fmt::init(); + + let matches = Command::new("pair") + .about("Pair with the device") + .arg( + Arg::new("ip") + .value_name("IP") + .help("The IP of the Apple TV") + .required(true) + .index(1), + ) + .arg( + Arg::new("port") + .value_name("port") + .help("The port of the Apple TV") + .required(true) + .index(2), + ) + .arg( + Arg::new("about") + .long("about") + .help("Show about information") + .action(clap::ArgAction::SetTrue), + ) + .get_matches(); + + if matches.get_flag("about") { + println!("pair - pair with the Apple TV"); + println!("Copyright (c) 2025 Jackson Coxson"); + return; + } + + let ip = matches.get_one::("ip").expect("no IP passed"); + let port = matches.get_one::("port").expect("no port passed"); + let port = port.parse::().unwrap(); + + let conn = + tokio::net::TcpStream::connect((IpAddr::from_str(ip).expect("failed to parse IP"), port)) + .await + .expect("Failed to connect"); + + let host = "idevice-rs-jkcoxson"; + let mut rpf = RpPairingFile::generate(host); + let mut rpc = RemotePairingClient::new(conn, host, &mut rpf); + rpc.connect( + async |_| { + let mut buf = String::new(); + print!("Enter the Apple TV pin: "); + std::io::stdout().flush().unwrap(); + std::io::stdin() + .read_line(&mut buf) + .expect("Failed to read line"); + buf.trim_end().to_string() + }, + 0u8, // we need no state, so pass a single byte that will hopefully get optimized out + ) + .await + .expect("no pair"); + + // now that we are paired, we should be good + println!("Reconnecting..."); + let conn = + tokio::net::TcpStream::connect((IpAddr::from_str(ip).expect("failed to parse IP"), port)) + .await + .expect("Failed to connect"); + let mut rpc = RemotePairingClient::new(conn, host, &mut rpf); + rpc.connect( + async |_| { + panic!("we tried to pair again :("); + }, + 0u8, + ) + .await + .expect("no reconnect"); + + rpf.write_to_file("atv_pairing_file.plist").await.unwrap(); + println!("Pairing file validated and written to disk. Have a nice day."); +}