Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Convert icloud-auth to async, integrate anisette-v3 and support SMS auth #18

Merged
merged 19 commits into from
May 5, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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
3 changes: 3 additions & 0 deletions apple-dev-apis/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,6 @@ omnisette = {path = "../omnisette"}
icloud_auth = {path = "../icloud-auth"}
hmac = "0.12.1"
sha2 = "0.10.6"

[dev-dependencies]
tokio = { version = "1", features = ["rt", "macros"] }
6 changes: 3 additions & 3 deletions apple-dev-apis/tests/xcsession.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ mod tests {
use apple_dev_apis::XcodeSession;
use icloud_auth::*;

#[test]
fn xcsession_test() {
#[tokio::test]
async fn xcsession_test() {
println!("gsa auth test");

let email = std::env::var("apple_email").unwrap_or_else(|_| {
Expand All @@ -29,7 +29,7 @@ mod tests {
std::io::stdin().read_line(&mut input).unwrap();
input.trim().to_string()
};
let acc = AppleAccount::login(appleid_closure, tfa_closure);
let acc = AppleAccount::login(appleid_closure, tfa_closure).await;
let session = XcodeSession::with(&acc.unwrap());
}
}
5 changes: 4 additions & 1 deletion icloud-auth/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,7 @@ cbc = { version = "0.1.2", features = ["std"] }
aes = "0.8.2"
pkcs7 = "0.3.0"
reqwest = { version = "0.11.14", features = ["blocking", "json", "default-tls"] }
omnisette = {path = "../omnisette"}
omnisette = {path = "../omnisette", features = ["remote-anisette-v3"]}

[dev-dependencies]
tokio = { version = "1", features = ["rt", "macros"] }
11 changes: 4 additions & 7 deletions icloud-auth/src/anisette.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,9 @@ pub struct AnisetteData {

impl AnisetteData {
/// Fetches the data at an anisette server
pub fn new() -> Result<Self, crate::Error> {
let base_headers = match AnisetteHeaders::get_anisette_headers_provider(
AnisetteConfiguration::new()
.set_configuration_path(PathBuf::new().join("anisette_test"))
.set_anisette_url("https://ani.f1sh.me".to_string()),
) {
Ok(mut b) => match b.get_authentication_headers() {
pub async fn new(config: AnisetteConfiguration) -> Result<Self, crate::Error> {
let base_headers = match AnisetteHeaders::get_anisette_headers_provider(config) {
Ok(mut b) => match b.provider.get_authentication_headers().await {
Ok(b) => b,
Err(_) => return Err(Error::ErrorGettingAnisette),
},
Expand All @@ -34,6 +30,7 @@ impl AnisetteData {
app_info: bool,
) -> HashMap<String, String> {
let mut headers = self.base_headers.clone();
println!("headers {:?}", headers);
let old_client_info = headers.remove("X-Mme-Client-Info");
if client_info {
let client_info = match old_client_info {
Expand Down
172 changes: 121 additions & 51 deletions icloud-auth/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ use crate::{anisette::AnisetteData, Error};
use aes::cipher::block_padding::Pkcs7;
use cbc::cipher::{BlockDecryptMut, KeyIvInit};
use hmac::{Hmac, Mac};
use omnisette::AnisetteConfiguration;
use reqwest::{
blocking::{Client, ClientBuilder, Response},
Client, ClientBuilder, Response,
header::{HeaderMap, HeaderName, HeaderValue},
Certificate,
};
Expand Down Expand Up @@ -107,10 +108,30 @@ pub enum LoginResponse {
// NeedsSMS2FASent(Send2FAToDevices),
NeedsDevice2FA(),
Needs2FAVerification(),
NeedsSMS2FA(),
NeedsSMS2FAVerification(VerifyBody),
NeedsLogin(),
Failed(Error),
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would maybe add a generic NeedSecondStep(action: String) too, to handle all the other cases (along with a way to skip the second step if Apple provided all the required tokens).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a state machine, and the "Logged in" state is the finished state. Are there any other steps other than those two? I think it's better to panic now to bring attention to them if they ever show up. But I guess it could also be an error state?

#[derive(Serialize)]
struct VerifyCode {
code: String,
}

#[derive(Serialize)]
struct PhoneNumber {
id: u32
Dadoum marked this conversation as resolved.
Show resolved Hide resolved
}

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct VerifyBody {
phone_number: PhoneNumber,
mode: String,
security_code: Option<VerifyCode>
}

// impl Send2FAToDevices {
// pub fn send_2fa_to_devices(&self) -> LoginResponse {
// self.account.send_2fa_to_devices().unwrap()
Expand All @@ -123,8 +144,8 @@ pub enum LoginResponse {
// }
// }

fn parse_response(res: Result<Response, reqwest::Error>) -> plist::Dictionary {
let res = res.unwrap().text().unwrap();
async fn parse_response(res: Result<Response, reqwest::Error>) -> plist::Dictionary {
let res = res.unwrap().text().await.unwrap();
println!("{:?}", res);
let res: plist::Dictionary = plist::from_bytes(res.as_bytes()).unwrap();
let res: plist::Value = res.get("Response").unwrap().to_owned();
Expand All @@ -135,8 +156,8 @@ fn parse_response(res: Result<Response, reqwest::Error>) -> plist::Dictionary {
}

impl AppleAccount {
pub fn new() -> Self {
let anisette = AnisetteData::new();
pub async fn new(config: AnisetteConfiguration) -> Self {
let anisette = AnisetteData::new(config).await;
Self::new_with_anisette(anisette.unwrap())
}

Expand All @@ -155,15 +176,16 @@ impl AppleAccount {
}
}

pub fn login(
pub async fn login(
appleid_closure: impl Fn() -> (String, String),
tfa_closure: impl Fn() -> String,
config: AnisetteConfiguration,
) -> Result<AppleAccount, Error> {
let anisette = AnisetteData::new();
AppleAccount::login_with_anisette(appleid_closure, tfa_closure, anisette.unwrap())
let anisette = AnisetteData::new(config);
AppleAccount::login_with_anisette(appleid_closure, tfa_closure, anisette.await.unwrap()).await
}

pub fn get_app_token(&self, app_name: &str) -> Result<AppToken, Error> {
pub async fn get_app_token(&self, app_name: &str) -> Result<AppToken, Error> {
let spd = self.spd.as_ref().unwrap();
// println!("spd: {:#?}", spd);
let dsid = spd.get("adsid").unwrap().as_string().unwrap();
Expand Down Expand Up @@ -224,8 +246,8 @@ impl AppleAccount {
.post(GSA_ENDPOINT)
.headers(gsa_headers.clone())
.body(buffer)
.send();
let res = parse_response(res);
.send().await;
let res = parse_response(res).await;
let err_check = Self::check_error(&res);
if err_check.is_err() {
return Err(err_check.err().unwrap());
Expand Down Expand Up @@ -265,30 +287,41 @@ impl AppleAccount {
/// ```
/// Note: You would not provide the 2FA code like this, you would have to actually ask input for it.
//TODO: add login_with_anisette and login, where login autodetcts anisette
pub fn login_with_anisette<F: Fn() -> (String, String), G: Fn() -> String>(
pub async fn login_with_anisette<F: Fn() -> (String, String), G: Fn() -> String>(
appleid_closure: F,
tfa_closure: G,
anisette: AnisetteData,
) -> Result<AppleAccount, Error> {
let mut _self = AppleAccount::new_with_anisette(anisette);
let (username, password) = appleid_closure();
let mut response = _self.login_email_pass(username.clone(), password.clone())?;
let mut response = _self.login_email_pass(username.clone(), password.clone()).await?;
loop {
match response {
LoginResponse::NeedsDevice2FA() => response = _self.send_2fa_to_devices().unwrap(),
LoginResponse::NeedsDevice2FA() => response = _self.send_2fa_to_devices().await.unwrap(),
LoginResponse::Needs2FAVerification() => {
response = _self.verify_2fa(tfa_closure()).unwrap()
response = _self.verify_2fa(tfa_closure()).await.unwrap()
}
LoginResponse::NeedsSMS2FA() => {
response = _self.send_sms_2fa_to_devices().await.unwrap()
}
LoginResponse::NeedsSMS2FAVerification(body) => {
response = _self.verify_sms_2fa(tfa_closure(), body).await.unwrap()
}
LoginResponse::NeedsLogin() => {
response = _self.login_email_pass(username.clone(), password.clone())?
response = _self.login_email_pass(username.clone(), password.clone()).await?
}
LoginResponse::LoggedIn(ac) => return Ok(ac),
LoginResponse::Failed(e) => return Err(e),
}
}
}

pub fn login_email_pass(
pub fn get_pet(&self) -> String {
self.spd.as_ref().unwrap().get("t").unwrap().as_dictionary().unwrap().get("com.apple.gs.idms.pet")
.unwrap().as_dictionary().unwrap().get("token").unwrap().as_string().unwrap().to_string()
}

pub async fn login_email_pass(
&mut self,
username: String,
password: String,
Expand Down Expand Up @@ -340,9 +373,9 @@ impl AppleAccount {
.post(GSA_ENDPOINT)
.headers(gsa_headers.clone())
.body(buffer)
.send();
.send().await;

let res = parse_response(res);
let res = parse_response(res).await;
let err_check = Self::check_error(&res);
if err_check.is_err() {
return Err(err_check.err().unwrap());
Expand Down Expand Up @@ -393,9 +426,9 @@ impl AppleAccount {
.post(GSA_ENDPOINT)
.headers(gsa_headers.clone())
.body(buffer)
.send();
.send().await;

let res = parse_response(res);
let res = parse_response(res).await;
let err_check = Self::check_error(&res);
if err_check.is_err() {
return Err(err_check.err().unwrap());
Expand All @@ -410,24 +443,14 @@ impl AppleAccount {

let status = res.get("Status").unwrap().as_dictionary().unwrap();

let needs2fa = match status.get("au") {
Some(plist::Value::String(s)) => {
if s == "trustedDeviceSecondaryAuth" {
println!("Trusted device authentication required");
true
} else {
println!("Unknown auth value {}", s);
// PHONE AUTH WILL CAUSE ERRORS!
false
}
}
_ => false,
};

self.spd = Some(decoded_spd);

if needs2fa {
return Ok(LoginResponse::NeedsDevice2FA());
if let Some(plist::Value::String(s)) = status.get("au") {
return match s.as_str() {
"trustedDeviceSecondaryAuth" => Ok(LoginResponse::NeedsDevice2FA()),
"secondaryAuth" => Ok(LoginResponse::NeedsSMS2FA()),
_unk => panic!("Unknown auth value {}", _unk)
}
}

Ok(LoginResponse::LoggedIn(self.clone().to_owned()))
Expand All @@ -453,23 +476,48 @@ impl AppleAccount {
.unwrap()
}

pub fn send_2fa_to_devices(&self) -> Result<LoginResponse, crate::Error> {
let headers = self.build_2fa_headers();
pub async fn send_2fa_to_devices(&self) -> Result<LoginResponse, crate::Error> {
let headers = self.build_2fa_headers(false);

let res = self
.client
.get("https://gsa.apple.com/auth/verify/trusteddevice")
.headers(headers)
.send();
.send().await;

if !res.as_ref().unwrap().status().is_success() {
return Err(Error::AuthSrp);
}

return Ok(LoginResponse::Needs2FAVerification());
}
pub fn verify_2fa(&self, code: String) -> Result<LoginResponse, Error> {
let headers = self.build_2fa_headers();

pub async fn send_sms_2fa_to_devices(&self) -> Result<LoginResponse, crate::Error> {
let headers = self.build_2fa_headers(true);

let body = VerifyBody {
phone_number: PhoneNumber {
id: 1
},
mode: "sms".to_string(),
security_code: None
};

let res = self
.client
.put("https://gsa.apple.com/auth/verify/phone/")
.headers(headers)
.json(&body)
.send().await;

if !res.as_ref().unwrap().status().is_success() {
return Err(Error::AuthSrp);
}

return Ok(LoginResponse::NeedsSMS2FAVerification(body));
}
pub async fn verify_2fa(&self, code: String) -> Result<LoginResponse, Error> {
let headers = self.build_2fa_headers(false);
println!("Recieved code: {}", code);
let res = self
.client
Expand All @@ -479,17 +527,37 @@ impl AppleAccount {
HeaderName::from_str("security-code").unwrap(),
HeaderValue::from_str(&code).unwrap(),
)
.send();
.send().await;

let res: plist::Dictionary =
plist::from_bytes(res.unwrap().text().unwrap().as_bytes()).unwrap();
plist::from_bytes(res.unwrap().text().await.unwrap().as_bytes()).unwrap();

let err_check = Self::check_error(&res);
if err_check.is_err() {
return Err(err_check.err().unwrap());
}

Ok(LoginResponse::LoggedIn(self.clone()))
Ok(LoginResponse::NeedsLogin())
}

pub async fn verify_sms_2fa(&self, code: String, mut body: VerifyBody) -> Result<LoginResponse, Error> {
let headers = self.build_2fa_headers(true);
println!("Recieved code: {}", code);

body.security_code = Some(VerifyCode { code });

let res = self
.client
.post("https://gsa.apple.com/auth/verify/phone/securitycode")
.headers(headers)
.json(&body)
.send().await.unwrap();

if res.status() != 200 {
return Err(Error::AuthSrp);
}

Ok(LoginResponse::NeedsLogin())
}

fn check_error(res: &plist::Dictionary) -> Result<(), Error> {
Expand All @@ -508,7 +576,7 @@ impl AppleAccount {
Ok(())
}

fn build_2fa_headers(&self) -> HeaderMap {
fn build_2fa_headers(&self, sms: bool) -> HeaderMap {
let spd = self.spd.as_ref().unwrap();
let dsid = spd.get("adsid").unwrap().as_string().unwrap();
let token = spd.get("GsIdmsToken").unwrap().as_string().unwrap();
Expand All @@ -526,11 +594,13 @@ impl AppleAccount {
);
});

headers.insert(
"Content-Type",
HeaderValue::from_str("text/x-xml-plist").unwrap(),
);
headers.insert("Accept", HeaderValue::from_str("text/x-xml-plist").unwrap());
if !sms {
headers.insert(
"Content-Type",
HeaderValue::from_str("text/x-xml-plist").unwrap(),
);
headers.insert("Accept", HeaderValue::from_str("text/x-xml-plist").unwrap());
}
headers.insert("User-Agent", HeaderValue::from_str("Xcode").unwrap());
TaeHagen marked this conversation as resolved.
Show resolved Hide resolved
headers.insert("Accept-Language", HeaderValue::from_str("en-us").unwrap());
headers.append(
Expand Down
Loading
Loading