mirror of
https://github.com/Sneed-Group/pypush-plus-plus
synced 2024-12-23 03:15:15 -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
|
||||
# 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
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")
|
||||
|
||||
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(' ')
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
30
imessage.py
30
imessage.py
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue