Skip to content

Commit 0164eea

Browse files
committed
SMSLess registration
1 parent 41dfa3c commit 0164eea

File tree

4 files changed

+279
-10
lines changed

4 files changed

+279
-10
lines changed

src/auth.rs

Lines changed: 109 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use std::{collections::HashMap, io::Cursor, marker::PhantomData, str::FromStr, s
22

33
use aes::{cipher::consts::U16, Aes128};
44
use cloudkit_proto::{octagon_pairing_message::{self, Step5}, CuttlefishPeer, OctagonPairingMessage, OctagonWrapper, SignedInfo};
5+
use deku::{DekuRead, DekuWrite};
56
use hkdf::Hkdf;
67
use icloud_auth::{AppleAccount, CircleSendMessage, LoginState};
78
use log::{debug, info, warn};
@@ -17,8 +18,9 @@ use tokio::sync::{watch, Mutex};
1718
use uuid::Uuid;
1819
use rand::Rng;
1920
use aes_gcm::{Aes128Gcm, KeyInit, Nonce, aead::Aead};
21+
use deku::{DekuContainerWrite, DekuUpdate};
2022

21-
use crate::{aps::{get_message, APSInterestToken}, ids::user::{IDSUser, IDSUserIdentity, IDSUserType}, keychain::{EncodedPeer, KeychainClient}, util::{base64_decode, base64_encode, decode_hex, duration_since_epoch, encode_hex, get_bag, gzip, gzip_normal, plist_to_bin, plist_to_buf, plist_to_string, ungzip, KeyPair, IDS_BAG, REQWEST}, APSConnection, APSConnectionResource, APSMessage, APSState, OSConfig, PushError};
23+
use crate::{APSConnection, APSConnectionResource, APSMessage, APSState, OSConfig, PushError, aps::{APSInterestToken, get_message}, ids::user::{IDSUser, IDSUserIdentity, IDSUserType}, keychain::{EncodedPeer, KeychainClient}, util::{IDS_BAG, KeyPair, PhoneNumberResponse, REQWEST, base64_decode, base64_encode, decode_hex, duration_since_epoch, encode_hex, get_bag, gzip, gzip_normal, plist_to_bin, plist_to_buf, plist_to_string, ungzip}};
2224

2325
#[derive(Serialize)]
2426
#[serde(rename_all = "kebab-case")]
@@ -272,7 +274,7 @@ struct AuthCertResponse {
272274
cert: Data,
273275
}
274276

275-
async fn authenticate(os_config: &dyn OSConfig, user_id: &str, request: Value, user_type: IDSUserType) -> Result<IDSUser, PushError> {
277+
fn generate_auth_csr(user_id: &str) -> Result<(PKey<Private>, Vec<u8>), PushError> {
276278
let key = PKey::from_rsa(Rsa::generate(2048)?)?;
277279

278280
let mut name = X509Name::builder()?;
@@ -284,9 +286,15 @@ async fn authenticate(os_config: &dyn OSConfig, user_id: &str, request: Value, u
284286
csr.set_subject_name(&name.build())?;
285287
csr.sign(&key, MessageDigest::sha1())?;
286288

289+
Ok((key, csr.build().to_der()?))
290+
}
291+
292+
async fn authenticate(os_config: &dyn OSConfig, user_id: &str, request: Value, user_type: IDSUserType) -> Result<IDSUser, PushError> {
293+
let (key, csr) = generate_auth_csr(user_id)?;
294+
287295
let auth_cert = AuthCertRequest {
288296
authentication_data: request,
289-
csr: csr.build().to_der()?.into(),
297+
csr: csr.into(),
290298
realm_user_id: user_id.to_string(),
291299
};
292300

@@ -337,6 +345,104 @@ pub async fn authenticate_phone(number: &str, phone: AuthPhone, os_config: &dyn
337345
authenticate(os_config, &format!("P:{number}"), plist::to_value(&phone)?, IDSUserType::Phone).await
338346
}
339347

348+
pub async fn authenticate_smsless(auth: &PhoneNumberResponse, host: &str, os_config: &dyn OSConfig, aps: &APSConnection) -> Result<IDSUser, PushError> {
349+
350+
#[derive(DekuWrite, Clone, Debug)]
351+
#[deku(endian = "big")]
352+
struct SMSLessSig {
353+
header: u16,
354+
domain_len: u8,
355+
domain: Vec<u8>,
356+
carrier_len: u16,
357+
carrier: Vec<u8>,
358+
}
359+
360+
let stripped_host = Url::parse(host).expect("Failed to parse host").domain().expect("No domain name?").to_string();
361+
let sig_data = base64_decode(&auth.signature);
362+
let sig = SMSLessSig {
363+
header: 0x0200,
364+
domain_len: stripped_host.len() as u8,
365+
domain: stripped_host.as_bytes().to_vec(),
366+
carrier_len: sig_data.len() as u16,
367+
carrier: sig_data,
368+
};
369+
370+
371+
let user_id = format!("P:{}", auth.phone_number);
372+
373+
#[derive(Serialize)]
374+
#[serde(rename_all = "kebab-case")]
375+
struct SmslessAuthRequest {
376+
csr: Data,
377+
sig: Data,
378+
tag: String,
379+
user_id: String,
380+
}
381+
382+
let (key, csr) = generate_auth_csr(&user_id)?;
383+
384+
let request = SmslessAuthRequest {
385+
csr: csr.into(),
386+
sig: sig.to_bytes()?.into(),
387+
tag: "SIM1".to_string(),
388+
user_id: user_id.clone(),
389+
};
390+
391+
#[derive(Serialize)]
392+
#[serde(rename_all = "kebab-case")]
393+
struct AuthMultipleUsers {
394+
push_token: Data,
395+
authentication_requests: Vec<SmslessAuthRequest>,
396+
}
397+
398+
let users = AuthMultipleUsers {
399+
push_token: aps.get_token().await.to_vec().into(),
400+
authentication_requests: vec![request]
401+
};
402+
403+
let url = get_bag(IDS_BAG, "id-authenticate-multiple-users").await?.into_string().unwrap();
404+
405+
let resp = REQWEST.post(url)
406+
.header("user-agent", format!("com.apple.invitation-registration {}", os_config.get_version_ua()))
407+
.header("x-protocol-version", os_config.get_protocol_version())
408+
.header("content-encoding", "gzip")
409+
.body(gzip_normal(&plist_to_buf(&users)?)?)
410+
.send().await?
411+
.bytes().await?;
412+
413+
#[derive(Deserialize)]
414+
#[serde(rename_all = "kebab-case")]
415+
struct ResponseMultipleUsers {
416+
authentication_responses: Vec<SmslessAuthResponse>,
417+
}
418+
419+
#[derive(Deserialize)]
420+
#[serde(rename_all = "kebab-case")]
421+
struct SmslessAuthResponse {
422+
cert: Data,
423+
user_id: String,
424+
status: u32,
425+
}
426+
427+
let mut result: ResponseMultipleUsers = plist::from_bytes(&resp)?;
428+
429+
let response = result.authentication_responses.remove(0);
430+
431+
if response.status != 0 {
432+
return Err(PushError::CertError(plist::from_bytes(&resp)?))
433+
}
434+
435+
let keypair = KeyPair { cert: response.cert.into(), private: key.private_key_to_der()? };
436+
437+
Ok(IDSUser {
438+
auth_keypair: keypair,
439+
user_id: user_id.to_string(),
440+
registration: HashMap::new(),
441+
user_type: IDSUserType::Phone,
442+
protocol_version: os_config.get_protocol_version(),
443+
})
444+
}
445+
340446
pub enum NonceType {
341447
HTTP,
342448
APNS,

src/error.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,4 +187,6 @@ pub enum PushError {
187187
MasterKeyNotFound,
188188
#[error("Resource Stalled!")]
189189
ResourceStalled,
190+
#[error("ICC Auth failed!")]
191+
ICCAuthFailed,
190192
}

src/lib.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,11 @@ use icloud_auth::LoginClientInfo;
3939
pub use imessage::messages::{TypingApp, SetTranscriptBackgroundMessage, UpdateProfileMessage, UpdateProfileSharingMessage, MessageInst, ShareProfileMessage, SharedPoster, ScheduleMode, PermanentDeleteMessage, OperatedChat, DeleteTarget, MoveToRecycleBinMessage, TextFormat, TextEffect, TextFlags, LinkMeta, LPLinkMetadata, ReactMessageType, ErrorMessage, Reaction, UnsendMessage, EditMessage, UpdateExtensionMessage, PartExtension, ReactMessage, ChangeParticipantMessage, LPImageMetadata, RichLinkImageAttachmentSubstitute, LPIconMetadata, AttachmentType, ExtensionApp, BalloonLayout, Balloon, ConversationData, Message, MessageType, Attachment, NormalMessage, RenameMessage, IconChangeMessage, MessageParts, MessagePart, MMCSFile, IndexedMessagePart};
4040
pub use imessage::aps_client::{IMClient, MADRID_SERVICE};
4141
use util::encode_hex;
42-
pub use util::{NSArrayClass, ResourceState, NSDictionaryClass, NSURL, NSArray, ResourceFailure, NSAttributedString, NSString, NSDictionaryTypedCoder, NSNumber, coder_encode_flattened, coder_decode_flattened, StCollapsedValue};
42+
pub use util::{NSArrayClass, EntitlementsResponse, EntitlementAuthState, ResourceState, NSDictionaryClass, NSURL, NSArray, ResourceFailure, NSAttributedString, NSString, NSDictionaryTypedCoder, NSNumber, coder_encode_flattened, coder_decode_flattened, StCollapsedValue};
4343
pub use ids::user::{IDSUser, register, IDSUserIdentity, IDSNGMIdentity, PrivateDeviceInfo, SupportAlert, SupportAction, ReportMessage};
4444
pub use ids::identity_manager::{SendJob, MessageTarget, IdentityManager};
4545
pub use ids::CertifiedContext;
46-
pub use auth::{authenticate_apple, login_apple_delegates, authenticate_phone, AuthPhone, LoginDelegate, CircleClientSession, TokenProvider};
46+
pub use auth::{authenticate_apple, login_apple_delegates, authenticate_phone, authenticate_smsless, AuthPhone, LoginDelegate, CircleClientSession, TokenProvider};
4747
pub use error::PushError;
4848
pub use cloudkit_proto;
4949
pub use cloudkit_derive;

src/util.rs

Lines changed: 166 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use std::collections::{HashMap, HashSet};
2+
use std::future::Future;
23
use std::io::Cursor;
34
use std::num::ParseIntError;
45
use std::ops::{Deref, DerefMut, Range};
@@ -23,8 +24,9 @@ use base64::Engine;
2324
use prost::Message;
2425
use reqwest::header::{HeaderMap, HeaderValue};
2526
use reqwest::{Certificate, Client, Proxy};
26-
use serde::de::value;
27+
use serde::de::{DeserializeOwned, value};
2728
use serde::{Deserialize, Deserializer, Serialize, Serializer};
29+
use serde_json::json;
2830
use sha2::{Digest, Sha256};
2931
use thiserror::Error;
3032
use tokio::select;
@@ -35,12 +37,12 @@ use uuid::Uuid;
3537
use std::io::{Write, Read};
3638
use std::fmt::{Display, Write as FmtWrite};
3739

38-
use rand::thread_rng;
40+
use rand::{Rng, thread_rng};
3941
use rand::seq::SliceRandom;
4042
use futures::FutureExt;
4143

4244
use crate::ids::CompactECKey;
43-
use crate::PushError;
45+
use crate::{APSConnection, OSConfig, PushError};
4446

4547
pub const APNS_BAG: &str = "http://init-p01st.push.apple.com/bag";
4648
pub const IDS_BAG: &str = "https://init.ess.apple.com/WebObjects/VCInit.woa/wa/getBag?ix=3";
@@ -453,11 +455,51 @@ impl CarrierAddress {
453455
#[serde(rename_all = "PascalCase")]
454456
struct Carrier {
455457
phone_number_registration_gateway_address: CarrierAddress,
458+
carrier_entitlements: CarrierEntitlements,
456459
}
457460

458461
const CARRIER_CONFIG: &str = "https://itunes.apple.com/WebObjects/MZStore.woa/wa/com.apple.jingle.appserver.client.MZITunesClientCheck/version?languageCode=en";
459462

460-
pub async fn get_gateways_for_mccmnc(mccmnc: &str) -> Result<String, PushError> {
463+
464+
#[derive(Deserialize)]
465+
#[serde(rename_all = "PascalCase")]
466+
pub struct CarrierEntitlements {
467+
server_address: String,
468+
user_agent: Option<String>,
469+
protocol_version: Option<String>,
470+
}
471+
472+
impl CarrierEntitlements {
473+
async fn invoke(&self, requests: &[serde_json::Value], config: &dyn OSConfig) -> Result<Vec<serde_json::Value>, PushError> {
474+
let user_agent = self.user_agent.as_ref().map(|i| i.as_str()).unwrap_or("Entitlement/$version ($device) iOS/$iOSVersion ($build) Carrier Settings/$carrierBundleVersion")
475+
.replace("$version", "2")
476+
.replace("$device", "iPhone")
477+
.replace("$iOSVersion", &config.get_debug_meta().user_version)
478+
.replace("$build", &config.get_register_meta().software_version);
479+
480+
let value = gzip_normal(&serde_json::to_vec(requests)?)?;
481+
482+
Ok(REQWEST.post(&self.server_address)
483+
.header("Content-Type", "application/json")
484+
.header("x-country-iso-code", "us")
485+
.header("Accept", "application/json")
486+
.header("Content-Encoding", "gzip")
487+
.header("User-Agent", format!("{} Carrier Settings/50.0.2", user_agent))
488+
.header("Accept-Language", "en")
489+
.header("Accept-Encoding", "gzip")
490+
.header("x-protocol-version", self.protocol_version.as_ref().map(|i| i.as_str()).unwrap_or("2"))
491+
.body(value)
492+
.send().await?
493+
.json().await?)
494+
}
495+
}
496+
497+
pub struct CarrierConfig {
498+
pub gateway: String,
499+
carrier: CarrierEntitlements,
500+
}
501+
502+
pub async fn get_gateways_for_mccmnc(mccmnc: &str) -> Result<CarrierConfig, PushError> {
461503
let data = REQWEST.get(CARRIER_CONFIG)
462504
.send().await?;
463505

@@ -482,13 +524,132 @@ pub async fn get_gateways_for_mccmnc(mccmnc: &str) -> Result<String, PushError>
482524
let mut out = vec![];
483525
archive.by_name(&carrier.to_string()).unwrap().read_to_end(&mut out)?;
484526

527+
info!("here {:?}", plist::from_bytes::<Value>(&out)?);
528+
485529
let parsed_file: Carrier = plist::from_bytes(&out)?;
486-
return Ok(parsed_file.phone_number_registration_gateway_address.vec().choose(&mut thread_rng()).ok_or(PushError::CarrierNotFound)?.clone())
530+
return Ok(CarrierConfig {
531+
gateway: parsed_file.phone_number_registration_gateway_address.vec().choose(&mut thread_rng()).ok_or(PushError::CarrierNotFound)?.clone(),
532+
carrier: parsed_file.carrier_entitlements,
533+
})
487534
}
488535

489536
Err(PushError::CarrierNotFound)
490537
}
491538

539+
#[derive(Serialize, Deserialize)]
540+
pub struct EntitlementAuthState {
541+
device_account_identifier: String,
542+
unique_id: String,
543+
token: Option<String>,
544+
subscriber: String,
545+
mccmnc: String,
546+
}
547+
548+
fn find_entitlement_result<T: DeserializeOwned>(entitlements: &[serde_json::Value], id: u64) -> Result<T, PushError> {
549+
let entitlement = entitlements.iter().find(|i| {
550+
let serde_json::Value::Object(o) = i else { return false };
551+
let Some(serde_json::Value::Number(n)) = o.get("response-id") else { return false };
552+
n.as_u64().expect("not u64") == id
553+
}).expect("Entitlement response not found!");
554+
Ok(serde_json::from_value(entitlement.clone())?)
555+
}
556+
557+
#[derive(Deserialize, Serialize, Debug)]
558+
#[serde(rename_all = "kebab-case")]
559+
pub struct PhoneNumberResponse {
560+
pub phone_number: String,
561+
pub signature: String,
562+
}
563+
564+
#[derive(Deserialize, Serialize, Debug)]
565+
pub struct EntitlementsResponse {
566+
pub phone: PhoneNumberResponse,
567+
pub host: String,
568+
}
569+
570+
impl EntitlementAuthState {
571+
572+
pub fn new(subscriber: String, mccmnc: String, imei: String) -> Self {
573+
Self {
574+
device_account_identifier: Uuid::new_v4().to_string().to_uppercase(),
575+
unique_id: imei,
576+
subscriber,
577+
mccmnc,
578+
token: None,
579+
}
580+
}
581+
582+
pub async fn get_entitlements<Fut: Future<Output = Result<String, PushError>>>(&mut self, config: &dyn OSConfig, aps: &APSConnection, process_challenge: impl FnOnce(String) -> Fut) -> Result<EntitlementsResponse, PushError> {
583+
let entitlements = get_gateways_for_mccmnc(&self.mccmnc).await?.carrier;
584+
585+
let mut starting_id = rand::thread_rng().gen_range(3..5);
586+
587+
let auth_challenge = if let Some(token) = &self.token {
588+
json!({
589+
"device-account-identifier": &self.device_account_identifier,
590+
"auth-type": "EAP-AKA",
591+
"action-name": "getAuthentication",
592+
"subscriber-id": base64_encode(&[b"\x02\x00\x00;\x01", self.subscriber.as_bytes()].concat()),
593+
"request-id": starting_id,
594+
"unique-id": &self.unique_id,
595+
"token": token,
596+
})
597+
} else {
598+
let challenge = entitlements.invoke(&[
599+
json!({
600+
"device-account-identifier": &self.device_account_identifier,
601+
"auth-type": "EAP-AKA",
602+
"action-name": "getAuthentication",
603+
"subscriber-id": base64_encode(&[b"\x02\x00\x00;\x01", self.subscriber.as_bytes()].concat()),
604+
"request-id": starting_id,
605+
"unique-id": &self.unique_id
606+
})
607+
], config).await?;
608+
609+
#[derive(Deserialize)]
610+
struct ChallengeResponse {
611+
challenge: String,
612+
}
613+
let result: ChallengeResponse = find_entitlement_result(&challenge, starting_id)?;
614+
615+
let response = process_challenge(result.challenge).await?;
616+
617+
starting_id = rand::thread_rng().gen_range(10..13);
618+
619+
json!({
620+
"payload": response,
621+
"action-name": "postChallenge",
622+
"request-id": starting_id,
623+
})
624+
};
625+
626+
let response = entitlements.invoke(&[
627+
auth_challenge,
628+
json!({
629+
"client-nonce": base64_encode(&aps.get_token().await),
630+
"action-name": "getPhoneNumber",
631+
"request-id": starting_id + 1,
632+
})
633+
], config).await?;
634+
635+
#[derive(Deserialize)]
636+
#[serde(rename_all = "kebab-case")]
637+
struct AuthResponse {
638+
token: String,
639+
}
640+
641+
let auth: AuthResponse = find_entitlement_result(&response, starting_id)?;
642+
self.token = Some(auth.token);
643+
644+
let phone: PhoneNumberResponse = find_entitlement_result(&response, starting_id + 1)?;
645+
646+
Ok(EntitlementsResponse {
647+
phone,
648+
host: entitlements.server_address.clone()
649+
})
650+
}
651+
}
652+
492653

493654

494655
pub trait Resource: Send + Sync + Sized {

0 commit comments

Comments
 (0)