refactor gsa.py

This commit is contained in:
JJTech0130 2023-10-19 08:33:08 -04:00
parent 0775466b98
commit 8e75cd2969
No known key found for this signature in database
GPG key ID: 23C92EBCCF8F93D6
2 changed files with 137 additions and 272 deletions

View file

@ -26,13 +26,8 @@ else:
USERNAME = input("Username: ")
PASSWORD = input("Password: ")
anisette = gsa.Anisette()
print("Anisette headers:", anisette.generate_headers())
print("Authenticating with Grand Slam...")
g = gsa.authenticate(USERNAME, PASSWORD, anisette)
g = gsa.authenticate(USERNAME, PASSWORD)
#print(g)
pet = g["t"]["com.apple.gs.idms.pet"]["token"]
print("Authenticated!")
@ -55,14 +50,13 @@ else:
headers = {
"X-Apple-ADSID": g["adsid"],
"X-Mme-Nas-Qualify": b64encode(v),
"User-Agent": "com.apple.iCloudHelper/282 CFNetwork/1408.0.4 Darwin/22.5.0"
"X-Mme-Nas-Qualify": b64encode(v).decode(),
"User-Agent": "com.apple.iCloudHelper/282 CFNetwork/1408.0.4 Darwin/22.5.0",
"X-Mme-Client-Info": gsa.build_client(emulated_app="accountsd") # Otherwise we get MOBILEME_TERMS_OF_SERVICE_UPDATE on some accounts
}
headers.update(anisette.generate_headers())
# Otherwise we get MOBILEME_TERMS_OF_SERVICE_UPDATE on some accounts
# Really should just change it in gsa.py
headers["X-Mme-Client-Info"]= "<MacBookPro18,3> <Mac OS X;13.4.1;22F82> <com.apple.AOSKit/282 (com.apple.accountsd/113)>"
#print(headers)
headers.update(gsa.generate_anisette_headers())
print(headers)
print("Logging in to iCloud...")
r = requests.post(
@ -71,18 +65,18 @@ else:
data=data,
headers=headers,
verify=False,
)
print(r)
print(r.headers)
r = plistlib.loads(r.content)
print(r)
search_party_token = r['delegates']['com.apple.mobileme']['service-data']['tokens']['searchPartyToken']
ds_prs_id = r['delegates']['com.apple.mobileme']['service-data']['appleAccountInfo']['dsPrsID'] # This can also be obtained from the grandslam response
#print(r)
print("Logged in!")
# print("Search Party Token: ", search_party_token)
with open(CONFIG_PATH, "w") as f:
json.dump({
"search_party_token": search_party_token,
@ -91,13 +85,10 @@ else:
import time
#print("Search Party Token: ", search_party_token)
r = requests.post(
"https://gateway.icloud.com/acsnservice/fetch",
auth=(ds_prs_id, search_party_token),
headers=gsa.Anisette().generate_headers(),
headers=gsa.generate_anisette_headers(),
json={
"search": [
{

342
gsa.py
View file

@ -17,15 +17,19 @@ import srp._pysrp as srp
from cryptography.hazmat.primitives import padding
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
# Constants
DEBUG = True # Allows using a proxy for debugging (disables SSL verification)
# Server to use for anisette generation
ANISETTE = False # Use local generation with AOSKit (macOS only)
# ANISETTE = "https://sign.rheaa.xyz/"
# ANISETTE = 'http://45.132.246.138:6969/'
ANISETTE = "https://ani.sidestore.io/"
#ANISETTE = "https://ani.sidestore.io/"
# ANISETTE = 'https://sideloadly.io/anisette/irGb3Quww8zrhgqnzmrx'
# ANISETTE = "http://jkcoxson.com:2052/"
# Created here so that it is consistent
USER_ID = uuid.uuid4()
DEVICE_ID = uuid.uuid4()
# Configure SRP library for compatibility with Apple's implementation
srp.rfc5054_enable()
srp.no_username_in_x()
@ -35,190 +39,7 @@ import urllib3
urllib3.disable_warnings()
def generate_anisette() -> dict:
import objc
from Foundation import NSBundle, NSClassFromString # type: ignore
AOSKitBundle = NSBundle.bundleWithPath_(
"/System/Library/PrivateFrameworks/AOSKit.framework"
)
objc.loadBundleFunctions(AOSKitBundle, globals(), [("retrieveOTPHeadersForDSID", b"")]) # type: ignore
util = NSClassFromString("AOSUtilities")
h = util.retrieveOTPHeadersForDSID_("-2")
o = {
"X-Apple-I-MD": str(h["X-Apple-MD"]),
"X-Apple-I-MD-M": str(h["X-Apple-MD-M"]),
}
# h["X-Apple-I-MD"] = str(h["X-Apple-MD"])
# h["X-Apple-I-MD-M"] = str(h["X-Apple-MD-M"])
# print(o)
return o
# r = requests.get(ANISETTE, verify=False if DEBUG else True, timeout=5)
# r = json.loads(r.text)
# return r
class Anisette:
@staticmethod
def _fetch(url: str) -> dict:
"""Fetches anisette data that we cannot calculate from a remote server"""
if url == False:
return generate_anisette()
r = requests.get(url, verify=False if DEBUG else True, timeout=5)
r = json.loads(r.text)
return r
def __init__(self, url: str = ANISETTE, name: str = "") -> None:
self._name = name
self._url = url
self._anisette = self._fetch(self._url)
# Generate a "user id": just a random UUID
# TODO: Figure out how to tie it to the user's account on the device
self._user_id = str(uuid.uuid4()).upper()
self._device_id = str(uuid.uuid4()).upper()
# override string printing
def __str__(self) -> str:
return f"{self._name} ({self.backend})"
@property
def url(self) -> str:
return self._url
@property
def backend(self) -> str:
if (
self._anisette["X-MMe-Client-Info"]
== "<MacBookPro15,1> <Mac OS X;10.15.2;19C57> <com.apple.AuthKit/1 (com.apple.dt.Xcode/3594.4.19)>"
):
return "AltServer"
elif (
self._anisette["X-MMe-Client-Info"]
== "<iMac11,3> <Mac OS X;10.15.6;19G2021> <com.apple.AuthKit/1 (com.apple.dt.Xcode/3594.4.19)>"
):
return "Provision"
else:
return f"Unknown ({self._anisette['X-MMe-Client-Info']})"
# Getters
@property
def timestamp(self) -> str:
"""'Timestamp'
Current timestamp in ISO 8601 format
"""
# We only want sencond precision, so we set the microseconds to 0
# We also add 'Z' to the end to indicate UTC
# An alternate way to write this is strftime("%FT%T%zZ")
return datetime.utcnow().replace(microsecond=0).isoformat() + "Z"
@property
def timezone(self) -> str:
"""'Time Zone'
Abbreviation of the timezone of the device (e.g. EST)"""
return str(datetime.utcnow().astimezone().tzinfo)
@property
def locale(self) -> str:
"""'Locale'
Locale of the device (e.g. en_US)
"""
return locale.getdefaultlocale()[0] or "en_US"
@property
def otp(self) -> str:
"""'One Time Password'
A seemingly random base64 string containing 28 bytes
TODO: Figure out how to generate this
"""
return self._anisette["X-Apple-I-MD"]
@property
def local_user(self) -> str:
"""'Local User ID'
There are 2 possible implementations of this value
1. Uppercase hex of the SHA256 hash of some unknown value (used by Windows based servers)
2. Base64 encoding of an uppercase UUID (used by android based servers)
I picked the second one because it's more fully understood.
"""
return b64encode(self._user_id.encode()).decode()
@property
def machine(self) -> str:
"""'Machine ID'
This is a base64 encoded string of 60 'random' bytes
We're not sure how this is generated, we have to rely on the server
TODO: Figure out how to generate this
"""
return self._anisette["X-Apple-I-MD-M"]
@property
def router(self) -> str:
"""'Routing Info'
This is a number, either 17106176 or 50660608
It doesn't seem to matter which one we use,
17106176 is used by Sideloadly and Provision (android) based servers
50660608 is used by Windows iCloud based servers
"""
return "17106176"
@property
def serial(self) -> str:
"""'Device Serial Number'
This is the serial number of the device
You can use a legitimate serial number, but Apple accepts '0' as well (for andriod devices)
See https://github.com/acidanthera/OpenCorePkg/blob/master/Utilities/macserial/macserial.c for how to generate a legit serial
"""
return "0"
@property
def device(self) -> str:
"""'Device Unique Identifier'
This is just an uppercase UUID"""
return self._device_id
def _build_client(self, emulated_device: str, emulated_app: str) -> str:
# TODO: Update OS version and app versions
model = emulated_device
if emulated_device == "PC":
# We're emulating a PC, so we run Windows (Vista?)
os = "Windows"
os_version = "6.2(0,0);9200"
else:
# We're emulating a Mac, so we run macOS (What is 15.6?)
os = "Mac OS X"
os_version = "10.15.6;19G2021"
if emulated_app == "Xcode":
app_bundle = "com.apple.dt.Xcode"
app_version = "3594.4.19"
else:
app_bundle = "com.apple.iCloud"
app_version = "7.21"
if os == "Windows":
authkit_bundle = "com.apple.AuthKitWin"
else:
authkit_bundle = "com.apple.AuthKit"
authkit_version = "1"
return f"<{model}> <{os};{os_version}> <{authkit_bundle}/{authkit_version} ({app_bundle}/{app_version})>"
@property
def client(self) -> str:
def build_client(emulated_device: str = "MacBookPro18,3", emulated_app: str = "accountsd") -> str:
"""'Client Information'
String in the following format:
<%MODEL%> <%OS%;%MAJOR%.%MINOR%(%SPMAJOR%,%SPMINOR%);%BUILD%> <%AUTHKIT_BUNDLE_ID%/%AUTHKIT_VERSION% (%APP_BUNDLE_ID%/%APP_VERSION%)>
@ -235,40 +56,37 @@ class Anisette:
APP_BUNDLE_ID: The bundle ID of the app (e.g. com.apple.dt.Xcode)
APP_VERSION: The version of the app (e.g. 3594.4.19)
"""
return self._build_client("iMac11,3", "Xcode")
def generate_headers(self, client_info: bool = False) -> dict:
h = {
# Current Time
"X-Apple-I-Client-Time": self.timestamp,
"X-Apple-I-TimeZone": self.timezone,
# Locale
# Some implementations only use this for locale
"loc": self.locale,
"X-Apple-Locale": self.locale,
# Anisette
"X-Apple-I-MD": self.otp, # 'One Time Password'
# 'Local User ID'
"X-Apple-I-MD-LU": self.local_user,
"X-Apple-I-MD-M": self.machine, # 'Machine ID'
# 'Routing Info', some implementations convert this to an integer
"X-Apple-I-MD-RINFO": self.router,
# Device information
# 'Device Unique Identifier'
"X-Mme-Device-Id": self.device,
# 'Device Serial Number'
"X-Apple-I-SRL-NO": self.serial,
}
model = emulated_device
if emulated_device == "PC":
# We're emulating a PC, so we run Windows (Vista?)
os = "Windows"
os_version = "6.2(0,0);9200"
else:
# We're emulating a Mac, so we run macOS Ventura
os = "Mac OS X"
os_version = "13.4.1;22F8"
# Additional client information only used in some requests
if client_info:
h["X-Mme-Client-Info"] = self.client
h["X-Apple-App-Info"] = "com.apple.gs.xcode.auth"
h["X-Xcode-Version"] = "11.2 (11B41)"
if emulated_app == "Xcode":
app_bundle = "com.apple.dt.Xcode"
app_version = "3594.4.19"
elif emulated_app == "accountsd":
app_bundle = "com.apple.accountsd"
app_version = "113"
else:
app_bundle = "com.apple.iCloud"
app_version = "7.21"
return h
if os == "Windows":
authkit_bundle = "com.apple.AuthKitWin"
authkit_version = "1"
else:
authkit_bundle = "com.apple.AOSKit"
authkit_version = "282"
def generate_cpd(self) -> dict:
return f"<{model}> <{os};{os_version}> <{authkit_bundle}/{authkit_version} ({app_bundle}/{app_version})>"
def _generate_cpd() -> dict:
cpd = {
# Many of these values are not strictly necessary, but may be tracked by Apple
# I've chosen to match the AltServer implementation
@ -285,17 +103,70 @@ class Anisette:
# 'prtn': 'ME349',
}
cpd.update(self.generate_headers())
cpd.update(generate_anisette_headers())
return cpd
def _generate_meta_headers(serial: str = "0", user_id: uuid = uuid.uuid4(), device_id: uuid = uuid.uuid4()) -> dict:
return {
"X-Apple-I-Client-Time": datetime.utcnow().replace(microsecond=0).isoformat() + "Z", # Current timestamp in ISO 8601 format
"X-Apple-I-TimeZone": str(datetime.utcnow().astimezone().tzinfo), # Abbreviation of the timezone of the device (e.g. EST)
def authenticated_request(parameters, anisette: Anisette) -> dict:
# Locale of the device (e.g. en_US)
"loc": locale.getdefaultlocale()[0] or "en_US",
"X-Apple-Locale": locale.getdefaultlocale()[0] or "en_US",
"X-Apple-I-MD-RINFO": "17106176", # either 17106176 or 50660608
"X-Apple-I-MD-LU": b64encode(str(user_id).upper().encode()).decode(), # 'Local User ID': Base64 encoding of an uppercase UUID
"X-Mme-Device-Id": str(device_id).upper(), # 'Device Unique Identifier', uppercase UUID
"X-Apple-I-SRL-NO": serial, # Serial number
}
def _generate_local_anisette() -> dict:
print("Using local anisette generation")
"""Generates anisette data using AOSKit locally"""
import objc
from Foundation import NSBundle, NSClassFromString # type: ignore
AOSKitBundle = NSBundle.bundleWithPath_(
"/System/Library/PrivateFrameworks/AOSKit.framework"
)
objc.loadBundleFunctions(AOSKitBundle, globals(), [("retrieveOTPHeadersForDSID", b"")]) # type: ignore
util = NSClassFromString("AOSUtilities")
h = util.retrieveOTPHeadersForDSID_("-2")
return {
"X-Apple-I-MD": str(h["X-Apple-MD"]),
"X-Apple-I-MD-M": str(h["X-Apple-MD-M"]),
}
def _generate_remote_anisette(url: str) -> dict:
print("Using remote anisette generation: " + url)
h = json.loads(requests.get(url, timeout=5).text)
return {
"X-Apple-I-MD": h["X-Apple-I-MD"],
"X-Apple-I-MD-M": h["X-Apple-I-MD-M"],
}
def generate_anisette_headers() -> dict:
if isinstance(ANISETTE, str) and ANISETTE.startswith("http"):
a = _generate_remote_anisette(ANISETTE)
else:
a =_generate_local_anisette()
a.update(_generate_meta_headers(user_id=USER_ID, device_id=DEVICE_ID))
return a
def authenticated_request(parameters) -> dict:
body = {
"Header": {
"Version": "1.0.1",
},
"Request": {
"cpd": anisette.generate_cpd(),
"cpd": _generate_cpd(),
},
}
body["Request"].update(parameters)
@ -305,11 +176,10 @@ def authenticated_request(parameters, anisette: Anisette) -> dict:
"Content-Type": "text/x-xml-plist",
"Accept": "*/*",
"User-Agent": "akd/1.0 CFNetwork/978.0.7 Darwin/18.7.0",
"X-MMe-Client-Info": anisette.client,
"X-MMe-Client-Info": build_client(emulated_app="Xcode"),
}
resp = requests.post(
# "https://17.32.194.2/grandslam/GsService2",
"https://gsa.apple.com/grandslam/GsService2",
headers=headers,
data=plist.dumps(body),
@ -361,7 +231,7 @@ def decrypt_cbc(usr: srp.User, data: bytes) -> bytes:
return padder.update(data) + padder.finalize()
def trusted_second_factor(dsid, idms_token, anisette: Anisette):
def trusted_second_factor(dsid, idms_token):
identity_token = b64encode((dsid + ":" + idms_token).encode()).decode()
headers = {
@ -370,9 +240,12 @@ def trusted_second_factor(dsid, idms_token, anisette: Anisette):
"Accept": "text/x-xml-plist",
"Accept-Language": "en-us",
"X-Apple-Identity-Token": identity_token,
"X-Apple-App-Info": "com.apple.gs.xcode.auth",
"X-Xcode-Version": "11.2 (11B41)",
"X-Mme-Client-Info": build_client(emulated_app="Xcode")
}
headers.update(anisette.generate_headers(client_info=True))
headers.update(generate_anisette_headers())
# This will trigger the 2FA prompt on trusted devices
# We don't care about the response, it's just some HTML with a form for entering the code
@ -403,7 +276,7 @@ def trusted_second_factor(dsid, idms_token, anisette: Anisette):
print("2FA successful")
def sms_second_factor(dsid, idms_token, anisette: Anisette):
def sms_second_factor(dsid, idms_token):
# TODO: Figure out how to make SMS 2FA work correctly
raise NotImplementedError("SMS 2FA is not yet implemented")
identity_token = b64encode((dsid + ":" + idms_token).encode()).decode()
@ -415,9 +288,12 @@ def sms_second_factor(dsid, idms_token, anisette: Anisette):
"Accept": "application/x-buddyml",
"Accept-Language": "en-us",
"X-Apple-Identity-Token": identity_token,
"X-Apple-App-Info": "com.apple.gs.xcode.auth",
"X-Xcode-Version": "11.2 (11B41)",
"X-Mme-Client-Info": build_client(emulated_app="Xcode")
}
headers.update(anisette.generate_headers(client_info=True))
headers.update(generate_anisette_headers())
body = {"serverInfo": {"phoneNumber.id": "1"}}
@ -457,7 +333,7 @@ def sms_second_factor(dsid, idms_token, anisette: Anisette):
# print("2FA successful")
def authenticate(username, password, anisette: Anisette):
def authenticate(username, password):
# Password is None as we'll provide it later
usr = srp.User(username, bytes(), hash_alg=srp.SHA256, ng_type=srp.NG_2048)
_, A = usr.start_authentication()
@ -469,8 +345,7 @@ def authenticate(username, password, anisette: Anisette):
# "ps": ["s2k"],
"u": username,
"o": "init",
},
anisette,
}
)
# Check for an error code
@ -497,8 +372,7 @@ def authenticate(username, password, anisette: Anisette):
"M1": M,
"u": username,
"o": "complete",
},
anisette,
}
)
if check_error(r):
@ -524,11 +398,11 @@ def authenticate(username, password, anisette: Anisette):
for k, v in spd.items():
if isinstance(v, bytes):
spd[k] = b64encode(v).decode()
trusted_second_factor(spd["adsid"], spd["GsIdmsToken"], anisette)
return authenticate(username, password, anisette)
trusted_second_factor(spd["adsid"], spd["GsIdmsToken"])
return authenticate(username, password)
elif "au" in r["Status"] and r["Status"]["au"] == "secondaryAuth":
print("SMS authentication required")
sms_second_factor(spd["adsid"], spd["GsIdmsToken"], anisette)
sms_second_factor(spd["adsid"], spd["GsIdmsToken"])
elif "au" in r["Status"]:
print(f"Unknown auth value {r['Status']['au']}")
return