Git · dadoum.zone
Repositories
Help
Report an Issue
apple-crates
Code
Commits
Branches
Tags
Search
Tree:
a74924e
Branches
Tags
main
apple-crates
sharing
src
lib.rs
Refactor and unify a lot of things for another project and implement airdrop support
Dadoum
commited
a74924e
at 2026-10-25 22:15:58
lib.rs
Blame
History
Raw
//! Apple's Sharing AppleID-identity service: the IDMS endpoints sharingd calls //! to obtain its AirDrop identity (certificate, RSA key and Apple-signed //! validation record). //! //! The endpoints are resolved from the GrandSlam URL bag by name: //! //! | bag key | request | //! |---|---| //! | `createCertificate` | [`identity::CreateCertificate`] | //! | `fetchCertificate` | [`identity::FetchCertificate`] | //! | `getMyInfo` | [`info::GetMyInfo`] | //! | `findPerson` | [`person::FindPerson`] | //! | `lookupPersonHandles` | [`person::LookupPersonHandles`] | //! //! All of them are JSON `POST`s authenticated with the GrandSlam app token for //! `com.apple.gs.authagent.auth`, sent as `X-Apple-GS-Token` next to the //! session's `X-Mme-*` device headers, exactly as sharingd's //! `SDAppleIDMutableURLRequest` does (no anisette headers). //! //! The app token is not minted here. On Linux, antarctic's akd owns the //! account and hands the token out: //! //! ```ignore //! use authkit::{AuthKitProxy, LoadAppTokenError}; //! use grandslam::AccountHTTPSession; //! use grandslam::http_session::AnisetteHTTPSession; //! use sharing::{SharingSession, SharingToken}; //! //! async fn sharing_session<'a, 'b>( //! auth: &AuthKitProxy<'_>, //! http: AnisetteHTTPSession<'a, 'b>, //! ) -> Result<SharingSession<'a, 'b>, LoadAppTokenError> { //! let credentials = auth.load_app_token::<SharingToken>().await?; //! let account = AccountHTTPSession::new(http, credentials.alt_dsid); //! Ok(SharingSession::new(account, credentials.app_token)) //! } //! ``` //! //! Recovered from macOS 27 `sharingd` and `Sharing.framework`. use adi::proxy::ADIResult; use base64::Engine; use base64::prelude::BASE64_STANDARD; use grandslam::{AccountHTTPSession, AppHTTPSession, AppToken, Token}; use plist::Value; use reqwest::{Method, RequestBuilder}; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use thiserror::Error; pub mod csr; pub mod identity; pub mod info; pub mod person; #[derive(Debug, Error)] pub enum SharingError { #[error("Failed to perform the demanded action: {0}")] Network(#[from] reqwest::Error), #[error("Failed to perform the demanded action: {0}")] Parsing(#[from] serde_json::Error), #[error("The GrandSlam URL bag has no `{0}` endpoint")] MissingEndpoint(&'static str), #[error("Failed to perform the demanded action: {body} ({status})")] Server { status: reqwest::StatusCode, body: String, }, #[error("The certificate is not ready yet; retry in {0} s")] Pending(u64), #[error("Unexpected server response: {0}")] Unexpected(&'static str), #[error("Cryptography error: {0}")] Crypto(String), #[error("Cannot generate device authentication data: {0}")] Anisette(#[from] adi::proxy::ADIError), #[error("The server rejected the request: {message} ({code})")] App { code: i64, message: String }, } /// The status fields the sharing endpoints return in an otherwise-`200` body /// when the request fails at the application layer. #[derive(Deserialize)] struct ServerStatus { #[serde(rename = "responseCode")] response_code: i64, #[serde(rename = "responseMessage")] response_message: Option<String>, } /// The GrandSlam app token for the AppleID-identity endpoints. /// /// The identifier comes from `SDAppleIDClearGSTokenCache`, which clears the /// account store's credential for exactly this service ID. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(transparent)] pub struct SharingToken(String); impl From<String> for SharingToken { fn from(token: String) -> Self { Self(token) } } impl AsRef<str> for SharingToken { fn as_ref(&self) -> &str { &self.0 } } impl AppToken for SharingToken { // What sharingd passes to `-[ACAccount aida_tokenForService:]` (call site 0x1001005b8). const APP_TOKEN_IDENTIFIER: &'static str = "com.apple.gs.authagent.auth"; } /// A request to one of the Sharing endpoints: the body is the request itself, /// serialized as JSON, and it goes to the URL the bag lists under `BAG_KEY`. pub trait SharingRequest: Serialize { type Response: DeserializeOwned; const BAG_KEY: &'static str; } macro_rules! impl_sharing_request { ($name: ty, $bag_key: literal, $response: ty) => { impl $crate::SharingRequest for $name { type Response = $response; const BAG_KEY: &'static str = $bag_key; } }; } pub(crate) use impl_sharing_request; pub struct SharingSession<'a, 'b> { pub http_session: AppHTTPSession<'a, 'b, SharingToken>, } impl<'a, 'b> SharingSession<'a, 'b> { pub fn new(http_session: AccountHTTPSession<'a, 'b>, token: Token<SharingToken>) -> Self { Self { http_session: AppHTTPSession::new(http_session, token), } } pub fn simple_request_builder(&self, method: Method, url: &str) -> RequestBuilder { self.http_session.simple_request_builder(method, url) } pub fn anisette_request_builder(&self, method: Method, url: &str) -> ADIResult<RequestBuilder> { self.http_session.anisette_request_builder(method, url) } pub fn account_request_builder(&self, method: Method, url: &str) -> ADIResult<RequestBuilder> { self.http_session.account_request_builder(method, url) } /// What `-[SDAppleIDMutableURLRequest initWithURL:gsToken:fetchType:]` sets: /// `Content-Type: application/json`, `X-Mme-Client-Info`, `X-Mme-Device-ID` /// (both from the session's default headers) and `X-Apple-GS-Token`. The real /// requests also carry anisette (`X-Apple-I-MD*`) and `x-apple-baa-*` /// attestation headers, added elsewhere; not sent here yet. fn sharing_request_builder(&self, method: Method, url: &str) -> ADIResult<RequestBuilder> { Ok(self .anisette_request_builder(method, url)? .header("Content-Type", "application/json") .header("X-Apple-GS-Token", self.gs_token_header())) } /// `base64("<altDSID>:<token>")`, as captured from macOS sharingd. The raw /// service token alone is rejected with "Invalid GsToken". fn gs_token_header(&self) -> String { let alt_dsid: &str = self.http_session.http_session.alt_dsid.as_ref(); let token = self.http_session.token.token.as_ref(); BASE64_STANDARD.encode(format!("{alt_dsid}:{token}")) } pub async fn perform_sharing_request<T: SharingRequest>( &self, request: &T, ) -> Result<T::Response, SharingError> { let url = self .http_session .http_session .http_session .url_bag() .get(T::BAG_KEY) .and_then(Value::as_string) .ok_or(SharingError::MissingEndpoint(T::BAG_KEY))?; let body = serde_json::to_vec(request).expect("Serialization should never fail."); let response = self .sharing_request_builder(Method::POST, url)? .body(body) .send() .await?; let status = response.status(); let response = response.bytes().await?; if !status.is_success() { return Err(SharingError::Server { status, body: String::from_utf8_lossy(&response).into_owned(), }); } // The sharing endpoints answer `200 OK` even for application errors, // carrying a non-zero `responseCode` and a `responseMessage`. Surface // those rather than deserializing an error body into an empty response. if let Ok(status) = serde_json::from_slice::<ServerStatus>(&response) && status.response_code != 0 { return Err(SharingError::App { code: status.response_code, message: status.response_message.unwrap_or_default(), }); } Ok(serde_json::from_slice(&response)?) } }