Git · dadoum.zone
Repositories
Help
Report an Issue
apple-crates
Code
Commits
Branches
Tags
Search
Tree:
198bf39
Branches
Tags
main
apple-crates
aps
examples
push_token.rs
Add FairPlay crate reimplementing fairplay-decrypt and fix APS
Dadoum
commited
198bf39
at 2026-10-25 21:12:14
push_token.rs
Blame
History
Raw
//! cargo run -p aps --example push_token -- STATE.plist [--device DEVICE.plist] [--check-connection] use aps::{ActivationDevice, MacOsActivationSigner, PushConnection, PushIdentity, PushToken}; use plist::Data; use rsa::{ RsaPrivateKey, pkcs8::{DecodePrivateKey, EncodePrivateKey}, }; use serde::{Deserialize, Serialize}; use std::{fs, io, path::PathBuf, time::Duration}; use thiserror::Error; #[derive(Debug, Error)] enum ExampleError { #[error("usage: push_token STATE.plist [--device DEVICE.plist] [--check-connection]")] Usage, #[error(transparent)] Io(#[from] io::Error), #[error(transparent)] Http(#[from] reqwest::Error), #[error(transparent)] Plist(#[from] plist::Error), #[error(transparent)] Key(#[from] rsa::pkcs8::Error), #[error(transparent)] Identity(#[from] aps::IdentityError), #[error(transparent)] Activation(#[from] aps::ActivationError<rsa::Error>), #[error(transparent)] Connect(#[from] aps::ConnectError), #[error(transparent)] Courier(#[from] aps::CourierError), } #[derive(Serialize, Deserialize)] struct SavedState { certificate: Data, private_key: Data, token: PushToken, } #[derive(Deserialize)] #[serde(rename_all = "PascalCase")] struct DeviceProfile { product_type: String, product_version: String, build_version: String, serial_number: String, #[serde(rename = "UniqueDeviceID")] unique_device_id: String, } #[tokio::main] async fn main() -> Result<(), ExampleError> { let mut args = std::env::args_os().skip(1); let state_path = PathBuf::from(args.next().ok_or(ExampleError::Usage)?); let mut device_profile = None; let mut check_connection = false; while let Some(argument) = args.next() { if argument == "--device" && device_profile.is_none() { device_profile = Some(PathBuf::from(args.next().ok_or(ExampleError::Usage)?)); } else if argument == "--check-connection" && !check_connection { check_connection = true; } else { return Err(ExampleError::Usage); } } let client = reqwest::Client::builder() .tls_certs_merge([reqwest::Certificate::from_der(aps::APPLE_ROOT_CA)?]) .timeout(Duration::from_secs(30)) .build()?; let mut connection = match fs::read(&state_path) { Ok(bytes) => { let state: SavedState = plist::from_bytes(&bytes)?; let identity = PushIdentity::new( state.certificate.into(), RsaPrivateKey::from_pkcs8_der(state.private_key.as_ref())?, )?; let connection = PushConnection::resume(&client, identity, &state.token).await?; println!( "Resumed APS; token unchanged: {}", connection.push_token() == &state.token ); connection } Err(error) if error.kind() == io::ErrorKind::NotFound => { let profile = match device_profile { Some(path) => plist::from_bytes::<DeviceProfile>(&fs::read(path)?)?, None => DeviceProfile { // Synthetic profile verified against Apple's activation service. product_type: "MacBookPro18,3".into(), product_version: "27.0.1".into(), build_version: "26A434".into(), serial_number: "C02SYNTHETIC".into(), unique_device_id: uuid::Uuid::new_v4().to_string().to_uppercase(), }, }; let device = ActivationDevice { device_class: "MacOS", product_type: &profile.product_type, product_version: &profile.product_version, build_version: &profile.build_version, serial_number: &profile.serial_number, unique_device_id: &profile.unique_device_id, }; let identity = aps::activate(&client, &device, &MacOsActivationSigner::new()).await?; println!( "Activated; device certificate is {} bytes", identity.certificate().len() ); PushConnection::connect(&client, identity).await? } Err(error) => return Err(error.into()), }; connection.set_topics(&["com.apple.idmsauth"]).await?; let state = SavedState { certificate: connection.identity().certificate().to_vec().into(), private_key: connection .identity() .private_key() .to_pkcs8_der()? .as_bytes() .to_vec() .into(), token: connection.push_token().clone(), }; let mut options = fs::OpenOptions::new(); options.write(true).create(true).truncate(true); #[cfg(unix)] { use std::os::unix::fs::OpenOptionsExt; options.mode(0o600); } plist::to_writer_xml(options.open(&state_path)?, &state)?; println!( "Connected; saved identity and {}-byte token to {}", connection.push_token().as_bytes().len(), state_path.display() ); println!( "Courier payload limit: {} bytes", connection.max_payload_size() ); if check_connection { // Drive the first keepalive (60s) and its response deadline (30s). // Do not print received payloads or credential values. let until = tokio::time::Instant::now() + Duration::from_secs(95); loop { match tokio::time::timeout_at(until, connection.receive()).await { Ok(Ok(aps::Event::Notification(notification))) => { connection.acknowledge(¬ification).await?; } Ok(Ok(aps::Event::Acknowledgement { .. })) => {} Ok(Err(error)) => return Err(error.into()), Err(_) => break, } } println!("95-second receive/keepalive check passed"); let token = connection.push_token().clone(); connection.reconnect(&client).await?; println!( "Reconnected; token unchanged: {}", connection.push_token() == &token ); // Persist again because reconnect may replace the token. let state = SavedState { token: connection.push_token().clone(), ..state }; plist::to_writer_xml(options.open(&state_path)?, &state)?; } connection.close().await?; Ok(()) }