2023-05-02 07:39:11 -05:00
import json
2023-07-26 06:50:48 -05:00
import logging
2023-07-31 18:38:28 -05:00
import os
2023-07-26 06:50:48 -05:00
import threading
import time
from base64 import b64decode , b64encode
2023-05-09 16:03:27 -05:00
from getpass import getpass
2023-10-23 10:05:02 -05:00
from cryptography import x509
2023-11-01 19:59:13 -05:00
import datetime
2023-05-02 07:39:11 -05:00
2023-07-23 17:55:13 -05:00
from rich . logging import RichHandler
2023-07-26 06:50:48 -05:00
import apns
import ids
2023-07-28 10:53:13 -05:00
import imessage
2023-07-26 06:50:48 -05:00
2023-08-19 10:27:44 -05:00
import trio
2023-08-27 08:27:24 -05:00
import argparse
2023-08-19 10:27:44 -05:00
2023-10-23 18:28:36 -05:00
from exceptions import *
2023-07-23 17:55:13 -05:00
logging . basicConfig (
2023-07-26 17:49:41 -05:00
level = logging . NOTSET , format = " %(message)s " , datefmt = " [ %X ] " , handlers = [ RichHandler ( ) ]
2023-07-23 17:55:13 -05:00
)
# Set sane log levels
logging . getLogger ( " urllib3 " ) . setLevel ( logging . WARNING )
2023-08-15 08:05:08 -05:00
logging . getLogger ( " py.warnings " ) . setLevel ( logging . ERROR ) # Ignore warnings from urllib3
2023-07-31 12:25:19 -05:00
logging . getLogger ( " asyncio " ) . setLevel ( logging . WARNING )
2023-07-23 17:55:13 -05:00
logging . getLogger ( " jelly " ) . setLevel ( logging . INFO )
logging . getLogger ( " nac " ) . setLevel ( logging . INFO )
2023-08-17 21:05:18 -05:00
logging . getLogger ( " apns " ) . setLevel ( logging . INFO )
2023-07-24 08:18:21 -05:00
logging . getLogger ( " albert " ) . setLevel ( logging . INFO )
2023-11-01 19:59:13 -05:00
logging . getLogger ( " ids " ) . setLevel ( logging . INFO )
2023-07-31 12:08:57 -05:00
logging . getLogger ( " bags " ) . setLevel ( logging . INFO )
2023-08-17 21:05:18 -05:00
logging . getLogger ( " imessage " ) . setLevel ( logging . INFO )
2023-05-02 19:53:18 -05:00
2023-08-09 11:38:09 -05:00
logging . captureWarnings ( True )
2023-05-09 16:03:27 -05:00
# Try and load config.json
try :
with open ( " config.json " , " r " ) as f :
CONFIG = json . load ( f )
except FileNotFoundError :
CONFIG = { }
2023-05-02 07:39:11 -05:00
2023-05-09 17:48:44 -05:00
def safe_b64decode ( s ) :
try :
return b64decode ( s )
except :
return None
2023-08-25 21:26:37 -05:00
def safe_config ( ) :
with open ( " config.json " , " w " ) as f :
json . dump ( CONFIG , f , indent = 4 )
2023-07-26 06:50:48 -05:00
2023-10-23 10:05:02 -05:00
def get_not_valid_after_timestamp ( cert_data ) :
try :
cert = x509 . load_pem_x509_certificate ( cert_data . encode ( ' utf-8 ' ) )
return cert . not_valid_after
except Exception as e :
return None # Return None in case of an error
2023-11-01 19:59:13 -05:00
expiration = None
2023-08-27 08:27:24 -05:00
async def main ( args : argparse . Namespace ) :
2023-11-01 19:59:13 -05:00
global expiration
2023-08-25 21:26:37 -05:00
# Load any existing push credentials
2023-08-17 20:14:44 -05:00
token = CONFIG . get ( " push " , { } ) . get ( " token " )
2023-08-25 21:26:37 -05:00
token = b64decode ( token ) if token is not None else b " "
2023-08-17 20:14:44 -05:00
2023-08-17 19:23:56 -05:00
push_creds = apns . PushCredentials (
2023-08-17 20:14:44 -05:00
CONFIG . get ( " push " , { } ) . get ( " key " , " " ) , CONFIG . get ( " push " , { } ) . get ( " cert " , " " ) , token )
2023-08-28 07:01:41 -05:00
def register ( conn , users ) :
import emulated . nac
vd = emulated . nac . generate_validation_data ( )
vd = b64encode ( vd ) . decode ( )
2023-10-23 10:05:02 -05:00
users = ids . register ( conn , users , vd , args . client_data or args . reg_notify )
2023-08-28 07:01:41 -05:00
return users
2023-11-01 19:59:13 -05:00
2023-11-16 15:55:08 -06:00
def expiration_identifier ( users : list [ ids . IDSUser ] ) - > datetime . datetime :
# Format time as HH:MM:SS PM/AM EST/EDT (X minutes from now)
expire_msg = lambda expiration : f " Number registration is valid until { str ( expiration . astimezone ( ) . strftime ( ' %x % I: % M: % S % p % Z ' ) ) } ( { str ( int ( ( expiration - datetime . datetime . now ( datetime . timezone . utc ) ) . total_seconds ( ) / 60 ) ) } minutes from now) "
for user in users :
# If this is a phone number user, then it's got to be the one we just linked
# so pull out the expiration date from the certificate
if " P: " in str ( user . user_id ) :
# There is not really a good reason to try/catch here: If we couldn't reregister, just crash (very unlikely we can recover)
cert = x509 . load_pem_x509_certificate ( user . id_cert . encode ( ' utf-8 ' ) )
expiration = cert . not_valid_after
# Make it a UTC aware timezone, for reasons
expiration = expiration . replace ( tzinfo = datetime . timezone . utc )
logging . info ( expire_msg ( expiration ) )
2023-11-01 19:59:13 -05:00
async def reregister ( conn : apns . APNSConnection , users : list [ ids . IDSUser ] ) - > datetime . datetime :
register ( conn , users )
CONFIG [ " users " ] = [ ]
expiration = None
2023-11-16 15:55:08 -06:00
2023-11-01 19:59:13 -05:00
email_user = None
email_addr = None # For HACK below
for user in users :
# Clear the config and re-save everything to match the new registration
CONFIG [ " users " ] . append ( {
" id " : user . user_id ,
" auth_key " : user . auth_keypair . key ,
" auth_cert " : user . auth_keypair . cert ,
" encryption_key " : user . encryption_identity . encryption_key if user . encryption_identity is not None else None ,
" signing_key " : user . encryption_identity . signing_key if user . encryption_identity is not None else None ,
" id_cert " : user . id_cert ,
" handles " : user . handles ,
} )
2023-11-16 15:55:08 -06:00
if not " P: " in str ( user . user_id ) :
2023-11-01 19:59:13 -05:00
email_user = user
for n in range ( len ( user . handles ) ) :
# HACK: Just pick the first email address they have to avoid picking the linked phone number
# TODO: Properly fix this, so that the linked phone number is not in the Apple ID user's list of handles
if " mailto: " in str ( user . handles [ n ] ) :
email_addr = user . handles [ n ]
2023-11-16 15:55:08 -06:00
expiration = expiration_identifier ( users )
2023-11-01 19:59:13 -05:00
# Save the config to disk
safe_config ( )
# Send the notification iMessage (if enabled)
if args . reg_notify :
im = imessage . iMessageUser ( conn , email_user )
im . current_handle = email_addr # HACK: See above
await im . send ( imessage . iMessage . create ( im , expire_msg ( expiration ) , [ email_addr ] ) )
return expiration
2023-08-17 19:23:56 -05:00
async with apns . APNSConnection . start ( push_creds ) as conn :
2023-08-25 21:26:37 -05:00
# Save the push credentials to the config
CONFIG [ " push " ] = {
" token " : b64encode ( conn . credentials . token ) . decode ( ) ,
" key " : conn . credentials . private_key ,
" cert " : conn . credentials . cert ,
}
safe_config ( )
# Activate the connection
2023-08-17 20:14:44 -05:00
await conn . set_state ( 1 )
await conn . filter ( [ " com.apple.madrid " ] )
2023-08-17 19:23:56 -05:00
2023-08-25 21:26:37 -05:00
# If the user wants a phone number, we need to register it WITH an Apple ID, then register the Apple ID again
# otherwise we encounter issues for some reason
users = [ ]
if " id " in CONFIG :
logging . debug ( " Restoring old-style identity... " )
users . append ( ids . IDSAppleUser ( conn , CONFIG [ " auth " ] [ " user_id " ] , ids . _helpers . KeyPair ( CONFIG [ " auth " ] [ " key " ] , CONFIG [ " auth " ] [ " cert " ] ) ,
ids . identity . IDSIdentity ( CONFIG [ " encryption " ] [ " ec_key " ] , CONFIG [ " encryption " ] [ " rsa_key " ] ) , CONFIG [ " id " ] [ " cert " ] ,
CONFIG [ " auth " ] [ " handles " ] ) )
if " users " in CONFIG :
logging . debug ( " Restoring new-style identity... " )
for user in CONFIG [ " users " ] :
users . append ( ids . IDSUser ( conn , user [ " id " ] , ids . _helpers . KeyPair ( user [ " auth_key " ] , user [ " auth_cert " ] ) ,
ids . identity . IDSIdentity ( user [ " signing_key " ] , user [ " encryption_key " ] ) , user [ " id_cert " ] ,
user [ " handles " ] ) )
2023-08-24 06:31:11 -05:00
else :
2023-08-25 21:26:37 -05:00
print ( " Would you like to register a phone number? (y/n) " )
if input ( " > " ) . lower ( ) == " y " :
2023-09-02 18:09:41 -05:00
import sms_registration
2023-10-23 18:28:36 -05:00
2023-08-25 21:26:37 -05:00
if " phone " in CONFIG :
phone_sig = b64decode ( CONFIG [ " phone " ] . get ( " sig " ) )
phone_number = CONFIG [ " phone " ] . get ( " number " )
2023-09-02 18:09:41 -05:00
elif args . pdu is not None :
2023-09-16 21:11:02 -05:00
phone_number , phone_sig = sms_registration . parse_pdu ( args . pdu , None )
2023-08-25 21:26:37 -05:00
else :
2023-11-01 19:59:13 -05:00
if args . phone is None :
#raise GatewayConnectionError("You did not supply an IP address.")
# Prompt for IP address
print ( " Please enter the IP address of your phone. " )
print ( " This should be displayed in the SMS registration helper app " )
print ( " You must be on the same network as your phone. " )
phone = input ( " > " )
else :
phone = args . phone
2023-08-25 21:26:37 -05:00
import sms_registration
2023-10-23 18:28:36 -05:00
phone_number , phone_sig = sms_registration . register ( push_token = conn . credentials . token ,
no_parse = args . trigger_pdu , gateway = args . gateway ,
2023-11-01 19:59:13 -05:00
phone_ip = phone )
2023-08-25 21:26:37 -05:00
CONFIG [ " phone " ] = {
" number " : phone_number ,
" sig " : b64encode ( phone_sig ) . decode ( ) ,
}
safe_config ( )
users . append ( ids . IDSPhoneUser . authenticate ( conn , phone_number , phone_sig ) )
print ( " Would you like sign in to your Apple ID (recommended)? (y/n) " )
if input ( " > " ) . lower ( ) == " y " :
username = input ( " Username: " )
password = input ( " Password: " )
users . append ( ids . IDSAppleUser . authenticate ( conn , username , password ) )
2023-08-24 06:31:11 -05:00
2023-11-01 19:59:13 -05:00
await reregister ( conn , users )
2023-08-24 06:31:11 -05:00
2023-11-01 19:59:13 -05:00
if args . daemon :
wait_time_minutes = 5 # this is in minutes. 5 recommended
2023-11-16 15:55:08 -06:00
if args . reregister :
expiration = await reregister ( conn , users )
else :
expiration = expiration_identifier ( users )
2023-11-01 19:59:13 -05:00
while True :
reregister_time = expiration - datetime . timedelta ( minutes = wait_time_minutes ) # wait_time_minutes before expiration
reregister_delta = ( reregister_time - datetime . datetime . now ( datetime . timezone . utc ) ) . total_seconds ( )
logging . info ( f " Reregistering in { int ( reregister_delta / 60 ) } minutes... " )
2023-11-16 15:55:08 -06:00
if ( reregister_delta > 0 ) :
await trio . sleep ( reregister_delta )
2023-10-23 10:05:02 -05:00
2023-11-01 19:59:13 -05:00
logging . info ( " Reregistering... " )
2023-11-06 16:08:36 -06:00
expiration = await reregister ( conn , users )
2023-11-01 19:59:13 -05:00
logging . info ( " Reregistered! " )
2023-11-16 15:55:08 -06:00
elif args . reregister :
2023-11-01 19:59:13 -05:00
await reregister ( conn , users )
2023-10-23 10:05:02 -05:00
2023-10-23 18:28:36 -05:00
print ( " Done! " )
2023-08-27 08:27:24 -05:00
2023-08-28 07:13:20 -05:00
if args . alive :
logging . getLogger ( " apns " ) . setLevel ( logging . DEBUG )
while True :
await trio . sleep ( 20 )
2023-08-24 06:31:11 -05:00
2023-08-19 10:27:44 -05:00
async def input_task ( im : imessage . iMessageUser ) :
while True :
cmd = await trio . to_thread . run_sync ( input , " > " , cancellable = True )
if cmd != " " :
2023-08-25 16:41:55 -05:00
await im . send ( imessage . iMessage . create ( im , cmd , [ " tel:+16106632676 " ] ) )
2023-08-19 10:27:44 -05:00
async def output_task ( im : imessage . iMessageUser ) :
while True :
msg = await im . receive ( )
print ( str ( msg ) )
2023-08-17 20:14:44 -05:00
if __name__ == " __main__ " :
2023-08-27 08:27:24 -05:00
parser = argparse . ArgumentParser ( )
parser . add_argument ( " --reregister " , action = " store_true " , help = " Force re-registration " )
2023-10-23 10:05:02 -05:00
parser . add_argument ( " --reg-notify " , action = " store_true " , help = " Get iMessage after each registration " )
2023-08-28 07:13:20 -05:00
parser . add_argument ( " --alive " , action = " store_true " , help = " Keep the connection alive " )
2023-08-28 07:01:41 -05:00
parser . add_argument ( " --client-data " , action = " store_true " , help = " Publish client data (only necessary for actually sending/receiving messages) " )
2023-09-02 18:09:41 -05:00
parser . add_argument ( " --trigger-pdu " , action = " store_true " , help = " Trigger a REG-REQ " )
# String arg to override pdu
parser . add_argument ( " --pdu " , type = str , help = " Override the PDU REG-RESP " )
parser . add_argument ( " --phone " , type = str , help = " Override the phone IP " )
parser . add_argument ( " --gateway " , type = str , help = " Override the gateway phone number " )
2023-11-01 19:59:13 -05:00
parser . add_argument ( " --daemon " , action = " store_true " , help = " Continuously reregister 5 minutes before the certificate expires " )
2023-09-02 18:09:41 -05:00
2023-08-27 08:27:24 -05:00
args = parser . parse_args ( )
2023-09-02 18:20:46 -05:00
2023-09-02 18:22:44 -05:00
if args . pdu is not None and not args . pdu . startswith ( " REG-RESP " ) :
2023-10-23 18:28:36 -05:00
raise InvalidResponseError ( " Received invalid REG-RESP PDU from Gateway Client. " )
2023-09-02 18:20:46 -05:00
2023-10-13 17:24:39 -05:00
trio . run ( main , args )