Skip to content

Instantly share code, notes, and snippets.

@JJTech0130
Last active April 21, 2025 16:54
Show Gist options
  • Save JJTech0130/049716196f5f1751b8944d93e73d3452 to your computer and use it in GitHub Desktop.
Save JJTech0130/049716196f5f1751b8944d93e73d3452 to your computer and use it in GitHub Desktop.
Apple's GrandSlam Authentication protocol
import base64
import hashlib
import hmac
import locale
import plistlib as plist
from datetime import datetime
import logging
import requests
import srp._pysrp as srp
import urllib3
from cryptography.hazmat.primitives import padding
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from anisette import generate_anisette
from useragent import (
DEVICE_UDID,
GSA_CLIENT_INFO,
GSA_SERIAL,
GSA_USER_AGENT,
USER_ID,
)
# Configure SRP library for compatibility with Apple's implementation
srp.rfc5054_enable()
srp.no_username_in_x()
# Disable SSL Warning
urllib3.disable_warnings()
logger = logging.getLogger(__name__)
def gsa_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()
r = gsa_authenticated_request(
{"A2k": A, "ps": ["s2k", "s2k_fo"], "u": username, "o": "init"}
)
if "sp" not in r:
logger.debug("Failed to authenticate: ", r)
raise Exception("Failed to authenticate", r)
if r["sp"] not in ["s2k", "s2k_fo"]:
logger.debug(
f"This implementation only supports s2k and sk2_fo. Server returned {r['sp']}"
)
raise Exception("Unsupported protocol")
# Change the password out from under the SRP library, as we couldn't calculate it without the salt.
usr.p = encrypt_password(password, r["s"], r["i"], r["sp"])
M = usr.process_challenge(r["s"], r["B"])
# Make sure we processed the challenge correctly
if M is None:
logger.debug("Failed to process challenge")
raise Exception("Failed to process challenge")
r = gsa_authenticated_request(
{"c": r["c"], "M1": M, "u": username, "o": "complete"}
)
# logger.debug(r)
# Make sure that the server's session key matches our session key (and thus that they are not an imposter)
usr.verify_session(r["M2"])
if not usr.authenticated():
logger.debug("Failed to verify session")
raise Exception("Failed to verify session")
spd = decrypt_cbc(usr, r["spd"])
spd = plist.loads(spd, fmt=plist.FMT_XML)
return r, spd
def gsa_authenticated_request(parameters):
body = {
"Header": {"Version": "1.0.1"},
"Request": {"cpd": generate_cpd()},
}
body["Request"].update(parameters)
headers = {
"Content-Type": "text/x-xml-plist",
"Accept": "*/*",
"User-Agent": GSA_USER_AGENT,
"X-MMe-Client-Info": GSA_CLIENT_INFO,
}
resp = requests.post(
"https://gsa.apple.com/grandslam/GsService2",
headers=headers,
data=plist.dumps(body),
verify=False,
timeout=5,
)
return plist.loads(resp.content)["Response"]
def generate_cpd():
cpd = {
# Many of these values are not strictly necessary, but may be tracked by Apple
"bootstrap": True, # All implementations set this to true
"icscrec": True, # Only AltServer sets this to true
"pbe": False, # All implementations explicitly set this to false
"prkgen": True, # I've also seen ckgen
"svct": "iCloud", # In certian circumstances, this can be 'iTunes' or 'iCloud'
}
cpd.update(generate_meta_headers())
cpd.update(generate_anisette())
return cpd
def generate_meta_headers():
return {
"X-Apple-I-Client-Time": datetime.utcnow().replace(microsecond=0).isoformat()
+ "Z",
"X-Apple-I-TimeZone": str(datetime.utcnow().astimezone().tzinfo),
"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": base64.b64encode(str(USER_ID).upper().encode()).decode(),
"X-Mme-Device-Id": str(DEVICE_UDID).upper(),
"X-Apple-I-SRL-NO": GSA_SERIAL, # Serial number
}
def encrypt_password(password, salt, iterations, protocol):
assert protocol in ["s2k", "s2k_fo"]
p = hashlib.sha256(password.encode("utf-8")).digest()
if protocol == "s2k_fo":
p = p.hex().encode("utf-8")
return hashlib.pbkdf2_hmac("sha256", p, salt, iterations, 32)
def create_session_key(usr, name):
k = usr.get_session_key()
if k is None:
raise Exception("No session key")
return hmac.new(k, name.encode(), hashlib.sha256).digest()
def decrypt_cbc(usr, data):
extra_data_key = create_session_key(usr, "extra data key:")
extra_data_iv = create_session_key(usr, "extra data iv:")
# Get only the first 16 bytes of the iv
extra_data_iv = extra_data_iv[:16]
# Decrypt with AES CBC
cipher = Cipher(algorithms.AES(extra_data_key), modes.CBC(extra_data_iv))
decryptor = cipher.decryptor()
data = decryptor.update(data) + decryptor.finalize()
# Remove PKCS#7 padding
padder = padding.PKCS7(128).unpadder()
return padder.update(data) + padder.finalize()
def trigger_trusted_factor(dsid, idms_token):
identity_token = base64.b64encode((dsid + ":" + idms_token).encode()).decode()
headers = {
"Content-Type": "text/x-xml-plist",
"User-Agent": "Xcode",
"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": GSA_CLIENT_INFO,
}
headers.update(generate_meta_headers())
headers.update(generate_anisette())
# 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
# Easier to just use a text prompt
requests.get(
"https://gsa.apple.com/auth/verify/trusteddevice",
headers=headers,
verify=False,
timeout=10,
)
def submit_trusted_factor(code, dsid, idms_token):
identity_token = base64.b64encode((dsid + ":" + idms_token).encode()).decode()
headers = {
"Content-Type": "text/x-xml-plist",
"User-Agent": "Xcode",
"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": GSA_CLIENT_INFO,
"security-code": code,
}
headers.update(generate_meta_headers())
headers.update(generate_anisette())
# Send the 2FA code to Apple
resp = requests.get(
"https://gsa.apple.com/grandslam/GsService2/validate",
headers=headers,
verify=False,
timeout=10,
)
if not resp.ok:
logger.debug("2FA failed")
return False
logger.debug("2FA successful")
return True
def trigger_sms_factor(dsid, idms_token):
identity_token = base64.b64encode((dsid + ":" + idms_token).encode()).decode()
# TODO: Actually do this request to get user prompt data
# a = requests.get("https://gsa.apple.com/auth", verify=False)
# This request isn't strictly necessary though,
# and most accounts should have their id 1 SMS, if not contribute ;)
headers = {
"User-Agent": "Xcode",
"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": GSA_CLIENT_INFO,
}
headers.update(generate_meta_headers())
headers.update(generate_anisette())
# TODO: Actually get the correct id, probably in the above GET
body = {"phoneNumber": {"id": 1}, "mode": "sms"}
# This will send the 2FA code to the user's phone over SMS
# We don't care about the response, it's just some HTML with a form for entering the code
# Easier to just use a text prompt
t = requests.put(
"https://gsa.apple.com/auth/verify/phone/",
json=body,
headers=headers,
verify=False,
timeout=5,
)
def submit_sms_factor(code, dsid, idms_token):
identity_token = base64.b64encode((dsid + ":" + idms_token).encode()).decode()
# TODO: Actually do this request to get user prompt data
# a = requests.get("https://gsa.apple.com/auth", verify=False)
# This request isn't strictly necessary though,
# and most accounts should have their id 1 SMS, if not contribute ;)
headers = {
"User-Agent": "Xcode",
"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": GSA_CLIENT_INFO,
}
headers.update(generate_meta_headers())
headers.update(generate_anisette())
logger.debug(headers)
body = {"phoneNumber": {"id": 1}, "mode": "sms"}
body["securityCode"] = {"code": code}
# Send the 2FA code to Apple
resp = requests.post(
"https://gsa.apple.com/auth/verify/phone/securitycode",
json=body,
headers=headers,
verify=False,
timeout=5,
)
if not resp.ok:
logger.debug("2FA failed")
return False
logger.debug("2FA successful")
return True
import uuid
# TODO: We should probably save these or pull them from the device
DEVICE_UDID = str(uuid.uuid4()).upper()
USER_ID = str(uuid.uuid4()).upper()
PRODUCT = "MacBookPro18,3"
OS_VERSION = "14.3.1"
GSA_SERIAL = "0"
GSA_USER_AGENT = "akd/1.0 CFNetwork/978.0.7 Darwin/18.7.0"
GSA_CLIENT_INFO = "<MacBookPro18,3> <Mac OS X;13.4.1;22F8> <com.apple.AOSKit/282 (com.apple.dt.Xcode/3594.4.19)>"
ANISETTE_USER_AGENT = GSA_USER_AGENT
ANISETTE_CLIENT_INFO = GSA_CLIENT_INFO
ICLOUD_USER_AGENT = "com.apple.iCloudHelper/282 CFNetwork/1408.0.4 Darwin/22.5.0"
ICLOUD_CLIENT_INFO = "<MacBookPro18,3> <Mac OS X;13.4.1;22F8> <com.apple.AOSKit/282 (com.apple.accountsd/113)>"
FINDMY_VERSION = "7.0"
FINDMY_USER_AGENT = "Find%20My/373.11 CFNetwork/1492.0.1 Darwin/23.3.0"
FINDMY_CLIENT_INFO = "<MacBookPro18,3> <macOS;14.3.1;23D60> <com.apple.AuthKit/1 (com.apple.findmy/373.11)>"
@JJTech0130
Copy link
Author

