mirror of
https://github.com/Sneed-Group/pypush-plus-plus
synced 2024-12-23 11:22:42 -06:00
Merge upstream
This commit is contained in:
commit
09d33c0d36
5 changed files with 121 additions and 17 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -160,4 +160,6 @@ cython_debug/
|
||||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
# 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
|
# 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.
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
#.idea/
|
.idea/
|
||||||
|
|
||||||
|
attachments/
|
33
README.md
Normal file
33
README.md
Normal 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
66
demo.py
|
@ -127,11 +127,33 @@ 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:+'):
|
||||||
|
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_participants = []
|
||||||
|
current_effect = None
|
||||||
while True:
|
while True:
|
||||||
msg = im.receive()
|
msg = im.receive()
|
||||||
if msg is not None:
|
if msg is not None:
|
||||||
print(f'[{msg.sender}] {msg.text}')
|
# print(f'[{msg.sender}] {msg.text}')
|
||||||
|
print(msg.to_string())
|
||||||
|
|
||||||
attachments = msg.attachments()
|
attachments = msg.attachments()
|
||||||
if len(attachments) > 0:
|
if len(attachments) > 0:
|
||||||
|
@ -142,7 +164,7 @@ while True:
|
||||||
with open(attachments_path + attachment.name, "wb") as attachment_file:
|
with open(attachments_path + attachment.name, "wb") as attachment_file:
|
||||||
attachment_file.write(attachment.versions[0].data())
|
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})")
|
f"in {attachments_path})")
|
||||||
|
|
||||||
if len(INPUT_QUEUE) > 0:
|
if len(INPUT_QUEUE) > 0:
|
||||||
|
@ -153,29 +175,61 @@ while True:
|
||||||
print('quit (q): quit')
|
print('quit (q): quit')
|
||||||
#print('send (s) [recipient] [message]: send a message')
|
#print('send (s) [recipient] [message]: send a message')
|
||||||
print('filter (f) [recipient]: set the current chat')
|
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('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)')
|
print('\\: escape commands (will be removed from message)')
|
||||||
elif msg == 'quit' or msg == 'q':
|
elif msg == 'quit' or msg == 'q':
|
||||||
break
|
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
|
# Set the curernt chat
|
||||||
msg = msg.split(' ')
|
msg = msg.split(' ')
|
||||||
if len(msg) < 2 or msg[1] == '':
|
if len(msg) < 2 or msg[1] == '':
|
||||||
print('filter [recipients]')
|
print('filter [recipients]')
|
||||||
else:
|
else:
|
||||||
print(f'Filtering to {msg[1:]}')
|
print(f'Filtering to {[fixup_handle(h) for h in msg[1:]]}')
|
||||||
current_participants = 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 != []:
|
elif current_participants != []:
|
||||||
if msg.startswith('\\'):
|
if msg.startswith('\\'):
|
||||||
msg = msg[1:]
|
msg = msg[1:]
|
||||||
im.send(imessage.iMessage(
|
im.send(imessage.iMessage(
|
||||||
text=msg,
|
text=msg,
|
||||||
participants=current_participants,
|
participants=current_participants,
|
||||||
sender=user.handles[0]
|
sender=user.current_handle,
|
||||||
|
effect=current_effect
|
||||||
))
|
))
|
||||||
|
current_effect = None
|
||||||
else:
|
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'):
|
# elif msg.startswith('send') or msg.startswith('s'):
|
||||||
# msg = msg.split(' ')
|
# msg = msg.split(' ')
|
||||||
# if len(msg) < 3:
|
# if len(msg) < 3:
|
||||||
|
|
|
@ -45,6 +45,8 @@ class IDSUser:
|
||||||
self._auth_keypair,
|
self._auth_keypair,
|
||||||
self._push_keypair,
|
self._push_keypair,
|
||||||
)
|
)
|
||||||
|
self.current_handle = self.handles[0]
|
||||||
|
|
||||||
|
|
||||||
# Uses an existing authentication keypair
|
# Uses an existing authentication keypair
|
||||||
def restore_authentication(
|
def restore_authentication(
|
||||||
|
@ -53,6 +55,7 @@ class IDSUser:
|
||||||
self._auth_keypair = auth_keypair
|
self._auth_keypair = auth_keypair
|
||||||
self.user_id = user_id
|
self.user_id = user_id
|
||||||
self.handles = handles
|
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
|
# 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):
|
def register(self, validation_data: str):
|
||||||
|
@ -79,5 +82,5 @@ class IDSUser:
|
||||||
self._id_keypair = id_keypair
|
self._id_keypair = id_keypair
|
||||||
|
|
||||||
def lookup(self, uris: list[str], topic: str = "com.apple.madrid") -> any:
|
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)
|
||||||
|
|
||||||
|
|
30
imessage.py
30
imessage.py
|
@ -57,9 +57,6 @@ class MMCSFile(AttachmentFile):
|
||||||
url=self.url,
|
url=self.url,
|
||||||
headers={
|
headers={
|
||||||
"User-Agent": f"IMTransferAgent/900 CFNetwork/596.2.3 Darwin/12.2.0 (x86_64) (Macmini5,1)",
|
"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-Url": self.url,
|
||||||
# "MMCS-Signature": str(base64.encodebytes(self.signature)),
|
# "MMCS-Signature": str(base64.encodebytes(self.signature)),
|
||||||
# "MMCS-Owner": self.owner
|
# "MMCS-Owner": self.owner
|
||||||
|
@ -146,6 +143,8 @@ class iMessage:
|
||||||
"""Group ID of the message, will be randomly generated if not provided"""
|
"""Group ID of the message, will be randomly generated if not provided"""
|
||||||
body: BalloonBody | None = None
|
body: BalloonBody | None = None
|
||||||
"""BalloonBody, may be None"""
|
"""BalloonBody, may be None"""
|
||||||
|
effect: str | None = None
|
||||||
|
"""iMessage effect sent with this message, may be None"""
|
||||||
|
|
||||||
_compressed: bool = True
|
_compressed: bool = True
|
||||||
"""Internal property representing whether the message should be compressed"""
|
"""Internal property representing whether the message should be compressed"""
|
||||||
|
@ -184,7 +183,7 @@ class iMessage:
|
||||||
|
|
||||||
return True
|
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"""
|
"""Create an `iMessage` from raw message bytes"""
|
||||||
compressed = False
|
compressed = False
|
||||||
try:
|
try:
|
||||||
|
@ -199,12 +198,11 @@ class iMessage:
|
||||||
text=message.get("t", ""),
|
text=message.get("t", ""),
|
||||||
xml=message.get("x"),
|
xml=message.get("x"),
|
||||||
participants=message.get("p", []),
|
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,
|
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,
|
group_id=uuid.UUID(message.get("gid")) if "gid" in message else None,
|
||||||
body=BalloonBody(message["bid"], message["b"])
|
body=BalloonBody(message["bid"], message["b"]) if "bid" in message and "b" in message else None,
|
||||||
if "bid" in message and "b" in message
|
effect=message["iid"] if "iid" in message else None,
|
||||||
else None,
|
|
||||||
_compressed=compressed,
|
_compressed=compressed,
|
||||||
_raw=message,
|
_raw=message,
|
||||||
)
|
)
|
||||||
|
@ -223,6 +221,7 @@ class iMessage:
|
||||||
"pv": 0,
|
"pv": 0,
|
||||||
"gv": "8",
|
"gv": "8",
|
||||||
"v": "1",
|
"v": "1",
|
||||||
|
"iid": self.effect
|
||||||
}
|
}
|
||||||
|
|
||||||
# Remove keys that are None
|
# Remove keys that are None
|
||||||
|
@ -237,6 +236,12 @@ class iMessage:
|
||||||
|
|
||||||
return d
|
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:
|
class iMessageUser:
|
||||||
"""Represents a logged in and connected iMessage user.
|
"""Represents a logged in and connected iMessage user.
|
||||||
|
@ -432,14 +437,21 @@ class iMessageUser:
|
||||||
|
|
||||||
decrypted = self._decrypt_payload(payload)
|
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]] = {}
|
KEY_CACHE: dict[bytes, tuple[bytes, bytes]] = {}
|
||||||
"""Mapping of push token : (public key, session token)"""
|
"""Mapping of push token : (public key, session token)"""
|
||||||
USER_CACHE: dict[str, list[bytes]] = {}
|
USER_CACHE: dict[str, list[bytes]] = {}
|
||||||
"""Mapping of handle : [push tokens]"""
|
"""Mapping of handle : [push tokens]"""
|
||||||
|
|
||||||
def _cache_keys(self, participants: list[str]):
|
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
|
# 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]):
|
if all([p in self.USER_CACHE for p in participants]):
|
||||||
return
|
return
|
||||||
|
|
Loading…
Reference in a new issue