From 182210cf9cfe0fa0cac914fb8ba5cf2c67a23b30 Mon Sep 17 00:00:00 2001 From: JJTech0130 Date: Fri, 11 Aug 2023 15:45:17 -0400 Subject: [PATCH 01/11] stuff we did on the call --- demo.py | 4 ++-- development/printer.py | 4 ++-- ids/identity.py | 7 ++++++- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/demo.py b/demo.py index ef522ff..e41ff70 100644 --- a/demo.py +++ b/demo.py @@ -22,7 +22,7 @@ logging.getLogger("py.warnings").setLevel(logging.ERROR) # Ignore warnings from logging.getLogger("asyncio").setLevel(logging.WARNING) logging.getLogger("jelly").setLevel(logging.INFO) logging.getLogger("nac").setLevel(logging.INFO) -logging.getLogger("apns").setLevel(logging.INFO) +logging.getLogger("apns").setLevel(logging.DEBUG) logging.getLogger("albert").setLevel(logging.INFO) logging.getLogger("ids").setLevel(logging.DEBUG) logging.getLogger("bags").setLevel(logging.INFO) @@ -51,7 +51,7 @@ def safe_b64decode(s): conn.connect(token=safe_b64decode(CONFIG.get("push", {}).get("token"))) conn.set_state(1) -conn.filter(["com.apple.madrid"]) +conn.filter(["com.apple.madrid", "com.apple.private.alloy.sms"]) user = ids.IDSUser(conn) diff --git a/development/printer.py b/development/printer.py index 46711e6..3efe427 100644 --- a/development/printer.py +++ b/development/printer.py @@ -217,7 +217,7 @@ def pretty_print_payload( print(f" {bcolors.WARNING}Topic{bcolors.ENDC}: {topic}", end="") - if topic == "com.apple.madrid": + if topic == "com.apple.madrid" or topic == "com.apple.private.alloy.sms": print(f" {bcolors.FAIL}Madrid{bcolors.ENDC}", end="") orig_payload = payload payload = plistlib.loads(_get_field(payload[1], 3)) @@ -254,7 +254,7 @@ def pretty_print_payload( for key in payload: print(f" {bcolors.OKBLUE}{key}{bcolors.ENDC}: {payload[key]}") - if 'dtl' in payload: + if 'dtl' in payload and False: print("OVERRIDE DTL") payload['dtl'][0].update({'sT': b64decode("jJ86jTYbv1mGVwO44PyfuZ9lh3o56QjOE39Jk8Z99N8=")}) diff --git a/ids/identity.py b/ids/identity.py index 51fa0b9..92b9409 100644 --- a/ids/identity.py +++ b/ids/identity.py @@ -95,14 +95,19 @@ def register( uris = [{"uri": handle} for handle in handles] body = { + "device-name": "pypush", "hardware-version": "MacBookPro18,3", "language": "en-US", "os-version": "macOS,13.2.1,22D68", "software-version": "22D68", "services": [ { - "capabilities": [{"flags": 17, "name": "Messenger", "version": 1}], + "capabilities": [{"flags": 1, "name": "Messenger", "version": 1}], "service": "com.apple.madrid", + "sub-services": ["com.apple.private.alloy.sms", + "com.apple.private.alloy.gelato", + "com.apple.private.alloy.biz", + "com.apple.private.alloy.gamecenter.imessage"], "users": [ { "client-data": { From dfc6302b3d280773d3406f0961d20660d36e610d Mon Sep 17 00:00:00 2001 From: JJTech0130 Date: Sat, 12 Aug 2023 07:36:30 -0400 Subject: [PATCH 02/11] add uuid to registration message to make sms forwarding happy --- ids/identity.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ids/identity.py b/ids/identity.py index 92b9409..a918f55 100644 --- a/ids/identity.py +++ b/ids/identity.py @@ -93,13 +93,16 @@ def register( ): logger.debug(f"Registering IDS identity for {handles}") uris = [{"uri": handle} for handle in handles] - + import uuid body = { "device-name": "pypush", "hardware-version": "MacBookPro18,3", "language": "en-US", "os-version": "macOS,13.2.1,22D68", "software-version": "22D68", + "private-device-data": { + "u": uuid.uuid4().hex.upper(), + }, "services": [ { "capabilities": [{"flags": 1, "name": "Messenger", "version": 1}], From 0ea957b536b31d3237af875558d7ebda56e39ae6 Mon Sep 17 00:00:00 2001 From: JJTech0130 Date: Sat, 12 Aug 2023 07:41:41 -0400 Subject: [PATCH 03/11] logging and sms decoding --- imessage.py | 74 ++++++++++++++++++++++++++++++----------------------- 1 file changed, 42 insertions(+), 32 deletions(-) diff --git a/imessage.py b/imessage.py index 6722f44..dd7a3e1 100644 --- a/imessage.py +++ b/imessage.py @@ -80,44 +80,48 @@ class Attachment: versions: list[AttachmentFile] def __init__(self, message_raw_content: dict, xml_element: ElementTree.Element): - attrs = xml_element.attrib + attrib = xml_element.attrib - self.name = attrs["name"] if "name" in attrs else None - self.mime_type = attrs["mime-type"] if "mime-type" in attrs else None + self.name = attrib["name"] if "name" in attrib else None + self.mime_type = attrib["mime-type"] if "mime-type" in attrib else None - if "inline-attachment" in attrs: + if "inline-attachment" in attrib: # just grab the inline attachment ! - self.versions = [InlineFile(message_raw_content[attrs["inline-attachment"]])] + self.versions = [InlineFile(message_raw_content[attrib["inline-attachment"]])] else: # suffer - versions = [] - for attribute in attrs: - if attribute.startswith("mmcs") or \ - attribute.startswith("decryption-key") or \ - attribute.startswith("file-size"): - segments = attribute.split('-') - if segments[-1].isnumeric(): - index = int(segments[-1]) - attribute_name = segments[:-1] - else: - index = 0 - attribute_name = attribute + - while index >= len(versions): - versions.append(MMCSFile()) + versions = [InlineFile(b"")] - val = attrs[attribute_name] - match attribute_name: - case "mmcs-url": - versions[index].url = val - case "mmcs-owner": - versions[index].owner = val - case "mmcs-signature-hex": - versions[index].signature = base64.b16decode(val) - case "file-size": - versions[index].size = int(val) - case "decryption-key": - versions[index].decryption_key = base64.b16decode(val)[1:] + print(attrib) + # for attribute in attrs: + # if attribute.startswith("mmcs") or \ + # attribute.startswith("decryption-key") or \ + # attribute.startswith("file-size"): + # segments = attribute.split('-') + # if segments[-1].isnumeric(): + # index = int(segments[-1]) + # attribute_name = segments[:-1] + # else: + # index = 0 + # attribute_name = attribute + + # while index >= len(versions): + # versions.append(MMCSFile()) + + # val = attrs[attribute_name] + # match attribute_name: + # case "mmcs-url": + # versions[index].url = val + # case "mmcs-owner": + # versions[index].owner = val + # case "mmcs-signature-hex": + # versions[index].signature = base64.b16decode(val) + # case "file-size": + # versions[index].size = int(val) + # case "decryption-key": + # versions[index].decryption_key = base64.b16decode(val)[1:] self.versions = versions @@ -194,6 +198,8 @@ class iMessage: message = plistlib.loads(message) + logger.debug(f"Decompressed message : {message}") + return iMessage( text=message.get("t", ""), xml=message.get("x"), @@ -260,7 +266,7 @@ class iMessageUser: def check_response(x): if x[0] != 0x0A: return False - if apns._get_field(x[1], 2) != sha1("com.apple.madrid".encode()).digest(): + if apns._get_field(x[1], 2) != sha1("com.apple.madrid".encode()).digest() and apns._get_field(x[1], 2) != sha1("com.apple.private.alloy.sms".encode()).digest(): return False resp_body = apns._get_field(x[1], 3) if resp_body is None: @@ -435,7 +441,11 @@ class iMessageUser: if not self._verify_payload(payload, body['sP'], body["t"]): raise Exception("Failed to verify payload") + logger.debug(f"Encrypted body : {body}") + decrypted = self._decrypt_payload(payload) + + #logger.debug(f"Decrypted payload : {plistlib.loads(decrypted)}") return iMessage.from_raw(decrypted, body['sP']) From 04b139f9411fa934025568af32e54b7b3af90cf3 Mon Sep 17 00:00:00 2001 From: JJTech0130 Date: Sun, 13 Aug 2023 19:26:57 -0400 Subject: [PATCH 04/11] sms forwarding better --- apns.py | 5 ++ demo.py | 6 +- ids/query.py | 3 + imessage.py | 177 ++++++++++++++++++++++++++++++++++++++------------- 4 files changed, 144 insertions(+), 47 deletions(-) diff --git a/apns.py b/apns.py index 755abf6..96cbd0e 100644 --- a/apns.py +++ b/apns.py @@ -179,7 +179,12 @@ class APNSConnection: return self.token + old_topics = [] # Keep old topics so that we can add topics one by one def filter(self, topics: list[str]): + if topics == self.old_topics: + return + topics = list(set(topics + self.old_topics)) + self.old_topics = topics logger.debug(f"Sending filter message with topics {topics}") fields = [(1, self.token)] diff --git a/demo.py b/demo.py index e41ff70..49b91cf 100644 --- a/demo.py +++ b/demo.py @@ -22,11 +22,11 @@ logging.getLogger("py.warnings").setLevel(logging.ERROR) # Ignore warnings from logging.getLogger("asyncio").setLevel(logging.WARNING) logging.getLogger("jelly").setLevel(logging.INFO) logging.getLogger("nac").setLevel(logging.INFO) -logging.getLogger("apns").setLevel(logging.DEBUG) +logging.getLogger("apns").setLevel(logging.INFO) logging.getLogger("albert").setLevel(logging.INFO) logging.getLogger("ids").setLevel(logging.DEBUG) logging.getLogger("bags").setLevel(logging.INFO) -logging.getLogger("imessage").setLevel(logging.DEBUG) +logging.getLogger("imessage").setLevel(logging.INFO) logging.captureWarnings(True) @@ -153,6 +153,8 @@ def fixup_handle(handle): current_participants = [] current_effect = None while True: + if not im._received_activation_message: + im._activate_sms() msg = im.receive() if msg is not None: # print(f'[{msg.sender}] {msg.text}') diff --git a/ids/query.py b/ids/query.py index 875372b..0737481 100644 --- a/ids/query.py +++ b/ids/query.py @@ -30,6 +30,9 @@ def lookup( "x-id-self-uri": self_uri, "x-protocol-version": PROTOCOL_VERSION, } + + if 'alloy' in topic: + headers["x-id-sub-service"] = topic # Hack, if it has alloy in the name it's probably a sub-service signing.add_id_signature(headers, body, BAG_KEY, id_keypair, push_token) msg_id = random.randbytes(16) diff --git a/imessage.py b/imessage.py index dd7a3e1..6dabf06 100644 --- a/imessage.py +++ b/imessage.py @@ -200,18 +200,23 @@ class iMessage: logger.debug(f"Decompressed message : {message}") - return iMessage( - text=message.get("t", ""), - xml=message.get("x"), - participants=message.get("p", []), - sender=sender if sender is not None else message.get("p", [])[-1] if "p" in message else None, - id=uuid.UUID(message.get("r")) if "r" in message else None, - group_id=uuid.UUID(message.get("gid")) if "gid" in message else None, - body=BalloonBody(message["bid"], message["b"]) if "bid" in message and "b" in message else None, - effect=message["iid"] if "iid" in message else None, - _compressed=compressed, - _raw=message, - ) + try: + return iMessage( + text=message["t"], # Cause it to "fail to parse" if there isn't any good text to display, temp hack + xml=message.get("x"), + participants=message.get("p", []), + sender=sender if sender is not None else message.get("p", [])[-1] if "p" in message else None, + id=uuid.UUID(message.get("r")) if "r" in message else None, + group_id=uuid.UUID(message.get("gid")) if "gid" in message else None, + body=BalloonBody(message["bid"], message["b"]) if "bid" in message and "b" in message else None, + effect=message["iid"] if "iid" in message else None, + _compressed=compressed, + _raw=message, + ) + except: + import json + dmp = json.dumps(message, indent=4) + return iMessage(text=f"failed to parse: {dmp}", _raw=message) def to_raw(self) -> bytes: """Convert an `iMessage` to raw message bytes""" @@ -404,13 +409,13 @@ class iMessageUser: def _verify_payload(self, payload: bytes, sender: str, sender_token: str) -> bool: # Get the public key for the sender - self._cache_keys([sender]) + self._cache_keys([sender], "com.apple.madrid") if not sender_token in self.KEY_CACHE: logger.warning("Unable to find the public key of the sender, cannot verify") return False - identity_keys = ids.identity.IDSIdentity.decode(self.KEY_CACHE[sender_token][0]) + identity_keys = ids.identity.IDSIdentity.decode(self.KEY_CACHE[sender_token]["com.apple.madrid"][0]) sender_ec_key = ids._helpers.parse_key(identity_keys.signing_public_key) payload = iMessageUser._parse_payload(payload) @@ -450,12 +455,12 @@ class iMessageUser: return iMessage.from_raw(decrypted, body['sP']) KEY_CACHE_HANDLE: str = "" - KEY_CACHE: dict[bytes, tuple[bytes, bytes]] = {} - """Mapping of push token : (public key, session token)""" + KEY_CACHE: dict[bytes, dict[str, tuple[bytes, bytes]]] = {} + """Mapping of push token : topic : (public key, session token)""" USER_CACHE: dict[str, list[bytes]] = {} """Mapping of handle : [push tokens]""" - def _cache_keys(self, participants: list[str]): + def _cache_keys(self, participants: list[str], topic: str): # Clear the cache if the handle has changed if self.KEY_CACHE_HANDLE != self.user.current_handle: self.KEY_CACHE_HANDLE = self.user.current_handle @@ -467,7 +472,7 @@ class iMessageUser: return # Look up the public keys for the participants, and cache a token : public key mapping - lookup = self.user.lookup(participants) + lookup = self.user.lookup(participants, topic=topic) for key, participant in lookup.items(): if not key in self.USER_CACHE: @@ -487,44 +492,126 @@ class iMessageUser: # print(identity) - self.KEY_CACHE[identity["push-token"]] = ( + if not identity["push-token"] in self.KEY_CACHE: + self.KEY_CACHE[identity["push-token"]] = {} + + self.KEY_CACHE[identity["push-token"]][topic] = ( identity["client-data"]["public-message-identity-key"], identity["session-token"], ) + + def _encode_multiple(self, participants: list[str], sender: str, topic="com.apple.madrid", to_encrypt: bytes | None = None,) -> list[dict]: + self._cache_keys(participants, topic) + + out = [] + for participant in participants: + for push_token in self.USER_CACHE[participant]: + if push_token == self.connection.token: + continue # Don't send to ourselves + + identity_keys = ids.identity.IDSIdentity.decode( + self.KEY_CACHE[push_token][topic][0] + ) + + if to_encrypt != None: + payload = self._encrypt_sign_payload(identity_keys, to_encrypt) + else: + payload = None + + p = { + "tP": participant, + "D": not participant == sender, + "sT": self.KEY_CACHE[push_token][topic][1], + "t": push_token, + } + + if payload is not None: + p["P"] = payload + + logger.debug(f"Encoded payload : {p}") + + out.append(p) + + return out + + _received_activation_message: bool = False + + def _activate_sms(self) -> bool: + # Check if we have received an SMS forwarding activation message + if not self._received_activation_message: + # Check if it is in the queue + def check(x): + if x[0] != 0x0A: + return False + if apns._get_field(x[1], 2) != sha1("com.apple.private.alloy.sms".encode()).digest(): + return False + resp_body = apns._get_field(x[1], 3) + if resp_body is None: + return False + resp_body = plistlib.loads(resp_body) + if "c" not in resp_body or resp_body["c"] != 145: + return False + return True + + payload = self.connection.incoming_queue.pop_find(check) + if payload is None: + return False + payload = apns._get_field(payload[1], 3) + payload = plistlib.loads(payload) + + + dec = self._decrypt_payload(payload['P']) + # Try gzip decompression + try: + dec = gzip.decompress(dec) + except: + pass + dec = plistlib.loads(dec) + logger.debug(f"Think we got an SMS forwarding payload : {payload}") + logger.debug(f"Decrypted : {dec}") + + self._received_activation_message = True + + # Send out the activation message + + msg_id = random.randbytes(4) + body = { + "fcn": 1, + "c": 147, + "ua": "[macOS,13.4.1,22F82,MacBookPro18,3]", + "U": uuid.uuid4().bytes, + "v": 8, + "i": int.from_bytes(msg_id, "big"), + "dtl": self._encode_multiple([self.user.current_handle], self.user.current_handle), + "nr": 1, + "sP": self.user.current_handle, + } + + logger.debug(f"Sending activation message : {body}") + + body = plistlib.dumps(body, fmt=plistlib.FMT_BINARY) + + self.connection.send_message("com.apple.private.alloy.sms", body, msg_id) + + #logger.debug(f"Sent activation message : {body}") + + return True + + + + + + def send(self, message: iMessage): # Set the sender, if it isn't already if message.sender is None: message.sender = self.user.handles[0] # TODO : Which handle to use? message.sanity_check() # Sanity check MUST be called before caching keys, so that the sender is added to the list of participants - self._cache_keys(message.participants) + self._cache_keys(message.participants, "com.apple.madrid") - # Turn the message into a raw message - raw = message.to_raw() - import base64 - - bundled_payloads = [] - for participant in message.participants: - for push_token in self.USER_CACHE[participant]: - if push_token == self.connection.token: - continue # Don't send to ourselves - - identity_keys = ids.identity.IDSIdentity.decode( - self.KEY_CACHE[push_token][0] - ) - payload = self._encrypt_sign_payload(identity_keys, raw) - - bundled_payloads.append( - { - "tP": participant, - "D": not participant - == message.sender, # TODO: Should this be false sometimes? For self messages? - "sT": self.KEY_CACHE[push_token][1], - "P": payload, - "t": push_token, - } - ) + bundled_payloads = self._encode_multiple(message.participants, message.sender, message.to_raw()) msg_id = random.randbytes(4) body = { From e41ed2c6a20e5e1962baf88cb8fe2f8f2f1ba47d Mon Sep 17 00:00:00 2001 From: JJTech0130 Date: Sun, 13 Aug 2023 23:24:34 -0400 Subject: [PATCH 05/11] refactoring and cleanup --- demo.py | 4 +- imessage.py | 313 ++++++++++++++++++++++++---------------------------- 2 files changed, 148 insertions(+), 169 deletions(-) diff --git a/demo.py b/demo.py index 49b91cf..dfcbfaa 100644 --- a/demo.py +++ b/demo.py @@ -153,8 +153,7 @@ def fixup_handle(handle): current_participants = [] current_effect = None while True: - if not im._received_activation_message: - im._activate_sms() + im.activate_sms() # We must call this always since SMS could be turned off and on again, and it might have been on before this. msg = im.receive() if msg is not None: # print(f'[{msg.sender}] {msg.text}') @@ -201,6 +200,7 @@ while True: else: print(f'Filtering to {[fixup_handle(h) for h in msg[1:]]}') current_participants = [fixup_handle(h) for h in msg[1:]] + im._cache_keys(current_participants, "com.apple.madrid") # Just to make things faster, and to make it error on invalid addresses elif msg == 'handle' or msg.startswith('handle '): msg = msg.split(' ') if len(msg) < 2 or msg[1] == '': diff --git a/imessage.py b/imessage.py index 6dabf06..9bb064f 100644 --- a/imessage.py +++ b/imessage.py @@ -53,15 +53,18 @@ class MMCSFile(AttachmentFile): def data(self) -> bytes: import requests - logger.info(requests.get( - url=self.url, - headers={ - "User-Agent": f"IMTransferAgent/900 CFNetwork/596.2.3 Darwin/12.2.0 (x86_64) (Macmini5,1)", - # "MMCS-Url": self.url, - # "MMCS-Signature": str(base64.encodebytes(self.signature)), - # "MMCS-Owner": self.owner - }, - ).headers) + + logger.info( + requests.get( + url=self.url, + headers={ + "User-Agent": f"IMTransferAgent/900 CFNetwork/596.2.3 Darwin/12.2.0 (x86_64) (Macmini5,1)", + # "MMCS-Url": self.url, + # "MMCS-Signature": str(base64.encodebytes(self.signature)), + # "MMCS-Owner": self.owner + }, + ).headers + ) return b"" @@ -87,10 +90,11 @@ class Attachment: if "inline-attachment" in attrib: # just grab the inline attachment ! - self.versions = [InlineFile(message_raw_content[attrib["inline-attachment"]])] + self.versions = [ + InlineFile(message_raw_content[attrib["inline-attachment"]]) + ] else: # suffer - versions = [InlineFile(b"")] @@ -158,7 +162,11 @@ class iMessage: def attachments(self) -> list[Attachment]: if self.xml is not None: - return [Attachment(self._raw, elem) for elem in ElementTree.fromstring(self.xml)[0] if elem.tag == "FILE"] + return [ + Attachment(self._raw, elem) + for elem in ElementTree.fromstring(self.xml)[0] + if elem.tag == "FILE" + ] else: return [] @@ -202,20 +210,29 @@ class iMessage: try: return iMessage( - text=message["t"], # Cause it to "fail to parse" if there isn't any good text to display, temp hack + text=message[ + "t" + ], # Cause it to "fail to parse" if there isn't any good text to display, temp hack xml=message.get("x"), participants=message.get("p", []), - sender=sender if sender is not None else message.get("p", [])[-1] if "p" in message else None, + sender=sender + if sender is not None + else message.get("p", [])[-1] + if "p" in message + else None, id=uuid.UUID(message.get("r")) if "r" in message else None, group_id=uuid.UUID(message.get("gid")) if "gid" in message else None, - body=BalloonBody(message["bid"], message["b"]) if "bid" in message and "b" in message else None, + body=BalloonBody(message["bid"], message["b"]) + if "bid" in message and "b" in message + else None, effect=message["iid"] if "iid" in message else None, _compressed=compressed, _raw=message, ) except: - import json - dmp = json.dumps(message, indent=4) + #import json + + dmp = str(message) return iMessage(text=f"failed to parse: {dmp}", _raw=message) def to_raw(self) -> bytes: @@ -232,7 +249,7 @@ class iMessage: "pv": 0, "gv": "8", "v": "1", - "iid": self.effect + "iid": self.effect, } # Remove keys that are None @@ -262,39 +279,11 @@ class iMessageUser: self.connection = connection self.user = user - def _get_raw_message(self): - """ - Returns a raw APNs message corresponding to the next conforming notification in the queue - Returns None if no conforming notification is found - """ - - def check_response(x): - if x[0] != 0x0A: - return False - if apns._get_field(x[1], 2) != sha1("com.apple.madrid".encode()).digest() and apns._get_field(x[1], 2) != sha1("com.apple.private.alloy.sms".encode()).digest(): - return False - resp_body = apns._get_field(x[1], 3) - if resp_body is None: - # logger.debug("Rejecting madrid message with no body") - return False - resp_body = plistlib.loads(resp_body) - if "P" not in resp_body: - # logger.debug(f"Rejecting madrid message with no payload : {resp_body}") - return False - return True - - payload = self.connection.incoming_queue.pop_find(check_response) - if payload is None: - return None - id = apns._get_field(payload[1], 4) - - return payload - def _parse_payload(payload: bytes) -> tuple[bytes, bytes]: payload = BytesIO(payload) tag = payload.read(1) - #print("TAG", tag) + # print("TAG", tag) body_length = int.from_bytes(payload.read(2), "big") body = payload.read(body_length) @@ -415,7 +404,9 @@ class iMessageUser: logger.warning("Unable to find the public key of the sender, cannot verify") return False - identity_keys = ids.identity.IDSIdentity.decode(self.KEY_CACHE[sender_token]["com.apple.madrid"][0]) + identity_keys = ids.identity.IDSIdentity.decode( + self.KEY_CACHE[sender_token]["com.apple.madrid"][0] + ) sender_ec_key = ids._helpers.parse_key(identity_keys.signing_public_key) payload = iMessageUser._parse_payload(payload) @@ -435,24 +426,22 @@ class iMessageUser: """ Will return the next iMessage in the queue, or None if there are no messages """ - raw = self._get_raw_message() - if raw is None: + + body = self._receive_raw(100, "com.apple.madrid") + if body is None: return None - body = apns._get_field(raw[1], 3) - body = plistlib.loads(body) - #print(f"Got body message {body}") payload = body["P"] - if not self._verify_payload(payload, body['sP'], body["t"]): + if not self._verify_payload(payload, body["sP"], body["t"]): raise Exception("Failed to verify payload") - + logger.debug(f"Encrypted body : {body}") - + decrypted = self._decrypt_payload(payload) - #logger.debug(f"Decrypted payload : {plistlib.loads(decrypted)}") - - return iMessage.from_raw(decrypted, body['sP']) + # logger.debug(f"Decrypted payload : {plistlib.loads(decrypted)}") + + return iMessage.from_raw(decrypted, body["sP"]) KEY_CACHE_HANDLE: str = "" KEY_CACHE: dict[bytes, dict[str, tuple[bytes, bytes]]] = {} @@ -466,7 +455,7 @@ class iMessageUser: self.KEY_CACHE_HANDLE = self.user.current_handle self.KEY_CACHE = {} self.USER_CACHE = {} - + # Check to see if we have cached the keys for all of the participants if all([p in self.USER_CACHE for p in participants]): return @@ -474,6 +463,11 @@ class iMessageUser: # Look up the public keys for the participants, and cache a token : public key mapping lookup = self.user.lookup(participants, topic=topic) + logger.debug(f"Lookup response : {lookup}") + for key, participant in lookup.items(): + if len(participant["identities"]) == 0: + logger.warning(f"Participant {key} has no identities, this is probably not a real account") + for key, participant in lookup.items(): if not key in self.USER_CACHE: self.USER_CACHE[key] = [] @@ -500,162 +494,147 @@ class iMessageUser: identity["session-token"], ) - - def _encode_multiple(self, participants: list[str], sender: str, topic="com.apple.madrid", to_encrypt: bytes | None = None,) -> list[dict]: + def _send_raw( + self, + type: int, + participants: list[str], + topic: str, + payload: bytes | None = None, + id: uuid.UUID | None = None, + extra: dict = {}, + ): self._cache_keys(participants, topic) - out = [] + dtl = [] for participant in participants: for push_token in self.USER_CACHE[participant]: if push_token == self.connection.token: - continue # Don't send to ourselves + continue # Don't send to ourselves identity_keys = ids.identity.IDSIdentity.decode( self.KEY_CACHE[push_token][topic][0] ) - if to_encrypt != None: - payload = self._encrypt_sign_payload(identity_keys, to_encrypt) - else: - payload = None - p = { "tP": participant, - "D": not participant == sender, + "D": not participant == self.user.current_handle, "sT": self.KEY_CACHE[push_token][topic][1], "t": push_token, } if payload is not None: - p["P"] = payload + p["P"] = self._encrypt_sign_payload(identity_keys, payload) logger.debug(f"Encoded payload : {p}") - out.append(p) + dtl.append(p) - return out - - _received_activation_message: bool = False - - def _activate_sms(self) -> bool: - # Check if we have received an SMS forwarding activation message - if not self._received_activation_message: - # Check if it is in the queue - def check(x): - if x[0] != 0x0A: - return False - if apns._get_field(x[1], 2) != sha1("com.apple.private.alloy.sms".encode()).digest(): - return False - resp_body = apns._get_field(x[1], 3) - if resp_body is None: - return False - resp_body = plistlib.loads(resp_body) - if "c" not in resp_body or resp_body["c"] != 145: - return False - return True - - payload = self.connection.incoming_queue.pop_find(check) - if payload is None: - return False - payload = apns._get_field(payload[1], 3) - payload = plistlib.loads(payload) - + message_id = random.randbytes(4) - dec = self._decrypt_payload(payload['P']) - # Try gzip decompression - try: - dec = gzip.decompress(dec) - except: - pass - dec = plistlib.loads(dec) - logger.debug(f"Think we got an SMS forwarding payload : {payload}") - logger.debug(f"Decrypted : {dec}") + if id is None: + id = uuid.uuid4() - self._received_activation_message = True - - # Send out the activation message - - msg_id = random.randbytes(4) body = { + "c": type, "fcn": 1, - "c": 147, - "ua": "[macOS,13.4.1,22F82,MacBookPro18,3]", - "U": uuid.uuid4().bytes, "v": 8, - "i": int.from_bytes(msg_id, "big"), - "dtl": self._encode_multiple([self.user.current_handle], self.user.current_handle), - "nr": 1, + "i": int.from_bytes(message_id, "big"), + "U": id.bytes, + "dtl": dtl, "sP": self.user.current_handle, } - logger.debug(f"Sending activation message : {body}") + body.update(extra) body = plistlib.dumps(body, fmt=plistlib.FMT_BINARY) - self.connection.send_message("com.apple.private.alloy.sms", body, msg_id) + self.connection.send_message(topic, body, message_id) - #logger.debug(f"Sent activation message : {body}") - - return True - + def _receive_raw(self, type: int, topic: str) -> dict | None: + def check_response(x): + if x[0] != 0x0A: + return False + if apns._get_field(x[1], 2) != sha1(topic.encode()).digest(): + return False + resp_body = apns._get_field(x[1], 3) + if resp_body is None: + return False + resp_body = plistlib.loads(resp_body) + if "c" not in resp_body or resp_body["c"] != type: + return False + return True + payload = self.connection.incoming_queue.pop_find(check_response) + if payload is None: + return None + body = apns._get_field(payload[1], 3) + body = plistlib.loads(body) + return body + _received_activation_message: bool = False + def activate_sms(self) -> bool: + """ + Try to activate SMS forwarding + Returns True if we are able to perform SMS forwarding, False otherwise + Call repeatedly until it returns True + """ + act_message = self._receive_raw(145, "com.apple.private.alloy.sms") + if act_message is None: + return False + + self._send_raw( + 147, + [self.user.current_handle], + "com.apple.private.alloy.sms", + extra={ + "nr": 1 + } + ) def send(self, message: iMessage): # Set the sender, if it isn't already if message.sender is None: message.sender = self.user.handles[0] # TODO : Which handle to use? - message.sanity_check() # Sanity check MUST be called before caching keys, so that the sender is added to the list of participants + message.sanity_check() # Sanity check MUST be called before caching keys, so that the sender is added to the list of participants self._cache_keys(message.participants, "com.apple.madrid") - bundled_payloads = self._encode_multiple(message.participants, message.sender, message.to_raw()) + self._send_raw( + 100, + message.participants, + "com.apple.madrid", + message.to_raw(), + message.id, + { + "E": "pair", + } + ) - msg_id = random.randbytes(4) - body = { - "fcn": 1, - "c": 100, - "E": "pair", - "ua": "[macOS,13.4.1,22F82,MacBookPro18,3]", - "v": 8, - "i": int.from_bytes(msg_id, "big"), - "U": message.id.bytes, - "dtl": bundled_payloads, - "sP": message.sender, - } + # Check for delivery + count = 0 + total = 0 - body = plistlib.dumps(body, fmt=plistlib.FMT_BINARY) + import time + start = time.time() - self.connection.send_message("com.apple.madrid", body, msg_id) + for p in message.participants: + for t in self.USER_CACHE[p]: + if t == self.connection.token: + continue + total += 1 - # This code can check to make sure we got a success response, but waiting for the response is annoying, - # so for now we just YOLO it and assume it worked + while count < total and time.time() - start < 2: + resp = self._receive_raw(255, "com.apple.madrid") + if resp is None: + continue + count += 1 - # def check_response(x): - # if x[0] != 0x0A: - # return False - # if apns._get_field(x[1], 2) != sha1("com.apple.madrid".encode()).digest(): - # return False - # resp_body = apns._get_field(x[1], 3) - # if resp_body is None: - # return False - # resp_body = plistlib.loads(resp_body) - # if "c" not in resp_body or resp_body["c"] != 255: - # return False - # return True - + logger.debug(f"Received response : {resp}") - # num_recv = 0 - # while True: - # if num_recv == len(bundled_payloads): - # break - # payload = self.connection.incoming_queue.wait_pop_find(check_response) - # if payload is None: - # continue + if resp["s"] != 0: + logger.warning(f"Message delivery to {base64.b64encode(resp['t']).decode()} failed : {resp['s']}") - # resp_body = apns._get_field(payload[1], 3) - # resp_body = plistlib.loads(resp_body) - # logger.error(resp_body) - # num_recv += 1 + if count < total: + logger.error(f"Unable to deliver message to all devices (got {count} of {total})") From 1d74e568cfb295168c71341391968fd54461405f Mon Sep 17 00:00:00 2001 From: JJTech0130 Date: Mon, 14 Aug 2023 09:40:33 -0400 Subject: [PATCH 06/11] add new message parsing stuff --- demo.py | 22 ++++---- imessage.py | 149 +++++++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 142 insertions(+), 29 deletions(-) diff --git a/demo.py b/demo.py index dfcbfaa..62aa78f 100644 --- a/demo.py +++ b/demo.py @@ -157,19 +157,19 @@ while True: msg = im.receive() if msg is not None: # print(f'[{msg.sender}] {msg.text}') - print(msg.to_string()) + print(str(msg)) - attachments = msg.attachments() - if len(attachments) > 0: - attachments_path = f"attachments/{msg.id}/" - os.makedirs(attachments_path, exist_ok=True) + # attachments = msg.attachments() + # if len(attachments) > 0: + # attachments_path = f"attachments/{msg.id}/" + # os.makedirs(attachments_path, exist_ok=True) - for attachment in attachments: - with open(attachments_path + attachment.name, "wb") as attachment_file: - attachment_file.write(attachment.versions[0].data()) + # for attachment in attachments: + # with open(attachments_path + attachment.name, "wb") as attachment_file: + # attachment_file.write(attachment.versions[0].data()) - print(f"({len(attachments)} attachment{'s have' if len(attachments) != 1 else ' has'} been downloaded and put " - f"in {attachments_path})") + # print(f"({len(attachments)} attachment{'s have' if len(attachments) != 1 else ' has'} been downloaded and put " + # f"in {attachments_path})") if len(INPUT_QUEUE) > 0: msg = INPUT_QUEUE.pop() @@ -223,7 +223,7 @@ while True: elif current_participants != []: if msg.startswith('\\'): msg = msg[1:] - im.send(imessage.iMessage( + im.send(imessage.OldiMessage( text=msg, participants=current_participants, sender=user.current_handle, diff --git a/imessage.py b/imessage.py index 9bb064f..baa3176 100644 --- a/imessage.py +++ b/imessage.py @@ -132,9 +132,101 @@ class Attachment: def __repr__(self): return f'' +class Message: + def __init__(self, text: str, sender: str, participants: list[str], id: uuid.UUID, _raw: dict, _compressed: bool = True): + self.text = text + self.sender = sender + self.id = id + self._raw = _raw + self._compressed = _compressed + + def from_raw(message: bytes, sender: str | None = None) -> "Message": + """Create a `Message` from raw message bytes""" + + raise NotImplementedError() + + def __str__(): + raise NotImplementedError() + +class SMSReflectedMessage(Message): + def from_raw(message: bytes, sender: str | None = None) -> "SMSReflectedMessage": + """Create a `SMSIncomingMessage` from raw message bytes""" + + # Decompress the message + try: + message = gzip.decompress(message) + compressed = True + except: + compressed = False + + message = plistlib.loads(message) + + return SMSReflectedMessage( + text=message["mD"]["plain-body"], + sender=sender, + participants=[re["id"] for re in message["re"]] + [sender], + id=uuid.UUID(message["mD"]["guid"]), + _raw=message, + _compressed=compressed, + ) + + def __str__(self): + return f"[SMS {self.sender}] '{self.text}'" + +class SMSIncomingMessage(Message): + def from_raw(message: bytes, sender: str | None = None) -> "SMSIncomingMessage": + """Create a `SMSIncomingMessage` from raw message bytes""" + + # Decompress the message + try: + message = gzip.decompress(message) + compressed = True + except: + compressed = False + + message = plistlib.loads(message) + + logger.debug(f"Decompressed message : {message}") + + return SMSIncomingMessage( + text=message["k"][0]["data"].decode(), + sender=message["h"], # Don't use sender parameter, that is the phone that forwarded the message + participants=[message["h"], message["co"]], + id=uuid.UUID(message["g"]), + _raw=message, + _compressed=compressed, + ) + + def __str__(self): + return f"[SMS {self.sender}] '{self.text}'" + +class iMessage(Message): + def from_raw(message: bytes, sender: str | None = None) -> "iMessage": + """Create a `iMessage` from raw message bytes""" + + # Decompress the message + try: + message = gzip.decompress(message) + compressed = True + except: + compressed = False + + message = plistlib.loads(message) + + return iMessage( + text=message["t"], + participants=message["p"], + sender=sender, + id=uuid.UUID(message["r"]), + _raw=message, + _compressed=compressed, + ) + + def __str__(self): + return f"[iMessage {self.sender}] '{self.text}'" @dataclass -class iMessage: +class OldiMessage: """Represents an iMessage""" text: str = "" @@ -195,7 +287,7 @@ class iMessage: return True - def from_raw(message: bytes, sender: str | None = None) -> "iMessage": + def from_raw(message: bytes, sender: str | None = None) -> "OldiMessage": """Create an `iMessage` from raw message bytes""" compressed = False try: @@ -209,7 +301,7 @@ class iMessage: logger.debug(f"Decompressed message : {message}") try: - return iMessage( + return OldiMessage( text=message[ "t" ], # Cause it to "fail to parse" if there isn't any good text to display, temp hack @@ -233,7 +325,7 @@ class iMessage: #import json dmp = str(message) - return iMessage(text=f"failed to parse: {dmp}", _raw=message) + return OldiMessage(text=f"failed to parse: {dmp}", _raw=message) def to_raw(self) -> bytes: """Convert an `iMessage` to raw message bytes""" @@ -422,26 +514,34 @@ class iMessageUser: except: return False - def receive(self) -> iMessage | None: + def receive(self) -> Message | None: """ Will return the next iMessage in the queue, or None if there are no messages """ + # Check for iMessages body = self._receive_raw(100, "com.apple.madrid") + t = iMessage + if body is None: + # Check for SMS messages + body = self._receive_raw(143, "com.apple.private.alloy.sms") + t = SMSReflectedMessage + if body is None: + # Check for SMS incoming messages + body = self._receive_raw(140, "com.apple.private.alloy.sms") + t = SMSIncomingMessage if body is None: return None - payload = body["P"] + - if not self._verify_payload(payload, body["sP"], body["t"]): + if not self._verify_payload(body["P"], body["sP"], body["t"]): raise Exception("Failed to verify payload") logger.debug(f"Encrypted body : {body}") - decrypted = self._decrypt_payload(payload) + decrypted = self._decrypt_payload(body["P"]) - # logger.debug(f"Decrypted payload : {plistlib.loads(decrypted)}") - - return iMessage.from_raw(decrypted, body["sP"]) + return t.from_raw(decrypted, body["sP"]) KEY_CACHE_HANDLE: str = "" KEY_CACHE: dict[bytes, dict[str, tuple[bytes, bytes]]] = {} @@ -550,17 +650,32 @@ class iMessageUser: self.connection.send_message(topic, body, message_id) - def _receive_raw(self, type: int, topic: str) -> dict | None: + def _receive_raw(self, c: int | list[int], topic: str | list[str]) -> dict | None: def check_response(x): if x[0] != 0x0A: return False - if apns._get_field(x[1], 2) != sha1(topic.encode()).digest(): - return False + # Check if it matches any of the topics + if isinstance(topic, list): + for t in topic: + if apns._get_field(x[1], 2) == sha1(t.encode()).digest(): + break + else: + return False + else: + if apns._get_field(x[1], 2) != sha1(topic.encode()).digest(): + return False + resp_body = apns._get_field(x[1], 3) if resp_body is None: return False resp_body = plistlib.loads(resp_body) - if "c" not in resp_body or resp_body["c"] != type: + + #logger.debug(f"See type {resp_body['c']}") + + if isinstance(c, list): + if not resp_body["c"] in c: + return False + elif resp_body["c"] != c: return False return True @@ -571,8 +686,6 @@ class iMessageUser: body = plistlib.loads(body) return body - _received_activation_message: bool = False - def activate_sms(self) -> bool: """ Try to activate SMS forwarding @@ -593,7 +706,7 @@ class iMessageUser: } ) - def send(self, message: iMessage): + def send(self, message: OldiMessage): # Set the sender, if it isn't already if message.sender is None: message.sender = self.user.handles[0] # TODO : Which handle to use? From 10a57717910605bd44613d14d04b7a637a827367 Mon Sep 17 00:00:00 2001 From: JJTech0130 Date: Mon, 14 Aug 2023 09:49:11 -0400 Subject: [PATCH 07/11] dataclasses and test effects --- imessage.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/imessage.py b/imessage.py index baa3176..d6f72b8 100644 --- a/imessage.py +++ b/imessage.py @@ -132,13 +132,14 @@ class Attachment: def __repr__(self): return f'' +@dataclass class Message: - def __init__(self, text: str, sender: str, participants: list[str], id: uuid.UUID, _raw: dict, _compressed: bool = True): - self.text = text - self.sender = sender - self.id = id - self._raw = _raw - self._compressed = _compressed + text: str + sender: str + participants: list[str] + id: uuid.UUID + _raw: dict + _compressed: bool = True def from_raw(message: bytes, sender: str | None = None) -> "Message": """Create a `Message` from raw message bytes""" @@ -148,6 +149,7 @@ class Message: def __str__(): raise NotImplementedError() +@dataclass class SMSReflectedMessage(Message): def from_raw(message: bytes, sender: str | None = None) -> "SMSReflectedMessage": """Create a `SMSIncomingMessage` from raw message bytes""" @@ -173,6 +175,7 @@ class SMSReflectedMessage(Message): def __str__(self): return f"[SMS {self.sender}] '{self.text}'" +@dataclass class SMSIncomingMessage(Message): def from_raw(message: bytes, sender: str | None = None) -> "SMSIncomingMessage": """Create a `SMSIncomingMessage` from raw message bytes""" @@ -200,7 +203,10 @@ class SMSIncomingMessage(Message): def __str__(self): return f"[SMS {self.sender}] '{self.text}'" +@dataclass class iMessage(Message): + effect: str | None = None + def from_raw(message: bytes, sender: str | None = None) -> "iMessage": """Create a `iMessage` from raw message bytes""" @@ -220,6 +226,7 @@ class iMessage(Message): id=uuid.UUID(message["r"]), _raw=message, _compressed=compressed, + effect=message["iid"] if "iid" in message else None, ) def __str__(self): From 21fdfc97a997dad65b3fe3fd320c1920712f344f Mon Sep 17 00:00:00 2001 From: JJTech0130 Date: Mon, 14 Aug 2023 20:31:03 -0400 Subject: [PATCH 08/11] implement more SMS handling stuff --- imessage.py | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/imessage.py b/imessage.py index d6f72b8..a7e5be1 100644 --- a/imessage.py +++ b/imessage.py @@ -140,6 +140,7 @@ class Message: id: uuid.UUID _raw: dict _compressed: bool = True + xml: str | None = None def from_raw(message: bytes, sender: str | None = None) -> "Message": """Create a `Message` from raw message bytes""" @@ -163,6 +164,8 @@ class SMSReflectedMessage(Message): message = plistlib.loads(message) + logger.debug(f"Decoding SMSReflectedMessage: {message}") + return SMSReflectedMessage( text=message["mD"]["plain-body"], sender=sender, @@ -189,7 +192,7 @@ class SMSIncomingMessage(Message): message = plistlib.loads(message) - logger.debug(f"Decompressed message : {message}") + logger.debug(f"Decoding SMSIncomingMessage: {message}") return SMSIncomingMessage( text=message["k"][0]["data"].decode(), @@ -202,6 +205,14 @@ class SMSIncomingMessage(Message): def __str__(self): return f"[SMS {self.sender}] '{self.text}'" + +@dataclass +class SMSIncomingImage(Message): + def from_raw(message: bytes, sender: str | None = None) -> "SMSIncomingImage": + """Create a `SMSIncomingImage` from raw message bytes""" + + # TODO: Implement this + return "SMSIncomingImage" @dataclass class iMessage(Message): @@ -219,11 +230,14 @@ class iMessage(Message): message = plistlib.loads(message) + logger.debug(f"Decoding iMessage: {message}") + return iMessage( text=message["t"], participants=message["p"], sender=sender, id=uuid.UUID(message["r"]), + xml=message["x"] if "x" in message else None, _raw=message, _compressed=compressed, effect=message["iid"] if "iid" in message else None, @@ -533,10 +547,18 @@ class iMessageUser: # Check for SMS messages body = self._receive_raw(143, "com.apple.private.alloy.sms") t = SMSReflectedMessage + if body is None: + # Check for SMS reflected images + body = self._receive_raw(144, "com.apple.private.alloy.sms") + t = SMSReflectedMessage if body is None: # Check for SMS incoming messages body = self._receive_raw(140, "com.apple.private.alloy.sms") t = SMSIncomingMessage + if body is None: + # Incoming images + body = self._receive_raw(141, "com.apple.private.alloy.sms") + t = SMSIncomingImage if body is None: return None @@ -677,7 +699,7 @@ class iMessageUser: return False resp_body = plistlib.loads(resp_body) - #logger.debug(f"See type {resp_body['c']}") + #logger.info(f"See type {resp_body['c']}") if isinstance(c, list): if not resp_body["c"] in c: From 8a09a6eecdbad1c7b6b6dd23a22c3689e531463b Mon Sep 17 00:00:00 2001 From: JJTech0130 Date: Tue, 15 Aug 2023 09:05:08 -0400 Subject: [PATCH 09/11] add commit hash check (and fmt) --- demo.py | 161 ++++++++++++++++++++++++++++++++++---------------------- 1 file changed, 97 insertions(+), 64 deletions(-) diff --git a/demo.py b/demo.py index 62aa78f..38e822d 100644 --- a/demo.py +++ b/demo.py @@ -5,6 +5,7 @@ import threading import time from base64 import b64decode, b64encode from getpass import getpass +from subprocess import PIPE, Popen from rich.logging import RichHandler @@ -18,7 +19,7 @@ logging.basicConfig( # Set sane log levels logging.getLogger("urllib3").setLevel(logging.WARNING) -logging.getLogger("py.warnings").setLevel(logging.ERROR) # Ignore warnings from urllib3 +logging.getLogger("py.warnings").setLevel(logging.ERROR) # Ignore warnings from urllib3 logging.getLogger("asyncio").setLevel(logging.WARNING) logging.getLogger("jelly").setLevel(logging.INFO) logging.getLogger("nac").setLevel(logging.INFO) @@ -30,6 +31,11 @@ logging.getLogger("imessage").setLevel(logging.INFO) logging.captureWarnings(True) +process = Popen(["git", "rev-parse", "HEAD"], stdout=PIPE) +(commit_hash, err) = process.communicate() +exit_code = process.wait() +commit_hash = commit_hash.decode().strip() + # Try and load config.json try: with open("config.json", "r") as f: @@ -37,6 +43,13 @@ try: except FileNotFoundError: CONFIG = {} +# Re-register if the commit hash has changed +if CONFIG.get("commit_hash") != commit_hash: + logging.warning("pypush commit is different, forcing re-registration...") + CONFIG["commit_hash"] = commit_hash + if "id" in CONFIG: + del CONFIG["id"] + conn = apns.APNSConnection( CONFIG.get("push", {}).get("key"), CONFIG.get("push", {}).get("cert") ) @@ -116,44 +129,48 @@ im = imessage.iMessageUser(conn, user) INPUT_QUEUE = apns.IncomingQueue() + def input_thread(): from prompt_toolkit import prompt - while True: + while True: try: - msg = prompt('>> ') + msg = prompt(">> ") except: - msg = 'quit' + msg = "quit" INPUT_QUEUE.append(msg) + threading.Thread(target=input_thread, daemon=True).start() -print("Type 'help' for help") +print("Type 'help' for help") + def fixup_handle(handle): - if handle.startswith('tel:+'): + if handle.startswith("tel:+"): return handle - elif handle.startswith('mailto:'): + elif handle.startswith("mailto:"): return handle - elif handle.startswith('tel:'): - return 'tel:+' + handle[4:] - elif handle.startswith('+'): - return 'tel:' + handle + elif handle.startswith("tel:"): + return "tel:+" + handle[4:] + elif handle.startswith("+"): + return "tel:" + handle # If the handle starts with a number elif handle[0].isdigit(): # If the handle is 10 digits, assume it's a US number if len(handle) == 10: - return 'tel:+1' + handle + return "tel:+1" + handle # If the handle is 11 digits, assume it's a US number with country code elif len(handle) == 11: - return 'tel:+' + handle - else: # Assume it's an email - return 'mailto:' + handle + return "tel:+" + handle + else: # Assume it's an email + return "mailto:" + handle + current_participants = [] current_effect = None while True: - im.activate_sms() # We must call this always since SMS could be turned off and on again, and it might have been on before this. + im.activate_sms() # We must call this always since SMS could be turned off and on again, and it might have been on before this. msg = im.receive() if msg is not None: # print(f'[{msg.sender}] {msg.text}') @@ -170,79 +187,95 @@ while True: # print(f"({len(attachments)} attachment{'s have' if len(attachments) != 1 else ' has'} been downloaded and put " # f"in {attachments_path})") - + if len(INPUT_QUEUE) > 0: msg = INPUT_QUEUE.pop() - if msg == '': continue - if msg == 'help' or msg == 'h': - print('help (h): show this message') - print('quit (q): quit') - #print('send (s) [recipient] [message]: send a message') - print('filter (f) [recipient]: set the current chat') - print('effect (e): adds an iMessage effect to the next sent message') - print('note: recipient must start with tel: or mailto: and include the country code') - print('handle : set the current handle (for sending messages)') - print('\\: escape commands (will be removed from message)') - elif msg == 'quit' or msg == 'q': + if msg == "": + continue + if msg == "help" or msg == "h": + print("help (h): show this message") + print("quit (q): quit") + # print('send (s) [recipient] [message]: send a message') + print("filter (f) [recipient]: set the current chat") + print("effect (e): adds an iMessage effect to the next sent message") + print( + "note: recipient must start with tel: or mailto: and include the country code" + ) + print("handle : set the current handle (for sending messages)") + print("\\: escape commands (will be removed from message)") + elif msg == "quit" or msg == "q": break - elif msg == 'effect' or msg == 'e' or msg.startswith("effect ") or msg.startswith("e "): + elif ( + msg == "effect" + or msg == "e" + or msg.startswith("effect ") + or msg.startswith("e ") + ): msg = msg.split(" ") if len(msg) < 2 or msg[1] == "": print("effect [effect namespace]") else: print(f"next message will be sent with [{msg[1]}]") current_effect = msg[1] - elif msg == 'filter' or msg == 'f' or msg.startswith('filter ') or msg.startswith('f '): + elif ( + msg == "filter" + or msg == "f" + or msg.startswith("filter ") + or msg.startswith("f ") + ): # Set the curernt chat - msg = msg.split(' ') - if len(msg) < 2 or msg[1] == '': - print('filter [recipients]') + msg = msg.split(" ") + if len(msg) < 2 or msg[1] == "": + print("filter [recipients]") else: - print(f'Filtering to {[fixup_handle(h) for h in msg[1:]]}') + print(f"Filtering to {[fixup_handle(h) for h in msg[1:]]}") current_participants = [fixup_handle(h) for h in msg[1:]] - im._cache_keys(current_participants, "com.apple.madrid") # Just to make things faster, and to make it error on invalid addresses - elif msg == 'handle' or msg.startswith('handle '): - msg = msg.split(' ') - if len(msg) < 2 or msg[1] == '': - print('handle [handle]') - print('Available handles:') + im._cache_keys( + current_participants, "com.apple.madrid" + ) # Just to make things faster, and to make it error on invalid addresses + elif msg == "handle" or msg.startswith("handle "): + msg = msg.split(" ") + if len(msg) < 2 or msg[1] == "": + print("handle [handle]") + print("Available handles:") for h in user.handles: if h == user.current_handle: - print(f'\t{h} (current)') + print(f"\t{h} (current)") else: - print(f'\t{h}') + print(f"\t{h}") else: h = msg[1] h = fixup_handle(h) if h in user.handles: - print(f'Using {h} as handle') + print(f"Using {h} as handle") user.current_handle = h else: - print(f'Handle {h} not found') + print(f"Handle {h} not found") elif current_participants != []: - if msg.startswith('\\'): + if msg.startswith("\\"): msg = msg[1:] - im.send(imessage.OldiMessage( - text=msg, - participants=current_participants, - sender=user.current_handle, - effect=current_effect - )) + im.send( + imessage.OldiMessage( + text=msg, + participants=current_participants, + sender=user.current_handle, + effect=current_effect, + ) + ) current_effect = None else: - print('No chat selected, use help for help') + print("No chat selected, use help for help") time.sleep(0.1) - - # elif msg.startswith('send') or msg.startswith('s'): - # msg = msg.split(' ') - # if len(msg) < 3: - # print('send [recipient] [message]') - # else: - # im.send(imessage.iMessage( - # text=' '.join(msg[2:]), - # participants=[msg[1], user.handles[0]], - # #sender=user.handles[0] - # )) - + + # elif msg.startswith('send') or msg.startswith('s'): + # msg = msg.split(' ') + # if len(msg) < 3: + # print('send [recipient] [message]') + # else: + # im.send(imessage.iMessage( + # text=' '.join(msg[2:]), + # participants=[msg[1], user.handles[0]], + # #sender=user.handles[0] + # )) From da483b995b9e6705f55da83dc17072bdc2a895b2 Mon Sep 17 00:00:00 2001 From: JJTech0130 Date: Tue, 15 Aug 2023 16:28:02 -0400 Subject: [PATCH 10/11] fix some bugs, remove old parser --- demo.py | 27 +++------ imessage.py | 154 ++++++++-------------------------------------------- 2 files changed, 33 insertions(+), 148 deletions(-) diff --git a/demo.py b/demo.py index 38e822d..c1015d9 100644 --- a/demo.py +++ b/demo.py @@ -255,27 +255,18 @@ while True: elif current_participants != []: if msg.startswith("\\"): msg = msg[1:] - im.send( - imessage.OldiMessage( - text=msg, - participants=current_participants, - sender=user.current_handle, - effect=current_effect, - ) + + imsg = imessage.iMessage.create( + im, + msg, + current_participants, ) + + imsg.effect = current_effect + + im.send(imsg) current_effect = None else: print("No chat selected, use help for help") time.sleep(0.1) - - # elif msg.startswith('send') or msg.startswith('s'): - # msg = msg.split(' ') - # if len(msg) < 3: - # print('send [recipient] [message]') - # else: - # im.send(imessage.iMessage( - # text=' '.join(msg[2:]), - # participants=[msg[1], user.handles[0]], - # #sender=user.handles[0] - # )) diff --git a/imessage.py b/imessage.py index a7e5be1..cefefd4 100644 --- a/imessage.py +++ b/imessage.py @@ -1,9 +1,3 @@ -# LOW LEVEL imessage function, decryption etc -# Don't handle APNS etc, accept it already setup - -## HAVE ANOTHER FILE TO SETUP EVERYTHING AUTOMATICALLY, etc -# JSON parsing of keys, don't pass around strs?? - import base64 import gzip import logging @@ -138,7 +132,7 @@ class Message: sender: str participants: list[str] id: uuid.UUID - _raw: dict + _raw: dict | None = None _compressed: bool = True xml: str | None = None @@ -218,6 +212,19 @@ class SMSIncomingImage(Message): class iMessage(Message): effect: str | None = None + def create(user: "iMessageUser", text: str, participants: list[str]) -> "iMessage": + """Creates a basic outgoing `iMessage` from the given text and participants""" + + sender = user.user.current_handle + participants += [sender] + + return iMessage( + text=text, + sender=sender, + participants=participants, + id=uuid.uuid4(), + ) + def from_raw(message: bytes, sender: str | None = None) -> "iMessage": """Create a `iMessage` from raw message bytes""" @@ -236,129 +243,21 @@ class iMessage(Message): text=message["t"], participants=message["p"], sender=sender, - id=uuid.UUID(message["r"]), + id=uuid.UUID(message["r"]) if "r" in message else None, xml=message["x"] if "x" in message else None, _raw=message, _compressed=compressed, effect=message["iid"] if "iid" in message else None, ) - def __str__(self): - return f"[iMessage {self.sender}] '{self.text}'" - -@dataclass -class OldiMessage: - """Represents an iMessage""" - - text: str = "" - """Plain text of message, always required, may be an empty string""" - xml: str | None = None - """XML portion of message, may be None""" - participants: list[str] = field(default_factory=list) - """List of participants in the message, including the sender""" - sender: str | None = None - """Sender of the message""" - id: uuid.UUID | None = None - """ID of the message, will be randomly generated if not provided""" - group_id: uuid.UUID | None = None - """Group ID of the message, will be randomly generated if not provided""" - body: BalloonBody | None = None - """BalloonBody, may be None""" - effect: str | None = None - """iMessage effect sent with this message, may be None""" - - _compressed: bool = True - """Internal property representing whether the message should be compressed""" - - _raw: dict | None = None - """Internal property representing the original raw message, may be None""" - - def attachments(self) -> list[Attachment]: - if self.xml is not None: - return [ - Attachment(self._raw, elem) - for elem in ElementTree.fromstring(self.xml)[0] - if elem.tag == "FILE" - ] - else: - return [] - - def sanity_check(self): - """Corrects any missing fields""" - if self.id is None: - self.id = uuid.uuid4() - - if self.group_id is None: - self.group_id = uuid.uuid4() - - if self.sender is None: - if len(self.participants) > 1: - self.sender = self.participants[-1] - else: - logger.warning( - "Message has no sender, and only one participant, sanity check failed" - ) - return False - - if self.sender not in self.participants: - self.participants.append(self.sender) - - if self.xml != None: - self._compressed = False # XML is never compressed for some reason - - return True - - def from_raw(message: bytes, sender: str | None = None) -> "OldiMessage": - """Create an `iMessage` from raw message bytes""" - compressed = False - try: - message = gzip.decompress(message) - compressed = True - except: - pass - - message = plistlib.loads(message) - - logger.debug(f"Decompressed message : {message}") - - try: - return OldiMessage( - text=message[ - "t" - ], # Cause it to "fail to parse" if there isn't any good text to display, temp hack - xml=message.get("x"), - participants=message.get("p", []), - sender=sender - if sender is not None - else message.get("p", [])[-1] - if "p" in message - else None, - id=uuid.UUID(message.get("r")) if "r" in message else None, - group_id=uuid.UUID(message.get("gid")) if "gid" in message else None, - body=BalloonBody(message["bid"], message["b"]) - if "bid" in message and "b" in message - else None, - effect=message["iid"] if "iid" in message else None, - _compressed=compressed, - _raw=message, - ) - except: - #import json - - dmp = str(message) - return OldiMessage(text=f"failed to parse: {dmp}", _raw=message) - def to_raw(self) -> bytes: """Convert an `iMessage` to raw message bytes""" - if not self.sanity_check(): - raise ValueError("Message failed sanity check") d = { "t": self.text, "x": self.xml, "p": self.participants, "r": str(self.id).upper(), - "gid": str(self.group_id).upper(), "pv": 0, "gv": "8", "v": "1", @@ -376,13 +275,9 @@ class OldiMessage: d = gzip.compress(d, mtime=0) return d - - def to_string(self) -> str: - message_str = f"[{self.sender}] '{self.text}'" - if self.effect is not None: - message_str += f" with effect [{self.effect}]" - return message_str - + + def __str__(self): + return f"[iMessage {self.sender}] '{self.text}'" class iMessageUser: """Represents a logged in and connected iMessage user. @@ -570,7 +465,11 @@ class iMessageUser: decrypted = self._decrypt_payload(body["P"]) - return t.from_raw(decrypted, body["sP"]) + try: + return t.from_raw(decrypted, body["sP"]) + except Exception as e: + logger.error(f"Failed to parse message : {e}") + return None KEY_CACHE_HANDLE: str = "" KEY_CACHE: dict[bytes, dict[str, tuple[bytes, bytes]]] = {} @@ -735,12 +634,7 @@ class iMessageUser: } ) - def send(self, message: OldiMessage): - # Set the sender, if it isn't already - if message.sender is None: - message.sender = self.user.handles[0] # TODO : Which handle to use? - - message.sanity_check() # Sanity check MUST be called before caching keys, so that the sender is added to the list of participants + def send(self, message: "iMessage"): self._cache_keys(message.participants, "com.apple.madrid") self._send_raw( From 3904bf9e3faed8879cd9752b6c25721e5c4ccd2c Mon Sep 17 00:00:00 2001 From: JJTech0130 Date: Wed, 16 Aug 2023 13:31:15 -0400 Subject: [PATCH 11/11] preparing some stuff --- demo.py | 25 +++++++---- imessage.py | 126 +++++++++++++++++++++++++++++++++++++--------------- 2 files changed, 108 insertions(+), 43 deletions(-) diff --git a/demo.py b/demo.py index c1015d9..231b096 100644 --- a/demo.py +++ b/demo.py @@ -168,6 +168,7 @@ def fixup_handle(handle): current_participants = [] +sms = False current_effect = None while True: im.activate_sms() # We must call this always since SMS could be turned off and on again, and it might have been on before this. @@ -228,6 +229,13 @@ while True: if len(msg) < 2 or msg[1] == "": print("filter [recipients]") else: + if msg[1] == "sms": + print("Filtering to SMS") + msg = msg[1:] + sms = True + else: + sms = False + print(f"Filtering to {[fixup_handle(h) for h in msg[1:]]}") current_participants = [fixup_handle(h) for h in msg[1:]] im._cache_keys( @@ -256,15 +264,16 @@ while True: if msg.startswith("\\"): msg = msg[1:] - imsg = imessage.iMessage.create( - im, - msg, - current_participants, - ) + if sms: + import uuid + m = imessage.SMSReflectedMessage( + msg, user.current_handle, current_participants, uuid.uuid4() + ) + else: + m = imessage.iMessage.create(im, msg, current_participants) + m.effect = current_effect - imsg.effect = current_effect - - im.send(imsg) + im.send(m) current_effect = None else: print("No chat selected, use help for help") diff --git a/imessage.py b/imessage.py index cefefd4..8951cde 100644 --- a/imessage.py +++ b/imessage.py @@ -147,7 +147,7 @@ class Message: @dataclass class SMSReflectedMessage(Message): def from_raw(message: bytes, sender: str | None = None) -> "SMSReflectedMessage": - """Create a `SMSIncomingMessage` from raw message bytes""" + """Create a `SMSReflectedMessage` from raw message bytes""" # Decompress the message try: @@ -158,7 +158,7 @@ class SMSReflectedMessage(Message): message = plistlib.loads(message) - logger.debug(f"Decoding SMSReflectedMessage: {message}") + logger.info(f"Decoding SMSReflectedMessage: {message}") return SMSReflectedMessage( text=message["mD"]["plain-body"], @@ -168,7 +168,42 @@ class SMSReflectedMessage(Message): _raw=message, _compressed=compressed, ) - + + def to_raw(self) -> bytes: + # {'re': [{'id': '+14155086773', 'uID': '4155086773', 'n': 'us'}], 'ic': 0, 'mD': {'handle': '+14155086773', 'guid': imessage.py:201 + # '35694E24-E265-4D5C-8CA7-9499E35D0402', 'replyToGUID': '4F9BC76B-B09C-2A60-B312-9029D529706B', 'plain-body': 'Test sms', 'service': + # 'SMS', 'sV': '1'}, 'fR': True, 'chat-style': 'im'} + #pass + # Strip tel: from participants, making sure they are all phone numbers + #participants = [p.replace("tel:", "") for p in self.participants] + + d = { + "re": [{"id": p} for p in self.participants], + "ic": 0, + "mD": { + "handle": self.participants[0] if len(self.participants) == 1 else None, + #"handle": self.sender, + "guid": str(self.id).upper(), + #"replyToGUID": "3B4B465F-F419-40FD-A8EF-94A110518E9F", + #"replyToGUID": str(self.id).upper(), + "xhtml": f"{self.text}", + "plain-body": self.text, + "service": "SMS", + "sV": "1", + }, + #"fR": True, + "chat-style": "im" if len(self.participants) == 1 else "chat" + } + + # Dump as plist + d = plistlib.dumps(d, fmt=plistlib.FMT_BINARY) + + # Compress + if self._compressed: + d = gzip.compress(d, mtime=0) + + return d + def __str__(self): return f"[SMS {self.sender}] '{self.text}'" @@ -279,6 +314,22 @@ class iMessage(Message): def __str__(self): return f"[iMessage {self.sender}] '{self.text}'" +MESSAGE_TYPES = { + 100: ("com.apple.madrid", iMessage), + 140: ("com.apple.private.alloy.sms", SMSIncomingMessage), + 141: ("com.apple.private.alloy.sms", SMSIncomingImage), + 143: ("com.apple.private.alloy.sms", SMSReflectedMessage), + 144: ("com.apple.private.alloy.sms", SMSReflectedMessage), +} + +def maybe_decompress(message: bytes) -> bytes: + """Decompresses a message if it is compressed, otherwise returns the original message""" + try: + message = gzip.decompress(message) + except: + pass + return message + class iMessageUser: """Represents a logged in and connected iMessage user. This abstraction should probably be reworked into IDS some time...""" @@ -434,27 +485,12 @@ class iMessageUser: """ Will return the next iMessage in the queue, or None if there are no messages """ - - # Check for iMessages - body = self._receive_raw(100, "com.apple.madrid") - t = iMessage - if body is None: - # Check for SMS messages - body = self._receive_raw(143, "com.apple.private.alloy.sms") - t = SMSReflectedMessage - if body is None: - # Check for SMS reflected images - body = self._receive_raw(144, "com.apple.private.alloy.sms") - t = SMSReflectedMessage - if body is None: - # Check for SMS incoming messages - body = self._receive_raw(140, "com.apple.private.alloy.sms") - t = SMSIncomingMessage - if body is None: - # Incoming images - body = self._receive_raw(141, "com.apple.private.alloy.sms") - t = SMSIncomingImage - if body is None: + for type, (topic, cls) in MESSAGE_TYPES.items(): + body = self._receive_raw(type, topic) + if body is not None: + t = cls + break + else: return None @@ -485,8 +521,9 @@ class iMessageUser: self.USER_CACHE = {} # Check to see if we have cached the keys for all of the participants - if all([p in self.USER_CACHE for p in participants]): - return + #if all([p in self.USER_CACHE for p in participants]): + # return + # TODO: This doesn't work since it doesn't check if they are cached for all topics # Look up the public keys for the participants, and cache a token : public key mapping lookup = self.user.lookup(participants, topic=topic) @@ -625,6 +662,16 @@ class iMessageUser: if act_message is None: return False + logger.info(f"Received SMS activation message : {act_message}") + # Decrypt the payload + act_message = self._decrypt_payload(act_message["P"]) + act_message = plistlib.loads(maybe_decompress(act_message)) + + if act_message == {'wc': False, 'ar': True}: + logger.info("SMS forwarding activated, sending response") + else: + logger.info("SMS forwarding de-activated, sending response") + self._send_raw( 147, [self.user.current_handle], @@ -634,19 +681,28 @@ class iMessageUser: } ) - def send(self, message: "iMessage"): - self._cache_keys(message.participants, "com.apple.madrid") + def send(self, message: Message): + # Check what type of message we are sending + for t, (topic, cls) in MESSAGE_TYPES.items(): + if isinstance(message, cls): + break + else: + raise Exception("Unknown message type") + + send_to = message.participants if isinstance(message, iMessage) else [self.user.current_handle] + + self._cache_keys(send_to, topic) self._send_raw( - 100, - message.participants, - "com.apple.madrid", + t, + send_to, + topic, message.to_raw(), message.id, { - "E": "pair", + "E": "pair", # TODO: Do we need the nr field for SMS? } - ) + ) # Check for delivery count = 0 @@ -655,14 +711,14 @@ class iMessageUser: import time start = time.time() - for p in message.participants: + for p in send_to: for t in self.USER_CACHE[p]: if t == self.connection.token: continue total += 1 while count < total and time.time() - start < 2: - resp = self._receive_raw(255, "com.apple.madrid") + resp = self._receive_raw(255, topic) if resp is None: continue count += 1