JJTech0130 commented Jan 5, 2025

For anyone trying to use this, you'll need an implementation of Anisette, which you can find here: https://gist.github.com/JJTech0130/fa240e86f639c3a3519599417ffccf61

Don't use this for anything more then a proof-of-concept or a toy to play around with, it makes no effort to appear identical to the real implementation, and may be easily flagged by Apple in the future.

@ProYT303
Copy link

ProYT303 commented Jan 9, 2025

could you make this like log in into an app?

@JJTech0130
Copy link
Author

You mean some third party app that does "Log in with Apple"? Probably, though I haven't experimented extensively with that.

@ProYT303
Copy link

Yea, I dont have any apple devices to debug it with. Would really appreciate that. Thanks

@JJTech0130
Copy link
Author

I mean, a lot of that is going to be up to the individual app's implementation and how it handles Apple ID credentials. I think the traditional way of handling it is using the AuthenticationServices framework, which provides a JWT for the app to do something with?

@ProYT303
Copy link

Yeah, I think it's something related to JWT.

In windows, it opens "https://appleid.apple.com/auth/authorize"

But in macos/ios it opens a popup. Should be related to JWT though.

@ProYT303
Copy link

Yes, it is.

@serios123
Copy link

Hello, please write to me in telegram if you have an offer @Apple17390

@serios123
Copy link

Hello, please write to me in telegram if you have an offer @Apple17390

@svinc33
Copy link

svinc33 commented Apr 21, 2025

hey! i want to apologize in advance for messaging in here but i would appreciate if you could contact me over Telegram.

My TG is: @Niko454

I love your work and would be very interested in a quick discussion.

Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment