more refactoring, make cloudkit more robust

This commit is contained in:
JJTech0130 2023-10-22 17:25:32 -04:00
parent e4a26ae1fa
commit 7594eabf7d
No known key found for this signature in database
GPG key ID: 23C92EBCCF8F93D6
4 changed files with 195 additions and 44 deletions

View file

@ -9,6 +9,7 @@ import json
import random import random
import icloud.gsa as gsa import icloud.gsa as gsa
import icloud.cloudkit as cloudkit import icloud.cloudkit as cloudkit
import icloud
from rich.logging import RichHandler from rich.logging import RichHandler
import logging import logging
@ -21,12 +22,13 @@ def main():
# See if we have a search party token saved # See if we have a search party token saved
import os import os
if os.path.exists(CONFIG_PATH): if os.path.exists(CONFIG_PATH):
print("Using saved config...") logging.info("Using saved config...")
#print("Found search party token!") #print("Found search party token!")
with open(CONFIG_PATH, "r") as f: with open(CONFIG_PATH, "r") as f:
j = json.load(f) j = json.load(f)
cloudkit_token = j["cloudkit_token"] cloudkit_token = j["cloudkit_token"]
ds_prs_id = j["ds_prs_id"] ds_prs_id = j["ds_prs_id"]
mme_token = j["mme_token"]
else: else:
# Prompt for username and password # Prompt for username and password
@ -36,45 +38,22 @@ def main():
r = icloud.login(USERNAME, PASSWORD, delegates=["com.apple.mobileme"]) r = icloud.login(USERNAME, PASSWORD, delegates=["com.apple.mobileme"])
cloudkit_token = r['delegates']['com.apple.mobileme']['service-data']['tokens']['cloudKitToken'] cloudkit_token = r['delegates']['com.apple.mobileme']['service-data']['tokens']['cloudKitToken']
mme_token = r['delegates']['com.apple.mobileme']['service-data']['tokens']['mmeAuthToken']
ds_prs_id = r['delegates']['com.apple.mobileme']['service-data']['appleAccountInfo']['dsPrsID'] # This can also be obtained from the grandslam response ds_prs_id = r['delegates']['com.apple.mobileme']['service-data']['appleAccountInfo']['dsPrsID'] # This can also be obtained from the grandslam response
print("Logged in!") logging.info("Logged in!")
with open(CONFIG_PATH, "w") as f: with open(CONFIG_PATH, "w") as f:
json.dump({ json.dump({
"cloudkit_token": cloudkit_token, "cloudkit_token": cloudkit_token,
"ds_prs_id": ds_prs_id, "ds_prs_id": ds_prs_id,
"mme_token": mme_token,
}, f, indent=4) }, f, indent=4)
print("CloudKit token: ", cloudkit_token) logging.debug("CloudKit token: ", cloudkit_token)
headers = { ck = cloudkit.CloudKit(ds_prs_id, cloudkit_token, mme_token, sandbox=True)
"x-cloudkit-authtoken": cloudkit_token, ck.container("iCloud.dev.jjtech.experiments.cktest").save_record(cloudkit.Record(uuid.uuid4(), "ToDoItem", {"title": "Test"}))
"x-cloudkit-userid": "_ec5fa262446ad56fb4bda84d00e981ff", # Hash of bundle id and icloud id
"x-cloudkit-containerid": "iCloud.dev.jjtech.experiments.cktest",
"x-cloudkit-bundleid": "dev.jjtech.experiments.cktest",
"x-cloudkit-bundleversion": "1",
"x-cloudkit-databasescope": "Public",
"x-cloudkit-environment": "Sandbox",
"accept": "application/x-protobuf",
"content-type": 'application/x-protobuf; desc="https://gateway.icloud.com:443/static/protobuf/CloudDB/CloudDBClient.desc"; messageType=RequestOperation; delimited=true',
"x-apple-operation-id": random.randbytes(8).hex(),
"x-apple-request-uuid": str(uuid.uuid4()).upper()
}
headers.update(gsa.generate_anisette_headers())
body = cloudkit.build_record_save_request(cloudkit.Record(uuid.uuid4(), "ToDoItem", {"title": "Test"}), "iCloud.dev.jjtech.experiments.cktest", sandbox=True)
r = requests.post(
"https://gateway.icloud.com/ckdatabase/api/client/record/save",
headers=headers,
data=body,
verify=False
)
print(r.content)
if __name__ == "__main__": if __name__ == "__main__":
main() main()

