Files
Nugget/devicemanagement/device_manager.py
2025-03-30 17:17:24 -04:00

461 lines
22 KiB
Python

import traceback
import plistlib
from tempfile import TemporaryDirectory
from PySide6.QtWidgets import QMessageBox
from PySide6.QtCore import QSettings, QThread
from pymobiledevice3 import usbmux
from pymobiledevice3.lockdown import create_using_usbmux
from pymobiledevice3.exceptions import MuxException, PasswordRequiredError
from devicemanagement.constants import Device, Version
from devicemanagement.data_singleton import DataSingleton
from gui.apply_worker import ApplyAlertMessage
from tweaks.tweaks import tweaks, FeatureFlagTweak, EligibilityTweak, AITweak, BasicPlistTweak, AdvancedPlistTweak, RdarFixTweak, NullifyFileTweak
from tweaks.custom_gestalt_tweaks import CustomGestaltTweaks
from tweaks.posterboard_tweak import PosterboardTweak
from tweaks.basic_plist_locations import FileLocationsList, RiskyFileLocationsList
from Sparserestore.restore import restore_files, FileToRestore
def show_error_msg(txt: str, title: str = "Error!", icon = QMessageBox.Critical, detailed_txt: str = None):
detailsBox = QMessageBox()
detailsBox.setIcon(icon)
detailsBox.setWindowTitle(title)
detailsBox.setText(txt)
if detailed_txt != None:
detailsBox.setDetailedText(detailed_txt)
detailsBox.exec()
def show_apply_error(e: Exception, update_label=lambda x: None):
print(traceback.format_exc())
update_label("Failed to restore")
if "Find My" in str(e):
return ApplyAlertMessage("Find My must be disabled in order to use this tool.",
detailed_txt="Disable Find My from Settings (Settings -> [Your Name] -> Find My) and then try again.")
elif "Encrypted Backup MDM" in str(e):
return ApplyAlertMessage("Nugget cannot be used on this device. Click Show Details for more info.",
detailed_txt="Your device is managed and MDM backup encryption is on. This must be turned off in order for Nugget to work. Please do not use Nugget on your school/work device!")
elif "SessionInactive" in str(e):
return ApplyAlertMessage("The session was terminated. Refresh the device list and try again.")
elif "PasswordRequiredError" in str(e):
return ApplyAlertMessage("Device is password protected! You must trust the computer on your device.",
detailed_txt="Unlock your device. On the popup, click \"Trust\", enter your password, then try again.")
else:
return ApplyAlertMessage(type(e).__name__ + ": " + repr(e), detailed_txt=str(traceback.format_exc()))
class DeviceManager:
## Class Functions
def __init__(self):
self.devices: list[Device] = []
self.data_singleton = DataSingleton()
self.current_device_index = 0
# preferences
# TODO: Move to its own class
self.apply_over_wifi = False
self.auto_reboot = True
self.allow_risky_tweaks = False
self.show_all_spoofable_models = False
self.skip_setup = True
self.supervised = False
self.organization_name = ""
def get_devices(self, settings: QSettings):
self.devices.clear()
# handle errors when failing to get connected devices
try:
connected_devices = usbmux.list_devices()
except:
show_error_msg(
"""
Failed to get device list. Click \"Show Details\" for the traceback.
If you are on Windows, make sure you have the \"Apple Devices\" app from the Microsoft Store or iTunes from Apple's website.
If you are on Linux, make sure you have usbmuxd and libimobiledevice installed.
""", detailed_txt=str(traceback.format_exc())
)
self.set_current_device(index=None)
return
# Connect via usbmuxd
for device in connected_devices:
if self.apply_over_wifi or device.is_usb:
try:
ld = create_using_usbmux(serial=device.serial)
vals = ld.all_values
model = vals['ProductType']
hardware = vals['HardwareModel']
cpu = vals['HardwarePlatform']
try:
product_type = settings.value(device.serial + "_model", "", type=str)
hardware_type = settings.value(device.serial + "_hardware", "", type=str)
cpu_type = settings.value(device.serial + "_cpu", "", type=str)
if product_type == "":
# save the new product type
settings.setValue(device.serial + "_model", model)
else:
model = product_type
if hardware_type == "":
# save the new hardware model
settings.setValue(device.serial + "_hardware", hardware)
else:
hardware = hardware_type
if cpu_type == "":
# save the new cpu model
settings.setValue(device.serial + "_cpu", cpu)
else:
cpu = cpu_type
except:
pass
dev = Device(
uuid=device.serial,
usb=device.is_usb,
name=vals['DeviceName'],
version=vals['ProductVersion'],
build=vals['BuildVersion'],
model=model,
hardware=hardware,
cpu=cpu,
locale=ld.locale,
ld=ld
)
tweaks["RdarFix"].get_rdar_mode(model)
self.devices.append(dev)
except MuxException as e:
# there is probably a cable issue
print(f"MUX ERROR with lockdown device with UUID {device.serial}")
show_error_msg("MuxException: " + repr(e) + "\n\nIf you keep receiving this error, try using a different cable or port.",
detailed_txt=str(traceback.format_exc()))
except Exception as e:
print(f"ERROR with lockdown device with UUID {device.serial}")
show_error_msg(type(e).__name__ + ": " + repr(e), detailed_txt=str(traceback.format_exc()))
if len(self.devices) > 0:
self.set_current_device(index=0)
else:
self.set_current_device(index=None)
## CURRENT DEVICE
def set_current_device(self, index: int = None):
if index == None or len(self.devices) == 0:
self.data_singleton.current_device = None
self.data_singleton.device_available = False
self.data_singleton.gestalt_path = None
self.current_device_index = 0
tweaks["SpoofModel"].value[0] = "Placeholder"
tweaks["SpoofHardware"].value[0] = "Placeholder"
tweaks["SpoofCPU"].value[0] = "Placeholder"
else:
self.data_singleton.current_device = self.devices[index]
if Version(self.devices[index].version) < Version("17.0"):
self.data_singleton.device_available = False
self.data_singleton.gestalt_path = None
else:
self.data_singleton.device_available = True
tweaks["SpoofModel"].value[0] = self.data_singleton.current_device.model
tweaks["SpoofHardware"].value[0] = self.data_singleton.current_device.hardware
tweaks["SpoofCPU"].value[0] = self.data_singleton.current_device.cpu
self.current_device_index = index
def get_current_device_name(self) -> str:
if self.data_singleton.current_device == None:
return "No Device"
else:
return self.data_singleton.current_device.name
def get_current_device_version(self) -> str:
if self.data_singleton.current_device == None:
return ""
else:
return self.data_singleton.current_device.version
def get_current_device_build(self) -> str:
if self.data_singleton.current_device == None:
return ""
else:
return self.data_singleton.current_device.build
def get_current_device_uuid(self) -> str:
if self.data_singleton.current_device == None:
return ""
else:
return self.data_singleton.current_device.uuid
def get_current_device_model(self) -> str:
if self.data_singleton.current_device == None:
return ""
else:
return self.data_singleton.current_device.model
def get_current_device_supported(self) -> bool:
if self.data_singleton.current_device == None:
return False
else:
return self.data_singleton.current_device.supported()
def get_current_device_patched(self) -> bool:
if self.data_singleton.current_device == None:
return True
else:
return self.data_singleton.current_device.is_exploit_fully_patched()
def reset_device_pairing(self):
# first, unpair it
if self.data_singleton.current_device == None:
return
self.data_singleton.current_device.ld.unpair()
# next, pair it again
self.data_singleton.current_device.ld.pair()
QMessageBox.information(None, "Pairing Reset", "Your device's pairing was successfully reset. Refresh the device list before applying.")
def add_skip_setup(self, files_to_restore: list[FileToRestore], restoring_domains: bool):
if self.skip_setup and (not self.get_current_device_supported() or restoring_domains):
# add the 2 skip setup files
cloud_config_plist: dict = {
"SkipSetup": ["WiFi", "Location", "Restore", "SIMSetup", "Android", "AppleID", "IntendedUser", "TOS", "Siri", "ScreenTime", "Diagnostics", "SoftwareUpdate", "Passcode", "Biometric", "Payment", "Zoom", "DisplayTone", "MessagingActivationUsingPhoneNumber", "HomeButtonSensitivity", "CloudStorage", "ScreenSaver", "TapToSetup", "Keyboard", "PreferredLanguage", "SpokenLanguage", "WatchMigration", "OnBoarding", "TVProviderSignIn", "TVHomeScreenSync", "Privacy", "TVRoom", "iMessageAndFaceTime", "AppStore", "Safety", "Multitasking", "ActionButton", "TermsOfAddress", "AccessibilityAppearance", "Welcome", "Appearance", "RestoreCompleted", "UpdateCompleted"],
"AllowPairing": True,
"ConfigurationWasApplied": True,
"CloudConfigurationUIComplete": True,
"ConfigurationSource": 0,
"PostSetupProfileWasInstalled": True,
"IsSupervised": False,
}
if self.supervised == True:
cloud_config_plist["IsSupervised"] = True
cloud_config_plist["OrganizationName"] = self.organization_name
files_to_restore.append(FileToRestore(
contents=plistlib.dumps(cloud_config_plist),
restore_path="Library/ConfigurationProfiles/CloudConfigurationDetails.plist",
domain="SysSharedContainerDomain-systemgroup.com.apple.configurationprofiles"
))
purplebuddy_plist: dict = {
"SetupDone": True,
"SetupFinishedAllSteps": True,
"UserChoseLanguage": True
}
files_to_restore.append(FileToRestore(
contents=plistlib.dumps(purplebuddy_plist),
restore_path="mobile/com.apple.purplebuddy.plist",
domain="ManagedPreferencesDomain"
))
def get_domain_for_path(self, path: str, owner: int = 501) -> str:
# returns Domain: str?, Path: str
if self.get_current_device_supported() and not path.startswith("/var/mobile/") and not owner == 0:
# don't do anything on sparserestore versions
return path, ""
fully_patched = self.get_current_device_patched()
# just make the Sys Containers to use the regular way (won't work for mga)
sysSharedContainer = "SysSharedContainerDomain-"
sysContainer = "SysContainerDomain-"
if not fully_patched:
sysSharedContainer += "."
sysContainer += "."
mappings: dict = {
"/var/Managed Preferences/": "ManagedPreferencesDomain",
"/var/root/": "RootDomain",
"/var/preferences/": "SystemPreferencesDomain",
"/var/MobileDevice/": "MobileDeviceDomain",
"/var/mobile/": "HomeDomain",
"/var/db/": "DatabaseDomain",
"/var/containers/Shared/SystemGroup/": sysSharedContainer,
"/var/containers/Data/SystemGroup/": sysContainer
}
for mapping in mappings.keys():
if path.startswith(mapping):
new_path = path.replace(mapping, "")
new_domain = mappings[mapping]
# if patched, include the next part of the path in the domain
if fully_patched and (new_domain == sysSharedContainer or new_domain == sysContainer):
parts = new_path.split("/")
new_domain += parts[0]
new_path = new_path.replace(parts[0] + "/", "")
return new_path, new_domain
return path, ""
def concat_file(self, contents: str, path: str, files_to_restore: list[FileToRestore], owner: int = 501, group: int = 501):
# TODO: try using inodes here instead
file_path, domain = self.get_domain_for_path(path, owner=owner)
files_to_restore.append(FileToRestore(
contents=contents,
restore_path=file_path,
domain=domain,
owner=owner, group=group
))
## APPLYING OR REMOVING TWEAKS AND RESTORING
def apply_changes(self, resetting: bool = False, update_label=lambda x: None, show_alert=lambda x: None):
try:
# set the tweaks and apply
# first open the file in read mode
update_label("Applying changes to files...")
gestalt_plist = None
if self.data_singleton.gestalt_path != None:
with open(self.data_singleton.gestalt_path, 'rb') as in_fp:
gestalt_plist = plistlib.load(in_fp)
# create the other plists
flag_plist: dict = {}
eligibility_files = None
ai_file = None
basic_plists: dict = {}
basic_plists_ownership: dict = {}
files_data: dict = {}
uses_domains: bool = False
# create the restore file list
files_to_restore: dict[FileToRestore] = [
]
tmp_pb_dir = None # temporary directory for unzipping pb files
# set the plist keys
if not resetting:
for tweak_name in tweaks:
tweak = tweaks[tweak_name]
if isinstance(tweak, FeatureFlagTweak):
flag_plist = tweak.apply_tweak(flag_plist)
elif isinstance(tweak, EligibilityTweak):
eligibility_files = tweak.apply_tweak()
elif isinstance(tweak, AITweak):
ai_file = tweak.apply_tweak()
elif isinstance(tweak, BasicPlistTweak) or isinstance(tweak, RdarFixTweak) or isinstance(tweak, AdvancedPlistTweak):
basic_plists = tweak.apply_tweak(basic_plists, self.allow_risky_tweaks)
basic_plists_ownership[tweak.file_location] = tweak.owner
if tweak.enabled and tweak.owner == 0:
uses_domains = True
elif isinstance(tweak, NullifyFileTweak):
tweak.apply_tweak(files_data)
if tweak.enabled and tweak.file_location.value.startswith("/var/mobile/"):
uses_domains = True
elif isinstance(tweak, PosterboardTweak):
tmp_pb_dir = TemporaryDirectory()
tweak.apply_tweak(
files_to_restore=files_to_restore, output_dir=tmp_pb_dir.name,
version=self.get_current_device_version(), update_label=update_label
)
if tweak.enabled:
uses_domains = True
else:
if gestalt_plist != None:
gestalt_plist = tweak.apply_tweak(gestalt_plist)
elif tweak.enabled:
# no mobilegestalt file provided but applying mga tweaks, give warning
show_alert(show_error_msg("No mobilegestalt file provided! Please select your file to apply mobilegestalt tweaks.", exec=False))
update_label("Failed.")
return
# set the custom gestalt keys
if gestalt_plist != None:
gestalt_plist = CustomGestaltTweaks.apply_tweaks(gestalt_plist)
gestalt_data = None
if resetting:
gestalt_data = b""
elif gestalt_plist != None:
gestalt_data = plistlib.dumps(gestalt_plist)
# Generate backup
update_label("Generating backup...")
self.concat_file(
contents=plistlib.dumps(flag_plist),
path="/var/preferences/FeatureFlags/Global.plist",
files_to_restore=files_to_restore
)
self.add_skip_setup(files_to_restore, uses_domains)
if gestalt_data != None:
self.concat_file(
contents=gestalt_data,
path="/var/containers/Shared/SystemGroup/systemgroup.com.apple.mobilegestaltcache/Library/Caches/com.apple.MobileGestalt.plist",
files_to_restore=files_to_restore
)
if eligibility_files:
new_eligibility_files: dict[FileToRestore] = []
if not self.get_current_device_supported():
# update the files
for file in eligibility_files:
self.concat_file(
contents=file.contents,
path=file.restore_path,
files_to_restore=new_eligibility_files
)
else:
new_eligibility_files = eligibility_files
files_to_restore += new_eligibility_files
if ai_file != None:
self.concat_file(
contents=ai_file.contents,
path=ai_file.restore_path,
files_to_restore=files_to_restore
)
for location, plist in basic_plists.items():
ownership = basic_plists_ownership[location]
self.concat_file(
contents=plistlib.dumps(plist),
path=location.value,
files_to_restore=files_to_restore,
owner=ownership, group=ownership
)
for location, data in files_data.items():
self.concat_file(
contents=data,
path=location.value,
files_to_restore=files_to_restore,
owner=ownership, group=ownership
)
# reset basic tweaks
if resetting:
empty_data = plistlib.dumps({})
for location in FileLocationsList:
self.concat_file(
contents=empty_data,
path=location.value,
files_to_restore=files_to_restore
)
if self.allow_risky_tweaks:
for location in RiskyFileLocationsList:
self.concat_file(
contents=empty_data,
path=location.value,
files_to_restore=files_to_restore
)
# restore to the device
update_label("Restoring to device...")
restore_files(files=files_to_restore, reboot=self.auto_reboot, lockdown_client=self.data_singleton.current_device.ld)
if tmp_pb_dir != None:
tmp_pb_dir.cleanup()
msg = "Your device will now restart."
if not self.auto_reboot:
msg = "Please restart your device to see changes."
show_alert(ApplyAlertMessage(txt="All done! " + msg, title="Success!", icon=QMessageBox.Information))
update_label("Success!")
except Exception as e:
if tmp_pb_dir != None:
tmp_pb_dir.cleanup()
show_alert(show_apply_error(e, update_label))
## RESETTING MOBILE GESTALT
def reset_mobilegestalt(self, settings: QSettings, update_label=lambda x: None):
# restore to the device
update_label("Restoring to device...")
try:
# remove the saved device model, hardware, and cpu
settings.setValue(self.data_singleton.current_device.uuid + "_model", "")
settings.setValue(self.data_singleton.current_device.uuid + "_hardware", "")
settings.setValue(self.data_singleton.current_device.uuid + "_cpu", "")
file_path, domain = self.get_domain_for_path(
"/var/containers/Shared/SystemGroup/systemgroup.com.apple.mobilegestaltcache/Library/Caches/com.apple.MobileGestalt.plist"
)
restore_files(files=[FileToRestore(
contents=b"",
restore_path=file_path,
domain=domain
)], reboot=self.auto_reboot, lockdown_client=self.data_singleton.current_device.ld)
msg = "Your device will now restart."
if not self.auto_reboot:
msg = "Please restart your device to see changes."
QMessageBox.information(None, "Success!", "All done! " + msg)
update_label("Success!")
except Exception as e:
show_error_msg(str(e))