#! /usr/bin/env python """ A Command Line Wrapper to allow easy use of iCloudPy for command line scripts, and related. """ # from builtins import input import argparse import pickle import sys from click import confirm from icloudpy import ICloudPyService, utils from icloudpy.exceptions import ICloudPyFailedLoginException DEVICE_ERROR = "Please use the --device switch to indicate which device to use." def create_pickled_data(idevice, filename): """ This helper will output the idevice to a pickled file named after the passed filename. This allows the data to be used without resorting to screen / pipe scrapping. """ pickle_file = open(filename, "wb") pickle.dump(idevice.content, pickle_file, protocol=pickle.HIGHEST_PROTOCOL) pickle_file.close() def main(args=None): """Main commandline entrypoint.""" if args is None: args = sys.argv[1:] parser = argparse.ArgumentParser(description="Find My iPhone CommandLine Tool") parser.add_argument( "--username", action="store", dest="username", default="", help="Apple ID to Use", ) parser.add_argument( "--password", action="store", dest="password", default="", help=( "Apple ID Password to Use; if unspecified, password will be " "fetched from the system keyring." ), ) parser.add_argument( "-n", "--non-interactive", action="store_false", dest="interactive", default=True, help="Disable interactive prompts.", ) parser.add_argument( "--delete-from-keyring", action="store_true", dest="delete_from_keyring", default=False, help="Delete stored password in system keyring for this username.", ) parser.add_argument( "--list", action="store_true", dest="list", default=False, help="Short Listings for Device(s) associated with account", ) parser.add_argument( "--llist", action="store_true", dest="longlist", default=False, help="Detailed Listings for Device(s) associated with account", ) parser.add_argument( "--locate", action="store_true", dest="locate", default=False, help="Retrieve Location for the iDevice (non-exclusive).", ) # Restrict actions to a specific devices UID / DID parser.add_argument( "--device", action="store", dest="device_id", default=False, help="Only effect this device", ) # Trigger Sound Alert parser.add_argument( "--sound", action="store_true", dest="sound", default=False, help="Play a sound on the device", ) # Trigger Message w/Sound Alert parser.add_argument( "--message", action="store", dest="message", default=False, help="Optional Text Message to display with a sound", ) # Trigger Message (without Sound) Alert parser.add_argument( "--silentmessage", action="store", dest="silentmessage", default=False, help="Optional Text Message to display with no sounds", ) # Lost Mode parser.add_argument( "--lostmode", action="store_true", dest="lostmode", default=False, help="Enable Lost mode for the device", ) parser.add_argument( "--lostphone", action="store", dest="lost_phone", default=False, help="Phone Number allowed to call when lost mode is enabled", ) parser.add_argument( "--lostpassword", action="store", dest="lost_password", default=False, help="Forcibly active this passcode on the idevice", ) parser.add_argument( "--lostmessage", action="store", dest="lost_message", default="", help="Forcibly display this message when activating lost mode.", ) # Output device data to an pickle file parser.add_argument( "--outputfile", action="store_true", dest="output_to_file", default="", help="Save device data to a file in the current directory.", ) # Path to session directory parser.add_argument( "--session-directory", action="store", dest="session_directory", default=None, help="Path to save session information", ) # Server region - global or china parser.add_argument( "--region", action="store", dest="region", default="global", help="Server region - global or china", ) command_line = parser.parse_args(args) username = command_line.username password = command_line.password session_directory = command_line.session_directory server_region = command_line.region if username and command_line.delete_from_keyring: utils.delete_password_in_keyring(username) failure_count = 0 while True: # Which password we use is determined by your username, so we # do need to check for this first and separately. if not username: parser.error("No username supplied") if not password: password = utils.get_password( username, interactive=command_line.interactive ) if not password: parser.error("No password supplied") try: api = ( ICloudPyService( apple_id=username.strip(), password=password.strip(), cookie_directory=session_directory, home_endpoint="https://www.icloud.com.cn", setup_endpoint="https://setup.icloud.com.cn/setup/ws/1", ) if server_region == "china" else ICloudPyService( apple_id=username.strip(), password=password.strip(), cookie_directory=session_directory, ) ) if ( not utils.password_exists_in_keyring(username) and command_line.interactive and confirm("Save password in keyring?") ): utils.store_password_in_keyring(username, password) if api.requires_2fa: # fmt: off print( "\nTwo-step authentication required.", "\nPlease enter validation code" ) # fmt: on code = input("(string) --> ") if not api.validate_2fa_code(code): print("Failed to verify verification code") sys.exit(1) print("") elif api.requires_2sa: # fmt: off print( "\nTwo-step authentication required.", "\nYour trusted devices are:" ) # fmt: on devices = api.trusted_devices for i, device in enumerate(devices): print( f' {i}: {device.get("deviceName", "SMS to " + device.get("phoneNumber"))}' ) print("\nWhich device would you like to use?") device = int(input("(number) --> ")) device = devices[device] if not api.send_verification_code(device): print("Failed to send verification code") sys.exit(1) print("\nPlease enter validation code") code = input("(string) --> ") if not api.validate_verification_code(device, code): print("Failed to verify verification code") sys.exit(1) print("") break except ICloudPyFailedLoginException as error: # If they have a stored password; we just used it and # it did not work; let's delete it if there is one. if utils.password_exists_in_keyring(username): utils.delete_password_in_keyring(username) message = f"Bad username or password for {username}" password = None failure_count += 1 if failure_count >= 3: raise RuntimeError(message) from error print(message, file=sys.stderr) for dev in api.devices: if not command_line.device_id or ( command_line.device_id.strip().lower() == dev.content["id"].strip().lower() ): # List device(s) if command_line.locate: dev.location() if command_line.output_to_file: create_pickled_data( dev, filename=(dev.content["name"].strip().lower() + ".fmip_snapshot"), ) contents = dev.content if command_line.longlist: print("-" * 30) print(contents["name"]) for key in contents: print(f"{key} - {contents[key]}") elif command_line.list: print("-" * 30) print(f"Name - {contents['name']}") print(f"Display Name - {contents['deviceDisplayName']}") print(f"Location - {contents['location']}") print(f"Battery Level - {contents['batteryLevel']}") print(f"Battery Status- {contents['batteryStatus']}") print(f"Device Class - {contents['deviceClass']}") print(f"Device Model - {contents['deviceModel']}") # Play a Sound on a device if command_line.sound: if command_line.device_id: dev.play_sound() else: raise RuntimeError( f"\n\n\t\tSounds can only be played on a singular device. {DEVICE_ERROR}\n\n" ) # Display a Message on the device if command_line.message: if command_line.device_id: dev.display_message( subject="A Message", message=command_line.message, sounds=True ) else: raise RuntimeError( f"Messages can only be played on a singular device. {DEVICE_ERROR}" ) # Display a Silent Message on the device if command_line.silentmessage: if command_line.device_id: dev.display_message( subject="A Silent Message", message=command_line.silentmessage, sounds=False, ) else: raise RuntimeError( f"Silent Messages can only be played on a singular device. {DEVICE_ERROR}" ) # Enable Lost mode if command_line.lostmode: if command_line.device_id: dev.lost_device( number=command_line.lost_phone.strip(), text=command_line.lost_message.strip(), newpasscode=command_line.lost_password.strip(), ) else: raise RuntimeError( f"Lost Mode can only be activated on a singular device. {DEVICE_ERROR}" ) sys.exit(0) if __name__ == "__main__": main()