73
icloud/_utils.py Normal file
View file

@ -0,0 +1,73 @@
# https://en.wikipedia.org/wiki/LEB128
#
# LEB128 or Little Endian Base 128 is a form of variable-length code
# compression used to store an arbitrarily large integer in a small number of
# bytes. LEB128 is used in the DWARF debug file format and the WebAssembly
# binary encoding for all integer literals.
# Taken from https://github.com/mohanson/pywasm/blob/master/pywasm/leb128.py under the MIT license
import typing
class ULEB128:
@staticmethod
def encode(i: int) -> bytearray:
assert i >= 0
r = []
while True:
byte = i & 0x7f
i = i >> 7
if i == 0:
r.append(byte)
return bytearray(r)
r.append(0x80 | byte)
@staticmethod
def decode(b: bytearray) -> int:
r = 0
for i, e in enumerate(b):
r = r + ((e & 0x7f) << (i * 7))
return r
@staticmethod
def decode_reader(r: typing.BinaryIO) -> (int, int):
a = bytearray()
while True:
b = ord(r.read(1))
a.append(b)
if (b & 0x80) == 0:
break
return ULEB128.decode(a), len(a)
class ILEB128:
@staticmethod
def encode(i: int) -> bytearray:
r = []
while True:
byte = i & 0x7f
i = i >> 7
if (i == 0 and byte & 0x40 == 0) or (i == -1 and byte & 0x40 != 0):
r.append(byte)
return bytearray(r)
r.append(0x80 | byte)
@staticmethod
def decode(b: bytearray) -> int:
r = 0
for i, e in enumerate(b):
r = r + ((e & 0x7f) << (i * 7))
if e & 0x40 != 0:
r |= - (1 << (i * 7) + 7)
return r
@staticmethod
def decode_reader(r: typing.BinaryIO) -> (int, int):
a = bytearray()
while True:
b = ord(r.read(1))
a.append(b)
if (b & 0x80) == 0:
break
return ILEB128.decode(a), len(a)

View file

