Skip to content
Open
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
184 changes: 127 additions & 57 deletions isideload/src/auth/apple_account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,35 @@ pub struct SMSTwoFactorError {
pub message: String,
}

#[derive(Debug)]
enum SmsSendOutcome {
Sent,
ActiveChallenge,
ServiceError(SMSTwoFactorError),
}

#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct SmsChallengeResponse {
mode: String,
#[serde(rename = "type")]
challenge_type: String,
authentication_type: String,
trusted_phone_numbers: Vec<TrustedNumber>,
trusted_phone_number: TrustedNumber,
security_code: SmsSecurityCode,
}

#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct SmsSecurityCode {
length: u8,
too_many_codes_sent: bool,
too_many_codes_validated: bool,
security_code_locked: bool,
security_code_cooldown: bool,
}

impl AppleAccount {
/// Create a new AppleAccountBuilder with the given email
///
Expand Down Expand Up @@ -466,46 +495,46 @@ impl AppleAccount {
.await
.context("Failed to request SMS 2FA")?;

if !res.status().is_success() {
let status = res.status();
let text = res
.text()
let status = res.status();
let text = if status.is_success() {
String::new()
} else {
res.text()
.await
.context("Failed to read SMS 2FA error response text")?;
// try to parse as json, if it fails, just bail with the text
let error = Self::parse_sms_error(text, status.as_u16())?;

if error.code == "-28248" {
// Verification codes can’t be sent to this phone number at this time. Please try again later.
warn!("{} - {}", error.title, error.message);
self.last_error = format!("{} - {}", error.title, error.message).into();
return Ok(LoginState::NeedsUnknown2FA);
}
.context("Failed to read SMS 2FA response text")?
};

if error.code == "-22979" {
// Too many verification codes have been sent. - Enter the last code you received or try again later.
warn!("{} - {}", error.title, error.message);
self.last_error = format!("{} - {}", error.title, error.message).into();
return Ok(LoginState::NeedsUnknown2FA);
match Self::classify_sms_send_response(status.as_u16(), &text, id)? {
SmsSendOutcome::Sent => {
info!("SMS 2FA request sent");
}

if error.code == "-22981" {
// Too many verification codes have been sent. - Enter the last code you received or try again later.
// Not sure why there are two identical errors with different codes
warn!("{} - {}", error.title, error.message);
self.last_error = format!("{} - {}", error.title, error.message).into();
return Ok(LoginState::NeedsUnknown2FA);
SmsSendOutcome::ActiveChallenge => {
info!("SMS 2FA challenge already active, proceeding to verification");
}
SmsSendOutcome::ServiceError(error) => {
if error.code == "-28248" {
warn!("{} - {}", error.title, error.message);
self.last_error = format!("{} - {}", error.title, error.message).into();
return Ok(LoginState::NeedsUnknown2FA);
}

bail!(
"SMS 2FA request failed (code {}): {} - {}",
error.code,
error.title,
error.message
);
};
if matches!(error.code.as_str(), "-22979" | "-22981") {
// Apple refused to send a new SMS, but the last code sent is
// still valid: keep the selected number and let the user
// enter it without triggering another send.
warn!("{} - {}", error.title, error.message);
self.last_error = format!("{} - {}", error.title, error.message).into();
return Ok(LoginState::NeedsSMS2FAVerification(id));
}

info!("SMS 2FA request sent");
bail!(
"SMS 2FA request failed (code {}): {} - {}",
error.code,
error.title,
error.message
);
}
}

Ok(LoginState::NeedsSMS2FAVerification(id))
}
Expand Down Expand Up @@ -564,37 +593,78 @@ impl AppleAccount {
Ok(LoginState::NeedsLogin)
}

fn parse_sms_error(text: String, status: u16) -> Result<SMSTwoFactorError, Report> {
if let Ok(json) = serde_json::from_str::<serde_json::Value>(&text)
&& let Some(service_errors) = json.get("serviceErrors")
&& let Some(first_error) = service_errors.as_array().and_then(|arr| arr.first())
fn classify_sms_send_response(
status: u16,
text: &str,
requested_number_id: u32,
) -> Result<SmsSendOutcome, Report> {
if (200..300).contains(&status) {
return Ok(SmsSendOutcome::Sent);
}

if let Some(error) = Self::parse_sms_service_error(text) {
return Ok(SmsSendOutcome::ServiceError(error));
}

if status == 412
&& let Ok(challenge) = serde_json::from_str::<SmsChallengeResponse>(text)
&& challenge.mode == "sms"
&& challenge.challenge_type == "verification"
&& challenge.authentication_type == "hsa2"
&& challenge.trusted_phone_number.id == requested_number_id
&& challenge
.trusted_phone_numbers
.iter()
.any(|number| number.id == requested_number_id)
&& challenge.security_code.length == 6
&& !challenge.security_code.too_many_codes_sent
&& !challenge.security_code.too_many_codes_validated
&& !challenge.security_code.security_code_locked
&& !challenge.security_code.security_code_cooldown
{
let code = first_error
.get("code")
.and_then(|c| c.as_str())
.unwrap_or("unknown");
let title = first_error
.get("title")
.and_then(|t| t.as_str())
.unwrap_or("No title provided");
let message = first_error
.get("message")
.and_then(|m| m.as_str())
.unwrap_or("No message provided");

return Ok(SMSTwoFactorError {
code: code.to_string(),
title: title.to_string(),
message: message.to_string(),
});
return Ok(SmsSendOutcome::ActiveChallenge);
}

bail!(
"SMS 2FA code submission failed with http status {}: {}",
"SMS 2FA request failed with http status {}: {}",
status,
text
);
}

fn parse_sms_service_error(text: &str) -> Option<SMSTwoFactorError> {
let json = serde_json::from_str::<serde_json::Value>(text).ok()?;
let first_error = json.get("serviceErrors")?.as_array()?.first()?;
let code = first_error
.get("code")
.and_then(|c| c.as_str())
.unwrap_or("unknown");
let title = first_error
.get("title")
.and_then(|t| t.as_str())
.unwrap_or("No title provided");
let message = first_error
.get("message")
.and_then(|m| m.as_str())
.unwrap_or("No message provided");

Some(SMSTwoFactorError {
code: code.to_string(),
title: title.to_string(),
message: message.to_string(),
})
}

fn parse_sms_error(text: String, status: u16) -> Result<SMSTwoFactorError, Report> {
Self::parse_sms_service_error(&text).ok_or_else(|| {
report!(
"SMS 2FA code submission failed with http status {}: {}",
status,
text
)
})
}

async fn get_trusted_numbers(&mut self) -> Result<Vec<TrustedNumber>, Report> {
let anisette_data = self
.anisette_generator
Expand Down