mirror of
https://github.com/jkcoxson/idevice.git
synced 2026-03-02 14:36:16 +01:00
Initial rppairing support
This commit is contained in:
242
Cargo.lock
generated
242
Cargo.lock
generated
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
718
idevice/src/remote_pairing/mod.rs
Normal file
718
idevice/src/remote_pairing/mod.rs
Normal file
@@ -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::<sha2::Sha512>::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::<sha2::Sha512>::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<Fut, S>(
|
||||
&mut self,
|
||||
pin_callback: impl Fn(S) -> Fut,
|
||||
state: S,
|
||||
) -> Result<(), IdeviceError>
|
||||
where
|
||||
Fut: std::future::Future<Output = String>,
|
||||
{
|
||||
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::<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 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<serde_json::Value, IdeviceError> {
|
||||
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<Fut, S>(
|
||||
&mut self,
|
||||
pin_callback: impl Fn(S) -> Fut,
|
||||
state: S,
|
||||
) -> Result<(), IdeviceError>
|
||||
where
|
||||
Fut: std::future::Future<Output = String>,
|
||||
{
|
||||
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<Fut, S>(
|
||||
&mut self,
|
||||
pin_callback: impl Fn(S) -> Fut,
|
||||
state: S,
|
||||
) -> Result<(Vec<u8>, Vec<u8>, String), IdeviceError>
|
||||
where
|
||||
Fut: std::future::Future<Output = String>,
|
||||
{
|
||||
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<Vec<u8>, IdeviceError> {
|
||||
let client = SrpClient::<Sha512>::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<Vec<tlv::TLV8Entry>, IdeviceError> {
|
||||
let salt = b"Pair-Setup-Encrypt-Salt";
|
||||
let info = b"Pair-Setup-Encrypt-Info";
|
||||
|
||||
let hk = Hkdf::<Sha512>::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::<sha2::Sha512>::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::<sha2::Sha512>::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::<Sha512>::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<serde_json::Value, IdeviceError> {
|
||||
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<serde_json::Value, IdeviceError> {
|
||||
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<R: ReadWrite> 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()
|
||||
}
|
||||
}
|
||||
165
idevice/src/remote_pairing/opack.rs
Normal file
165
idevice/src/remote_pairing/opack.rs
Normal file
@@ -0,0 +1,165 @@
|
||||
// Jackson Coxson
|
||||
|
||||
use plist::Value;
|
||||
|
||||
pub fn plist_to_opack(value: &Value) -> Vec<u8> {
|
||||
let mut buf = Vec::new();
|
||||
plist_to_opack_inner(value, &mut buf);
|
||||
|
||||
buf
|
||||
}
|
||||
|
||||
fn plist_to_opack_inner(node: &Value, buf: &mut Vec<u8>) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
113
idevice/src/remote_pairing/rp_pairing_file.rs
Normal file
113
idevice/src/remote_pairing/rp_pairing_file.rs
Normal file
@@ -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<Path>) -> 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<Path>) -> Result<Self, IdeviceError> {
|
||||
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()
|
||||
}
|
||||
}
|
||||
123
idevice/src/remote_pairing/tlv.rs
Normal file
123
idevice/src/remote_pairing/tlv.rs
Normal file
@@ -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<u8>,
|
||||
}
|
||||
|
||||
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),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
89
tools/src/pair_apple_tv.rs
Normal file
89
tools/src/pair_apple_tv.rs
Normal file
@@ -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::<String>("ip").expect("no IP passed");
|
||||
let port = matches.get_one::<String>("port").expect("no port passed");
|
||||
let port = port.parse::<u16>().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.");
|
||||
}
|
||||
Reference in New Issue
Block a user