pypush-plus-plus/ids.py

442 lines
18 KiB
Python
Raw Normal View History

import plistlib
import random
import uuid
import zlib
2023-04-11 11:17:53 -05:00
from base64 import b64decode, b64encode
from datetime import datetime
import requests
2023-04-11 11:17:53 -05:00
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
import apns
2023-04-12 12:13:47 -05:00
import bags
import gsa
USER_AGENT = "com.apple.madrid-lookup [macOS,13.2.1,22D68,MacBookPro18,3]"
# NOTE: The push token MUST be registered with the account for self-uri!
# This is an actual valid one for my account, since you can look it up anyway.
PUSH_TOKEN = "5V7AY+ikHr4DiSfq1W2UBa71G3FLGkpUSKTrOLg81yk="
SELF_URI = "mailto:jjtech@jjtech.dev"
# Nonce Format:
# 01000001876bd0a2c0e571093967fce3d7
# 01 # version
# 000001876d008cc5 # unix time
# r1r2r3r4r5r6r7r8 # random bytes
def generate_nonce() -> bytes:
2023-04-11 11:17:53 -05:00
return (
b"\x01"
+ int(datetime.now().timestamp() * 1000).to_bytes(8, "big")
+ random.randbytes(8)
)
def load_keys() -> tuple[str, str]:
# Load the private key and certificate from files
with open("ids.key", "r") as f:
ids_key = f.read()
with open("ids.crt", "r") as f:
ids_cert = f.read()
return ids_key, ids_cert
2023-04-11 11:17:53 -05:00
def _create_payload(
bag_key: str,
query_string: str,
push_token: str,
payload: bytes,
nonce: bytes = None,
2023-04-11 11:17:53 -05:00
) -> tuple[str, bytes]:
# Generate the nonce
if nonce is None:
nonce = generate_nonce()
push_token = b64decode(push_token)
2023-04-11 11:17:53 -05:00
return (
nonce
+ len(bag_key).to_bytes(4)
+ bag_key.encode()
+ len(query_string).to_bytes(4)
+ query_string.encode()
+ len(payload).to_bytes(4)
+ payload
+ len(push_token).to_bytes(4)
+ push_token,
nonce,
)
def sign_payload(
private_key: str, bag_key: str, query_string: str, push_token: str, payload: bytes
) -> tuple[str, bytes]:
# Load the private key
2023-04-11 11:17:53 -05:00
key = serialization.load_pem_private_key(
private_key.encode(), password=None, backend=default_backend()
)
payload, nonce = _create_payload(bag_key, query_string, push_token, payload)
sig = key.sign(payload, padding.PKCS1v15(), hashes.SHA1())
sig = b"\x01\x01" + sig
sig = b64encode(sig).decode()
return sig, nonce
2023-04-11 11:17:53 -05:00
# global_key, global_cert = load_keys()
2023-04-12 12:13:47 -05:00
def _send_request(conn: apns.APNSConnection, bag_key: str, body: bytes) -> bytes:
2023-04-11 11:17:53 -05:00
body = zlib.compress(body, wbits=16 + zlib.MAX_WBITS)
# Sign the request
2023-04-12 12:13:47 -05:00
signature, nonce = sign_payload(global_key, bag_key, "", PUSH_TOKEN, body)
2023-04-11 11:17:53 -05:00
headers = {
"x-id-cert": global_cert.replace("-----BEGIN CERTIFICATE-----", "")
.replace("-----END CERTIFICATE-----", "")
.replace("\n", ""),
"x-id-nonce": b64encode(nonce).decode(),
"x-id-sig": signature,
"x-push-token": PUSH_TOKEN,
"x-id-self-uri": SELF_URI,
"User-Agent": USER_AGENT,
"x-protocol-version": "1630",
}
2023-04-11 11:17:53 -05:00
req = {
"cT": "application/x-apple-plist",
"U": b"\x16%D\xd5\xcd:D1\xa1\xa7z6\xa9\xe2\xbc\x8f", # Just random bytes?
"c": 96,
"ua": USER_AGENT,
2023-04-12 12:13:47 -05:00
"u": bags.ids_bag()[bag_key],
2023-04-11 11:17:53 -05:00
"h": headers,
"v": 2,
"b": body,
}
2023-04-11 11:17:53 -05:00
conn.send_message("com.apple.madrid", plistlib.dumps(req, fmt=plistlib.FMT_BINARY))
resp = conn.wait_for_packet(0x0A)
2023-04-11 11:24:38 -05:00
resp_body = apns._get_field(resp[1], 3)
2023-04-11 11:24:38 -05:00
if resp_body is None:
raise (Exception(f"Got invalid response: {resp}"))
2023-04-11 11:24:38 -05:00
return resp_body
2023-04-11 11:17:53 -05:00
def lookup(conn: apns.APNSConnection, query: list[str]) -> any:
query = {"uris": query}
resp = _send_request(conn, "id-query", plistlib.dumps(query))
resp = plistlib.loads(resp)
resp = zlib.decompress(resp["b"], 16 + zlib.MAX_WBITS)
resp = plistlib.loads(resp)
return resp
def _auth_token_request(username: str, password: str) -> any:
# Turn the PET into an auth token
data = {
"apple-id": username,
"client-id": str(uuid.uuid4()),
"delegates": {"com.apple.private.ids": {"protocol-version": "4"}},
"password": password,
}
data = plistlib.dumps(data)
r = requests.post(
"https://setup.icloud.com/setup/prefpane/loginDelegates",
auth=(username, password),
data=data,
verify=False,
)
r = plistlib.loads(r.content)
return r
2023-04-23 12:42:43 -05:00
# Gets an IDS auth token for the given username and password
# If use_gsa is True, GSA authentication will be used, which requires anisette
# If use_gsa is False, it will use a old style 2FA code
# If factor_gen is not None, it will be called to get the 2FA code, otherwise it will be prompted
# Returns (realm user id, auth token)
2023-04-23 12:42:43 -05:00
def _get_auth_token(
username: str, password: str, use_gsa: bool = False, factor_gen: callable = None
) -> tuple[str, str]:
if use_gsa:
g = gsa.authenticate(username, password, gsa.Anisette())
pet = g["t"]["com.apple.gs.idms.pet"]["token"]
else:
# Make the request without the 2FA code to make the prompt appear
_auth_token_request(username, password)
# Now make the request with the 2FA code
if factor_gen is None:
pet = password + input("Enter 2FA code: ")
else:
pet = password + factor_gen()
r = _auth_token_request(username, pet)
2023-04-23 12:42:43 -05:00
# print(r)
if "description" in r:
raise Exception(f"Error: {r['description']}")
service_data = r["delegates"]["com.apple.private.ids"]["service-data"]
realm_user_id = service_data["realm-user-id"]
auth_token = service_data["auth-token"]
# print(f"Auth token for {realm_user_id}: {auth_token}")
return realm_user_id, auth_token
2023-04-23 12:42:43 -05:00
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding, rsa
from cryptography.x509.oid import NameOID
def _generate_csr(private_key: rsa.RSAPrivateKey) -> str:
csr = (
x509.CertificateSigningRequestBuilder()
.subject_name(
x509.Name(
[
2023-04-23 12:42:43 -05:00
x509.NameAttribute(NameOID.COMMON_NAME, random.randbytes(20).hex()),
]
)
)
.sign(private_key, hashes.SHA256())
)
csr = csr.public_bytes(serialization.Encoding.PEM).decode("utf-8")
return (
csr.replace("-----BEGIN CERTIFICATE REQUEST-----", "")
.replace("-----END CERTIFICATE REQUEST-----", "")
.replace("\n", "")
)
# Gets an IDS auth cert for the given user id and auth token
# Returns [private key PEM, certificate PEM]
def _get_auth_cert(user_id, token) -> tuple[str, str]:
private_key = rsa.generate_private_key(
public_exponent=65537, key_size=2048, backend=default_backend()
)
body = {
"authentication-data": {"auth-token": token},
"csr": b64decode(_generate_csr(private_key)),
"realm-user-id": user_id,
}
2023-04-23 12:42:43 -05:00
# print(body["csr"])
body = plistlib.dumps(body)
r = requests.post(
"https://profile.ess.apple.com/WebObjects/VCProfileService.woa/wa/authenticateDS",
data=body,
headers={"x-protocol-version": "1630"},
verify=False,
)
r = plistlib.loads(r.content)
if r["status"] != 0:
raise (Exception(f"Failed to get auth cert: {r}"))
# return b64encode(r["cert"]).decode()
# cert = x509.load_pem_x509_certificate(b64encode(r["cert"]).decode())
cert = x509.load_der_x509_certificate(r["cert"])
# cert = cert.public_bytes(serialization.Encoding.PEM).decode("utf-8")
return (
private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption(),
)
.decode("utf-8")
.strip(),
cert.public_bytes(serialization.Encoding.PEM).decode("utf-8").strip(),
)
2023-04-24 10:35:48 -05:00
2023-04-23 20:04:25 -05:00
def _register_request(push_token, user_id, auth_cert, auth_key, push_cert, push_key):
2023-04-24 10:35:48 -05:00
# body = {'device-name': 'Test'}
body = {
"device-name": "Jamess Laptop",
"hardware-version": "MacBookPro18,3",
"language": "en-US",
"os-version": "macOS,13.2.1,22D68",
# "private-device-data": {
# "ap": "0",
# "d": "703987510.082306",
# "dt": 1,
# "gt": "0",
# "h": "1",
# "ktf": "0",
# "ktv": 54,
# "m": "0",
# "p": "0",
# "pb": "22D68",
# "pn": "macOS",
# "pv": "13.2.1",
# "s": "0",
# "t": "0",
# "u": "E451BD65-51B0-44F3-805A-A92BDD8A5000",
# "v": "1",
# },
"retry-count": 0,
"services": [
{
"capabilities": [{"flags": 1, "name": "Messenger", "version": 1}],
"service": "com.apple.madrid",
# "sub-services": [
# "com.apple.private.alloy.gelato",
# "com.apple.private.alloy.gamecenter.imessage",
# "com.apple.private.alloy.sms",
# "com.apple.private.alloy.biz",
# ],
"users": [
# {
# "client-data": {
# "ec-version": 1,
# "kt-version": 5,
# "nicknames-version": 1,
# "optionally-receive-typing-indicators": True,
# "prefers-sdr": False,
# "public-message-identity-key": b"0\x81\xf6\x81C\x00A\x04\x87\x1e\xeb\xe4u\x0b\xa3\x9e\x9c\xbc\xf8rK\x1e\xfe44%f$\x1d\xe8\xbb\xc6\xbdCD\x9ckv K\xc1\x1e\xb1\xdf4\xc8S6\x0f\x92\xd0=\x1e\x84\x9c\xc5\xa5\xb6\xb7}\xdd\xec\x1e\x1e\xd8Q\xd8\xca\xdb\x07'\xc7\x82\x81\xae\x00\xac0\x81\xa9\x02\x81\xa1\x00\xa8 \xfc\x9f\xa6\xb0V2\xce\x1c\xa7\x13\x9e\x03\xd1\xd8\x97a\xbb\xdd\xac\x86\xb8\x10(\x89\x13QP\x8f\xf0+EP\xd1\xb06\xee\x94\xcd\xa8\x9e\xf1\xedp\xa4\x9726\x1e\xe9\xab\xd4\xcb\xac\x05\xd7\x8c?\xbb\xa2\xde,\xfe\r\x1a\xb9\x88W@\x99\xec\xa0]\r\x1a>dV\xb2@\xc5P\xf3m\x80y\xf5\xa0G\xae\xd8h\x92\xef\xca\x85\xcbB\xed\xa9W\x8c\x13\xd4O\xdbYI2\xdcM\x1f\xf6c\x17\x1c\xd1v\xdd\xbcc\xac,&V\xfd\x07\xa0\xc3\x9f\x00\x1f\xc6\xe4\x02u\x12p\x8f\xe2\xb0\x14\xfai\x12\xbb\xa6\x9a6Q\xa5\xde+\x9e{\xcf\xc8\x1b}\x02\x03\x01\x00\x01",
# "public-message-identity-ngm-version": 12,
# "public-message-identity-version": 2,
# "public-message-ngm-device-prekey-data-key": b"\n v\xd6=#\xb7\xde\xe9~n\xdd\x94|xw\x1c#W\xa5\xe0\xf3\x15\xed\xd1\xa0\xab\xa8\xfd\xa9\x8c\xdd\x16\xb0\x12@x\xb4\xafE\xc4\x1b\xcd\xe9\xb1\x8f\x8aI\x03\xd2&F\x95\x9b\x99R\x92\x07/\x12\xaeM\x10\xf3\xa2u\x7f5]\xd9\x19\xc3\x91\xb5\xb4\xbdO\x9c\x1f\r\xae\xa9\xf3+\x9c\x00M2\x83\x147\xb3X\xa11\x00\xaeV\xbe_\x19\x10\xafg\x19\xbf\x10\xd9A",
# "supports-ack-v1": True,
# "supports-animoji-v2": True,
# "supports-audio-messaging-v2": True,
# "supports-autoloopvideo-v1": True,
# "supports-be-v1": True,
# "supports-ca-v1": True,
# "supports-certified-delivery-v1": True,
# "supports-cross-platform-sharing": True,
# "supports-dq-nr": True,
# "supports-fsm-v1": True,
# "supports-fsm-v2": True,
# "supports-fsm-v3": True,
# "supports-hdr": True,
# "supports-heif": True,
# "supports-ii-v1": True,
# "supports-impact-v1": True,
# "supports-inline-attachments": True,
# "supports-keep-receipts": True,
# "supports-location-sharing": True,
# "supports-media-v2": True,
# "supports-original-timestamp-v1": True,
# "supports-people-request-messages": True,
# "supports-people-request-messages-v2": True,
# "supports-photos-extension-v1": True,
# "supports-photos-extension-v2": True,
# "supports-protobuf-payload-data-v2": True,
# "supports-rem": True,
# "supports-sa-v1": True,
# "supports-st-v1": True,
# "supports-update-attachments-v1": True,
# },
# #"kt-loggable-data": b'\n"\n \rl\xbe\xca\xf7\xe8\xb2\x89k\x18\x1e\xb9,d\xf8\xe2\n\xbf\x8d\xe1E\xd6\xf3T\xcb\xd9\x99d\xd1mk\xeb\x10\x0c\x18\x05"E\x08\x01\x12A\x04\x99\x16\xc3\xd8\x85\x80qPr\xbf\x0c\xdb\x9f\x1bHK\xb2:)\x01\x88\x91\xb1\x08do\xf3\x16\xc7\xaa\xd3nb\xddQF\x8f\xb2a\xb1\xbbK\xdf\xd0\xfa\x95\xa29XZ\xcaRh\xbex\xc4f\xe6G`\x1f\xf2\xf3[',
# "uris": [{"uri": "mailto:user_test2@icloud.com"}],
# "user-id": "D:20994360971",
# }
],
}
],
"software-version": "22D68",
# "validation-data": b"v\x02V`\xd8N\xf3V\xb0\xc4'=\x137+\x16-o\x1d\x00\x1c\xf53\xe0g\xd9\x83x\xe0\xderh\xa0\xc7\x00\x00\x01\xe0\x07\x00\x00\x00\x01\x00\x00\x01\x806\x81<\xb9G\xf0,(CQX\xc5\x1e\xbc\xfe}a\xf7y\xfcg\xa1j/B\xb6k\xf4\xeey\x7f=e\xe6\xea\xa3\xfd\xb2\x18\x8e.\x19\x9e'\x9eO]\xca\xe3{\x9f\x10I\x94\x1a\xe0\xef>\xce\x9dl\xb0\xb2u\x88KT\x0b\xcc\x915\x92\xd3\x86\x1b\xb3\xe5\x04\x9f\x8d\x8a\x82$\x11\xfb\xf2t\xda&\x96@U<lP\"/\xf6->\xec\x84\x13\xe7p\xffS\x02\xde\x8c\xdd\xfcqA\x14!\xa4\x07\x82\xd0\x9fm\xe9~\xc4\xcf\x96\xd6D\xa3\xf0\xb9\xa7\xa2}\xc5\x0e.\x0fvYz\x07\xc2\x9f$s:\xd4<\x13u]\x06f[\xcd\x95\xd1\xad\xe9\xb3\xb3\x9f|\nh-\xa2\xa6\xb9c\xa1\x8d\xf2gx\x84\xbe\x1d\xc4\x03}^\xbf\x9ck#\xa8\xad\xa5\x87\x04\x88D\xd0\xee>\x9f\x0f\xa63;\x7fE\x14\x89\x1c]\x8b\x13o\xbd\xf6\x84`R\xa2\xb7Z\xcc\xdf+\xc5\xe5>\xf73?\x84\xe2d\x97\xd3\x07\x10V\xb6\xb4\nB7\xfc\x8bReeA\x15t\x94\xcf\xa8\x957\x1f\x1d>\xe1\xa4\xc5X\xb0!\x81\xcah\x11.'$\xf6\x12\xb3`\xe9\xa93\x07\xe4}\x02\xee\x95hW\xb7\xfb '_\xafC.\xdd\x13\x8df\xcf(H\x06\x18\xe2\xe7\xce\x93+\xf9\xe0\xf5\x17r\xb5.g2M\x8a\xb7\x80j\xb3\x00*\xa6Z\x1f\x07\xf3\x82\xcfj\xd2R\xc1%\xcc\xbe\x10\x9c\xb4qp\x1f\xd7W\x8c\x1aL2\xfa\xeb\x01s\x01m\xa3$\x9cJ\xf8X?\x99r`pT\xa7\xa6\x80\xcc\xc0\x97\xb3|[%\xc8Usj\x1d\x00\x00\x00\x00\x00\x00\x00O\x01P\xdc\"\x98\xc3\xd9\xb5\xbaK\xdc.\xeb\x81\x87r\x8d\xa4q^\xcb\x00\x00\x006\t\x01\x92\x91\x8fI\xff\xff\xd5g\xf2\x1d\x04G\xf4_\xe4\x90dE\xc7\xb7\xcaO~V\x1e[\x7ff?e|d\xae\xb8\x92(F\xd1_\xe9\xb2\x9e\xb0\xf1\x99\x92\x07\xf8\xb2&I\xed",
"validation-data": random.randbytes(10),
}
2023-04-23 20:04:25 -05:00
body = plistlib.dumps(body)
body = zlib.compress(body, wbits=16 + zlib.MAX_WBITS)
2023-04-24 10:35:48 -05:00
push_sig, push_nonce = sign_payload(push_key, "id-register", "", push_token, body)
auth_sig, auth_nonce = sign_payload(auth_key, "id-register", "", push_token, body)
2023-04-23 20:04:25 -05:00
headers = {
2023-04-24 10:35:48 -05:00
"x-protocol-version": "1640",
"content-type": "application/x-apple-plist",
"content-encoding": "gzip",
"x-auth-sig-0": auth_sig,
"x-auth-cert-0": auth_cert.replace("\n", "")
.replace("-----BEGIN CERTIFICATE-----", "")
.replace("-----END CERTIFICATE-----", ""),
"x-auth-user-id-0": user_id,
"x-auth-nonce-0": b64encode(auth_nonce),
"x-pr-nonce": b64encode(auth_nonce),
"x-push-token": push_token,
"x-push-sig": push_sig,
"x-push-cert": push_cert.replace("\n", "")
.replace("-----BEGIN CERTIFICATE-----", "")
.replace("-----END CERTIFICATE-----", ""),
"x-push-nonce": b64encode(push_nonce),
2023-04-23 20:04:25 -05:00
}
2023-04-24 10:35:48 -05:00
# headers.update(gsa.Anisette().generate_headers())
r = requests.post(
"https://identity.ess.apple.com/WebObjects/TDIdentityService.woa/wa/register",
headers=headers,
data=body,
verify=False,
)
2023-04-23 20:04:25 -05:00
print(r.text)
def test():
import getpass
import json
# Open config as read and write
2023-04-24 10:35:48 -05:00
try:
with open("config.json", "r") as f:
config = json.load(f)
except FileNotFoundError:
config = {}
2023-04-24 10:35:48 -05:00
# If no username is set, prompt for it
if "username" not in config:
config["username"] = input("Enter iCloud username: ")
# If no password is set, prompt for it
if "password" not in config:
config["password"] = getpass.getpass("Enter iCloud password: ")
# If grandslam authentication is not set, prompt for it
if "use_gsa" not in config:
config["use_gsa"] = input("Use grandslam authentication? [y/N] ").lower() == "y"
2023-04-23 12:42:43 -05:00
def factor_gen():
return input("Enter iCloud 2FA code: ")
2023-04-23 12:42:43 -05:00
user_id, token = _get_auth_token(
config["username"], config["password"], config["use_gsa"], factor_gen=factor_gen
2023-04-23 12:42:43 -05:00
)
config["user_id"] = user_id
config["token"] = token
key, cert = _get_auth_cert(user_id, token)
config["key"] = key
config["cert"] = cert
2023-04-24 10:35:48 -05:00
# print(key, cert)
2023-04-23 20:04:25 -05:00
conn1 = apns.APNSConnection()
conn1.connect()
conn1.filter(["com.apple.madrid"])
2023-04-24 10:35:48 -05:00
_register_request(
b64encode(conn1.token), user_id, cert, key, conn1.cert, conn1.private_key
)
2023-04-23 20:04:25 -05:00
# Save config
with open("config.json", "w") as f:
json.dump(config, f, indent=4)
if __name__ == "__main__":
test()