Merge upstream

This commit is contained in:
Dadoum 2023-08-01 01:46:38 +02:00
commit 09d33c0d36
5 changed files with 121 additions and 17 deletions

4
.gitignore vendored
View file

@ -160,4 +160,6 @@ cython_debug/
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
.idea/
attachments/

33
README.md Normal file
View file

@ -0,0 +1,33 @@
# pypush
`pypush` is a POC demo of my recent iMessage reverse-engineering.
It can currently register as a new device on an Apple ID, set up encryption keys, and ***send and receive iMessages***!
`pypush` is completely platform-independent, and does not require a Mac or other Apple device to use!
## Installation
It's pretty self explanatory:
1. `git clone https://github.com/JJTech0130/pypush`
2. `pip3 install -r requirements.txt`
3. `python3 ./demo.py`
## Troubleshooting
If you have any issues, please join [the Discord](https://discord.gg/BVvNukmfTC) and ask for help.
## Operation
`pypush` will generate a `config.json` in the repository when you run demo.py. DO NOT SHARE THIS FILE.
It contains all the encryption keys necessary to log into you Apple ID and send iMessages as you.
Once it loads, it should prompt you with `>>`. Type `help` and press enter for a list of supported commands.
## Special Notes
### Unicorn dependency
`pypush` currently uses the Unicorn CPU emulator and a custom MachO loader to load a framework from an old version of macOS,
in order to call some obfuscated functions.
This is only necessary during initial registration, so theoretically you can register on one device, and then copy the `config.json`
to another device that doesn't support the Unicorn emulator. Or you could switch out the emulator for another x86 emulator if you really wanted to.
### Public key caching
iMessage will cache public keys. If you get decryption errors in pypush or can only send and not receive messages from another device,
try logging out and back into iMessage on that device, forcing it to refresh it's key cache. Alternatively, you can wait and the cache should
expire eventually.

66
demo.py
View file

@ -127,11 +127,33 @@ threading.Thread(target=input_thread, daemon=True).start()
print("Type 'help' for help")
def fixup_handle(handle):
if handle.startswith('tel:+'):
return handle
elif handle.startswith('mailto:'):
return 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
# 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
current_participants = []
current_effect = None
while True:
msg = im.receive()
if msg is not None:
print(f'[{msg.sender}] {msg.text}')
# print(f'[{msg.sender}] {msg.text}')
print(msg.to_string())
attachments = msg.attachments()
if len(attachments) > 0:
@ -142,7 +164,7 @@ while True:
with open(attachments_path + attachment.name, "wb") as attachment_file:
attachment_file.write(attachment.versions[0].data())
print(f"({len(attachments)} attachment{'s' if len(attachments) == 1 else ''} have been downloaded and put "
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:
@ -153,28 +175,60 @@ while True:
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 <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.startswith('filter ') or msg.startswith('f '):
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 '):
# Set the curernt chat
msg = msg.split(' ')
if len(msg) < 2 or msg[1] == '':
print('filter [recipients]')
else:
print(f'Filtering to {msg[1:]}')
current_participants = msg[1:]
print(f'Filtering to {[fixup_handle(h) for h in msg[1:]]}')
current_participants = [fixup_handle(h) for h in msg[1:]]
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)')
else:
print(f'\t{h}')
else:
h = msg[1]
h = fixup_handle(h)
if h in user.handles:
print(f'Using {h} as handle')
user.current_handle = h
else:
print(f'Handle {h} not found')
elif current_participants != []:
if msg.startswith('\\'):
msg = msg[1:]
im.send(imessage.iMessage(
text=msg,
participants=current_participants,
sender=user.handles[0]
sender=user.current_handle,
effect=current_effect
))
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(' ')

View file

@ -45,6 +45,8 @@ class IDSUser:
self._auth_keypair,
self._push_keypair,
)
self.current_handle = self.handles[0]
# Uses an existing authentication keypair
def restore_authentication(
@ -53,6 +55,7 @@ class IDSUser:
self._auth_keypair = auth_keypair
self.user_id = user_id
self.handles = handles
self.current_handle = self.handles[0]
# This is a separate call so that the user can make sure the first part succeeds before asking for validation data
def register(self, validation_data: str):
@ -79,5 +82,5 @@ class IDSUser:
self._id_keypair = id_keypair
def lookup(self, uris: list[str], topic: str = "com.apple.madrid") -> any:
return query.lookup(self.push_connection, self.handles[0], self._id_keypair, uris, topic)
return query.lookup(self.push_connection, self.current_handle, self._id_keypair, uris, topic)

View file

@ -57,9 +57,6 @@ class MMCSFile(AttachmentFile):
url=self.url,
headers={
"User-Agent": f"IMTransferAgent/900 CFNetwork/596.2.3 Darwin/12.2.0 (x86_64) (Macmini5,1)",
"x-apple-mmcs-proto-version": f"basic {base64.encodebytes(f'benjiebabioles@outlook.com:!Qwerty4!'.encode('utf-8'))}",
"x-apple-mmcs-dataclass": f"basic {base64.encodebytes(f'benjiebabioles@outlook.com:!Qwerty4!'.encode('utf-8'))}",
# "Authorization": f"basic {base64.encodebytes(f'benjiebabioles@outlook.com:!Qwerty4!'.encode('utf-8'))}",
# "MMCS-Url": self.url,
# "MMCS-Signature": str(base64.encodebytes(self.signature)),
# "MMCS-Owner": self.owner
@ -146,6 +143,8 @@ class iMessage:
"""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"""
@ -184,7 +183,7 @@ class iMessage:
return True
def from_raw(message: bytes) -> "iMessage":
def from_raw(message: bytes, sender: str | None = None) -> "iMessage":
"""Create an `iMessage` from raw message bytes"""
compressed = False
try:
@ -199,12 +198,11 @@ class iMessage:
text=message.get("t", ""),
xml=message.get("x"),
participants=message.get("p", []),
sender=message.get("p", [])[-1] if message.get("p", []) != [] 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,
)
@ -223,6 +221,7 @@ class iMessage:
"pv": 0,
"gv": "8",
"v": "1",
"iid": self.effect
}
# Remove keys that are None
@ -237,6 +236,12 @@ class iMessage:
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
class iMessageUser:
"""Represents a logged in and connected iMessage user.
@ -432,14 +437,21 @@ class iMessageUser:
decrypted = self._decrypt_payload(payload)
return iMessage.from_raw(decrypted)
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)"""
USER_CACHE: dict[str, list[bytes]] = {}
"""Mapping of handle : [push tokens]"""
def _cache_keys(self, participants: list[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
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