pypush-plus-plus/imessage.py

783 lines
24 KiB
Python
Raw Normal View History

2023-07-27 10:04:57 -05:00
# 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??
2023-07-31 18:38:28 -05:00
import base64
2023-07-31 12:03:45 -05:00
import gzip
import logging
2023-07-27 10:52:20 -05:00
import plistlib
2023-07-31 12:03:45 -05:00
import random
import uuid
from dataclasses import dataclass, field
from hashlib import sha1, sha256
2023-07-27 10:52:20 -05:00
from io import BytesIO
from cryptography.hazmat.primitives import hashes
2023-07-31 12:03:45 -05:00
from cryptography.hazmat.primitives.asymmetric import ec, padding
2023-07-27 10:52:20 -05:00
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
2023-07-31 18:38:28 -05:00
from xml.etree import ElementTree
2023-07-31 12:03:45 -05:00
import apns
import ids
2023-07-27 10:52:20 -05:00
2023-07-27 16:34:38 -05:00
logger = logging.getLogger("imessage")
2023-07-31 12:03:45 -05:00
NORMAL_NONCE = b"\x00" * 15 + b"\x01" # This is always used as the AES nonce
2023-07-27 10:52:20 -05:00
class BalloonBody:
2023-07-31 12:03:45 -05:00
"""Represents the special parts of message extensions etc."""
def __init__(self, type: str, data: bytes):
self.type = type
self.data = data
# TODO : Register handlers based on type id
2023-07-31 12:03:45 -05:00
2023-07-31 18:38:28 -05:00
class AttachmentFile:
def data(self) -> bytes:
raise NotImplementedError()
@dataclass
class MMCSFile(AttachmentFile):
url: str | None = None
size: int | None = None
owner: str | None = None
signature: bytes | None = None
decryption_key: bytes | None = None
def data(self) -> bytes:
import requests
2023-08-13 22:24:34 -05:00
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
)
2023-07-31 18:38:28 -05:00
return b""
@dataclass
class InlineFile(AttachmentFile):
_data: bytes
def data(self) -> bytes:
return self._data
@dataclass
class Attachment:
name: str
mime_type: str
versions: list[AttachmentFile]
def __init__(self, message_raw_content: dict, xml_element: ElementTree.Element):
2023-08-12 06:41:41 -05:00
attrib = xml_element.attrib
2023-07-31 18:38:28 -05:00
2023-08-12 06:41:41 -05:00
self.name = attrib["name"] if "name" in attrib else None
self.mime_type = attrib["mime-type"] if "mime-type" in attrib else None
2023-07-31 18:38:28 -05:00
2023-08-12 06:41:41 -05:00
if "inline-attachment" in attrib:
2023-07-31 18:38:28 -05:00
# just grab the inline attachment !
2023-08-13 22:24:34 -05:00
self.versions = [
InlineFile(message_raw_content[attrib["inline-attachment"]])
]
2023-07-31 18:38:28 -05:00
else:
# suffer
2023-08-12 06:41:41 -05:00
versions = [InlineFile(b"")]
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:]
2023-07-31 18:38:28 -05:00
self.versions = versions
def __repr__(self):
return f'<Attachment name="{self.name}" type="{self.mime_type}">'
2023-08-14 08:49:11 -05:00
@dataclass
2023-08-14 08:40:33 -05:00
class Message:
2023-08-14 08:49:11 -05:00
text: str
sender: str
participants: list[str]
id: uuid.UUID
_raw: dict
_compressed: bool = True
2023-08-14 19:31:03 -05:00
xml: str | None = None
2023-08-14 08:40:33 -05:00
def from_raw(message: bytes, sender: str | None = None) -> "Message":
"""Create a `Message` from raw message bytes"""
raise NotImplementedError()
def __str__():
raise NotImplementedError()
2023-08-14 08:49:11 -05:00
@dataclass
2023-08-14 08:40:33 -05:00
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)
2023-08-14 19:31:03 -05:00
logger.debug(f"Decoding SMSReflectedMessage: {message}")
2023-08-14 08:40:33 -05:00
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}'"
2023-08-14 08:49:11 -05:00
@dataclass
2023-08-14 08:40:33 -05:00
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)
2023-08-14 19:31:03 -05:00
logger.debug(f"Decoding SMSIncomingMessage: {message}")
2023-08-14 08:40:33 -05:00
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}'"
2023-08-14 19:31:03 -05:00
@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"
2023-08-14 08:40:33 -05:00
2023-08-14 08:49:11 -05:00
@dataclass
2023-08-14 08:40:33 -05:00
class iMessage(Message):
2023-08-14 08:49:11 -05:00
effect: str | None = None
2023-08-14 08:40:33 -05:00
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)
2023-08-14 19:31:03 -05:00
logger.debug(f"Decoding iMessage: {message}")
2023-08-14 08:40:33 -05:00
return iMessage(
text=message["t"],
participants=message["p"],
sender=sender,
id=uuid.UUID(message["r"]),
2023-08-14 19:31:03 -05:00
xml=message["x"] if "x" in message else None,
2023-08-14 08:40:33 -05:00
_raw=message,
_compressed=compressed,
2023-08-14 08:49:11 -05:00
effect=message["iid"] if "iid" in message else None,
2023-08-14 08:40:33 -05:00
)
def __str__(self):
return f"[iMessage {self.sender}] '{self.text}'"
2023-07-31 18:38:28 -05:00
2023-07-31 12:03:45 -05:00
@dataclass
2023-08-14 08:40:33 -05:00
class OldiMessage:
2023-07-31 12:03:45 -05:00
"""Represents an iMessage"""
2023-07-28 18:20:32 -05:00
text: str = ""
2023-07-31 12:03:45 -05:00
"""Plain text of message, always required, may be an empty string"""
xml: str | None = None
2023-07-31 12:03:45 -05:00
"""XML portion of message, may be None"""
participants: list[str] = field(default_factory=list)
"""List of participants in the message, including the sender"""
2023-07-28 18:20:32 -05:00
sender: str | None = None
2023-07-31 12:03:45 -05:00
"""Sender of the message"""
2023-07-31 18:38:28 -05:00
id: uuid.UUID | None = None
2023-07-31 12:03:45 -05:00
"""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
2023-07-31 12:03:45 -05:00
"""BalloonBody, may be None"""
2023-07-31 13:35:34 -05:00
effect: str | None = None
"""iMessage effect sent with this message, may be None"""
2023-07-28 18:20:32 -05:00
_compressed: bool = True
2023-07-31 12:03:45 -05:00
"""Internal property representing whether the message should be compressed"""
2023-07-28 18:20:32 -05:00
_raw: dict | None = None
2023-07-31 12:03:45 -05:00
"""Internal property representing the original raw message, may be None"""
2023-07-31 18:38:28 -05:00
def attachments(self) -> list[Attachment]:
if self.xml is not None:
2023-08-13 22:24:34 -05:00
return [
Attachment(self._raw, elem)
for elem in ElementTree.fromstring(self.xml)[0]
if elem.tag == "FILE"
]
2023-07-31 18:38:28 -05:00
else:
return []
2023-07-31 12:03:45 -05:00
def sanity_check(self):
"""Corrects any missing fields"""
2023-07-31 18:38:28 -05:00
if self.id is None:
self.id = uuid.uuid4()
2023-07-31 12:03:45 -05:00
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
2023-07-31 12:03:45 -05:00
if self.sender not in self.participants:
self.participants.append(self.sender)
2023-07-31 12:03:45 -05:00
if self.xml != None:
self._compressed = False # XML is never compressed for some reason
2023-07-31 12:03:45 -05:00
return True
2023-08-14 08:40:33 -05:00
def from_raw(message: bytes, sender: str | None = None) -> "OldiMessage":
2023-07-31 12:03:45 -05:00
"""Create an `iMessage` from raw message bytes"""
compressed = False
try:
message = gzip.decompress(message)
compressed = True
except:
pass
2023-07-31 12:03:45 -05:00
message = plistlib.loads(message)
2023-08-12 06:41:41 -05:00
logger.debug(f"Decompressed message : {message}")
2023-08-13 18:26:57 -05:00
try:
2023-08-14 08:40:33 -05:00
return OldiMessage(
2023-08-13 22:24:34 -05:00
text=message[
"t"
], # Cause it to "fail to parse" if there isn't any good text to display, temp hack
2023-08-13 18:26:57 -05:00
xml=message.get("x"),
participants=message.get("p", []),
2023-08-13 22:24:34 -05:00
sender=sender
if sender is not None
else message.get("p", [])[-1]
if "p" in message
else None,
2023-08-13 18:26:57 -05:00
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,
2023-08-13 22:24:34 -05:00
body=BalloonBody(message["bid"], message["b"])
if "bid" in message and "b" in message
else None,
2023-08-13 18:26:57 -05:00
effect=message["iid"] if "iid" in message else None,
_compressed=compressed,
_raw=message,
)
except:
2023-08-13 22:24:34 -05:00
#import json
dmp = str(message)
2023-08-14 08:40:33 -05:00
return OldiMessage(text=f"failed to parse: {dmp}", _raw=message)
2023-07-28 18:20:32 -05:00
2023-07-31 12:03:45 -05:00
def to_raw(self) -> bytes:
"""Convert an `iMessage` to raw message bytes"""
if not self.sanity_check():
raise ValueError("Message failed sanity check")
2023-07-31 09:58:01 -05:00
d = {
"t": self.text,
"x": self.xml,
"p": self.participants,
2023-07-31 18:38:28 -05:00
"r": str(self.id).upper(),
2023-07-31 12:03:45 -05:00
"gid": str(self.group_id).upper(),
2023-07-30 16:00:04 -05:00
"pv": 0,
2023-07-31 12:03:45 -05:00
"gv": "8",
"v": "1",
2023-08-13 22:24:34 -05:00
"iid": self.effect,
}
2023-07-31 12:03:45 -05:00
2023-07-31 09:58:01 -05:00
# Remove keys that are None
2023-07-31 12:03:45 -05:00
d = {k: v for k, v in d.items() if v is not None}
# Serialize as a plist
d = plistlib.dumps(d, fmt=plistlib.FMT_BINARY)
# Compression
if self._compressed:
d = gzip.compress(d, mtime=0)
return d
2023-07-31 13:35:34 -05:00
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
2023-07-27 10:04:57 -05:00
class iMessageUser:
2023-07-31 12:03:45 -05:00
"""Represents a logged in and connected iMessage user.
This abstraction should probably be reworked into IDS some time..."""
2023-07-27 10:04:57 -05:00
2023-07-27 10:52:20 -05:00
def __init__(self, connection: apns.APNSConnection, user: ids.IDSUser):
self.connection = connection
self.user = user
2023-07-27 10:04:57 -05:00
2023-07-27 10:52:20 -05:00
def _parse_payload(payload: bytes) -> tuple[bytes, bytes]:
payload = BytesIO(payload)
2023-07-27 10:04:57 -05:00
2023-07-27 10:52:20 -05:00
tag = payload.read(1)
2023-08-13 22:24:34 -05:00
# print("TAG", tag)
2023-07-27 10:52:20 -05:00
body_length = int.from_bytes(payload.read(2), "big")
body = payload.read(body_length)
2023-07-31 12:03:45 -05:00
2023-07-27 10:52:20 -05:00
signature_len = payload.read(1)[0]
signature = payload.read(signature_len)
return (body, signature)
2023-07-31 12:03:45 -05:00
2023-07-28 18:20:32 -05:00
def _construct_payload(body: bytes, signature: bytes) -> bytes:
2023-07-31 12:03:45 -05:00
payload = (
b"\x02"
+ len(body).to_bytes(2, "big")
+ body
+ len(signature).to_bytes(1, "big")
+ signature
)
2023-07-28 18:20:32 -05:00
return payload
2023-07-31 09:58:01 -05:00
def _hash_identity(id: bytes) -> bytes:
iden = ids.identity.IDSIdentity.decode(id)
# TODO: Combine this with serialization code in ids.identity
output = BytesIO()
2023-07-31 12:03:45 -05:00
output.write(b"\x00\x41\x04")
output.write(
ids._helpers.parse_key(iden.signing_public_key)
.public_numbers()
.x.to_bytes(32, "big")
)
output.write(
ids._helpers.parse_key(iden.signing_public_key)
.public_numbers()
.y.to_bytes(32, "big")
)
2023-07-31 09:58:01 -05:00
2023-07-31 12:03:45 -05:00
output.write(b"\x00\xAC")
output.write(b"\x30\x81\xA9")
output.write(b"\x02\x81\xA1")
output.write(
ids._helpers.parse_key(iden.encryption_public_key)
.public_numbers()
.n.to_bytes(161, "big")
)
output.write(b"\x02\x03\x01\x00\x01")
2023-07-31 09:58:01 -05:00
return sha256(output.getvalue()).digest()
2023-07-28 18:20:32 -05:00
2023-07-31 12:03:45 -05:00
def _encrypt_sign_payload(
self, key: ids.identity.IDSIdentity, message: bytes
) -> bytes:
2023-07-28 18:20:32 -05:00
# Generate a random AES key
2023-07-31 09:58:01 -05:00
random_seed = random.randbytes(11)
# Create the HMAC
import hmac
2023-07-31 12:03:45 -05:00
hm = hmac.new(
random_seed,
message
+ b"\x02"
+ iMessageUser._hash_identity(self.user.encryption_identity.encode())
+ iMessageUser._hash_identity(key.encode()),
sha256,
).digest()
2023-07-31 09:58:01 -05:00
aes_key = random_seed + hm[:5]
2023-07-31 12:03:45 -05:00
# print(len(aes_key))
2023-07-28 18:20:32 -05:00
# Encrypt the message with the AES key
cipher = Cipher(algorithms.AES(aes_key), modes.CTR(NORMAL_NONCE))
encrypted = cipher.encryptor().update(message)
# Encrypt the AES key with the public key of the recipient
recipient_key = ids._helpers.parse_key(key.encryption_public_key)
rsa_body = recipient_key.encrypt(
2023-07-31 12:03:45 -05:00
aes_key + encrypted[:100],
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA1()),
algorithm=hashes.SHA1(),
label=None,
),
2023-07-28 18:20:32 -05:00
)
# Construct the payload
body = rsa_body + encrypted[100:]
2023-07-31 12:03:45 -05:00
sig = ids._helpers.parse_key(self.user.encryption_identity.signing_key).sign(
body, ec.ECDSA(hashes.SHA1())
)
2023-07-28 18:20:32 -05:00
payload = iMessageUser._construct_payload(body, sig)
return payload
2023-07-31 12:03:45 -05:00
2023-07-27 10:52:20 -05:00
def _decrypt_payload(self, payload: bytes) -> dict:
payload = iMessageUser._parse_payload(payload)
body = BytesIO(payload[0])
2023-07-31 12:03:45 -05:00
rsa_body = ids._helpers.parse_key(
self.user.encryption_identity.encryption_key
).decrypt(
2023-07-27 10:52:20 -05:00
body.read(160),
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA1()),
algorithm=hashes.SHA1(),
label=None,
),
)
cipher = Cipher(algorithms.AES(rsa_body[:16]), modes.CTR(NORMAL_NONCE))
decrypted = cipher.decryptor().update(rsa_body[16:] + body.read())
2023-07-31 12:08:57 -05:00
2023-07-31 12:03:45 -05:00
return decrypted
2023-07-27 10:52:20 -05:00
def _verify_payload(self, payload: bytes, sender: str, sender_token: str) -> bool:
# Get the public key for the sender
2023-08-13 18:26:57 -05:00
self._cache_keys([sender], "com.apple.madrid")
2023-07-27 10:52:20 -05:00
2023-07-31 12:03:45 -05:00
if not sender_token in self.KEY_CACHE:
logger.warning("Unable to find the public key of the sender, cannot verify")
return False
2023-07-27 10:52:20 -05:00
2023-08-13 22:24:34 -05:00
identity_keys = ids.identity.IDSIdentity.decode(
self.KEY_CACHE[sender_token]["com.apple.madrid"][0]
)
2023-07-27 10:52:20 -05:00
sender_ec_key = ids._helpers.parse_key(identity_keys.signing_public_key)
payload = iMessageUser._parse_payload(payload)
try:
# Verify the signature (will throw an exception if it fails)
sender_ec_key.verify(
payload[1],
payload[0],
ec.ECDSA(hashes.SHA1()),
)
return True
except:
return False
2023-08-14 08:40:33 -05:00
def receive(self) -> Message | None:
"""
Will return the next iMessage in the queue, or None if there are no messages
"""
2023-08-13 22:24:34 -05:00
2023-08-14 08:40:33 -05:00
# Check for iMessages
2023-08-13 22:24:34 -05:00
body = self._receive_raw(100, "com.apple.madrid")
2023-08-14 08:40:33 -05:00
t = iMessage
if body is None:
# Check for SMS messages
body = self._receive_raw(143, "com.apple.private.alloy.sms")
t = SMSReflectedMessage
2023-08-14 19:31:03 -05:00
if body is None:
# Check for SMS reflected images
body = self._receive_raw(144, "com.apple.private.alloy.sms")
t = SMSReflectedMessage
2023-08-14 08:40:33 -05:00
if body is None:
# Check for SMS incoming messages
body = self._receive_raw(140, "com.apple.private.alloy.sms")
t = SMSIncomingMessage
2023-08-14 19:31:03 -05:00
if body is None:
# Incoming images
body = self._receive_raw(141, "com.apple.private.alloy.sms")
t = SMSIncomingImage
2023-08-13 22:24:34 -05:00
if body is None:
return None
2023-08-14 08:40:33 -05:00
2023-07-31 12:03:45 -05:00
2023-08-14 08:40:33 -05:00
if not self._verify_payload(body["P"], body["sP"], body["t"]):
2023-07-31 12:03:45 -05:00
raise Exception("Failed to verify payload")
2023-08-13 22:24:34 -05:00
2023-08-12 06:41:41 -05:00
logger.debug(f"Encrypted body : {body}")
2023-08-13 22:24:34 -05:00
2023-08-14 08:40:33 -05:00
decrypted = self._decrypt_payload(body["P"])
2023-08-13 22:24:34 -05:00
2023-08-14 08:40:33 -05:00
return t.from_raw(decrypted, body["sP"])
2023-07-31 12:03:45 -05:00
2023-07-31 15:30:06 -05:00
KEY_CACHE_HANDLE: str = ""
2023-08-13 18:26:57 -05:00
KEY_CACHE: dict[bytes, dict[str, tuple[bytes, bytes]]] = {}
"""Mapping of push token : topic : (public key, session token)"""
2023-07-31 12:03:45 -05:00
USER_CACHE: dict[str, list[bytes]] = {}
"""Mapping of handle : [push tokens]"""
2023-08-13 18:26:57 -05:00
def _cache_keys(self, participants: list[str], topic: str):
2023-07-31 15:30:06 -05:00
# 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
self.KEY_CACHE = {}
self.USER_CACHE = {}
2023-08-13 22:24:34 -05:00
2023-07-31 12:03:45 -05:00
# 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
2023-07-29 12:57:20 -05:00
# Look up the public keys for the participants, and cache a token : public key mapping
2023-08-13 18:26:57 -05:00
lookup = self.user.lookup(participants, topic=topic)
2023-07-29 12:57:20 -05:00
2023-08-13 22:24:34 -05:00
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")
2023-07-29 12:57:20 -05:00
for key, participant in lookup.items():
if not key in self.USER_CACHE:
self.USER_CACHE[key] = []
2023-07-31 12:03:45 -05:00
for identity in participant["identities"]:
if not "client-data" in identity:
2023-07-29 12:57:20 -05:00
continue
2023-07-31 12:03:45 -05:00
if not "public-message-identity-key" in identity["client-data"]:
2023-07-29 12:57:20 -05:00
continue
2023-07-31 12:03:45 -05:00
if not "push-token" in identity:
2023-07-29 12:57:20 -05:00
continue
2023-07-31 12:03:45 -05:00
if not "session-token" in identity:
2023-07-29 16:14:25 -05:00
continue
2023-07-29 12:57:20 -05:00
2023-07-31 12:03:45 -05:00
self.USER_CACHE[key].append(identity["push-token"])
2023-07-29 12:57:20 -05:00
2023-07-31 12:03:45 -05:00
# print(identity)
2023-08-13 18:26:57 -05:00
if not identity["push-token"] in self.KEY_CACHE:
self.KEY_CACHE[identity["push-token"]] = {}
self.KEY_CACHE[identity["push-token"]][topic] = (
2023-07-31 12:03:45 -05:00
identity["client-data"]["public-message-identity-key"],
identity["session-token"],
)
2023-07-29 16:14:25 -05:00
2023-08-13 22:24:34 -05:00
def _send_raw(
self,
type: int,
participants: list[str],
topic: str,
payload: bytes | None = None,
id: uuid.UUID | None = None,
extra: dict = {},
):
2023-08-13 18:26:57 -05:00
self._cache_keys(participants, topic)
2023-07-31 12:03:45 -05:00
2023-08-13 22:24:34 -05:00
dtl = []
2023-08-13 18:26:57 -05:00
for participant in participants:
2023-07-29 12:57:20 -05:00
for push_token in self.USER_CACHE[participant]:
2023-07-31 12:57:50 -05:00
if push_token == self.connection.token:
2023-08-13 22:24:34 -05:00
continue # Don't send to ourselves
2023-07-31 12:57:50 -05:00
2023-07-31 12:03:45 -05:00
identity_keys = ids.identity.IDSIdentity.decode(
2023-08-13 18:26:57 -05:00
self.KEY_CACHE[push_token][topic][0]
2023-07-31 12:03:45 -05:00
)
2023-08-13 18:26:57 -05:00
p = {
"tP": participant,
2023-08-13 22:24:34 -05:00
"D": not participant == self.user.current_handle,
2023-08-13 18:26:57 -05:00
"sT": self.KEY_CACHE[push_token][topic][1],
"t": push_token,
}
if payload is not None:
2023-08-13 22:24:34 -05:00
p["P"] = self._encrypt_sign_payload(identity_keys, payload)
2023-08-13 18:26:57 -05:00
logger.debug(f"Encoded payload : {p}")
2023-08-13 22:24:34 -05:00
dtl.append(p)
2023-08-13 18:26:57 -05:00
2023-08-13 22:24:34 -05:00
message_id = random.randbytes(4)
2023-08-13 18:26:57 -05:00
2023-08-13 22:24:34 -05:00
if id is None:
id = uuid.uuid4()
2023-08-13 18:26:57 -05:00
body = {
2023-08-13 22:24:34 -05:00
"c": type,
2023-08-13 18:26:57 -05:00
"fcn": 1,
"v": 8,
2023-08-13 22:24:34 -05:00
"i": int.from_bytes(message_id, "big"),
"U": id.bytes,
"dtl": dtl,
2023-08-13 18:26:57 -05:00
"sP": self.user.current_handle,
}
2023-08-13 22:24:34 -05:00
body.update(extra)
2023-08-13 18:26:57 -05:00
body = plistlib.dumps(body, fmt=plistlib.FMT_BINARY)
2023-08-13 22:24:34 -05:00
self.connection.send_message(topic, body, message_id)
2023-08-13 18:26:57 -05:00
2023-08-14 08:40:33 -05:00
def _receive_raw(self, c: int | list[int], topic: str | list[str]) -> dict | None:
2023-08-13 22:24:34 -05:00
def check_response(x):
if x[0] != 0x0A:
return False
2023-08-14 08:40:33 -05:00
# 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
2023-08-13 22:24:34 -05:00
resp_body = apns._get_field(x[1], 3)
if resp_body is None:
return False
resp_body = plistlib.loads(resp_body)
2023-08-14 08:40:33 -05:00
2023-08-14 19:31:03 -05:00
#logger.info(f"See type {resp_body['c']}")
2023-08-14 08:40:33 -05:00
if isinstance(c, list):
if not resp_body["c"] in c:
return False
elif resp_body["c"] != c:
2023-08-13 22:24:34 -05:00
return False
return True
2023-08-13 18:26:57 -05:00
2023-08-13 22:24:34 -05:00
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
2023-08-13 18:26:57 -05:00
2023-08-13 22:24:34 -05:00
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
"""
2023-08-13 18:26:57 -05:00
2023-08-13 22:24:34 -05:00
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
}
)
2023-08-13 18:26:57 -05:00
2023-08-14 08:40:33 -05:00
def send(self, message: OldiMessage):
2023-08-13 18:26:57 -05:00
# Set the sender, if it isn't already
if message.sender is None:
message.sender = self.user.handles[0] # TODO : Which handle to use?
2023-08-13 22:24:34 -05:00
message.sanity_check() # Sanity check MUST be called before caching keys, so that the sender is added to the list of participants
2023-08-13 18:26:57 -05:00
self._cache_keys(message.participants, "com.apple.madrid")
2023-08-13 22:24:34 -05:00
self._send_raw(
100,
message.participants,
"com.apple.madrid",
message.to_raw(),
message.id,
{
"E": "pair",
}
)
2023-08-13 18:26:57 -05:00
2023-08-13 22:24:34 -05:00
# Check for delivery
count = 0
total = 0
2023-07-29 12:57:20 -05:00
2023-08-13 22:24:34 -05:00
import time
start = time.time()
2023-07-29 12:57:20 -05:00
2023-08-13 22:24:34 -05:00
for p in message.participants:
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")
if resp is None:
continue
count += 1
logger.debug(f"Received response : {resp}")
if resp["s"] != 0:
logger.warning(f"Message delivery to {base64.b64encode(resp['t']).decode()} failed : {resp['s']}")
2023-07-29 12:57:20 -05:00
2023-08-13 22:24:34 -05:00
if count < total:
logger.error(f"Unable to deliver message to all devices (got {count} of {total})")