@ -1,8 +1,13 @@
from typing import Literal from typing import Literal
from . import cloudkit_pb2 from . import cloudkit_pb2, gsa, _utils
import uuid import uuid
import dataclasses import dataclasses
import typing import typing
import random
import requests
import logging
logger = logging.getLogger("cloudkit")
@dataclasses.dataclass @dataclasses.dataclass
class Record: class Record:
@ -10,7 +15,98 @@ class Record:
type: str type: str
fields: dict[str, typing.Any] fields: dict[str, typing.Any]
def build_record_save_request( class CloudKit:
def __init__(self, dsid: str, cloudkit_token: str, mme_token: str, sandbox: bool = False):
"""
Represents a CloudKit user.
`dsid`: The user's DSID.
`cloudkit_token`: `cloudKitToken` from the `com.apple.mobileme` delegate.
`mme_token`: `mmeAuthToken` from the `com.apple.mobileme` delegate.
`sandbox`: Whether to use the CloudKit sandbox environment.
"""
self.dsid = dsid
self.cloudkit_token = cloudkit_token
self.mme_token = mme_token
self.sandbox = sandbox
def container(self, container: str, scope: Literal["PUBLIC"] | Literal["PRIVATE"] | Literal["SHARED"] = "PUBLIC") -> "CloudKitContainer":
"""
Convenience method for creating a CloudKitContainer object.
"""
return CloudKitContainer(container, self, scope)
class CloudKitContainer:
def __init__(self, container: str, user: CloudKit, scope: Literal["PUBLIC"] | Literal["PRIVATE"] | Literal["SHARED"] = "PUBLIC"):
"""
Represents a CloudKit container.
container: The CloudKit container ID. (e.g. "iCloud.dev.jjtech.experiments.cktest")
user: The CloudKit user to use for authentication.
scope: The CloudKit database scope to use.
"""
self.container = container
self.user = user
self.scope = scope
self.user_id = self._fetch_user_id()
def _fetch_user_id(self):
headers = {
"x-cloudkit-containerid": self.container,
"x-cloudkit-bundleid": ".".join(self.container.split(".")[1:]), # Remove the "iCloud." prefix
"x-cloudkit-databasescope": self.scope,
"x-cloudkit-environment": "Sandbox" if self.user.sandbox else "Production",
"accept": "application/x-protobuf",
"x-apple-operation-id": random.randbytes(8).hex(),
"x-apple-request-uuid": str(uuid.uuid4()).upper()
}
headers.update(gsa.generate_anisette_headers())
r = requests.post("https://gateway.icloud.com/setup/setup/ck/v1/ckAppInit", params={"container": self.container}, headers=headers, auth=(self.user.dsid, self.user.mme_token), verify=False)
logger.debug("Got app init response: ", r.content)
return r.json()["cloudKitUserId"]
def save_record(self, record: Record, zone: str = "_defaultZone", owner: str = "_defaultOwner") -> None:
"""
Saves a record to the container.
"""
logger.info(f"Saving record {record.name} to {self.container}")
headers = {
"x-cloudkit-authtoken": self.user.cloudkit_token,
"x-cloudkit-userid": self.user_id,
"x-cloudkit-containerid": self.container,
"x-cloudkit-bundleid": ".".join(self.container.split(".")[1:]), # Remove the "iCloud." prefix
"x-cloudkit-databasescope": self.scope,
"x-cloudkit-environment": "Sandbox" if self.user.sandbox else "Production",
"accept": "application/x-protobuf",
"content-type": 'application/x-protobuf; desc="https://gateway.icloud.com:443/static/protobuf/CloudDB/CloudDBClient.desc"; messageType=RequestOperation; delimited=true',
"x-apple-operation-id": random.randbytes(8).hex(),
"x-apple-request-uuid": str(uuid.uuid4()).upper(),
"user-agent": "CloudKit/2060.11 (22F82)"
}
headers.update(gsa.generate_anisette_headers())
body = _build_record_save_request(record, self.container, self.user.sandbox, self.scope, zone, owner)
r = requests.post(
"https://gateway.icloud.com/ckdatabase/api/client/record/save",
headers=headers,
data=body,
verify=False
)
print(r.content)
def _build_record_save_request(
record: Record, record: Record,
container: str, container: str,
sandbox: bool = False, sandbox: bool = False,
@ -18,8 +114,6 @@ def build_record_save_request(
zone: str = "_defaultZone", zone: str = "_defaultZone",
owner: str = "_defaultOwner", owner: str = "_defaultOwner",
): ):
MAGIC_BYTES = b"\xfe\x03"
hardware_id = uuid.uuid4() # Generate a new hardware ID for each request? hardware_id = uuid.uuid4() # Generate a new hardware ID for each request?
operation_uuid = uuid.uuid4() # Generate a new operation UUID for each request? operation_uuid = uuid.uuid4() # Generate a new operation UUID for each request?
record_id = uuid.uuid4() # Generate a new record ID for each request? record_id = uuid.uuid4() # Generate a new record ID for each request?
@ -62,4 +156,6 @@ def build_record_save_request(
request.recordSaveRequest.record.recordField[-1].value.type = cloudkit_pb2.Record.Field.Value.Type.STRING_TYPE request.recordSaveRequest.record.recordField[-1].value.type = cloudkit_pb2.Record.Field.Value.Type.STRING_TYPE
request.recordSaveRequest.record.recordField[-1].value.stringValue = value request.recordSaveRequest.record.recordField[-1].value.stringValue = value
return MAGIC_BYTES + request.SerializeToString() len_bytes = _utils.ULEB128.encode(len(request.SerializeToString()))
return len_bytes + request.SerializeToString()

View file

@ -17,6 +17,9 @@ import srp._pysrp as srp
from cryptography.hazmat.primitives import padding from cryptography.hazmat.primitives import padding
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
import logging
logger = logging.getLogger("gsa")
# Server to use for anisette generation # Server to use for anisette generation
ANISETTE = False # Use local generation with AOSKit (macOS only) ANISETTE = False # Use local generation with AOSKit (macOS only)
@ -126,7 +129,7 @@ def _generate_meta_headers(serial: str = "0", user_id: uuid = uuid.uuid4(), devi
} }
def _generate_local_anisette() -> dict: def _generate_local_anisette() -> dict:
print("Using local anisette generation") logger.debug("Using local anisette generation")
"""Generates anisette data using AOSKit locally""" """Generates anisette data using AOSKit locally"""
import objc import objc
@ -146,7 +149,7 @@ def _generate_local_anisette() -> dict:
} }
def _generate_remote_anisette(url: str) -> dict: def _generate_remote_anisette(url: str) -> dict:
print("Using remote anisette generation: " + url) logger.debug("Using remote anisette generation: " + url)
h = json.loads(requests.get(url, timeout=5).text) h = json.loads(requests.get(url, timeout=5).text)
return { return {
"X-Apple-I-MD": h["X-Apple-I-MD"], "X-Apple-I-MD": h["X-Apple-I-MD"],
@ -276,7 +279,7 @@ def trusted_second_factor(dsid, idms_token):
if check_error(r): if check_error(r):
return return
print("2FA successful") logger.info("2FA successful")
def sms_second_factor(dsid, idms_token): def sms_second_factor(dsid, idms_token):
@ -356,7 +359,7 @@ def authenticate(username, password):
return return
if r["sp"] != "s2k": if r["sp"] != "s2k":
print(f"This implementation only supports s2k. Server returned {r['sp']}") logger.error(f"This implementation only supports s2k. Server returned {r['sp']}")
return return
# Change the password out from under the SRP library, as we couldn't calculate it without the salt. # Change the password out from under the SRP library, as we couldn't calculate it without the salt.
@ -366,7 +369,7 @@ def authenticate(username, password):
# Make sure we processed the challenge correctly # Make sure we processed the challenge correctly
if M is None: if M is None:
print("Failed to process challenge") logger.critical("Failed to process challenge")
return return
r = authenticated_request( r = authenticated_request(
@ -384,7 +387,7 @@ def authenticate(username, password):
# Make sure that the server's session key matches our session key (and thus that they are not an imposter) # 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"]) usr.verify_session(r["M2"])
if not usr.authenticated(): if not usr.authenticated():
print("Failed to verify session") logger.critical("Failed to verify session")
return return
spd = decrypt_cbc(usr, r["spd"]) spd = decrypt_cbc(usr, r["spd"])
@ -396,7 +399,7 @@ def authenticate(username, password):
spd = plist.loads(PLISTHEADER + spd) spd = plist.loads(PLISTHEADER + spd)
if "au" in r["Status"] and r["Status"]["au"] == "trustedDeviceSecondaryAuth": if "au" in r["Status"] and r["Status"]["au"] == "trustedDeviceSecondaryAuth":
print("Trusted device authentication required") logger.info("Trusted device authentication required")
# Replace bytes with strings # Replace bytes with strings
for k, v in spd.items(): for k, v in spd.items():
if isinstance(v, bytes): if isinstance(v, bytes):
@ -404,10 +407,10 @@ def authenticate(username, password):
trusted_second_factor(spd["adsid"], spd["GsIdmsToken"]) trusted_second_factor(spd["adsid"], spd["GsIdmsToken"])
return authenticate(username, password) return authenticate(username, password)
elif "au" in r["Status"] and r["Status"]["au"] == "secondaryAuth": elif "au" in r["Status"] and r["Status"]["au"] == "secondaryAuth":
print("SMS authentication required") logger.info("SMS authentication required")
sms_second_factor(spd["adsid"], spd["GsIdmsToken"]) sms_second_factor(spd["adsid"], spd["GsIdmsToken"])
elif "au" in r["Status"]: elif "au" in r["Status"]:
print(f"Unknown auth value {r['Status']['au']}") logger.info(f"Unknown auth value {r['Status']['au']}")
return return
else: else:
# print("Assuming 2FA is not required") # print("Assuming 2FA is not required")