Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 15 additions & 6 deletions src/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,17 +114,26 @@ impl<T: AnisetteProvider> TokenProvider<T> {
})
}

pub async fn set_mme_delegate(&self, delegate: MobileMeDelegateResponse) {
// `refreshed` is when this delegate was actually obtained. Pass `SystemTime::now()` for one
// that was just logged in for; a delegate restored from disk must pass its original time, or
// the week-long freshness window below silently re-arms and hands out expired tokens.
pub async fn set_mme_delegate(&self, delegate: MobileMeDelegateResponse, refreshed: SystemTime) {
*self.mme_delegate.lock().await = Some(delegate);
*self.mme_refreshed.lock().await = SystemTime::now();
*self.mme_refreshed.lock().await = refreshed;
}

pub async fn get_storage_info(&self) -> Result<QuotaData, PushError> {
let token = self.get_mme_token("mmeAuthToken").await?;

let quota_url = self.mme_delegate.lock().await.as_ref().expect("no MMe?")
.config.get("com.apple.Dataclass.Quota").expect("No Quota?").as_dictionary().unwrap()
.get("storageInfoURL").expect("no storage info url?").as_string().unwrap().to_string();
// `config` is `#[serde(default)]` because the iosbuddy endpoint omits it, so every lookup
// under it is genuinely optional and cannot be an `expect`.
let quota_url = self.mme_delegate.lock().await.as_ref()
.and_then(|delegate| delegate.config.get("com.apple.Dataclass.Quota"))
.and_then(|quota| quota.as_dictionary())
.and_then(|quota| quota.get("storageInfoURL"))
.and_then(|url| url.as_string())
.ok_or(PushError::MobileMeConfigMissing("com.apple.Dataclass.Quota storageInfoURL"))?
.to_string();

let account = self.account.lock().await;
let dsid = account.spd.as_ref().unwrap().get("DsPrsId").expect("no dsid???s").as_unsigned_integer().unwrap().to_string();
Expand Down Expand Up @@ -176,7 +185,7 @@ impl<T: AnisetteProvider> TokenProvider<T> {

pub async fn get_mme_token(&self, token: &str) -> Result<String, PushError> {
// refresh every week
if self.mme_delegate.lock().await.is_none() || SystemTime::now().duration_since(*self.mme_refreshed.lock().await).unwrap()
if self.mme_delegate.lock().await.is_none() || SystemTime::now().duration_since(*self.mme_refreshed.lock().await).unwrap_or(Duration::MAX)
> Duration::from_secs(60 * 60 * 24 * 7) {
self.refresh_mme().await?;
}
Expand Down
4 changes: 4 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,10 @@ pub enum PushError {
ReportSpamError(u32),
#[error("Token missing")]
TokenMissing,
#[error("MobileMe delegate config has no {0}")]
MobileMeConfigMissing(&'static str),
#[error("Beacon ratchet has no secret; this accessory carries no ratchet state and cannot derive location keys")]
BeaconRatchetUninitialized,
#[error("APS not ready! {0}")]
APSNotReady(&'static str),
#[error("Circle http error {0}")]
Expand Down
9 changes: 9 additions & 0 deletions src/findmy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -572,6 +572,10 @@ struct CommunicationId {
ids: CommunicationIdIds
}

// `Default` exists for consumers that assemble a `BeaconAccessory` purely to read its
// `master_record` / `naming` (the key-export path) and have no ratchet state to supply. Such a
// ratchet has an empty secret and cannot derive location keys — `get_current` rejects it rather
// than ratcheting nothing into more nothing.
#[derive(Clone, Default, Serialize, Deserialize)]
pub struct BeaconRatchet {
index: usize,
Expand Down Expand Up @@ -719,6 +723,11 @@ impl BeaconAccessory {
}

fn get_current(&mut self) -> Result<Vec<(usize, EcKey<Private>)>, PushError> {
// An empty secret ratchets to another empty secret, so without this the accessory would
// quietly produce keys that decrypt nothing rather than reporting that it has no state.
if self.primary_ratchet.secret.is_empty() || self.secondary_ratchet.secret.is_empty() {
return Err(PushError::BeaconRatchetUninitialized);
}
let mut primary = self.get_current_primary();
primary.extend(self.get_current_secondary());
primary.into_iter().map(|i| Ok((i.index, self.derive_ps_key(&i.secret)?))).collect()
Expand Down
16 changes: 15 additions & 1 deletion src/icloud/keychain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1038,12 +1038,26 @@ pub struct KeychainClientState {
pub items: HashMap<String, SavedKeychainZone>,
}

// `MobileMeDelegateResponse::config` is `#[serde(default)]`, so an absent dataclass is ordinary
// rather than exceptional — but a bare `None` at the call site is indistinguishable from an account
// that genuinely has no such service, so say which key was missing.
pub(crate) fn mme_config_url(delegate: &MobileMeDelegateResponse, dataclass: &str, key: &str) -> Option<String> {
let url = delegate.config.get(dataclass)
.and_then(|config| config.as_dictionary())
.and_then(|config| config.get(key))
.and_then(|url| url.as_string());
if url.is_none() {
warn!("MobileMe delegate config has no {dataclass} {key}; that service is unavailable for this account");
}
url.map(str::to_string)
}

impl KeychainClientState {
pub fn new(dsid: String, adsid: String, delegate: &MobileMeDelegateResponse) -> Option<KeychainClientState> {
Some(KeychainClientState {
dsid,
adsid,
host: delegate.config.get("com.apple.Dataclass.KeychainSync")?.as_dictionary().unwrap().get("escrowProxyUrl")?.as_string().unwrap().to_string(),
host: mme_config_url(delegate, "com.apple.Dataclass.KeychainSync", "escrowProxyUrl")?,
state_token: None,
state: HashMap::new(),
user_identity: None,
Expand Down
2 changes: 1 addition & 1 deletion src/sharedstreams.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ impl SharedStreamsState {
pub fn new(dsid: String, delegate: &MobileMeDelegateResponse) -> Option<SharedStreamsState> {
Some(SharedStreamsState {
dsid,
host: delegate.config.get("com.apple.Dataclass.SharedStreams")?.as_dictionary().unwrap().get("url")?.as_string().unwrap().to_string(),
host: crate::icloud::keychain::mme_config_url(delegate, "com.apple.Dataclass.SharedStreams", "url")?,
albums: vec![],
})
}
Expand Down