├── .gitattributes ├── .gitignore ├── 0-Shortcuts ├── Credits.txt └── Save MobileGestalt.shortcut ├── LICENSE ├── README.md ├── compile.py ├── controllers ├── __init__.py ├── aar │ ├── LICENSE │ ├── __init__.py │ └── aar.py ├── files_handler.py ├── path_handler.py ├── plist_handler.py ├── video_handler.py ├── web_request_handler.py └── xml_handler.py ├── credits ├── LeminLimez.png ├── big_nugget.png └── small_nugget.png ├── devicemanagement ├── __init__.py ├── constants.py ├── data_singleton.py ├── device_manager.py └── generate_mga.py ├── documentation.md ├── entitlements.plist ├── exceptions ├── __init__.py ├── nugget_exception.py └── posterboard_exceptions.py ├── files ├── SSLconf │ └── TrustStore.sqlite3 ├── eligibility │ ├── Config.plist │ └── eligibility.plist └── posterboard │ ├── 1F20C883-EA98-4CCE-9923-0C9A01359721 │ ├── com.apple.posterkit.provider.descriptor.identifier │ ├── providerInfo.plist │ └── versions │ │ └── 0 │ │ └── contents │ │ ├── .com.apple.posterkit.provider.contents.configurableOptions.plist │ │ ├── 0EFB6A0F-7052-4D24-8859-AB22BADF2E93 │ │ ├── input.segmentation │ │ │ └── asset.resource │ │ │ │ └── contents.plist │ │ ├── output.layerStack │ │ │ └── Contents.json │ │ └── style.plist │ │ ├── DescriptorModel.plist │ │ ├── com.apple.posterkit.provider.contents.galleryOptions │ │ └── com.apple.posterkit.provider.contents.userInfo │ ├── VideoCAML │ ├── com.apple.posterkit.provider.descriptor.identifier │ ├── com.apple.posterkit.role.identifier │ ├── providerInfo.plist │ └── versions │ │ └── 1 │ │ └── contents │ │ ├── 9183.Custom-810w-1080h@2x~ipad.wallpaper │ │ ├── 9183.Custom_Floating-810w-1080h@2x~ipad.ca │ │ │ ├── index.xml │ │ │ └── main.caml │ │ └── Wallpaper.plist │ │ └── com.apple.posterkit.provider.contents.userInfo │ └── contents.plist ├── gui ├── __init__.py ├── apply_worker.py ├── custom_qt_elements │ ├── multicombobox.py │ └── resizable_image_label.py ├── dialogs.py ├── main_window.py ├── pages │ ├── __init__.py │ ├── main │ │ ├── home.py │ │ └── settings.py │ ├── page.py │ └── tools │ │ ├── daemons.py │ │ ├── eligibility.py │ │ ├── featureflags.py │ │ ├── gestalt.py │ │ ├── internal.py │ │ ├── posterboard.py │ │ ├── risky.py │ │ └── springboard.py ├── pb_tutorial1.png ├── pb_tutorial2.png └── transparent.png ├── icon ├── app-indicator.svg ├── arrow-clockwise.svg ├── arrow.clockwise.svg ├── brush.svg ├── caret-down-fill.svg ├── check-circle.svg ├── chevron.down.svg ├── chevron.up.svg ├── compass.svg ├── currency-dollar.svg ├── discord.svg ├── export.svg ├── file-earmark-zip.svg ├── flag.svg ├── folder.svg ├── gear.svg ├── geo-alt.svg ├── github.svg ├── globe.svg ├── hdd.svg ├── heart-fill.svg ├── house.svg ├── import.svg ├── iphone-island.svg ├── pencil.svg ├── phone.svg ├── photo-stack.svg ├── photo.svg ├── plus.svg ├── questionmark.circle.svg ├── shippingbox.svg ├── star.svg ├── toggles.svg ├── trash.svg ├── twitter.svg ├── wallpaper.svg ├── wifi.svg └── x-lg.svg ├── main_app.py ├── mainwindow_ui.py ├── nugget.ico ├── qt ├── mainwindow.ui ├── mainwindow_ui.py └── ui_mainwindow.py ├── requirements.txt ├── resources.qrc ├── resources_rc.py ├── restore ├── __init__.py ├── backup.py ├── mbdb.py └── restore.py ├── tweaks ├── __init__.py ├── basic_plist_locations.py ├── custom_gestalt_tweaks.py ├── daemons_tweak.py ├── eligibility_tweak.py ├── posterboard │ ├── posterboard_tweak.py │ ├── template_file.py │ ├── template_options │ │ ├── __init__.py │ │ ├── picker_option.py │ │ ├── remove_option.py │ │ ├── replace_option.py │ │ ├── set_option.py │ │ └── template_options.py │ └── tendie_file.py ├── tweak_classes.py └── tweaks.py └── version.txt /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | build 3 | com.apple.MobileGestalt.plist 4 | .DS_Store 5 | __pycache__ 6 | *.spec 7 | /dist 8 | -------------------------------------------------------------------------------- /0-Shortcuts/Credits.txt: -------------------------------------------------------------------------------- 1 | [LeminLimez](https://github.com/leminlimez) for [Nugget](https://github.com/leminlimez/Nugget) -------------------------------------------------------------------------------- /0-Shortcuts/Save MobileGestalt.shortcut: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LoveJessyChen/Nugget-iOS/eb917fe4a75ef61eca7e098b7ad2afcc736a2515/0-Shortcuts/Save MobileGestalt.shortcut -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Artboard][NuggetLogo] 2 | 3 | # Nugget 4 | Unlock your device's full potential! 5 | 6 | Customize your device with animated wallpapers, disable pesky daemons, and more! 7 | 8 | Make sure you have installed the [requirements](#requirements) if you are on Windows or Linux. 9 | 10 | > [!NOTE] 11 | > Please back up your data before using! Nugget may cause unforseen problems, so it is better to be same than sorry. We are not responsible for any damange done to your device. 12 | 13 | ## Features 14 |
15 | iOS 17.0+ 16 | 17 | - PosterBoard: Animated wallpapers and descriptors. 18 | - Community wallpapers can be found [here][WallpapersWebsite] 19 | - Converting videos to wallpapers 20 | - Customizing community-made wallpapers via batter files 21 | - See documentation on the structure of tendies and batter files in [documentation.md](documentation.md) 22 | - Springboard Options (from [Cowabunga Lite][CowabungaLite]) 23 | - Set Lock Screen Footnote 24 | - Disable Lock After Respring 25 | - Disable Screen Dimming While Charging 26 | - Disable Low Battery Alerts 27 | - Internal Options (from [Cowabunga Lite][CowabungaLite]) 28 | - Build Version in Status Bar 29 | - Force Right to Left 30 | - Force Metal HUD Debug 31 | - iMessage Diagnostics 32 | - IDS Diagnostics 33 | - VC Diagnostics 34 | - App Store Debug Gesture 35 | - Notes App Debug Mode 36 | - Disable Daemons: 37 | - OTAd 38 | - UsageTrackingAgent 39 | - Game Center 40 | - Screen Time Agent 41 | - Logs, Dumps, and Crash Reports 42 | - ATWAKEUP 43 | - Tipsd 44 | - VPN 45 | - Chinese WLAN service 46 | - HealthKit 47 | - AirPrint 48 | - Assistive Touch 49 | - iCloud 50 | - Internet Tethering (aka Personal Hotspot) 51 | - PassBook 52 | - Spotlight 53 | - Voice Control 54 | - Risky (Hidden) Options: 55 | - Disable thermalmonitord 56 | - OTA Killer 57 | - Custom Resolution 58 |
59 |
60 | iOS 17.0 - 18.1.1 61 | 62 | - Enable Dynamic Island on any device 63 | - Enable iPhone X gestures on iPhone SEs 64 | - Change Device Model Name (ie what shows in the Settings app) 65 | - Enable Boot Chime 66 | - Enable Charge Limit 67 | - Enable Tap to Wake on unsupported devices (ie iPhone SEs) 68 | - Enable Collision SOS 69 | - Enable Stage Manager 70 | - Disable the Wallpaper Parallax 71 | - Disable Region Restrictions (ie. Shutter Sound) 72 | - Note: This does not include enabling EU sideloading outside the EU. That will come later. 73 | - Show the Apple Pencil options in Settings app 74 | - Show the Action Button options in Settings app 75 | - Show Internal Storage info (Might cause problems on some devices, use at your own risk) 76 | - EU Enabler (iOS 17.6-) 77 |
78 |
79 | iOS 18.0 - iOS 18.0.1 80 | 81 | - Feature Flags (iOS 18.1b4-): 82 | - Enabling lock screen clock animation, lock screen page duplication button, and more! 83 | - Disabling the new iOS 18 Photos UI (iOS 18.0 betas only, unknown which patched it) 84 |
85 |
86 | iOS 18.0 - iOS 18.1.1 87 | 88 | - Enable iPhone 16 camera button page in the Settings app 89 | - Enable AOD & AOD Vibrancy on any device 90 |
91 |
92 | iOS 18.1 - 18.1.1 93 | 94 | - AI Enabler 95 | - Device Spoofing 96 |
97 | 98 | ## Requirements: 99 |
100 | Windows 101 | 102 | - Either [Apple Devices (from Microsoft Store)][AppleDevices] App or [iTunes (from Apple website)][iTunes] 103 |
104 | 105 |
106 | Linux 107 | 108 | - [usbmuxd][usbmuxdGitHub] 109 | - [libimobiledevice][libimobiledeviceGitHub] 110 |
111 | 112 |
113 | For Running Python 114 | 115 | - [pymobiledevice3][pymobiledevice3GitHub] 116 | - [PySide6][PySide6Doc] 117 | - Python 3.8 or newer 118 |
119 | 120 | ## Running the Python Program 121 | > [!NOTE] 122 | > It is highly recommended to use a virtual environment: 123 | > ```py 124 | > python3 -m venv .env # only needed once 125 | > ``` 126 | macOS/Linux: 127 | ```py 128 | source .env/bin/activate 129 | ``` 130 | Windows: 131 | ```py 132 | .env/Scripts/activate.bat 133 | ``` 134 | Install Packages: 135 | ```py 136 | pip3 install -r requirements.txt # only needed once 137 | python3 main_app.py 138 | ``` 139 | > [!NOTE] 140 | > It may be either `python`/`pip` or `python3`/`pip3` depending on your path. 141 | 142 | The CLI version can be ran with: 143 | ```py 144 | python3 cli_app.py 145 | ``` 146 | 147 | ## Getting the File 148 | On iOS 18.1.1 and below, you may need to get the mobilegestalt file that is specific to your device. To do that, follow these steps: 149 | 1. Install the [Shortcuts][ShortcutsApp] app from the iOS app store. 150 | 2. Download this shortcut: [Save MobileGestalt][MobilegestaltShortcut] 151 | 3. Save the file and share it to your computer. 152 | 4. Place it in the same folder as the python file (or specify the path in the program) 153 | 154 | ## Building 155 | To compile `mainwindow.ui` for Python, run the following command: 156 | ```py 157 | pyside6-uic qt/mainwindow.ui -o qt/ui_mainwindow.py 158 | ``` 159 | 160 | To compile the resources file for Python, run the following command: 161 | ```py 162 | pyside6-rcc resources.qrc -o resources_rc.py 163 | ``` 164 | 165 | The application itself can be compiled by running `compile.py`. 166 | 167 | ## Sparserestore Info 168 | This uses the sparserestore exploit to write to files outside of the intended restore location, like mobilegestalt. Read the [Getting the File](#getting-the-file) section to learn how to get your mobilegestalt file. 169 | 170 | Sparserestore works on all versions iOS 17.0-18.1.1. There is partial support for iOS 18.2 developer beta 3 and newer not using any exploits. 171 | 172 | > [!NOTE] 173 | > **Mobilegestalt and AI Enabler tweaks are not supported on iOS 18.2+.** It will never be supported, do not make issues asking for when it is supported. 174 | 175 | ## Read More 176 | If you would like to read more about the inner workings of the exploit and iOS restore system, I made a write up which you can read [here][ReadMoreGist]. 177 | For clarity, up to iOS 18.2 developer beta 2 (public beta 1) is fully supported by Nugget. I said iOS 18.1.1 because mentioning the betas confused people. 178 | 179 | 180 | 181 | ## Credits 182 | - [LeminLimez](https://github.com/leminlimez) for [Nugget](https://github.com/leminlimez/Nugget) 183 | - [JJTech][JJTechGitHub] for Sparserestore/[TrollRestore][TrollStoreGitHub] 184 | - [PosterRestore][PosterRestoreDiscord] for their help with PosterBoard 185 | - Special thanks to [dootskyre][dootskyreX], [Middo][MiddoX], [dulark][dularkGitHub], forcequitOS, and pingubow for their work on this. It would not have been possible without them! 186 | - Thanks to [Snoolie for aar handling][python-aar-stuffGitHub]! 187 | - Thanks to [SerStars][SerStarsX] for creating [the website][WallpapersWebsite]! 188 | - [disfordottie][disfordottieX] for some global flag features 189 | - [Mikasa-san][Mikasa-sanGitHub] for [Quiet Daemon][QuietDaemonGitHub] 190 | - [sneakyf1shy][sneakyf1shyGitHub] for [AI Eligibility][AIEligibilityGist] (iOS 18.1 beta 4 and below) 191 | - [lrdsnow][lrdsnowGitHub] for [EU Enabler][EUEnablerGitHub] 192 | - [pymobiledevice3][pymobiledevice3GitHub] for restoring and device algorithms. 193 | - [PySide6][PySide6Doc] for the GUI library. 194 | 195 | [NuggetLogo]: https://raw.githubusercontent.com/leminlimez/Nugget/refs/heads/main/credits/small_nugget.png 196 | [CowabungaLite]: https://github.com/leminlimez/CowabungaLite 197 | [WallpapersWebsite]: https://cowabun.ga/wallpapers 198 | [AppleDevices]: https://apps.microsoft.com/detail/9np83lwlpz9k 199 | [iTunes]: https://support.apple.com/en-us/106372 200 | [usbmuxdGitHub]: https://github.com/libimobiledevice/usbmuxd 201 | [libimobiledeviceGitHub]: https://github.com/libimobiledevice/libimobiledevice 202 | [ShortcutsApp]: https://apps.apple.com/us/app/shortcuts/id915249334 203 | [MobilegestaltShortcut]: https://www.icloud.com/shortcuts/d6f0a136ddda4714a80750512911c53b 204 | [ReadMoreGist]: https://gist.github.com/leminlimez/c602c067349140fe979410ef69d39c28 205 | 206 | [JJTechGitHub]: https://github.com/JJTech0130 207 | [TrollStoreGitHub]: https://github.com/JJTech0130/TrollRestore 208 | [PosterRestoreDiscord]: https://discord.gg/gWtzTVhMvh 209 | [dootskyreX]: https://x.com/dootskyre 210 | [MiddoX]: https://x.com/MWRevamped 211 | [dularkGitHub]: https://github.com/dularkian 212 | [SerStarsX]: https://x.com/SerStars_lol 213 | [disfordottieX]: https://x.com/disfordottie 214 | [Mikasa-sanGitHub]: https://github.com/Mikasa-san 215 | [QuietDaemonGitHub]: https://github.com/Mikasa-san/QuietDaemon 216 | [sneakyf1shyGitHub]: https://github.com/f1shy-dev 217 | [lrdsnowGitHub]: https://github.com/Lrdsnow 218 | [EUEnablerGitHub]: https://github.com/Lrdsnow/EUEnabler 219 | [pymobiledevice3GitHub]: https://github.com/doronz88/pymobiledevice3 220 | [PySide6Doc]: https://doc.qt.io/qtforpython-6/ 221 | [python-aar-stuffGitHub]: https://github.com/0xilis/python-aar-stuff 222 | [AIEligibilityGist]: https://gist.github.com/f1shy-dev/23b4a78dc283edd30ae2b2e6429129b5 223 | -------------------------------------------------------------------------------- /compile.py: -------------------------------------------------------------------------------- 1 | from sys import platform 2 | import os 3 | 4 | import PyInstaller.__main__ 5 | 6 | # configuration 7 | universal: bool = False # issues with cross compiling since cpython only gives a single arch 8 | 9 | args = [ 10 | 'main_app.py', 11 | # '--hidden-import=ipsw_parser', 12 | '--hidden-import=zeroconf', 13 | '--hidden-import=pyimg4', 14 | '--hidden-import=zeroconf._utils.ipaddress', 15 | '--hidden-import=zeroconf._handlers.answers', 16 | '--add-data=files/:./files', 17 | '--copy-metadata=pyimg4', 18 | '--onedir', 19 | '--noconfirm', 20 | '--name=Nugget', 21 | '--icon=nugget.ico', 22 | '--optimize=2' 23 | ] 24 | 25 | if platform == "darwin": 26 | # add --windowed arg for macOS 27 | args.append('--windowed') 28 | args.append('--osx-bundle-identifier=com.leemin.Nugget') 29 | if universal: 30 | args.append('--target-arch=universal2') 31 | # codesigning resources 32 | try: 33 | import secrets.compile_config as compile_config 34 | args.append('--osx-entitlements-file=entitlements.plist') 35 | args.append(f"--codesign-identity={compile_config.CODESIGN_HASH}") 36 | except ImportError: 37 | print("No compile_config found, ignoring codesign...") 38 | elif os.name == 'nt': 39 | # add windows version info 40 | args.append('--version-file=version.txt') 41 | 42 | PyInstaller.__main__.run(args) 43 | -------------------------------------------------------------------------------- /controllers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LoveJessyChen/Nugget-iOS/eb917fe4a75ef61eca7e098b7ad2afcc736a2515/controllers/__init__.py -------------------------------------------------------------------------------- /controllers/aar/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 0xilis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /controllers/aar/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LoveJessyChen/Nugget-iOS/eb917fe4a75ef61eca7e098b7ad2afcc736a2515/controllers/aar/__init__.py -------------------------------------------------------------------------------- /controllers/aar/aar.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created by Snoolie K (0xilis) on 2025 March 18. 3 | Licensed under MIT. Please credit if using. 4 | ''' 5 | 6 | import struct 7 | 8 | # make a .aar of one contexts.plist file 9 | def wrap_in_aar(input_file, input_mov, output_file): 10 | # Since I only need to care about a contents.plist (and in the future the mov file), I am hardcoding the header... 11 | header = bytearray.fromhex("4141303125005459503146504154500E00636F6E74656E74732E706C697374444154418E13") 12 | 13 | with open(input_file, 'rb') as f: 14 | file_data = f.read() 15 | 16 | file_size = len(file_data) 17 | 18 | if file_size <= 0xFFFF: 19 | # Our size can fit in a 2 byte AA_FIELD_TYPE_BLOB, yay 20 | header[-2:] = struct.pack(' FRAME_LIMIT: 59 | raise VideoLengthException(FRAME_LIMIT) 60 | try: 61 | # creating a folder named data 62 | if not os.path.exists(assets_path): 63 | os.makedirs(assets_path, exist_ok=True) 64 | # if not created then raise error 65 | except OSError: 66 | print ('Error: Creating directory of data') 67 | 68 | # frame 69 | currentframe = 0 70 | width = int(cam.get(cv2.CAP_PROP_FRAME_WIDTH)) 71 | height = int(cam.get(cv2.CAP_PROP_FRAME_HEIGHT)) 72 | 73 | with open(os.path.join(output_file, "main.caml"), "w") as caml: 74 | # write caml header 75 | fps = cam.get(cv2.CAP_PROP_FPS) 76 | duration = frame_count / fps 77 | caml.write(f""" 78 | 79 | 80 | 148 | 149 | """) 150 | 151 | # Release all space and windows once done 152 | cam.release() 153 | cv2.destroyAllWindows() 154 | 155 | # Write the other caml 156 | with open(os.path.join(output_file, "index.xml"), "w") as index: 157 | index.write(f""" 158 | 159 | 160 | 161 | assetManifest 162 | assetManifest.caml 163 | documentHeight 164 | {height} 165 | documentResizesToView 166 | 167 | documentWidth 168 | {width} 169 | dynamicGuidesEnabled 170 | 171 | geometryFlipped 172 | 173 | guidesEnabled 174 | 175 | interactiveMouseEventsEnabled 176 | 177 | interactiveShowsCursor 178 | 179 | interactiveTouchEventsEnabled 180 | 181 | loopEnd 182 | 0.0 183 | loopStart 184 | 0.0 185 | loopingEnabled 186 | 187 | multitouchDisablesMouse 188 | 189 | multitouchEnabled 190 | 191 | presentationMouseEventsEnabled 192 | 193 | presentationShowsCursor 194 | 195 | presentationTouchEventsEnabled 196 | 197 | rootDocument 198 | main.caml 199 | savesWindowFrame 200 | 201 | scalesToFitInPlayer 202 | 203 | showsTouches 204 | 205 | snappingEnabled 206 | 207 | timelineMarkers 208 | [(null)] 209 | touchesColor 210 | 1 1 0 0.8 211 | unitsInPixelsInPlayer 212 | 213 | 214 | 215 | """) -------------------------------------------------------------------------------- /controllers/web_request_handler.py: -------------------------------------------------------------------------------- 1 | from requests import get, RequestException 2 | from json import JSONDecodeError 3 | from devicemanagement.constants import Version 4 | 5 | Nugget_Repo = "leminlimez/Nugget/releases/latest" 6 | 7 | last_fetched_version: str = None 8 | 9 | def is_update_available(version: str, build: int) -> bool: 10 | # check github for if version < tag (or == tag but build > 0) 11 | latest_version = get_latest_version() 12 | if latest_version != None: 13 | if build > 0 and latest_version == version: # on beta version when there is a public release 14 | return True 15 | elif Version(latest_version) > Version(version): 16 | return True 17 | return False 18 | 19 | def get_latest_version() -> str: 20 | global last_fetched_version 21 | # get the cached version 22 | if last_fetched_version != None: 23 | return last_fetched_version 24 | # fetch with web requests 25 | try: 26 | response = get(f"https://api.github.com/repos/{Nugget_Repo}") 27 | response.raise_for_status() # To raise an exception for 4xx/5xx responses 28 | 29 | data = response.json() # Parse the JSON response 30 | 31 | # Check if "tag_name" exists in the response and compare the version 32 | tag_name = data.get("tag_name") 33 | if tag_name: 34 | last_fetched_version = tag_name.replace("v", "") # Remove 'v' from tag_name 35 | return last_fetched_version 36 | except RequestException as e: 37 | print(f"Error fetching data: {e}") 38 | except JSONDecodeError as e: 39 | print(f"Error parsing JSON: {e}") 40 | return None -------------------------------------------------------------------------------- /controllers/xml_handler.py: -------------------------------------------------------------------------------- 1 | import xml.etree.ElementTree as tree 2 | 3 | tree.register_namespace('', "http://www.apple.com/CoreAnimation/1.0") 4 | 5 | def parse_equation(eq: str, val: str): 6 | eqns = eq.split(',') 7 | value = [val] 8 | if ' ' in val: 9 | # convert to array 10 | value = val.split(' ') 11 | # map the value to floats 12 | value = list(map(lambda x: float(x), value)) 13 | keys = ['x', 'y', 'z', 'a'] 14 | results = [] 15 | mapped = dict(zip(keys, value)) 16 | for eqn in eqns: 17 | results.append(str(eval(eqn, {}, mapped))) 18 | # map back to string 19 | return ' '.join(results) 20 | 21 | def set_xml_value(file: str, id: str, key: str, val: any, use_ca_id: bool = False): 22 | set_xml_values(file=file, id=id, keys=[key], values=[val], use_ca_id=use_ca_id) 23 | 24 | def set_xml_values(file: str, id: str, keys: list[str], values: list[any], use_ca_id: bool = False): 25 | xml = tree.parse(file) 26 | root = xml.getroot() 27 | 28 | # convert bool to integer 29 | for i in range(len(values)): 30 | if isinstance(values[i], bool): 31 | values[i] = int(values[i]) 32 | 33 | # set all values with the nugget id passed by param 34 | if use_ca_id: 35 | idKey = "id" 36 | else: 37 | idKey = "nuggetId" 38 | for to_change in root.findall(f".//*[@{idKey}='{id}']"): 39 | eqn = to_change.get("nuggetOffset") 40 | # TODO: Allow offsets for more than just the first value 41 | for i in range(len(keys)): 42 | offsetVal = values[i] 43 | if i == 0 and eqn != None: 44 | offsetVal = parse_equation(eqn, offsetVal) 45 | # find the value type 46 | check_val = to_change.get(keys[i]) 47 | if check_val != None and isinstance(check_val, str) and not isinstance(offsetVal, str): 48 | offsetVal = str(offsetVal) 49 | to_change.set(keys[i], offsetVal) 50 | if use_ca_id: 51 | # also look for target id 52 | for to_change in root.findall(f".//*[@targetId='{id}']"): 53 | for i in range(len(keys)): 54 | # find the value type 55 | set_val = values[i] 56 | check_val = to_change.get(keys[i]) 57 | if check_val != None and isinstance(check_val, str) and not isinstance(set_val, str): 58 | set_val = str(set_val) 59 | to_change.set(keys[i], set_val) 60 | 61 | # write back to file 62 | xml.write(file, encoding="UTF-8", xml_declaration=True) 63 | 64 | def remove_from_root(root, search): 65 | for parent in root.findall(search + "/.."): 66 | for prop in parent.findall(search): 67 | parent.remove(prop) 68 | 69 | def delete_xml_value(file: str, id: str, use_ca_id: bool = False): 70 | xml = tree.parse(file) 71 | root = xml.getroot() 72 | 73 | # delete all elements with the nugget id passed by param 74 | if use_ca_id: 75 | idKey = "id" 76 | else: 77 | idKey = "nuggetId" 78 | remove_from_root(root, search=f".//*[@{idKey}='{id}']") 79 | if use_ca_id: 80 | # also remove the target ids 81 | remove_from_root(root, search=f".//*[@targetId='{id}']") 82 | 83 | # write back to file 84 | xml.write(file, encoding="UTF-8", xml_declaration=True) -------------------------------------------------------------------------------- /credits/LeminLimez.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LoveJessyChen/Nugget-iOS/eb917fe4a75ef61eca7e098b7ad2afcc736a2515/credits/LeminLimez.png -------------------------------------------------------------------------------- /credits/big_nugget.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LoveJessyChen/Nugget-iOS/eb917fe4a75ef61eca7e098b7ad2afcc736a2515/credits/big_nugget.png -------------------------------------------------------------------------------- /credits/small_nugget.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LoveJessyChen/Nugget-iOS/eb917fe4a75ef61eca7e098b7ad2afcc736a2515/credits/small_nugget.png -------------------------------------------------------------------------------- /devicemanagement/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LoveJessyChen/Nugget-iOS/eb917fe4a75ef61eca7e098b7ad2afcc736a2515/devicemanagement/__init__.py -------------------------------------------------------------------------------- /devicemanagement/constants.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from pymobiledevice3.lockdown import LockdownClient 3 | 4 | class Device: 5 | def __init__(self, uuid: int, usb: bool, name: str, version: str, build: str, model: str, hardware: str, cpu: str, locale: str, ld: LockdownClient): 6 | self.uuid = uuid 7 | self.connected_via_usb = usb 8 | self.name = name 9 | self.version = version 10 | self.build = build 11 | self.model = model 12 | self.hardware = hardware 13 | self.cpu = cpu 14 | self.locale = locale 15 | self.ld = ld 16 | 17 | def is_exploit_fully_patched(self) -> bool: 18 | parsed_ver: Version = Version(self.version) 19 | # mobile gestalt methods are completely patched on iOS 18.2 dev beta 3+ 20 | if (parsed_ver < Version("18.2") 21 | or self.build == "22C5109p" or self.build == "22C5125e"): 22 | return False 23 | return True 24 | 25 | def has_exploit(self) -> bool: 26 | parsed_ver: Version = Version(self.version) 27 | # make sure versions past 17.7.1 but before 18.0 aren't supported 28 | if (parsed_ver >= Version("17.7.1") and parsed_ver < Version("18.0")): 29 | return False 30 | if (parsed_ver < Version("18.1") 31 | or self.build == "22B5007p" or self.build == "22B5023e" 32 | or self.build == "22B5034e" or self.build == "22B5045g"): 33 | return True 34 | return False 35 | 36 | def supported(self) -> bool: 37 | return self.has_exploit() 38 | 39 | class Version: 40 | def __init__(self, major: int, minor: int = 0, patch: int = 0): 41 | self.major = major 42 | self.minor = minor 43 | self.patch = patch 44 | 45 | def __init__(self, ver: str): 46 | nums: list[str] = ver.split(".") 47 | self.major = int(nums[0]) 48 | self.minor = int(nums[1]) if len(nums) > 1 else 0 49 | self.patch = int(nums[2]) if len(nums) > 2 else 0 50 | 51 | # Comparison Functions 52 | def compare_to(self, other) -> int: 53 | if self.major > other.major: 54 | return 1 55 | elif self.major < other.major: 56 | return -1 57 | if self.minor > other.minor: 58 | return 1 59 | elif self.minor < other.minor: 60 | return -1 61 | if self.patch > other.patch: 62 | return 1 63 | elif self.patch < other.patch: 64 | return -1 65 | return 0 66 | 67 | def __gt__(self, other) -> bool: 68 | return self.compare_to(other) == 1 69 | def __ge__(self, other) -> bool: 70 | comp: int = self.compare_to(other) 71 | return comp == 0 or comp == 1 72 | 73 | def __lt__(self, other) -> bool: 74 | return self.compare_to(other) == -1 75 | def __le__(self, other) -> bool: 76 | comp: int = self.compare_to(other) 77 | return comp == 0 or comp == -1 78 | 79 | def __eq__(self, other) -> bool: 80 | return self.compare_to(other) == 0 81 | 82 | class Tweak(Enum): 83 | StatusBar = 'Status Bar' 84 | SpringboardOptions = 'Springboard Options' 85 | InternalOptions = 'Internal Options' 86 | SkipSetup = 'Setup Options' 87 | 88 | class FileLocation(Enum): 89 | # Control Center 90 | mute = "ControlCenter/ManagedPreferencesDomain/mobile/com.apple.control-center.MuteModule.plist" 91 | focus = "ControlCenter/ManagedPreferencesDomain/mobile/com.apple.FocusUIModule.plist" 92 | spoken = "ControlCenter/ManagedPreferencesDomain/mobile/com.apple.siri.SpokenNotificationsModule.plist" 93 | module_config = "ControlCenter/HomeDomain/Library/ControlCenter/ModuleConfiguration.plist" 94 | replay_kit_audio = "ControlCenter/ManagedPreferencesDomain/mobile/com.apple.replaykit.AudioConferenceControlCenterModule.plist" 95 | replay_kit_video = "ControlCenter/ManagedPreferencesDomain/mobile/com.apple.replaykit.VideoConferenceControlCenterModule.plist" 96 | 97 | # Status Bar 98 | status_bar = "StatusBar/HomeDomain/Library/SpringBoard/statusBarOverrides" 99 | 100 | # Springboard Options 101 | springboard = "SpringboardOptions/ManagedPreferencesDomain/mobile/com.apple.springboard.plist" 102 | footnote = "SpringboardOptions/ConfigProfileDomain/Library/ConfigurationProfiles/SharedDeviceConfiguration.plist" 103 | wifi = "SpringboardOptions/SystemPreferencesDomain/SystemConfiguration/com.apple.wifi.plist" 104 | uikit = "SpringboardOptions/ManagedPreferencesDomain/mobile/com.apple.UIKit.plist" 105 | accessibility = "SpringboardOptions/ManagedPreferencesDomain/mobile/com.apple.Accessibility.plist" 106 | wifi_debug = "SpringboardOptions/ManagedPreferencesDomain/mobile/com.apple.MobileWiFi.debug.plist" 107 | airdrop = "SpringboardOptions/ManagedPreferencesDomain/mobile/com.apple.sharingd.plist" 108 | 109 | # Internal Options 110 | global_prefs = "InternalOptions/ManagedPreferencesDomain/mobile/hiddendotGlobalPreferences.plist" 111 | app_store = "InternalOptions/ManagedPreferencesDomain/mobile/com.apple.AppStore.plist" 112 | backboardd = "InternalOptions/ManagedPreferencesDomain/mobile/com.apple.backboardd.plist" 113 | core_motion = "InternalOptions/ManagedPreferencesDomain/mobile/com.apple.CoreMotion.plist" 114 | pasteboard = "InternalOptions/HomeDomain/Library/Preferences/com.apple.Pasteboard.plist" 115 | notes = "InternalOptions/ManagedPreferencesDomain/mobile/com.apple.mobilenotes.plist" 116 | maps = "InternalOptions/AppDomain-com.apple.Maps/Library/Preferences/com.apple.Maps.plist" 117 | weather = "InternalOptions/AppDomain-com.apple.weather/Library/Preferences/com.apple.weather.plist" 118 | 119 | # Setup Options 120 | cloud_config = "SkipSetup/ConfigProfileDomain/Library/ConfigurationProfiles/CloudConfigurationDetails.plist" -------------------------------------------------------------------------------- /devicemanagement/data_singleton.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from devicemanagement.constants import Device, Tweak 4 | 5 | class DataSingleton: 6 | def __init__(self): 7 | self.current_device: Device 8 | self.device_available: bool = False 9 | self.gestalt_path = None -------------------------------------------------------------------------------- /devicemanagement/generate_mga.py: -------------------------------------------------------------------------------- 1 | from pymobiledevice3.lockdown import LockdownClient 2 | 3 | def get_model_name(product_type: str) -> str: 4 | return "iPhone something" 5 | 6 | def get_idiom(device_class: str) -> str: 7 | if device_class == 'iPhone': 8 | return 'phone' 9 | else: 10 | return 'pad' 11 | 12 | def generate_mga(ld: LockdownClient) -> dict: 13 | # queries the device values and generates a mobile gestalt dictionary to return 14 | vals = ld.all_values 15 | prod = vals['ProductType'] 16 | device_class = vals['DeviceClass'] 17 | region = vals['RegionInfo'].split("/") 18 | build = vals['BuildVersion'] 19 | model_name = get_model_name(prod) 20 | cache_extra = { 21 | # main dict to query 22 | "+3Uf0Pm5F8Xy7Onyvko0vA": device_class, # DeviceClass 23 | "/YYygAofPDbhrwToVsXdeA": vals['HardwareModel'], # HardwareModel 24 | "0+nc/Udy4WNG8S+Q7a/s1A": prod, # ProductType 25 | # "0GizaJLOyfzgAbxQ/5aniA" ## DeviceHousingColorUncooked 26 | # "4W7X4OWHjri5PGaAGsCWxw" ## MaxH264PlaybackLevel 27 | # "4qfpxrvLtWillIHpIsVgMA" ## SystemImageID 28 | "5pYKlGnYYBzGvAlIU8RjEQ": vals['HardwarePlatform'], # HardwarePlatform 29 | # "913P3Zsei09w0GSSOaBD+w" ## VolumeUpButtonNormalizedCGRect 30 | "91LyMcx4z1w3SGVeqteMnA": vals['BasebandRegionSKU'], # BasebandRegionSKU 31 | # "96GRvvjuBKkU4HzNsYcHPA" ## MinimumSupportediTunesVersion 32 | # "97JDvERpVwO+GHtthIh7hA" ## RegulatoryModelNumber 33 | "9MZ5AdH43csAUajl/dU+IQ": vals['SupportedDeviceFamilies'], # SupportedDeviceFamilies 34 | "9s45ldrCC1WF+7b6C4H2BA": prod, # GSDeviceName 35 | # "AoKnINTLPoKML3ctoP0AZg" ## IOSurfaceFormatDictionary 36 | "DViRIxZ/ZwO007CLcEYvZw": "", # DViRIxZ/ZwO007CLcEYvZw 37 | # "HXTqT3UXOKuTEklxz+wMAA" ## BasebandAPTimeSync 38 | "IMLaTlxS7ITtwfbRfPYWuA": region[1], # DeviceVariantGuess 39 | # "J1QHVh74Nnd6Rqyiq71/yw" ## AVDDecodingRate 40 | # "JHXk7RXOxvlqK+SxkwcM2A" ## LowPowerExpressModesSupported 41 | "JUWcn+5Ss0nvr5w/jk4WEg": device_class, # device-name 42 | # "JhEU414EIaDvAz8ki5DSqw" ## DeviceEnclosureColor 43 | # "LTI8wHvEYKy8zR1IXBW1uQ" ## ArtworkTraitDisplayGamut 44 | "LeSRsiLoJCMhjn6nd6GWbQ": vals['FirmwareVersion'], # FirmwareVersion 45 | # "NUYAz1eq3Flzt7ZQxXC/ng" ## FirstPartyLaunchTimeLimitScale 46 | # "NaA/zJV7myg2w4YNmSe4yQ" ## WifiChipset 47 | # "Nzu4E/VsXjEIa83CkRdZrQ" ## Image4CryptoHashMethod 48 | # "PdprWthPO6YyrO6p1vLRgQ" ## VolumeDownButtonCGRect 49 | # "QbQzuIbef01P4JeoL9EmKg" ## DeviceSceneUpdateTimeLimitScale 50 | # "SbXytSPZXB1jQ8GLZOxCPw" ## VolumeDownButtonNormalizedCGRect 51 | # "TZ/0j62wM3D0CuRt+Nc/Lw" ## ProductHash 52 | # "VuGdqp8UBpi9vPWHlPluVQ" ## CompatibleAppVariants 53 | # "WPEkba78QeFFU/wgqpOx6w" ## UserIntentPhysicalButtonNormalizedCGRect 54 | "Z/dqyWS6OZTRy10UcmUAhw": model_name, # marketing-name 55 | # "aD51uqjUwgRKjAC04BCrxg" ## VolumeUpButtonCGRect 56 | "bbtR9jQx50Fv5Af/affNtA": model_name, # PhysicalHardwareNameString 57 | # "c7fCSBIbX1mFaRoKT5zTIw" ## WifiVendor 58 | # "emXA9B552rnSoI7xXE91DA" ## DeviceLaunchTimeLimitScale 59 | # "gBw7IWiBnLHaA+lBrZBgWw" ## CameraMaxBurstLength 60 | # "gD8SNRcHQeIxCAvsp+2vjA" ## WSKU 61 | "h63QSdBCiT/z0WU6rdQv6Q": region[0], # RegionCode 62 | "h9jDsbgj7xIVeIQ8S3/X3Q": prod, # ProductType 63 | "ivIu8YTDnBSrYv/SN4G8Ag": vals['ProductName'], # ProductName 64 | # "k+KTni1jrwErpcDMEnn3aw" ## MobileDeviceMinimumVersion 65 | "k7QIBwZJJOVw+Sej/8h8VA": vals['CPUArchitecture'], # CPUArchitecture 66 | "mZfUC7qo4pURNhyMHZ62RQ": build, # BuildVersion 67 | "mumHZHMLEfAuTkkd28fHlQ": vals['DeviceColor'], # DeviceColor 68 | # "nSo8opze5rFk+EdBoR6tBw" ## RestrictedCountryCodes 69 | # "oBbtJ8x+s1q0OkaiocPuog" ## MainScreenStaticInfo 70 | "oPeik/9e8lQWMszEjbPzng": { # ArtworkTraits 71 | "ArtworkDeviceIdiom": get_idiom(device_class), 72 | "ArtworkDeviceProductDescription": model_name, 73 | # "ArtworkDeviceScaleFactor" 74 | # "ArtworkDeviceSubType" 75 | # "ArtworkDisplayGamut" 76 | # "ArtworkDynamicDisplayMode" 77 | # "CompatibleDeviceFallback" 78 | # "DevicePerformanceMemoryClass" 79 | # "GraphicsFeatureSetClass" 80 | # "GraphicsFeatureSetFallbacks" 81 | }, 82 | "oYicEKzVTz4/CxxE05pEgQ": vals['HardwareModel'], # TargetSubType 83 | # "pB5sZVvnp+QjZQtt2KfQvA" ## BasebandChipset 84 | # "pMeQxE5szZTjLMk10TisDQ" ## UserIntentPhysicalButtonCGRect 85 | "qNNddlUK+B/YlooNoymwgA": vals['ProductVersion'], # ProductVersion 86 | # "qwXfFvH5jPXPxrny0XuGtQ" ## BuildID 87 | # "rJkMAGeVLdhP5+10G5hVcA" ## UserIntentPhysicalButtonCGRectString 88 | "rkqlwPcRHwixY4gapPjanw": device_class, # DeviceName 89 | "vme9Buk6XiWFCXoHApxNFA": device_class, # MarketingDeviceFamilyName 90 | # "wYMBabAO8VguyDDVgCsPdg" ## WiFiChipsetRevision 91 | "xUHcyT2/HE8oi/4LaOI+Sw": vals['PartitionType'], # PartitionType 92 | # "xojWvSTQWT7Icy+xfVzjAQ" ## FramebufferIdentifier 93 | # "yUqD8AXE/c+IggkuYoxrqA" ## ChromeIdentifier 94 | # "ybGkijAwLTwevankfVzsDQ" ## MainScreenCanvasSizes 95 | "yjP8DgByZmLk04Ta6f6DWQ": "iOS", # PartitionStyle 96 | "zHeENZu+wbg7PUprwNwBWg": vals['RegionInfo'], # RegionInfo 97 | } 98 | return { 99 | # "CacheData": b"", 100 | "CacheExtra": cache_extra, 101 | # "CacheUUID": "", 102 | "CacheVersion": build # build number 103 | } -------------------------------------------------------------------------------- /entitlements.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | com.apple.security.cs.allow-jit 7 | 8 | com.apple.security.cs.allow-unsigned-executable-memory 9 | 10 | com.apple.security.cs.disable-library-validation 11 | 12 | 13 | -------------------------------------------------------------------------------- /exceptions/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LoveJessyChen/Nugget-iOS/eb917fe4a75ef61eca7e098b7ad2afcc736a2515/exceptions/__init__.py -------------------------------------------------------------------------------- /exceptions/nugget_exception.py: -------------------------------------------------------------------------------- 1 | class NuggetException(Exception): 2 | def __init__(self, message: str): 3 | self.message = message 4 | super().__init__(self.message) 5 | 6 | def __str__(self): 7 | return self.message -------------------------------------------------------------------------------- /exceptions/posterboard_exceptions.py: -------------------------------------------------------------------------------- 1 | from .nugget_exception import NuggetException 2 | 3 | class VideoLengthException(NuggetException): 4 | def __init__(self, frame_limit: int): 5 | super().__init__(f"Videos must be under {frame_limit} frames to loop. Either reduce the frame rate or make it shorter.") 6 | 7 | class PBTemplateException(Exception): 8 | def __init__(self, file: str, message: str): 9 | self.message = message 10 | super().__init__(self.message) 11 | self.filename = file 12 | def __str__(self): 13 | return self.message -------------------------------------------------------------------------------- /files/SSLconf/TrustStore.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LoveJessyChen/Nugget-iOS/eb917fe4a75ef61eca7e098b7ad2afcc736a2515/files/SSLconf/TrustStore.sqlite3 -------------------------------------------------------------------------------- /files/eligibility/Config.plist: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LoveJessyChen/Nugget-iOS/eb917fe4a75ef61eca7e098b7ad2afcc736a2515/files/eligibility/Config.plist -------------------------------------------------------------------------------- /files/posterboard/1F20C883-EA98-4CCE-9923-0C9A01359721/com.apple.posterkit.provider.descriptor.identifier: -------------------------------------------------------------------------------- 1 | 88D1F444-E2B3-43C4-BB15-790C67EDEA56 -------------------------------------------------------------------------------- /files/posterboard/1F20C883-EA98-4CCE-9923-0C9A01359721/providerInfo.plist: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LoveJessyChen/Nugget-iOS/eb917fe4a75ef61eca7e098b7ad2afcc736a2515/files/posterboard/1F20C883-EA98-4CCE-9923-0C9A01359721/providerInfo.plist -------------------------------------------------------------------------------- /files/posterboard/1F20C883-EA98-4CCE-9923-0C9A01359721/versions/0/contents/.com.apple.posterkit.provider.contents.configurableOptions.plist: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LoveJessyChen/Nugget-iOS/eb917fe4a75ef61eca7e098b7ad2afcc736a2515/files/posterboard/1F20C883-EA98-4CCE-9923-0C9A01359721/versions/0/contents/.com.apple.posterkit.provider.contents.configurableOptions.plist -------------------------------------------------------------------------------- /files/posterboard/1F20C883-EA98-4CCE-9923-0C9A01359721/versions/0/contents/0EFB6A0F-7052-4D24-8859-AB22BADF2E93/input.segmentation/asset.resource/contents.plist: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LoveJessyChen/Nugget-iOS/eb917fe4a75ef61eca7e098b7ad2afcc736a2515/files/posterboard/1F20C883-EA98-4CCE-9923-0C9A01359721/versions/0/contents/0EFB6A0F-7052-4D24-8859-AB22BADF2E93/input.segmentation/asset.resource/contents.plist -------------------------------------------------------------------------------- /files/posterboard/1F20C883-EA98-4CCE-9923-0C9A01359721/versions/0/contents/0EFB6A0F-7052-4D24-8859-AB22BADF2E93/output.layerStack/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "version" : 10, 3 | "layers" : [ 4 | { 5 | "frame" : { 6 | "Width" : 1042.4546142208774, 7 | "Height" : 2868.5230711043873, 8 | "Y" : -0.52307110438732707, 9 | "X" : 174.92435703479578 10 | }, 11 | "filename" : "portrait-layer_background.HEIC", 12 | "zPosition" : 5, 13 | "identifier" : "background" 14 | }, 15 | { 16 | "frame" : { 17 | "Width" : 1182.5, 18 | "Height" : 2569.25, 19 | "Y" : 149.37503513892489, 20 | "X" : 68.749995738267913 21 | }, 22 | "filename" : "portrait-layer_settling-video.MOV", 23 | "zPosition" : 5, 24 | "identifier" : "settling-video" 25 | } 26 | ], 27 | "properties" : { 28 | "portraitLayout" : { 29 | "clockIntersection" : 2, 30 | "deviceResolution" : { 31 | "Width" : 750, 32 | "Height" : 1334 33 | }, 34 | "visibleFrame" : { 35 | "Width" : 886.43178410794587, 36 | "Height" : 2569.25, 37 | "Y" : 149.37496486107506, 38 | "X" : 252.31008241168968 39 | }, 40 | "timeFrame" : { 41 | "Width" : 763.51324337831068, 42 | "Height" : 227.26499250374812, 43 | "Y" : 2135.0546874997553, 44 | "X" : 313.76935277650728 45 | }, 46 | "clockLayerOrder" : "ClockAboveForeground", 47 | "hasTopEdgeContact" : false, 48 | "inactiveFrame" : { 49 | "Width" : 913.02473763118462, 50 | "Height" : 2646.3274999999999, 51 | "Y" : 149.39630414678933, 52 | "X" : 239.01360565007042 53 | }, 54 | "imageSize" : { 55 | "Width" : 1320, 56 | "Height" : 2868 57 | }, 58 | "parallaxPadding" : { 59 | "Width" : 61.45927036481757, 60 | "Height" : 151.28435782108946 61 | } 62 | }, 63 | "settlingEffectEnabled" : true, 64 | "depthEnabled" : false, 65 | "clockAreaLuminance" : 0.64877637987012982, 66 | "parallaxDisabled" : false 67 | } 68 | } -------------------------------------------------------------------------------- /files/posterboard/1F20C883-EA98-4CCE-9923-0C9A01359721/versions/0/contents/0EFB6A0F-7052-4D24-8859-AB22BADF2E93/style.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | colorSuggestions 6 | 7 | kind 8 | Original 9 | parameters 10 | 11 | clockColor 12 | 13 | 1 14 | 1 15 | 1 16 | 17 | clockVibrancy 18 | 0.0 19 | headroomLook 20 | 2 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /files/posterboard/1F20C883-EA98-4CCE-9923-0C9A01359721/versions/0/contents/DescriptorModel.plist: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LoveJessyChen/Nugget-iOS/eb917fe4a75ef61eca7e098b7ad2afcc736a2515/files/posterboard/1F20C883-EA98-4CCE-9923-0C9A01359721/versions/0/contents/DescriptorModel.plist -------------------------------------------------------------------------------- /files/posterboard/1F20C883-EA98-4CCE-9923-0C9A01359721/versions/0/contents/com.apple.posterkit.provider.contents.galleryOptions: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LoveJessyChen/Nugget-iOS/eb917fe4a75ef61eca7e098b7ad2afcc736a2515/files/posterboard/1F20C883-EA98-4CCE-9923-0C9A01359721/versions/0/contents/com.apple.posterkit.provider.contents.galleryOptions -------------------------------------------------------------------------------- /files/posterboard/1F20C883-EA98-4CCE-9923-0C9A01359721/versions/0/contents/com.apple.posterkit.provider.contents.userInfo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LoveJessyChen/Nugget-iOS/eb917fe4a75ef61eca7e098b7ad2afcc736a2515/files/posterboard/1F20C883-EA98-4CCE-9923-0C9A01359721/versions/0/contents/com.apple.posterkit.provider.contents.userInfo -------------------------------------------------------------------------------- /files/posterboard/VideoCAML/com.apple.posterkit.provider.descriptor.identifier: -------------------------------------------------------------------------------- 1 | 9183 -------------------------------------------------------------------------------- /files/posterboard/VideoCAML/com.apple.posterkit.role.identifier: -------------------------------------------------------------------------------- 1 | PRPosterRoleLockScreen -------------------------------------------------------------------------------- /files/posterboard/VideoCAML/providerInfo.plist: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LoveJessyChen/Nugget-iOS/eb917fe4a75ef61eca7e098b7ad2afcc736a2515/files/posterboard/VideoCAML/providerInfo.plist -------------------------------------------------------------------------------- /files/posterboard/VideoCAML/versions/1/contents/9183.Custom-810w-1080h@2x~ipad.wallpaper/9183.Custom_Floating-810w-1080h@2x~ipad.ca/index.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | assetManifest 6 | assetManifest.caml 7 | documentHeight 8 | 3176 9 | documentResizesToView 10 | 11 | documentWidth 12 | 3176 13 | dynamicGuidesEnabled 14 | 15 | geometryFlipped 16 | 17 | guidesEnabled 18 | 19 | interactiveMouseEventsEnabled 20 | 21 | interactiveShowsCursor 22 | 23 | interactiveTouchEventsEnabled 24 | 25 | loopEnd 26 | 0.0 27 | loopStart 28 | 0.0 29 | loopingEnabled 30 | 31 | multitouchDisablesMouse 32 | 33 | multitouchEnabled 34 | 35 | presentationMouseEventsEnabled 36 | 37 | presentationShowsCursor 38 | 39 | presentationTouchEventsEnabled 40 | 41 | rootDocument 42 | main.caml 43 | savesWindowFrame 44 | 45 | scalesToFitInPlayer 46 | 47 | showsTouches 48 | 49 | snappingEnabled 50 | 51 | timelineMarkers 52 | [(null)] 53 | touchesColor 54 | 1 1 0 0.8 55 | unitsInPixelsInPlayer 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /files/posterboard/VideoCAML/versions/1/contents/9183.Custom-810w-1080h@2x~ipad.wallpaper/9183.Custom_Floating-810w-1080h@2x~ipad.ca/main.caml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 40 | 41 | -------------------------------------------------------------------------------- /files/posterboard/VideoCAML/versions/1/contents/9183.Custom-810w-1080h@2x~ipad.wallpaper/Wallpaper.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | appearanceAware 6 | 7 | assets 8 | 9 | lockAndHome 10 | 11 | default 12 | 13 | backgroundAnimationFileName 14 | 9183.Custom_Background-810w-1080h@2x~ipad.ca 15 | floatingAnimationFileNameKey 16 | 9183.Custom_Floating-810w-1080h@2x~ipad.ca 17 | identifier 18 | 9183 19 | name 20 | Chip 21 | type 22 | LayeredAnimation 23 | 24 | 25 | 26 | contentVersion 27 | 2.01 28 | family 29 | Chip 30 | identifier 31 | 9183 32 | logicalScreenClass 33 | 810w-1080h@2x~ipad 34 | name 35 | Chip 36 | preferredProminentColor 37 | 38 | dark 39 | #00000 40 | default 41 | #FFFFFF 42 | 43 | version 44 | 1 45 | 46 | 47 | -------------------------------------------------------------------------------- /files/posterboard/VideoCAML/versions/1/contents/com.apple.posterkit.provider.contents.userInfo: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | posterEnvironmentOverrides 6 | 7 | e30= 8 | 9 | wallpaperRepresentingFileName 10 | 9183.Custom-810w-1080h@2x~ipad.wallpaper 11 | wallpaperRepresentingIdentifier 12 | 9999 13 | 14 | 15 | -------------------------------------------------------------------------------- /files/posterboard/contents.plist: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LoveJessyChen/Nugget-iOS/eb917fe4a75ef61eca7e098b7ad2afcc736a2515/files/posterboard/contents.plist -------------------------------------------------------------------------------- /gui/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LoveJessyChen/Nugget-iOS/eb917fe4a75ef61eca7e098b7ad2afcc736a2515/gui/__init__.py -------------------------------------------------------------------------------- /gui/apply_worker.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtCore import Signal, QThread 2 | from PySide6.QtWidgets import QMessageBox 3 | 4 | class ApplyAlertMessage: 5 | def __init__(self, txt: str, title: str = "Error!", icon = QMessageBox.Critical, detailed_txt: str = None): 6 | self.txt = txt 7 | self.title = title 8 | self.icon = icon 9 | self.detailed_txt = detailed_txt 10 | 11 | class ApplyThread(QThread): 12 | progress = Signal(str) 13 | alert = Signal(ApplyAlertMessage) 14 | 15 | def update_label(self, txt: str): 16 | self.progress.emit(txt) 17 | def alert_window(self, msg: ApplyAlertMessage): 18 | self.alert.emit(msg) 19 | 20 | def __init__(self, manager, resetting: bool = False): 21 | super().__init__() 22 | self.manager = manager 23 | self.resetting = resetting 24 | 25 | def do_work(self): 26 | self.manager.apply_changes(self.resetting, self.update_label, self.alert_window) 27 | 28 | def run(self): 29 | self.do_work() 30 | 31 | class RefreshDevicesThread(QThread): 32 | alert = Signal(ApplyAlertMessage) 33 | 34 | def alert_window(self, msg: ApplyAlertMessage): 35 | self.alert.emit(msg) 36 | 37 | def __init__(self, manager, settings): 38 | super().__init__() 39 | self.manager = manager 40 | self.settings = settings 41 | 42 | def do_work(self): 43 | self.manager.get_devices(self.settings, self.alert_window) 44 | 45 | def run(self): 46 | self.do_work() -------------------------------------------------------------------------------- /gui/custom_qt_elements/multicombobox.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtGui import QStandardItemModel, QStandardItem 2 | from PySide6.QtWidgets import QComboBox 3 | from PySide6.QtCore import Qt 4 | 5 | 6 | class MultiComboBox(QComboBox): 7 | def __init__(self, parent=None, updateAction=lambda x: None): 8 | super().__init__(parent) 9 | self.setEditable(True) 10 | self.lineEdit().setReadOnly(True) 11 | self.setModel(QStandardItemModel(self)) 12 | self.updateAction = updateAction 13 | 14 | # Connect to the dataChanged signal to update the text 15 | self.model().dataChanged.connect(self.updateText) 16 | 17 | def addItem(self, text: str, data=None): 18 | item = QStandardItem() 19 | item.setText(text) 20 | item.setEnabled(True) 21 | item.setFlags(Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsUserCheckable) 22 | item.setData(Qt.CheckState.Unchecked, Qt.ItemDataRole.CheckStateRole) 23 | self.model().appendRow(item) 24 | 25 | def addItems(self, items_list: list): 26 | for text in items_list: 27 | self.addItem(text) 28 | 29 | def deselectAll(self): 30 | for i in range(self.model().rowCount()): 31 | self.model().item(i).setCheckState(Qt.CheckState.Unchecked) 32 | 33 | def selectIndices(self, indices: list[int]): 34 | for idx in indices: 35 | self.model().item(idx).setCheckState(Qt.CheckState.Checked) 36 | 37 | def updateText(self): 38 | selected_items = [self.model().item(i).text() for i in range(self.model().rowCount()) 39 | if self.model().item(i).checkState() == Qt.CheckState.Checked] 40 | if len(selected_items) == 0: 41 | self.lineEdit().setText(" None") 42 | elif len(selected_items) == 1: 43 | self.lineEdit().setText(f" {selected_items[0]}") 44 | else: 45 | self.lineEdit().setText(f" ({len(selected_items)})") 46 | if self.updateAction != None: 47 | self.updateAction(selected_items) 48 | 49 | def showPopup(self): 50 | super().showPopup() 51 | # Set the state of each item in the dropdown 52 | for i in range(self.model().rowCount()): 53 | item = self.model().item(i) 54 | combo_box_view = self.view() 55 | combo_box_view.setRowHidden(i, False) 56 | check_box = combo_box_view.indexWidget(item.index()) 57 | if check_box: 58 | check_box.setChecked(item.checkState() == Qt.CheckState.Checked) 59 | 60 | def hidePopup(self): 61 | # Update the check state of each item based on the checkbox state 62 | for i in range(self.model().rowCount()): 63 | item = self.model().item(i) 64 | combo_box_view = self.view() 65 | check_box = combo_box_view.indexWidget(item.index()) 66 | if check_box: 67 | item.setCheckState(Qt.CheckState.Checked if check_box.isChecked() else Qt.CheckState.Unchecked) 68 | super().hidePopup() -------------------------------------------------------------------------------- /gui/custom_qt_elements/resizable_image_label.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtWidgets import QLabel 2 | from PySide6.QtGui import QPixmap 3 | from PySide6.QtCore import Qt, QSize 4 | 5 | class ResizableImageLabel(QLabel): 6 | def __init__(self, parent=None): 7 | super().__init__(parent) 8 | self.original_pixmap = None 9 | # self.setScaledContents(True) 10 | 11 | def setPixmap(self, pixmap: QPixmap): 12 | self.original_pixmap = pixmap 13 | self.update_scaled_pixmap() 14 | 15 | def resizeEvent(self, event): 16 | if self.original_pixmap: 17 | self.update_scaled_pixmap() 18 | super().resizeEvent(event) 19 | 20 | def update_scaled_pixmap(self): 21 | if self.original_pixmap: 22 | scaled_pixmap = self.original_pixmap.scaled( 23 | self.width(), 24 | self.height(), 25 | Qt.AspectRatioMode.KeepAspectRatio, 26 | Qt.TransformationMode.SmoothTransformation 27 | ) 28 | super().setPixmap(scaled_pixmap) -------------------------------------------------------------------------------- /gui/dialogs.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtWidgets import QDialog, QDialogButtonBox, QLabel, QVBoxLayout, QHBoxLayout, QWidget, QToolButton, QSizePolicy 2 | from PySide6.QtGui import QFont, QIcon, QPixmap 3 | from PySide6.QtCore import QSize 4 | 5 | from webbrowser import open_new_tab 6 | 7 | from controllers.web_request_handler import Nugget_Repo, get_latest_version 8 | 9 | class GestaltDialog(QDialog): 10 | def __init__(self, device_manager, gestalt_label, selected_file, parent=None): 11 | super().__init__(parent) 12 | self.device_manager = device_manager 13 | self.gestalt_label = gestalt_label 14 | self.selected_file = selected_file 15 | 16 | QBtn = ( 17 | QDialogButtonBox.Ok | QDialogButtonBox.Cancel 18 | ) 19 | 20 | self.buttonBox = QDialogButtonBox(QBtn) 21 | self.buttonBox.accepted.connect(self.accept) 22 | self.buttonBox.rejected.connect(self.reject) 23 | 24 | layout = QVBoxLayout() 25 | message = QLabel("The gestalt file looks like it was made for a different device.\nAre you sure you want to use this one?") 26 | layout.addWidget(message) 27 | layout.addWidget(self.buttonBox) 28 | self.setLayout(layout) 29 | 30 | def accept(self): 31 | self.device_manager.data_singleton.gestalt_path = self.selected_file 32 | self.gestalt_label.setText(self.selected_file) 33 | super().accept() 34 | 35 | 36 | class PBHelpDialog(QDialog): 37 | def __init__(self, parent=None): 38 | super().__init__(parent) 39 | QBtn = ( 40 | QDialogButtonBox.Ok 41 | ) 42 | self.buttonBox = QDialogButtonBox(QBtn) 43 | self.buttonBox.accepted.connect(self.accept) 44 | self.setWindowTitle("PosterBoard Info") 45 | 46 | layout = QVBoxLayout() 47 | message = QLabel("Descriptors will be under the Collections section when adding a new wallpaper.\n\nIf the wallpapers don't appear in the menu, you either have to wait a bit for them to load,\nor you've reached the maximum amount of wallpapers (15) and have to wipe them.") 48 | layout.addWidget(message) 49 | 50 | imgBox = QWidget() 51 | imgBox.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) 52 | imgBox.setStyleSheet("QWidget { background: none; padding: 0px; border: none; }") 53 | hlayout = QHBoxLayout() 54 | tut1 = QToolButton() 55 | tut1.setIconSize(QSize(138, 300)) 56 | tut1.setStyleSheet("QToolButton { background: none; padding: 0px; border: none; }") 57 | tut1.setIcon(QIcon(QPixmap(":/gui/pb_tutorial1.png"))) 58 | hlayout.addWidget(tut1) 59 | tut2 = QToolButton() 60 | tut2.setIconSize(QSize(138, 300)) 61 | tut2.setStyleSheet("QToolButton { background: none; padding: 0px; border: none; }") 62 | tut2.setIcon(QIcon(QPixmap(":/gui/pb_tutorial2.png"))) 63 | hlayout.addWidget(tut2) 64 | imgBox.setLayout(hlayout) 65 | 66 | layout.addWidget(imgBox) 67 | layout.addWidget(self.buttonBox) 68 | self.setLayout(layout) 69 | 70 | 71 | class UpdateAppDialog(QDialog): 72 | def __init__(self, parent=None): 73 | super().__init__(parent) 74 | 75 | QBtn = ( 76 | QDialogButtonBox.Ok | QDialogButtonBox.Cancel 77 | ) 78 | 79 | self.buttonBox = QDialogButtonBox(QBtn) 80 | self.buttonBox.accepted.connect(self.accept) 81 | self.buttonBox.rejected.connect(self.reject) 82 | 83 | layout = QVBoxLayout() 84 | title = QLabel("Update Available") 85 | title_font = QFont() 86 | title_font.setBold(True) 87 | title.setFont(title_font) 88 | 89 | message_text = "" 90 | latest_version = get_latest_version() 91 | if latest_version != None: 92 | message_text += f"Nugget v{latest_version} is available. " 93 | message_text += "Would you like to go to the download on GitHub?" 94 | message = QLabel(message_text) 95 | 96 | layout.addWidget(title) 97 | layout.addWidget(message) 98 | layout.addWidget(self.buttonBox) 99 | self.setLayout(layout) 100 | 101 | def accept(self): 102 | # open up the repo page 103 | open_new_tab(f"https://github.com/{Nugget_Repo}") 104 | super().accept() -------------------------------------------------------------------------------- /gui/pages/__init__.py: -------------------------------------------------------------------------------- 1 | from .main.home import HomePage as Home 2 | from .tools.posterboard import PosterboardPage as Posterboard 3 | from .tools.gestalt import GestaltPage as MobileGestalt 4 | from .tools.eligibility import EligibilityPage as Eligibility 5 | from .tools.featureflags import FeatureFlagsPage as FeatureFlags 6 | from .tools.springboard import SpringboardPage as Springboard 7 | from .tools.internal import InternalPage as Internal 8 | from .tools.daemons import DaemonsPage as Daemons 9 | from .tools.risky import RiskyPage as Risky 10 | from .main.settings import SettingsPage as Settings -------------------------------------------------------------------------------- /gui/pages/main/home.py: -------------------------------------------------------------------------------- 1 | import webbrowser 2 | 3 | from ..page import Page 4 | from qt.ui_mainwindow import Ui_Nugget 5 | 6 | from devicemanagement.constants import Version 7 | 8 | class HomePage(Page): 9 | def __init__(self, window, ui: Ui_Nugget): 10 | super().__init__() 11 | self.window = window 12 | self.ui = ui 13 | self.show_uuid = False 14 | 15 | def load_page(self): 16 | ## HOME PAGE ACTIONS 17 | self.ui.phoneVersionLbl.linkActivated.connect(self.toggle_version_label) 18 | 19 | ## HOME PAGE LINKS 20 | self.ui.bigNuggetBtn.clicked.connect(self.on_bigNuggetBtn_clicked) 21 | self.ui.starOnGithubBtn.clicked.connect(self.on_githubBtn_clicked) 22 | 23 | self.ui.leminGithubBtn.clicked.connect(self.on_leminGitHubBtn_clicked) 24 | self.ui.leminTwitterBtn.clicked.connect(self.on_leminTwitterBtn_clicked) 25 | self.ui.leminKoFiBtn.clicked.connect(self.on_leminKoFiBtn_clicked) 26 | 27 | self.ui.posterRestoreBtn.clicked.connect(self.on_posterRestoreBtn_clicked) 28 | self.ui.snoolieBtn.clicked.connect(self.on_snoolieBtn_clicked) 29 | self.ui.disfordottieBtn.clicked.connect(self.on_disfordottieBtn_clicked) 30 | self.ui.mikasaBtn.clicked.connect(self.on_mikasaBtn_clicked) 31 | 32 | self.ui.libiBtn.clicked.connect(self.on_libiBtn_clicked) 33 | self.ui.jjtechBtn.clicked.connect(self.on_jjtechBtn_clicked) 34 | self.ui.qtBtn.clicked.connect(self.on_qtBtn_clicked) 35 | 36 | self.ui.discordBtn.clicked.connect(self.on_discordBtn_clicked) 37 | 38 | ## ACTIONS 39 | def updatePhoneInfo(self): 40 | # name label 41 | self.ui.phoneNameLbl.setText(self.window.device_manager.get_current_device_name()) 42 | # version label 43 | ver = self.window.device_manager.get_current_device_version() 44 | build = self.window.device_manager.get_current_device_build() 45 | self.show_uuid = False 46 | if ver != "": 47 | self.show_version_text(version=ver, build=build) 48 | else: 49 | self.ui.phoneVersionLbl.setText("Please connect a device.") 50 | 51 | def toggle_version_label(self): 52 | if self.show_uuid: 53 | self.show_uuid = False 54 | ver = self.window.device_manager.get_current_device_version() 55 | build = self.window.device_manager.get_current_device_build() 56 | if ver != "": 57 | self.show_version_text(version=ver, build=build) 58 | else: 59 | self.show_uuid = True 60 | uuid = self.window.device_manager.get_current_device_uuid() 61 | if uuid != "": 62 | self.ui.phoneVersionLbl.setText(f"{uuid}") 63 | 64 | def show_version_text(self, version: str, build: str): 65 | support_str: str = "Supported!" 66 | if Version(version) < Version("17.0"): 67 | support_str = "Not Supported." 68 | elif self.window.device_manager.get_current_device_patched(): 69 | # sparserestore fully patched 70 | support_str = "Partially Supported." 71 | self.ui.phoneVersionLbl.setText(f"iOS {version} ({build}) {support_str}") 72 | 73 | ## HOME PAGE LINKS 74 | def on_bigMilkBtn_clicked(self): 75 | webbrowser.open_new_tab("https://cowabun.ga") 76 | 77 | def on_leminGitHubBtn_clicked(self): 78 | webbrowser.open_new_tab("https://github.com/leminlimez") 79 | def on_leminTwitterBtn_clicked(self): 80 | webbrowser.open_new_tab("https://twitter.com/LeminLimez") 81 | def on_leminKoFiBtn_clicked(self): 82 | webbrowser.open_new_tab("https://ko-fi.com/leminlimez") 83 | 84 | def on_posterRestoreBtn_clicked(self): 85 | webbrowser.open_new_tab("https://discord.gg/gWtzTVhMvh") 86 | def on_snoolieBtn_clicked(self): 87 | webbrowser.open_new_tab("https://github.com/0xilis/python-aar-stuff") 88 | def on_disfordottieBtn_clicked(self): 89 | webbrowser.open_new_tab("https://twitter.com/disfordottie") 90 | def on_mikasaBtn_clicked(self): 91 | webbrowser.open_new_tab("https://github.com/Mikasa-san/QuietDaemon") 92 | 93 | def on_libiBtn_clicked(self): 94 | webbrowser.open_new_tab("https://github.com/doronz88/pymobiledevice3") 95 | def on_jjtechBtn_clicked(self): 96 | webbrowser.open_new_tab("https://github.com/JJTech0130/TrollRestore") 97 | def on_qtBtn_clicked(self): 98 | webbrowser.open_new_tab("https://www.qt.io/product/development-tools") 99 | 100 | def on_discordBtn_clicked(self): 101 | webbrowser.open_new_tab("https://discord.gg/MN8JgqSAqT") 102 | def on_githubBtn_clicked(self): 103 | webbrowser.open_new_tab("https://github.com/leminlimez/Nugget") 104 | def on_bigNuggetBtn_clicked(self): 105 | webbrowser.open_new_tab("https://cowabun.ga") -------------------------------------------------------------------------------- /gui/pages/main/settings.py: -------------------------------------------------------------------------------- 1 | from ..page import Page 2 | from qt.ui_mainwindow import Ui_Nugget 3 | 4 | from tweaks.tweaks import tweaks 5 | from controllers.video_handler import set_ignore_frame_limit 6 | 7 | class SettingsPage(Page): 8 | def __init__(self, window, ui: Ui_Nugget): 9 | super().__init__() 10 | self.window = window 11 | self.ui = ui 12 | 13 | def load_page(self): 14 | self.ui.allowWifiApplyingChk.toggled.connect(self.on_allowWifiApplyingChk_toggled) 15 | self.ui.autoRebootChk.toggled.connect(self.on_autoRebootChk_toggled) 16 | self.ui.showRiskyChk.toggled.connect(self.on_showRiskyChk_toggled) 17 | self.ui.showAllSpoofableChk.toggled.connect(self.on_showAllSpoofableChk_toggled) 18 | 19 | self.ui.revertRdarChk.toggled.connect(self.on_revertRdarChk_toggled) 20 | 21 | self.ui.skipSetupChk.toggled.connect(self.on_skipSetupChk_toggled) 22 | self.ui.supervisionChk.toggled.connect(self.on_supervisionChk_toggled) 23 | self.ui.supervisionOrganization.textEdited.connect(self.on_supervisionOrgTxt_textEdited) 24 | self.ui.resetPairBtn.clicked.connect(self.on_resetPairBtn_clicked) 25 | 26 | ## ACTIONS 27 | def on_allowWifiApplyingChk_toggled(self, checked: bool): 28 | self.window.device_manager.apply_over_wifi = checked 29 | # save the setting 30 | self.window.settings.setValue("apply_over_wifi", checked) 31 | def on_showRiskyChk_toggled(self, checked: bool): 32 | self.window.device_manager.allow_risky_tweaks = checked 33 | # save the setting 34 | self.window.settings.setValue("show_risky_tweaks", checked) 35 | # toggle the button visibility 36 | if checked: 37 | self.ui.advancedPageBtn.show() 38 | self.ui.ignorePBFrameLimitChk.show() 39 | try: 40 | self.ui.resetPBDrp.removeItem(4) 41 | except: 42 | pass 43 | self.ui.resetPBDrp.addItem("PB Extensions") 44 | else: 45 | self.ui.advancedPageBtn.hide() 46 | self.ui.ignorePBFrameLimitChk.hide() 47 | try: 48 | self.ui.resetPBDrp.removeItem(4) 49 | except: 50 | pass 51 | def on_ignorePBFrameLimitChk_toggled(self, checked: bool): 52 | set_ignore_frame_limit(checked) 53 | # save the setting 54 | self.window.settings.setValue("ignore_pb_frame_limit", checked) 55 | def on_showAllSpoofableChk_toggled(self, checked: bool): 56 | self.window.device_manager.show_all_spoofable_models = checked 57 | # save the setting 58 | self.window.settings.setValue("show_all_spoofable_models", checked) 59 | # refresh the list of spoofable models 60 | self.window.setup_spoofedModelDrp_models() 61 | def on_autoRebootChk_toggled(self, checked: bool): 62 | self.window.device_manager.auto_reboot = checked 63 | # save the setting 64 | self.window.settings.setValue("auto_reboot", checked) 65 | 66 | def on_revertRdarChk_toggled(self, checked: bool): 67 | tweaks["RdarFix"].set_enabled(checked) 68 | 69 | def on_skipSetupChk_toggled(self, checked: bool): 70 | self.window.device_manager.skip_setup = checked 71 | # save the setting 72 | self.window.settings.setValue("skip_setup", checked) 73 | # hide/show the warning label 74 | if checked: 75 | self.ui.skipSetupOnLbl.show() 76 | else: 77 | self.ui.skipSetupOnLbl.hide() 78 | def on_supervisionOrgTxt_textEdited(self, text: str): 79 | self.window.device_manager.organization_name = text 80 | self.window.settings.setValue("organization_name", text) 81 | def on_supervisionChk_toggled(self, checked: bool): 82 | self.window.device_manager.supervised = checked 83 | # save the setting 84 | self.window.settings.setValue("supervised", checked) 85 | 86 | # Device Options 87 | def on_resetPairBtn_clicked(self): 88 | self.window.device_manager.reset_device_pairing() -------------------------------------------------------------------------------- /gui/pages/page.py: -------------------------------------------------------------------------------- 1 | class Page: 2 | def __init__(self): 3 | self.loaded = False 4 | 5 | def load(self): 6 | if not self.loaded: 7 | self.load_page() 8 | self.loaded = True 9 | 10 | def load_page(self): 11 | raise NotImplementedError -------------------------------------------------------------------------------- /gui/pages/tools/daemons.py: -------------------------------------------------------------------------------- 1 | from ..page import Page 2 | from qt.ui_mainwindow import Ui_Nugget 3 | 4 | from tweaks.tweaks import tweaks 5 | from tweaks.daemons_tweak import Daemon 6 | 7 | class DaemonsPage(Page): 8 | def __init__(self, ui: Ui_Nugget): 9 | super().__init__() 10 | self.ui = ui 11 | 12 | def load_page(self): 13 | self.ui.modifyDaemonsChk.toggled.connect(self.on_modifyDaemonsChk_clicked) 14 | self.ui.thermalmonitordChk.toggled.connect(self.on_thermalmonitordChk_clicked) 15 | self.ui.otadChk.toggled.connect(self.on_otadChk_clicked) 16 | self.ui.usageTrackingAgentChk.toggled.connect(self.on_usageTrackingAgentChk_clicked) 17 | 18 | self.ui.gameCenterChk.toggled.connect(self.on_gameCenterChk_clicked) 19 | self.ui.screenTimeChk.toggled.connect(self.on_screenTimeChk_clicked) 20 | self.ui.clearScreenTimeAgentChk.toggled.connect(self.on_clearScreenTimeAgentChk_clicked) 21 | self.ui.crashReportsChk.toggled.connect(self.on_crashReportsChk_clicked) 22 | self.ui.atwakeupChk.toggled.connect(self.on_atwakeupChk_clicked) 23 | self.ui.tipsChk.toggled.connect(self.on_tipsChk_clicked) 24 | self.ui.vpndChk.toggled.connect(self.on_vpndChk_clicked) 25 | self.ui.wapicChk.toggled.connect(self.on_wapicChk_clicked) 26 | self.ui.healthdChk.toggled.connect(self.on_healthdChk_clicked) 27 | 28 | self.ui.airprintChk.toggled.connect(self.on_airprintChk_clicked) 29 | self.ui.assistiveTouchChk.toggled.connect(self.on_assistiveTouchChk_clicked) 30 | self.ui.icloudChk.toggled.connect(self.on_icloudChk_clicked) 31 | self.ui.hotspotChk.toggled.connect(self.on_hotspotChk_clicked) 32 | self.ui.passbookChk.toggled.connect(self.on_passbookChk_clicked) 33 | self.ui.spotlightChk.toggled.connect(self.on_spotlightChk_clicked) 34 | self.ui.voiceControlChk.toggled.connect(self.on_voiceControlChk_clicked) 35 | 36 | ## ACTIONS 37 | def on_modifyDaemonsChk_clicked(self, checked: bool): 38 | tweaks["Daemons"].set_enabled(checked) 39 | self.ui.daemonsPageContent.setDisabled(not checked) 40 | 41 | def on_thermalmonitordChk_clicked(self, checked: bool): 42 | tweaks["Daemons"].set_multiple_values(Daemon.thermalmonitord.value, value=checked) 43 | if checked: 44 | # set the modify toggle checked so it actually applies 45 | self.on_modifyDaemonsChk_clicked(True) 46 | def on_otadChk_clicked(self, checked: bool): 47 | tweaks["Daemons"].set_multiple_values(Daemon.OTA.value, value=checked) 48 | def on_usageTrackingAgentChk_clicked(self, checked: bool): 49 | tweaks["Daemons"].set_multiple_values(Daemon.UsageTrackingAgent.value, value=checked) 50 | def on_gameCenterChk_clicked(self, checked: bool): 51 | tweaks["Daemons"].set_multiple_values(Daemon.GameCenter.value, value=checked) 52 | def on_screenTimeChk_clicked(self, checked: bool): 53 | tweaks["Daemons"].set_multiple_values(Daemon.ScreenTime.value, value=checked) 54 | def on_clearScreenTimeAgentChk_clicked(self, checked: bool): 55 | tweaks["ClearScreenTimeAgentPlist"].set_enabled(checked) 56 | def on_crashReportsChk_clicked(self, checked: bool): 57 | tweaks["Daemons"].set_multiple_values(Daemon.CrashReports.value, value=checked) 58 | def on_atwakeupChk_clicked(self, checked: bool): 59 | tweaks["Daemons"].set_multiple_values(Daemon.ATWAKEUP.value, value=checked) 60 | def on_tipsChk_clicked(self, checked: bool): 61 | tweaks["Daemons"].set_multiple_values(Daemon.Tips.value, value=checked) 62 | def on_vpndChk_clicked(self, checked: bool): 63 | tweaks["Daemons"].set_multiple_values(Daemon.VPN.value, value=checked) 64 | def on_wapicChk_clicked(self, checked: bool): 65 | tweaks["Daemons"].set_multiple_values(Daemon.ChineseLAN.value, value=checked) 66 | def on_healthdChk_clicked(self, checked: bool): 67 | tweaks["Daemons"].set_multiple_values(Daemon.HealthKit.value, value=checked) 68 | 69 | def on_airprintChk_clicked(self, checked: bool): 70 | tweaks["Daemons"].set_multiple_values(Daemon.AirPrint.value, value=checked) 71 | def on_assistiveTouchChk_clicked(self, checked: bool): 72 | tweaks["Daemons"].set_multiple_values(Daemon.AssistiveTouch.value, value=checked) 73 | def on_icloudChk_clicked(self, checked: bool): 74 | tweaks["Daemons"].set_multiple_values(Daemon.iCloud.value, value=checked) 75 | def on_hotspotChk_clicked(self, checked: bool): 76 | tweaks["Daemons"].set_multiple_values(Daemon.InternetTethering.value, value=checked) 77 | def on_passbookChk_clicked(self, checked: bool): 78 | tweaks["Daemons"].set_multiple_values(Daemon.PassBook.value, value=checked) 79 | def on_spotlightChk_clicked(self, checked: bool): 80 | tweaks["Daemons"].set_multiple_values(Daemon.Spotlight.value, value=checked) 81 | def on_voiceControlChk_clicked(self, checked: bool): 82 | tweaks["Daemons"].set_multiple_values(Daemon.VoiceControl.value, value=checked) -------------------------------------------------------------------------------- /gui/pages/tools/eligibility.py: -------------------------------------------------------------------------------- 1 | from ..page import Page 2 | from qt.ui_mainwindow import Ui_Nugget 3 | 4 | from tweaks.tweaks import tweaks 5 | 6 | class EligibilityPage(Page): 7 | def __init__(self, window, ui: Ui_Nugget): 8 | super().__init__() 9 | self.window = window 10 | self.ui = ui 11 | 12 | def load_page(self): 13 | self.ui.euEnablerEnabledChk.toggled.connect(self.on_euEnablerEnabledChk_toggled) 14 | self.ui.methodChoiceDrp.activated.connect(self.on_methodChoiceDrp_activated) 15 | self.ui.regionCodeTxt.textEdited.connect(self.on_regionCodeTxt_textEdited) 16 | 17 | self.ui.enableAIChk.toggled.connect(self.on_enableAIChk_toggled) 18 | self.ui.eligFileChk.toggled.connect(self.on_eligFileChk_toggled) 19 | self.ui.languageTxt.hide() # to be removed later 20 | self.ui.languageLbl.hide() # to be removed later 21 | self.ui.languageTxt.textEdited.connect(self.on_languageTxt_textEdited) 22 | self.ui.spoofedModelDrp.activated.connect(self.on_spoofedModelDrp_activated) 23 | self.ui.spoofHardwareChk.toggled.connect(self.on_spoofHardwareChk_toggled) 24 | self.ui.spoofCPUChk.toggled.connect(self.on_spoofCPUChk_toggled) 25 | 26 | self.ui.aiEnablerContent.hide() 27 | 28 | ## ACTIONS 29 | def on_euEnablerEnabledChk_toggled(self, checked: bool): 30 | tweaks["EUEnabler"].set_enabled(checked) 31 | def on_methodChoiceDrp_activated(self, index: int): 32 | tweaks["EUEnabler"].set_selected_option(index) 33 | def on_regionCodeTxt_textEdited(self, text: str): 34 | tweaks["EUEnabler"].set_region_code(text) 35 | 36 | def on_enableAIChk_toggled(self, checked: bool): 37 | # tweaks["AIEligibility"].set_enabled(checked) 38 | tweaks["AIGestalt"].set_enabled(checked) 39 | # change the visibility of stuff 40 | if checked: 41 | self.ui.aiEnablerContent.show() 42 | else: 43 | self.ui.aiEnablerContent.hide() 44 | 45 | def on_eligFileChk_toggled(self, checked: bool): 46 | tweaks["AIEligibility"].set_enabled(checked) 47 | if checked: 48 | self.ui.languageTxt.show() 49 | self.ui.languageLbl.show() 50 | else: 51 | self.ui.languageTxt.hide() 52 | self.ui.languageLbl.hide() 53 | 54 | def on_languageTxt_textEdited(self, text: str): 55 | tweaks["AIEligibility"].set_language_code(text) 56 | 57 | def on_spoofedModelDrp_activated(self, index: int): 58 | idx_to_apply = index 59 | if not self.window.device_manager.show_all_spoofable_models and not self.window.device_manager.get_current_device_model().startswith("iPhone"): 60 | # offset the index for ipads 61 | idx_to_apply += 6 62 | tweaks["SpoofModel"].set_selected_option(idx_to_apply, is_enabled=(index != 0)) 63 | tweaks["SpoofHardware"].set_selected_option(idx_to_apply, is_enabled=(index != 0 and self.ui.spoofHardwareChk.isChecked())) 64 | tweaks["SpoofCPU"].set_selected_option(idx_to_apply, is_enabled=(index != 0 and self.ui.spoofCPUChk.isChecked())) 65 | def on_spoofHardwareChk_toggled(self, checked: bool): 66 | tweaks["SpoofHardware"].set_enabled(checked and tweaks["SpoofHardware"].selected_option != 0) 67 | def on_spoofCPUChk_toggled(self, checked: bool): 68 | tweaks["SpoofCPU"].set_enabled(checked and tweaks["SpoofCPU"].selected_option != 0) -------------------------------------------------------------------------------- /gui/pages/tools/featureflags.py: -------------------------------------------------------------------------------- 1 | from ..page import Page 2 | from qt.ui_mainwindow import Ui_Nugget 3 | 4 | from tweaks.tweaks import tweaks 5 | 6 | class FeatureFlagsPage(Page): 7 | def __init__(self, ui: Ui_Nugget): 8 | super().__init__() 9 | self.ui = ui 10 | 11 | def load_page(self): 12 | self.ui.clockAnimChk.toggled.connect(self.on_clockAnimChk_toggled) 13 | self.ui.lockscreenChk.toggled.connect(self.on_lockscreenChk_clicked) 14 | self.ui.photosChk.toggled.connect(self.on_photosChk_clicked) 15 | self.ui.aiChk.toggled.connect(self.on_aiChk_clicked) 16 | 17 | ## ACTIONS 18 | def on_clockAnimChk_toggled(self, checked: bool): 19 | tweaks["ClockAnim"].set_enabled(checked) 20 | def on_lockscreenChk_clicked(self, checked: bool): 21 | tweaks["Lockscreen"].set_enabled(checked) 22 | 23 | def on_photosChk_clicked(self, checked: bool): 24 | tweaks["PhotoUI"].set_enabled(checked) 25 | def on_aiChk_clicked(self, checked: bool): 26 | tweaks["AI"].set_enabled(checked) -------------------------------------------------------------------------------- /gui/pages/tools/gestalt.py: -------------------------------------------------------------------------------- 1 | from PySide6 import QtCore, QtWidgets, QtGui 2 | 3 | from ..page import Page 4 | from qt.ui_mainwindow import Ui_Nugget 5 | 6 | from tweaks.tweaks import tweaks 7 | from tweaks.custom_gestalt_tweaks import CustomGestaltTweaks, ValueTypeStrings 8 | 9 | class GestaltPage(Page): 10 | def __init__(self, window, ui: Ui_Nugget): 11 | super().__init__() 12 | self.window = window 13 | self.ui = ui 14 | 15 | def load_page(self): 16 | self.ui.dynamicIslandDrp.activated.connect(self.on_dynamicIslandDrp_activated) 17 | self.ui.rdarFixChk.clicked.connect(self.on_rdarFixChk_clicked) 18 | self.ui.modelNameChk.toggled.connect(self.on_modelNameChk_clicked) 19 | self.ui.modelNameTxt.textEdited.connect(self.on_modelNameTxt_textEdited) 20 | 21 | self.ui.bootChimeChk.clicked.connect(self.on_bootChimeChk_clicked) 22 | self.ui.chargeLimitChk.clicked.connect(self.on_chargeLimitChk_clicked) 23 | self.ui.tapToWakeChk.clicked.connect(self.on_tapToWakeChk_clicked) 24 | self.ui.iphone16SettingsChk.clicked.connect(self.on_iphone16SettingsChk_clicked) 25 | self.ui.parallaxChk.clicked.connect(self.on_parallaxChk_clicked) 26 | self.ui.stageManagerChk.clicked.connect(self.on_stageManagerChk_clicked) 27 | self.ui.enableMedusaChk.clicked.connect(self.on_enableMedusaChk_clicked) 28 | self.ui.ipadAppsChk.clicked.connect(self.on_ipadAppsChk_clicked) 29 | self.ui.shutterChk.clicked.connect(self.on_shutterChk_clicked) 30 | self.ui.findMyFriendsChk.clicked.connect(self.on_findMyFriendsChk_clicked) 31 | self.ui.pencilChk.clicked.connect(self.on_pencilChk_clicked) 32 | self.ui.actionButtonChk.clicked.connect(self.on_actionButtonChk_clicked) 33 | 34 | self.ui.internalInstallChk.clicked.connect(self.on_internalInstallChk_clicked) 35 | self.ui.internalStorageChk.clicked.connect(self.on_internalStorageChk_clicked) 36 | self.ui.collisionSOSChk.clicked.connect(self.on_collisionSOSChk_clicked) 37 | self.ui.aodChk.clicked.connect(self.on_aodChk_clicked) 38 | self.ui.aodVibrancyChk.clicked.connect(self.on_aodVibrancyChk_clicked) 39 | 40 | self.ui.addGestaltKeyBtn.clicked.connect(self.on_addGestaltKeyBtn_clicked) 41 | 42 | def setup_spoofedModelDrp_models(self): 43 | # hide all the models first 44 | for i in range(1, self.ui.spoofedModelDrp.count()): 45 | try: 46 | self.ui.spoofedModelDrp.removeItem(1) 47 | except: 48 | pass 49 | # indexes 1-6 for iPhones, 7-(len(values) - 1) for iPads 50 | # TODO: Make this get fetched from the gui on app startup 51 | spoof_drp_options = ["iPhone 15 Pro (iPhone16,1)", "iPhone 15 Pro Max (iPhone16,2)", "iPhone 16 (iPhone17,3)", "iPhone 16 Plus (iPhone17,4)", "iPhone 16 Pro (iPhone17,1)", "iPhone 16 Pro Max (iPhone17,2)", "iPad Mini (A17 Pro) (W) (iPad16,1)", "iPad Mini (A17 Pro) (C) (iPad16,2)", "iPad Pro (13-inch) (M4) (W) (iPad16,5)", "iPad Pro (13-inch) (M4) (C) (iPad16,6)", "iPad Pro (11-inch) (M4) (W) (iPad16,3)", "iPad Pro (11-inch) (M4) (C) (iPad16,4)", "iPad Pro (12.9-inch) (M2) (W) (iPad14,5)", "iPad Pro (12.9-inch) (M2) (C) (iPad14,6)", "iPad Pro (11-inch) (M2) (W) (iPad14,3)", "iPad Pro (11-inch) (M2) (C) (iPad14,4)", "iPad Air (13-inch) (M2) (W) (iPad14,10)", "iPad Air (13-inch) (M2) (C) (iPad14,11)", "iPad Air (11-inch) (M2) (W) (iPad14,8)", "iPad Air (11-inch) (M2) (C) (iPad14,9)", "iPad Pro (11-inch) (M1) (W) (iPad13,4)", "iPad Pro (11-inch) (M1) (C) (iPad13,5)", "iPad Pro (12.9-inch) (M1) (W) (iPad13,8)", "iPad Pro (12.9-inch) (M1) (C) (iPad13,9)", "iPad Air (M1) (W) (iPad13,16)", "iPad Air (M1) (C) (iPad13,17)"] 52 | if self.window.device_manager.show_all_spoofable_models or self.window.device_manager.get_current_device_model().startswith("iPhone"): 53 | # re-enable iPhone spoof models 54 | self.ui.spoofedModelDrp.addItems(spoof_drp_options[:6]) 55 | if self.window.device_manager.show_all_spoofable_models or self.window.device_manager.get_current_device_model().startswith("iPad"): 56 | # re-enable iPad spoof models 57 | self.ui.spoofedModelDrp.addItems(spoof_drp_options[6:]) 58 | 59 | ## ACTIONS 60 | def set_rdar_fix_label(self): 61 | rdar_title = tweaks["RdarFix"].get_rdar_title() 62 | if rdar_title == "hide": 63 | self.ui.rdarFixChk.hide() 64 | else: 65 | self.ui.rdarFixChk.show() 66 | self.ui.rdarFixChk.setText(f"{rdar_title} (modifies resolution)") 67 | 68 | def on_dynamicIslandDrp_activated(self, index: int): 69 | if index == 0: 70 | tweaks["DynamicIsland"].set_enabled(False) 71 | tweaks["RdarFix"].set_di_type(-1) 72 | else: 73 | # disable X gestures on devices other than iPhone SEs 74 | # the lazy way, better option would be to remove it from the menu but I didn't want to rework all that 75 | model = self.window.device_manager.get_current_device_model() 76 | if index != 1 or (model == "iPhone12,8" or model == "iPhone14,6"): 77 | tweaks["DynamicIsland"].set_selected_option(index - 1) 78 | else: 79 | tweaks["DynamicIsland"].set_enabled(False) 80 | tweaks["RdarFix"].set_di_type(tweaks["DynamicIsland"].value[tweaks["DynamicIsland"].get_selected_option()]) 81 | self.set_rdar_fix_label() 82 | def on_rdarFixChk_clicked(self, checked: bool): 83 | tweaks["RdarFix"].set_enabled(checked) 84 | 85 | def on_modelNameChk_clicked(self, checked: bool): 86 | tweaks["ModelName"].set_enabled(checked) 87 | def on_modelNameTxt_textEdited(self, text: str): 88 | tweaks["ModelName"].set_value(text, toggle_enabled=False) 89 | 90 | def on_bootChimeChk_clicked(self, checked: bool): 91 | tweaks["BootChime"].set_enabled(checked) 92 | def on_chargeLimitChk_clicked(self, checked: bool): 93 | tweaks["ChargeLimit"].set_enabled(checked) 94 | def on_tapToWakeChk_clicked(self, checked: bool): 95 | tweaks["TapToWake"].set_enabled(checked) 96 | def on_iphone16SettingsChk_clicked(self, checked: bool): 97 | tweaks["CameraButton"].set_enabled(checked) 98 | def on_parallaxChk_clicked(self, checked: bool): 99 | tweaks["Parallax"].set_enabled(checked) 100 | 101 | def on_stageManagerChk_clicked(self, checked: bool): 102 | tweaks["StageManager"].set_enabled(checked) 103 | def on_enableMedusaChk_clicked(self, checked: bool): 104 | tweaks["Medusa"].set_enabled(checked) 105 | def on_ipadAppsChk_clicked(self, checked: bool): 106 | tweaks["iPadApps"].set_enabled(checked) 107 | def on_shutterChk_clicked(self, checked: bool): 108 | # TODO: allow the user to select the region 109 | tweaks["Shutter"].set_enabled(checked) 110 | def on_findMyFriendsChk_clicked(self, checked: bool): 111 | tweaks["FindMyFriends"].set_enabled(checked) 112 | def on_pencilChk_clicked(self, checked: bool): 113 | tweaks["Pencil"].set_enabled(checked) 114 | def on_actionButtonChk_clicked(self, checked: bool): 115 | tweaks["ActionButton"].set_enabled(checked) 116 | 117 | def on_internalInstallChk_clicked(self, checked: bool): 118 | tweaks["InternalInstall"].set_enabled(checked) 119 | def on_internalStorageChk_clicked(self, checked: bool): 120 | tweaks["InternalStorage"].set_enabled(checked) 121 | 122 | def on_collisionSOSChk_clicked(self, checked: bool): 123 | tweaks["CollisionSOS"].set_enabled(checked) 124 | def on_aodChk_clicked(self, checked: bool): 125 | tweaks["AOD"].set_enabled(checked) 126 | def on_aodVibrancyChk_clicked(self, checked: bool): 127 | tweaks["AODVibrancy"].set_enabled(checked) 128 | 129 | def update_custom_gestalt_value_type(self, id, idx, valueField: QtWidgets.QLineEdit): 130 | new_str = CustomGestaltTweaks.set_tweak_value_type(id, idx) 131 | # update the value 132 | valueField.setText(new_str) 133 | 134 | def delete_custom_gestalt_key(self, id: int, widget: QtWidgets.QWidget): 135 | CustomGestaltTweaks.deactivate_tweak(id) 136 | self.ui.customKeysLayout.removeWidget(widget) 137 | widget.setParent(None) 138 | 139 | def on_addGestaltKeyBtn_clicked(self): 140 | # create a blank gestalt value with default value of 1 141 | key_identifier = CustomGestaltTweaks.create_tweak() 142 | 143 | widget = QtWidgets.QWidget() 144 | widget.setFixedHeight(35) 145 | widget.setStyleSheet("QWidget { background: none; border: 1px solid #3b3b3b; border-radius: 8px; }") 146 | hlayout = QtWidgets.QHBoxLayout(widget) 147 | hlayout.setContentsMargins(9, 2, 9, 2) 148 | 149 | # create the key field 150 | keyField = QtWidgets.QLineEdit(widget) 151 | # keyField.setMaximumWidth(200) 152 | keyField.setPlaceholderText("Key") 153 | keyField.setAlignment(QtCore.Qt.AlignmentFlag.AlignLeft) 154 | keyField.setTextMargins(5, 0, 5, 0) 155 | keyField.textEdited.connect(lambda txt, id=key_identifier: CustomGestaltTweaks.set_tweak_key(id, txt)) 156 | hlayout.addWidget(keyField) 157 | 158 | # create the delete button 159 | delBtn = QtWidgets.QToolButton(widget) 160 | delBtn.setIcon(QtGui.QIcon(":/icon/trash.svg")) 161 | delBtn.setStyleSheet("QToolButton { margin-right: 8px; background: none; border: none; }\nQToolButton:pressed { background: none; color: #FFFFFF; }") 162 | delBtn.clicked.connect(lambda _, id=key_identifier, w=widget: self.delete_custom_gestalt_key(id, w)) 163 | hlayout.addWidget(delBtn) 164 | 165 | # create the type dropdown 166 | valueTypeDrp = QtWidgets.QComboBox(widget) 167 | valueTypeDrp.setStyleSheet("QComboBox {\n background-color: #3b3b3b;\n border: none;\n color: #e8e8e8;\n font-size: 14px;\n padding-left: 8px;\n border-radius: 8px;\n}\n\nQComboBox::drop-down {\n image: url(:/icon/caret-down-fill.svg);\n icon-size: 16px;\n subcontrol-position: right center;\n margin-right: 8px;\n}\n\nQComboBox QAbstractItemView {\n background-color: #3b3b3b;\n outline: none;\n margin-top: 1px;\n}\n\nQComboBox QAbstractItemView::item {\n background-color: #3b3b3b;\n color: #e8e8e8;\n padding-left: 8px;\n}\n\nQComboBox QAbstractItemView::item:hover {\n background-color: #535353;\n color: #ffffff;\n}") 168 | valueTypeDrp.setFixedWidth(120) 169 | valueTypeDrp.addItems(ValueTypeStrings) 170 | valueTypeDrp.setCurrentIndex(0) 171 | 172 | # create the value edit field 173 | valueField = QtWidgets.QLineEdit(widget) 174 | valueField.setMaximumWidth(175) 175 | valueField.setPlaceholderText("Value") 176 | valueField.setText("1") 177 | valueField.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight) 178 | valueField.setTextMargins(5, 0, 5, 0) 179 | valueField.textEdited.connect(lambda txt, id=key_identifier: CustomGestaltTweaks.set_tweak_value(id, txt)) 180 | 181 | valueTypeDrp.activated.connect(lambda idx, id=key_identifier, vf=valueField: self.update_custom_gestalt_value_type(id, idx, vf)) 182 | hlayout.addWidget(valueTypeDrp) 183 | hlayout.addWidget(valueField) 184 | 185 | # add it to the main widget 186 | widget.setDisabled(False) 187 | self.ui.customKeysLayout.addWidget(widget) -------------------------------------------------------------------------------- /gui/pages/tools/internal.py: -------------------------------------------------------------------------------- 1 | from ..page import Page 2 | from qt.ui_mainwindow import Ui_Nugget 3 | 4 | from tweaks.tweaks import tweaks 5 | 6 | class InternalPage(Page): 7 | def __init__(self, ui: Ui_Nugget): 8 | super().__init__() 9 | self.ui = ui 10 | 11 | def load_page(self): 12 | self.ui.buildVersionChk.toggled.connect(self.on_buildVersionChk_clicked) 13 | self.ui.RTLChk.toggled.connect(self.on_RTLChk_clicked) 14 | self.ui.metalHUDChk.toggled.connect(self.on_metalHUDChk_clicked) 15 | self.ui.iMessageChk.toggled.connect(self.on_iMessageChk_clicked) 16 | self.ui.IDSChk.toggled.connect(self.on_IDSChk_clicked) 17 | self.ui.VCChk.toggled.connect(self.on_VCChk_clicked) 18 | self.ui.appStoreChk.toggled.connect(self.on_appStoreChk_clicked) 19 | self.ui.notesChk.toggled.connect(self.on_notesChk_clicked) 20 | self.ui.showTouchesChk.toggled.connect(self.on_showTouchesChk_clicked) 21 | self.ui.hideRespringChk.toggled.connect(self.on_hideRespringChk_clicked) 22 | self.ui.enableWakeVibrateChk.toggled.connect(self.on_enableWakeVibrateChk_clicked) 23 | self.ui.pasteSoundChk.toggled.connect(self.on_pasteSoundChk_clicked) 24 | self.ui.notifyPastesChk.toggled.connect(self.on_notifyPastesChk_clicked) 25 | 26 | ## ACTIONS 27 | def on_buildVersionChk_clicked(self, checked: bool): 28 | tweaks["SBBuildNumber"].set_enabled(checked) 29 | def on_RTLChk_clicked(self, checked: bool): 30 | tweaks["RTL"].set_enabled(checked) 31 | def on_metalHUDChk_clicked(self, checked: bool): 32 | tweaks["MetalForceHudEnabled"].set_enabled(checked) 33 | def on_iMessageChk_clicked(self, checked: bool): 34 | tweaks["iMessageDiagnosticsEnabled"].set_enabled(checked) 35 | def on_IDSChk_clicked(self, checked: bool): 36 | tweaks["IDSDiagnosticsEnabled"].set_enabled(checked) 37 | def on_VCChk_clicked(self, checked: bool): 38 | tweaks["VCDiagnosticsEnabled"].set_enabled(checked) 39 | 40 | def on_appStoreChk_clicked(self, checked: bool): 41 | tweaks["AppStoreDebug"].set_enabled(checked) 42 | def on_notesChk_clicked(self, checked: bool): 43 | tweaks["NotesDebugMode"].set_enabled(checked) 44 | 45 | def on_showTouchesChk_clicked(self, checked: bool): 46 | tweaks["BKDigitizerVisualizeTouches"].set_enabled(checked) 47 | def on_hideRespringChk_clicked(self, checked: bool): 48 | tweaks["BKHideAppleLogoOnLaunch"].set_enabled(checked) 49 | def on_enableWakeVibrateChk_clicked(self, checked: bool): 50 | tweaks["EnableWakeGestureHaptic"].set_enabled(checked) 51 | def on_pasteSoundChk_clicked(self, checked: bool): 52 | tweaks["PlaySoundOnPaste"].set_enabled(checked) 53 | def on_notifyPastesChk_clicked(self, checked: bool): 54 | tweaks["AnnounceAllPastes"].set_enabled(checked) -------------------------------------------------------------------------------- /gui/pages/tools/risky.py: -------------------------------------------------------------------------------- 1 | from ..page import Page 2 | from qt.ui_mainwindow import Ui_Nugget 3 | 4 | from tweaks.tweaks import tweaks 5 | 6 | class RiskyPage(Page): 7 | def __init__(self, ui: Ui_Nugget): 8 | super().__init__() 9 | self.ui = ui 10 | 11 | def load_page(self): 12 | self.ui.disableOTAChk.toggled.connect(self.on_disableOTAChk_clicked) 13 | self.ui.enableResolutionChk.toggled.connect(self.on_enableResolutionChk_clicked) 14 | self.ui.resHeightTxt.textEdited.connect(self.on_resHeightTxt_textEdited) 15 | self.ui.resWidthTxt.textEdited.connect(self.on_resWidthTxt_textEdited) 16 | self.ui.resChangerContent.hide() 17 | self.ui.resHeightWarningLbl.hide() 18 | self.ui.resWidthWarningLbl.hide() 19 | 20 | ## ACTIONS 21 | def on_disableOTAChk_clicked(self, checked: bool): 22 | tweaks["DisableOTAFile"].set_enabled(checked) 23 | 24 | def on_enableResolutionChk_clicked(self, checked: bool): 25 | tweaks["CustomResolution"].set_enabled(checked) 26 | # toggle the ui content 27 | if checked: 28 | self.ui.resChangerContent.show() 29 | else: 30 | self.ui.resChangerContent.hide() 31 | def on_resHeightTxt_textEdited(self, txt: str): 32 | if txt == "": 33 | # remove the canvas_height value 34 | tweaks["CustomResolution"].value.pop("canvas_height", None) 35 | self.ui.resHeightWarningLbl.hide() 36 | return 37 | try: 38 | val = int(txt) 39 | tweaks["CustomResolution"].value["canvas_height"] = val 40 | self.ui.resHeightWarningLbl.hide() 41 | except: 42 | self.ui.resHeightWarningLbl.show() 43 | def on_resWidthTxt_textEdited(self, txt: str): 44 | if txt == "": 45 | # remove the canvas_width value 46 | tweaks["CustomResolution"].value.pop("canvas_width", None) 47 | self.ui.resWidthWarningLbl.hide() 48 | return 49 | try: 50 | val = int(txt) 51 | tweaks["CustomResolution"].value["canvas_width"] = val 52 | self.ui.resWidthWarningLbl.hide() 53 | except: 54 | self.ui.resWidthWarningLbl.show() -------------------------------------------------------------------------------- /gui/pages/tools/springboard.py: -------------------------------------------------------------------------------- 1 | from ..page import Page 2 | from qt.ui_mainwindow import Ui_Nugget 3 | 4 | from tweaks.tweaks import tweaks 5 | 6 | class SpringboardPage(Page): 7 | def __init__(self, ui: Ui_Nugget): 8 | super().__init__() 9 | self.ui = ui 10 | 11 | def load_page(self): 12 | self.ui.footnoteTxt.textEdited.connect(self.on_footnoteTxt_textEdited) 13 | self.ui.disableLockRespringChk.toggled.connect(self.on_disableLockRespringChk_clicked) 14 | self.ui.disableDimmingChk.toggled.connect(self.on_disableDimmingChk_clicked) 15 | self.ui.disableBatteryAlertsChk.toggled.connect(self.on_disableBatteryAlertsChk_clicked) 16 | self.ui.disableCrumbChk.toggled.connect(self.on_disableCrumbChk_clicked) 17 | self.ui.enableSupervisionTextChk.toggled.connect(self.on_enableSupervisionTextChk_clicked) 18 | self.ui.enableAirPlayChk.toggled.connect(self.on_enableAirPlayChk_clicked) 19 | 20 | ## ACTIONS 21 | def on_footnoteTxt_textEdited(self, text: str): 22 | tweaks["LockScreenFootnote"].set_value(text, toggle_enabled=True) 23 | def on_disableLockRespringChk_clicked(self, checked: bool): 24 | tweaks["SBDontLockAfterCrash"].set_enabled(checked) 25 | def on_disableDimmingChk_clicked(self, checked: bool): 26 | tweaks["SBDontDimOrLockOnAC"].set_enabled(checked) 27 | def on_disableBatteryAlertsChk_clicked(self, checked: bool): 28 | tweaks["SBHideLowPowerAlerts"].set_enabled(checked) 29 | def on_disableCrumbChk_clicked(self, checked: bool): 30 | tweaks["SBNeverBreadcrumb"].set_enabled(checked) 31 | def on_enableSupervisionTextChk_clicked(self, checked: bool): 32 | tweaks["SBShowSupervisionTextOnLockScreen"].set_enabled(checked) 33 | def on_enableAirPlayChk_clicked(self, checked: bool): 34 | tweaks["AirplaySupport"].set_enabled(checked) -------------------------------------------------------------------------------- /gui/pb_tutorial1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LoveJessyChen/Nugget-iOS/eb917fe4a75ef61eca7e098b7ad2afcc736a2515/gui/pb_tutorial1.png -------------------------------------------------------------------------------- /gui/pb_tutorial2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LoveJessyChen/Nugget-iOS/eb917fe4a75ef61eca7e098b7ad2afcc736a2515/gui/pb_tutorial2.png -------------------------------------------------------------------------------- /gui/transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LoveJessyChen/Nugget-iOS/eb917fe4a75ef61eca7e098b7ad2afcc736a2515/gui/transparent.png -------------------------------------------------------------------------------- /icon/app-indicator.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /icon/arrow-clockwise.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /icon/arrow.clockwise.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /icon/brush.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /icon/caret-down-fill.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /icon/check-circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /icon/chevron.down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /icon/chevron.up.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /icon/compass.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /icon/currency-dollar.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /icon/discord.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /icon/export.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /icon/file-earmark-zip.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /icon/flag.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /icon/folder.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /icon/gear.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /icon/geo-alt.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /icon/github.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /icon/globe.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /icon/hdd.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /icon/heart-fill.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /icon/house.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /icon/import.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /icon/iphone-island.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /icon/pencil.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /icon/phone.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /icon/photo-stack.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /icon/photo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /icon/plus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /icon/questionmark.circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /icon/shippingbox.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /icon/star.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /icon/toggles.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /icon/trash.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /icon/twitter.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /icon/wallpaper.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /icon/wifi.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /icon/x-lg.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /main_app.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from PySide6 import QtGui, QtWidgets 3 | 4 | from gui.main_window import MainWindow 5 | from devicemanagement.device_manager import DeviceManager 6 | from tweaks.tweaks import tweaks 7 | 8 | if __name__ == "__main__": 9 | app = QtWidgets.QApplication([]) 10 | dm = DeviceManager() 11 | 12 | # set icon 13 | icon = QtGui.QIcon("nugget.ico") 14 | app.setWindowIcon(icon) 15 | 16 | widget = MainWindow(device_manager=dm) 17 | widget.resize(800, 600) 18 | widget.show() 19 | 20 | # import files from args 21 | for arg in sys.argv: 22 | if arg.endswith('.tendies'): 23 | # add tendies file 24 | tweaks["PosterBoard"].add_tendie(arg) 25 | elif arg.endswith('.batter'): 26 | # add batter file 27 | tweaks["PosterBoard"].add_template(arg) 28 | 29 | sys.exit(app.exec()) -------------------------------------------------------------------------------- /nugget.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LoveJessyChen/Nugget-iOS/eb917fe4a75ef61eca7e098b7ad2afcc736a2515/nugget.ico -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pymobiledevice3 2 | PySide6-Essentials 3 | PyInstaller 4 | ffmpeg 5 | ffmpeg-python 6 | opencv-python 7 | -------------------------------------------------------------------------------- /resources.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | credits/LeminLimez.png 4 | icon/app-indicator.svg 5 | icon/arrow-clockwise.svg 6 | icon/brush.svg 7 | icon/caret-down-fill.svg 8 | icon/check-circle.svg 9 | icon/compass.svg 10 | icon/gear.svg 11 | icon/hdd.svg 12 | icon/house.svg 13 | icon/phone.svg 14 | icon/toggles.svg 15 | icon/wifi.svg 16 | icon/x-lg.svg 17 | icon/discord.svg 18 | icon/heart-fill.svg 19 | icon/github.svg 20 | icon/twitter.svg 21 | icon/file-earmark-zip.svg 22 | icon/folder.svg 23 | icon/trash.svg 24 | icon/currency-dollar.svg 25 | icon/geo-alt.svg 26 | icon/star.svg 27 | icon/pencil.svg 28 | icon/import.svg 29 | icon/plus.svg 30 | icon/iphone-island.svg 31 | icon/flag.svg 32 | credits/big_nugget.png 33 | icon/photo-stack.svg 34 | icon/photo.svg 35 | icon/shippingbox.svg 36 | icon/wallpaper.svg 37 | icon/questionmark.circle.svg 38 | icon/globe.svg 39 | gui/pb_tutorial1.png 40 | gui/pb_tutorial2.png 41 | icon/arrow.clockwise.svg 42 | icon/chevron.down.svg 43 | icon/chevron.up.svg 44 | gui/transparent.png 45 | icon/export.svg 46 | 47 | 48 | -------------------------------------------------------------------------------- /restore/__init__.py: -------------------------------------------------------------------------------- 1 | from tempfile import TemporaryDirectory 2 | from pathlib import Path 3 | 4 | from pymobiledevice3.lockdown import create_using_usbmux 5 | from pymobiledevice3.services.mobilebackup2 import Mobilebackup2Service 6 | from pymobiledevice3.exceptions import PyMobileDevice3Exception 7 | from pymobiledevice3.services.diagnostics import DiagnosticsService 8 | from pymobiledevice3.lockdown import LockdownClient 9 | 10 | from . import backup 11 | 12 | def reboot_device(reboot: bool = False, lockdown_client: LockdownClient = None): 13 | if reboot and lockdown_client != None: 14 | print("Success! Rebooting your device...") 15 | with DiagnosticsService(lockdown_client) as diagnostics_service: 16 | diagnostics_service.restart() 17 | print("Remember to turn Find My back on!") 18 | 19 | def perform_restore(backup: backup.Backup, reboot: bool = False, lockdown_client: LockdownClient = None, progress_callback = lambda x: None): 20 | try: 21 | with TemporaryDirectory() as backup_dir: 22 | backup.write_to_directory(Path(backup_dir)) 23 | 24 | if lockdown_client == None: 25 | lockdown_client = create_using_usbmux() 26 | with Mobilebackup2Service(lockdown_client) as mb: 27 | mb.restore(backup_dir, system=True, reboot=False, copy=False, source=".", progress_callback=progress_callback, skip_apps=True) 28 | # reboot the device 29 | reboot_device(reboot, lockdown_client) 30 | except PyMobileDevice3Exception as e: 31 | if "Find My" in str(e): 32 | print("Find My must be disabled in order to use this tool.") 33 | print("Disable Find My from Settings (Settings -> [Your Name] -> Find My) and then try again.") 34 | raise e 35 | elif "crash_on_purpose" not in str(e): 36 | raise e 37 | else: 38 | reboot_device(reboot, lockdown_client) -------------------------------------------------------------------------------- /restore/backup.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from datetime import datetime 3 | import plistlib 4 | from pathlib import Path 5 | from base64 import b64decode 6 | from hashlib import sha1 7 | from . import mbdb 8 | from .mbdb import _FileMode 9 | from random import randbytes 10 | from typing import Optional 11 | 12 | # Default nugget file right 13 | # RWX:RX:RX 14 | DEFAULT = _FileMode.S_IRUSR | _FileMode.S_IWUSR | _FileMode.S_IXUSR | _FileMode.S_IRGRP | _FileMode.S_IXGRP | _FileMode.S_IROTH | _FileMode.S_IXOTH 15 | 16 | @dataclass 17 | class BackupFile: 18 | path: str 19 | domain: str 20 | 21 | def to_record(self) -> mbdb.MbdbRecord: 22 | raise NotImplementedError() 23 | 24 | @dataclass 25 | class ConcreteFile(BackupFile): 26 | contents: bytes 27 | src_path: Optional[str] = None 28 | owner: int = 0 29 | group: int = 0 30 | inode: Optional[int] = None 31 | mode: _FileMode = DEFAULT 32 | 33 | hash: bytes = None 34 | size: int = None 35 | 36 | def read_contents(self) -> bytes: 37 | contents = self.contents 38 | if self.contents == None: 39 | with open(self.src_path, "rb") as in_file: 40 | contents = in_file.read() 41 | # prepopulate hash and size 42 | self.hash = sha1(contents).digest() 43 | self.size = len(contents) 44 | return contents 45 | 46 | def to_record(self) -> mbdb.MbdbRecord: 47 | if self.inode is None: 48 | self.inode = int.from_bytes(randbytes(8), "big") 49 | if self.hash == None or self.size == None: 50 | self.read_contents() 51 | return mbdb.MbdbRecord( 52 | domain=self.domain, 53 | filename=self.path, 54 | link="", 55 | hash=self.hash, 56 | key=b"", 57 | mode=self.mode | _FileMode.S_IFREG, 58 | #unknown2=0, 59 | #unknown3=0, 60 | inode=self.inode, 61 | user_id=self.owner, 62 | group_id=self.group, 63 | mtime=int(datetime.now().timestamp()), 64 | atime=int(datetime.now().timestamp()), 65 | ctime=int(datetime.now().timestamp()), 66 | size=self.size, 67 | flags=4, 68 | properties=[] 69 | ) 70 | 71 | @dataclass 72 | class Directory(BackupFile): 73 | owner: int = 0 74 | group: int = 0 75 | mode: _FileMode = DEFAULT 76 | 77 | def to_record(self) -> mbdb.MbdbRecord: 78 | return mbdb.MbdbRecord( 79 | domain=self.domain, 80 | filename=self.path, 81 | link="", 82 | hash=b"", 83 | key=b"", 84 | mode=self.mode | _FileMode.S_IFDIR, 85 | #unknown2=0, 86 | #unknown3=0, 87 | inode=0, # inode is not respected for directories 88 | user_id=self.owner, 89 | group_id=self.group, 90 | mtime=int(datetime.now().timestamp()), 91 | atime=int(datetime.now().timestamp()), 92 | ctime=int(datetime.now().timestamp()), 93 | size=0, 94 | flags=4, 95 | properties=[] 96 | ) 97 | 98 | @dataclass 99 | class SymbolicLink(BackupFile): 100 | target: str 101 | owner: int = 0 102 | group: int = 0 103 | inode: Optional[int] = None 104 | mode: _FileMode = DEFAULT 105 | 106 | def to_record(self) -> mbdb.MbdbRecord: 107 | if self.inode is None: 108 | self.inode = int.from_bytes(randbytes(8), "big") 109 | return mbdb.MbdbRecord( 110 | domain=self.domain, 111 | filename=self.path, 112 | link=self.target, 113 | hash=b"", 114 | key=b"", 115 | mode=self.mode | _FileMode.S_IFLNK, 116 | #unknown2=0, 117 | #unknown3=0, 118 | inode=self.inode, 119 | user_id=self.owner, 120 | group_id=self.group, 121 | mtime=int(datetime.now().timestamp()), 122 | atime=int(datetime.now().timestamp()), 123 | ctime=int(datetime.now().timestamp()), 124 | size=0, 125 | flags=4, 126 | properties=[] 127 | ) 128 | 129 | @dataclass 130 | class AppBundle: 131 | identifier: str 132 | path: str 133 | container_content_class: str 134 | version: str = 804 135 | 136 | @dataclass 137 | class Backup: 138 | files: list[BackupFile] 139 | apps: list[AppBundle] 140 | 141 | def write_to_directory(self, directory: Path): 142 | for file in self.files: 143 | if isinstance(file, ConcreteFile): 144 | #print("Writing", file.path, "to", directory / sha1((file.domain + "-" + file.path).encode()).digest().hex()) 145 | with open(directory / sha1((file.domain + "-" + file.path).encode()).digest().hex(), "wb") as f: 146 | f.write(file.read_contents()) 147 | 148 | with open(directory / "Manifest.mbdb", "wb") as f: 149 | f.write(self.generate_manifest_db().to_bytes()) 150 | 151 | with open(directory / "Status.plist", "wb") as f: 152 | f.write(self.generate_status()) 153 | 154 | with open(directory / "Manifest.plist", "wb") as f: 155 | f.write(self.generate_manifest()) 156 | 157 | with open(directory / "Info.plist", "wb") as f: 158 | f.write(plistlib.dumps({})) 159 | 160 | 161 | def generate_manifest_db(self): # Manifest.mbdb 162 | records = [] 163 | for file in self.files: 164 | records.append(file.to_record()) 165 | return mbdb.Mbdb(records=records) 166 | 167 | def generate_status(self) -> bytes: # Status.plist 168 | return plistlib.dumps({ 169 | "BackupState": "new", 170 | "Date": datetime.fromisoformat("1970-01-01T00:00:00+00:00"), 171 | "IsFullBackup": False, 172 | "SnapshotState": "finished", 173 | "UUID": "00000000-0000-0000-0000-000000000000", 174 | "Version": "2.4" 175 | }) 176 | 177 | def generate_manifest(self) -> bytes: # Manifest.plist 178 | plist = { 179 | "BackupKeyBag": b64decode(""" 180 | VkVSUwAAAAQAAAAFVFlQRQAAAAQAAAABVVVJRAAAABDud41d1b9NBICR1BH9JfVtSE1D 181 | SwAAACgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAV1JBUAAA 182 | AAQAAAAAU0FMVAAAABRY5Ne2bthGQ5rf4O3gikep1e6tZUlURVIAAAAEAAAnEFVVSUQA 183 | AAAQB7R8awiGR9aba1UuVahGPENMQVMAAAAEAAAAAVdSQVAAAAAEAAAAAktUWVAAAAAE 184 | AAAAAFdQS1kAAAAoN3kQAJloFg+ukEUY+v5P+dhc/Welw/oucsyS40UBh67ZHef5ZMk9 185 | UVVVSUQAAAAQgd0cg0hSTgaxR3PVUbcEkUNMQVMAAAAEAAAAAldSQVAAAAAEAAAAAktU 186 | WVAAAAAEAAAAAFdQS1kAAAAoMiQTXx0SJlyrGJzdKZQ+SfL124w+2Tf/3d1R2i9yNj9z 187 | ZCHNJhnorVVVSUQAAAAQf7JFQiBOS12JDD7qwKNTSkNMQVMAAAAEAAAAA1dSQVAAAAAE 188 | AAAAAktUWVAAAAAEAAAAAFdQS1kAAAAoSEelorROJA46ZUdwDHhMKiRguQyqHukotrxh 189 | jIfqiZ5ESBXX9txi51VVSUQAAAAQfF0G/837QLq01xH9+66vx0NMQVMAAAAEAAAABFdS 190 | QVAAAAAEAAAAAktUWVAAAAAEAAAAAFdQS1kAAAAol0BvFhd5bu4Hr75XqzNf4g0fMqZA 191 | ie6OxI+x/pgm6Y95XW17N+ZIDVVVSUQAAAAQimkT2dp1QeadMu1KhJKNTUNMQVMAAAAE 192 | AAAABVdSQVAAAAAEAAAAA0tUWVAAAAAEAAAAAFdQS1kAAAAo2N2DZarQ6GPoWRgTiy/t 193 | djKArOqTaH0tPSG9KLbIjGTOcLodhx23xFVVSUQAAAAQQV37JVZHQFiKpoNiGmT6+ENM 194 | QVMAAAAEAAAABldSQVAAAAAEAAAAA0tUWVAAAAAEAAAAAFdQS1kAAAAofe2QSvDC2cV7 195 | Etk4fSBbgqDx5ne/z1VHwmJ6NdVrTyWi80Sy869DM1VVSUQAAAAQFzkdH+VgSOmTj3yE 196 | cfWmMUNMQVMAAAAEAAAAB1dSQVAAAAAEAAAAA0tUWVAAAAAEAAAAAFdQS1kAAAAo7kLY 197 | PQ/DnHBERGpaz37eyntIX/XzovsS0mpHW3SoHvrb9RBgOB+WblVVSUQAAAAQEBpgKOz9 198 | Tni8F9kmSXd0sENMQVMAAAAEAAAACFdSQVAAAAAEAAAAA0tUWVAAAAAEAAAAAFdQS1kA 199 | AAAo5mxVoyNFgPMzphYhm1VG8Fhsin/xX+r6mCd9gByF5SxeolAIT/ICF1VVSUQAAAAQ 200 | rfKB2uPSQtWh82yx6w4BoUNMQVMAAAAEAAAACVdSQVAAAAAEAAAAA0tUWVAAAAAEAAAA 201 | AFdQS1kAAAAo5iayZBwcRa1c1MMx7vh6lOYux3oDI/bdxFCW1WHCQR/Ub1MOv+QaYFVV 202 | SUQAAAAQiLXvK3qvQza/mea5inss/0NMQVMAAAAEAAAACldSQVAAAAAEAAAAA0tUWVAA 203 | AAAEAAAAAFdQS1kAAAAoD2wHX7KriEe1E31z7SQ7/+AVymcpARMYnQgegtZD0Mq2U55u 204 | xwNr2FVVSUQAAAAQ/Q9feZxLS++qSe/a4emRRENMQVMAAAAEAAAAC1dSQVAAAAAEAAAA 205 | A0tUWVAAAAAEAAAAAFdQS1kAAAAocYda2jyYzzSKggRPw/qgh6QPESlkZedgDUKpTr4Z 206 | Z8FDgd7YoALY1g=="""), 207 | "Lockdown": {}, 208 | "SystemDomainsVersion": "20.0", 209 | "Version": "9.1" 210 | } 211 | # add the apps 212 | if len(self.apps) > 0: 213 | plist["Applications"] = {} 214 | for app in self.apps: 215 | appInfo = { 216 | "CFBundleIdentifier": app.identifier, 217 | "CFBundleVersion": app.version, 218 | "ContainerContentClass": app.container_content_class, 219 | "Path": app.path 220 | } 221 | plist["Applications"][app.identifier] = appInfo 222 | return plistlib.dumps(plist) -------------------------------------------------------------------------------- /restore/mbdb.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from io import BytesIO 3 | 4 | # Mode bitfield 5 | from enum import IntFlag 6 | class _FileMode(IntFlag): 7 | S_IFMT = 0o0170000 8 | S_IFIFO = 0o0010000 9 | S_IFCHR = 0o0020000 10 | S_IFDIR = 0o0040000 11 | S_IFBLK = 0o0060000 12 | S_IFREG = 0o0100000 13 | S_IFLNK = 0o0120000 14 | S_IFSOCK = 0o0140000 15 | 16 | #S_IRWXU = 0o0000700 17 | S_IRUSR = 0o0000400 18 | S_IWUSR = 0o0000200 19 | S_IXUSR = 0o0000100 20 | 21 | #S_IRWXG = 0o0000070 22 | S_IRGRP = 0o0000040 23 | S_IWGRP = 0o0000020 24 | S_IXGRP = 0o0000010 25 | 26 | #S_IRWXO = 0o0000007 27 | S_IROTH = 0o0000004 28 | S_IWOTH = 0o0000002 29 | S_IXOTH = 0o0000001 30 | 31 | S_ISUID = 0o0004000 32 | S_ISGID = 0o0002000 33 | S_ISVTX = 0o0001000 34 | 35 | @dataclass 36 | class MbdbRecord: 37 | domain: str 38 | filename: str 39 | link: str 40 | hash: bytes 41 | key: bytes 42 | mode: _FileMode 43 | inode: int 44 | user_id: int 45 | group_id: int 46 | mtime: int 47 | atime: int 48 | ctime: int 49 | size: int 50 | flags: int 51 | properties: list 52 | 53 | @classmethod 54 | def from_stream(cls, d: BytesIO): 55 | #d = BytesIO(data) 56 | 57 | domain_len = int.from_bytes(d.read(2), "big") 58 | domain = d.read(domain_len).decode("utf-8") 59 | 60 | filename_len = int.from_bytes(d.read(2), "big") 61 | filename = d.read(filename_len).decode("utf-8") 62 | 63 | link_len = int.from_bytes(d.read(2), "big") 64 | link = d.read(link_len).decode("utf-8") if link_len != 0xffff else "" 65 | 66 | hash_len = int.from_bytes(d.read(2), "big") 67 | hash = d.read(hash_len) if hash_len != 0xffff else b"" 68 | 69 | key_len = int.from_bytes(d.read(2), "big") 70 | key = d.read(key_len) if key_len != 0xffff else b"" 71 | 72 | mode = _FileMode(int.from_bytes(d.read(2), "big")) 73 | #unknown2 = int.from_bytes(d.read(4), "big") 74 | #unknown3 = int.from_bytes(d.read(4), "big") 75 | inode = int.from_bytes(d.read(8), "big") 76 | user_id = int.from_bytes(d.read(4), "big") 77 | group_id = int.from_bytes(d.read(4), "big") 78 | mtime = int.from_bytes(d.read(4), "big") 79 | atime = int.from_bytes(d.read(4), "big") 80 | ctime = int.from_bytes(d.read(4), "big") 81 | size = int.from_bytes(d.read(8), "big") 82 | flags = int.from_bytes(d.read(1), "big") 83 | 84 | properties_count = int.from_bytes(d.read(1), "big") 85 | properties = [] 86 | 87 | for _ in range(properties_count): 88 | name_len = int.from_bytes(d.read(2), "big") 89 | name = d.read(name_len).decode("utf-8") if name_len != 0xffff else "" 90 | 91 | value_len = int.from_bytes(d.read(2), "big") 92 | value = d.read(value_len).decode("utf-8") if value_len != 0xffff else "" 93 | 94 | properties.append((name, value)) 95 | 96 | return cls(domain, filename, link, hash, key, mode, inode, user_id, group_id, mtime, atime, ctime, size, flags, properties) 97 | 98 | def to_bytes(self) -> bytes: 99 | d = BytesIO() 100 | 101 | d.write(len(self.domain).to_bytes(2, "big")) 102 | d.write(self.domain.encode("utf-8")) 103 | 104 | d.write(len(self.filename).to_bytes(2, "big")) 105 | d.write(self.filename.encode("utf-8")) 106 | 107 | d.write(len(self.link).to_bytes(2, "big")) 108 | d.write(self.link.encode("utf-8")) 109 | 110 | d.write(len(self.hash).to_bytes(2, "big")) 111 | d.write(self.hash) 112 | 113 | d.write(len(self.key).to_bytes(2, "big")) 114 | d.write(self.key) 115 | 116 | d.write(self.mode.to_bytes(2, "big")) 117 | #d.write(self.unknown2.to_bytes(4, "big")) 118 | #d.write(self.unknown3.to_bytes(4, "big")) 119 | d.write(self.inode.to_bytes(8, "big")) 120 | d.write(self.user_id.to_bytes(4, "big")) 121 | d.write(self.group_id.to_bytes(4, "big")) 122 | d.write(self.mtime.to_bytes(4, "big")) 123 | d.write(self.atime.to_bytes(4, "big")) 124 | d.write(self.ctime.to_bytes(4, "big")) 125 | d.write(self.size.to_bytes(8, "big")) 126 | d.write(self.flags.to_bytes(1, "big")) 127 | 128 | d.write(len(self.properties).to_bytes(1, "big")) 129 | 130 | for name, value in self.properties: 131 | d.write(len(name).to_bytes(2, "big")) 132 | d.write(name.encode("utf-8")) 133 | 134 | d.write(len(value).to_bytes(2, "big")) 135 | d.write(value.encode("utf-8")) 136 | 137 | return d.getvalue() 138 | 139 | @dataclass 140 | class Mbdb: 141 | records: list[MbdbRecord] 142 | 143 | @classmethod 144 | def from_bytes(cls, data: bytes): 145 | d = BytesIO(data) 146 | 147 | if d.read(4) != b"mbdb": 148 | raise ValueError("Invalid MBDB file") 149 | 150 | if d.read(2) != b"\x05\x00": 151 | raise ValueError("Invalid MBDB version") 152 | 153 | records = [] 154 | while d.tell() < len(data): 155 | records.append(MbdbRecord.from_stream(d)) 156 | 157 | return cls(records) 158 | 159 | def to_bytes(self) -> bytes: 160 | d = BytesIO() 161 | 162 | d.write(b"mbdb") 163 | d.write(b"\x05\x00") 164 | 165 | for record in self.records: 166 | d.write(record.to_bytes()) 167 | 168 | return d.getvalue() -------------------------------------------------------------------------------- /restore/restore.py: -------------------------------------------------------------------------------- 1 | from . import backup, perform_restore 2 | from .mbdb import _FileMode 3 | from pymobiledevice3.lockdown import LockdownClient 4 | from pymobiledevice3.services.installation_proxy import InstallationProxyService 5 | import os 6 | 7 | class FileToRestore: 8 | def __init__(self, 9 | contents: str, restore_path: str, contents_path: str = None, domain: str = "", 10 | owner: int = 501, group: int = 501, mode: _FileMode = None 11 | ): 12 | self.contents = contents 13 | self.contents_path = contents_path 14 | self.restore_path = restore_path 15 | self.domain = domain 16 | self.owner = owner 17 | self.group = group 18 | self.mode = mode 19 | 20 | def concat_exploit_file(file: FileToRestore, files_list: list[FileToRestore], last_domain: str) -> str: 21 | base_path = "/var/backup" 22 | # set it to work in the separate volumes (prevents a bootloop) 23 | if file.restore_path.startswith("/var/mobile/"): 24 | # required on iOS 17.0+ since /var/mobile is on a separate partition 25 | base_path = "/var/mobile/backup" 26 | elif file.restore_path.startswith("/private/var/mobile/"): 27 | base_path = "/private/var/mobile/backup" 28 | elif file.restore_path.startswith("/private/var/"): 29 | base_path = "/private/var/backup" 30 | # don't append the directory if it has already been added (restore will fail) 31 | path, name = os.path.split(file.restore_path) 32 | domain_path = f"SysContainerDomain-../../../../../../../..{base_path}{path}/" 33 | new_last_domain = last_domain 34 | if last_domain != domain_path: 35 | files_list.append(backup.Directory( 36 | "", 37 | f"{domain_path}", 38 | owner=file.owner, 39 | group=file.group 40 | )) 41 | new_last_domain = domain_path 42 | files_list.append(backup.ConcreteFile( 43 | "", 44 | f"{domain_path}{name}", 45 | owner=file.owner, 46 | group=file.group, 47 | contents=file.contents 48 | )) 49 | return new_last_domain 50 | 51 | def concat_regular_file(file: FileToRestore, files_list: list[FileToRestore], last_domain: str, last_path: str): 52 | path, name = os.path.split(file.restore_path) 53 | paths = path.split("/") 54 | new_last_domain = last_domain 55 | # append the domain first 56 | if last_domain != file.domain: 57 | files_list.append(backup.Directory( 58 | "", 59 | file.domain, 60 | owner=file.owner, 61 | group=file.group 62 | )) 63 | new_last_domain = file.domain 64 | # append each part of the path if it is not already there 65 | full_path = "" 66 | mode = file.mode 67 | if mode == None: 68 | mode = backup.DEFAULT 69 | for path_item in paths: 70 | if full_path != "": 71 | full_path += "/" 72 | full_path += path_item 73 | if not last_path.startswith(full_path): 74 | files_list.append(backup.Directory( 75 | full_path, 76 | file.domain, 77 | owner=file.owner, 78 | group=file.group, 79 | mode=mode 80 | )) 81 | last_path = full_path 82 | # finally, append the file 83 | files_list.append(backup.ConcreteFile( 84 | f"{full_path}/{name}", 85 | file.domain, 86 | owner=file.owner, 87 | group=file.group, 88 | contents=file.contents, 89 | src_path=file.contents_path, 90 | mode=mode 91 | )) 92 | return new_last_domain, full_path 93 | 94 | # files is a list of FileToRestore objects 95 | def restore_files(files: list[FileToRestore], reboot: bool = False, lockdown_client: LockdownClient = None, progress_callback = lambda x: None): 96 | # create the files to be backed up 97 | files_list = [ 98 | ] 99 | apps_list = [] 100 | active_bundle_ids = [] 101 | apps = None 102 | sorted_files = sorted(files, key=lambda x: (x.domain, x.restore_path), reverse=False) 103 | # add the file paths 104 | last_domain = "" 105 | last_path = "" 106 | exploit_only = True 107 | for file in sorted_files: 108 | if file.domain == "": 109 | last_domain = concat_exploit_file(file, files_list, last_domain) 110 | else: 111 | last_domain, last_path = concat_regular_file(file, files_list, last_domain, last_path) 112 | exploit_only = False 113 | # add the app bundle to the list 114 | if last_domain.startswith("AppDomain"): 115 | bundle_id = last_domain.removeprefix("AppDomain-") 116 | if not bundle_id in active_bundle_ids: 117 | if apps == None: 118 | apps = InstallationProxyService(lockdown=lockdown_client).get_apps(application_type="Any", calculate_sizes=False) 119 | app_info = apps[bundle_id] 120 | active_bundle_ids.append(bundle_id) 121 | apps_list.append(backup.AppBundle( 122 | identifier=bundle_id, 123 | path=app_info["Container"], 124 | version=app_info["CFBundleVersion"], 125 | container_content_class="Data/Application" 126 | )) 127 | 128 | # crash the restore to skip the setup (only works for exploit files) 129 | if exploit_only: 130 | files_list.append(backup.ConcreteFile("", "SysContainerDomain-../../../../../../../.." + "/crash_on_purpose", contents=b"")) 131 | 132 | # create the backup 133 | back = backup.Backup(files=files_list, apps=apps_list) 134 | 135 | perform_restore(backup=back, reboot=reboot, lockdown_client=lockdown_client, progress_callback=progress_callback) 136 | 137 | 138 | # DEPRECIATED 139 | def restore_file(fp: str, restore_path: str, restore_name: str, reboot: bool = False, lockdown_client: LockdownClient = None): 140 | # open the file and read the contents 141 | contents = open(fp, "rb").read() 142 | 143 | base_path = "/var/backup" 144 | if restore_path.startswith("/var/mobile/"): 145 | # required on iOS 17.0+ since /var/mobile is on a separate partition 146 | base_path = "/var/mobile/backup" 147 | 148 | # create the backup 149 | back = backup.Backup(files=[ 150 | # backup.Directory("", "HomeDomain"), 151 | # backup.Directory("Library", "HomeDomain"), 152 | # backup.Directory("Library/Preferences", "HomeDomain"), 153 | # backup.ConcreteFile("Library/Preferences/temp", "HomeDomain", owner=501, group=501, contents=contents, inode=0), 154 | backup.Directory( 155 | "", 156 | f"SysContainerDomain-../../../../../../../..{base_path}{restore_path}", 157 | owner=501, 158 | group=501 159 | ), 160 | backup.ConcreteFile( 161 | "", 162 | f"SysContainerDomain-../../../../../../../..{base_path}{restore_path}{restore_name}", 163 | owner=501, 164 | group=501, 165 | contents=contents#b"", 166 | # inode=0 167 | ), 168 | # backup.ConcreteFile( 169 | # "", 170 | # "SysContainerDomain-../../../../../../../../var/.backup.i/var/root/Library/Preferences/temp", 171 | # owner=501, 172 | # group=501, 173 | # contents=b"", 174 | # ), # Break the hard link 175 | backup.ConcreteFile("", "SysContainerDomain-../../../../../../../.." + "/crash_on_purpose", contents=b""), 176 | ]) 177 | 178 | 179 | perform_restore(backup=back, reboot=reboot, lockdown_client=lockdown_client) -------------------------------------------------------------------------------- /tweaks/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LoveJessyChen/Nugget-iOS/eb917fe4a75ef61eca7e098b7ad2afcc736a2515/tweaks/__init__.py -------------------------------------------------------------------------------- /tweaks/basic_plist_locations.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | class FileLocation(Enum): 4 | # Mobile Gestalt 5 | resolution = "/var/Managed Preferences/mobile/com.apple.iokit.IOMobileGraphicsFamily.plist" 6 | 7 | # Springboard Options 8 | springboard = "/var/Managed Preferences/mobile/com.apple.springboard.plist" 9 | footnote = "/var/containers/Shared/SystemGroup/systemgroup.com.apple.configurationprofiles/Library/ConfigurationProfiles/SharedDeviceConfiguration.plist" 10 | 11 | # Internal Options 12 | globalPreferences = "/var/Managed Preferences/mobile/.GlobalPreferences.plist" 13 | appStore = "/var/Managed Preferences/mobile/com.apple.AppStore.plist" 14 | backboardd = "/var/Managed Preferences/mobile/com.apple.backboardd.plist" 15 | coreMotion = "/var/Managed Preferences/mobile/com.apple.CoreMotion.plist" 16 | pasteboard = "/var/Managed Preferences/mobile/com.apple.Pasteboard.plist" 17 | notes = "/var/Managed Preferences/mobile/com.apple.mobilenotes.plist" 18 | 19 | # Daemons 20 | disabledDaemons = "/var/db/com.apple.xpc.launchd/disabled.plist" 21 | screentime = "/var/mobile/Library/Preferences/ScreenTimeAgent.plist" 22 | 23 | # Risky Options 24 | ota = "/var/Managed Preferences/mobile/com.apple.MobileAsset.plist" 25 | 26 | # support for older versions of python that cannot enumerate over enums 27 | FileLocationsList: list[FileLocation] = [ 28 | FileLocation.resolution, 29 | FileLocation.springboard, FileLocation.footnote, 30 | FileLocation.globalPreferences, FileLocation.appStore, FileLocation.backboardd, FileLocation.coreMotion, FileLocation.pasteboard, FileLocation.notes 31 | ] 32 | RiskyFileLocationsList: list[FileLocation] = [ 33 | FileLocation.ota 34 | ] -------------------------------------------------------------------------------- /tweaks/custom_gestalt_tweaks.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from json import loads 3 | from .tweak_classes import MobileGestaltTweak 4 | 5 | class ValueType(Enum): 6 | Integer = "Integer" 7 | Float = "Float" 8 | String = "String" 9 | Array = "Array" 10 | Dictionary = "Dictionary" 11 | 12 | ValueTypeStrings: list[ValueType] = [ 13 | ValueType.Integer.value, ValueType.Float.value, 14 | ValueType.String.value, 15 | ValueType.Array.value, ValueType.Dictionary.value 16 | ] 17 | 18 | class CustomGestaltTweak: 19 | def __init__(self, tweak: MobileGestaltTweak, value_type: ValueType): 20 | self.tweak = tweak 21 | self.value_type = value_type 22 | self.deactivated = False 23 | 24 | # TODO: change everything to not return the dict since it is passed by reference 25 | def apply_tweak(self, plist: dict) -> dict: 26 | if self.deactivated or self.tweak.key == "": 27 | # key was not set, don't apply (maybe user added it by accident) 28 | return plist 29 | self.tweak.enabled = True 30 | # set the value to be as the specified value type 31 | if self.value_type == ValueType.Integer: 32 | self.tweak.value = int(self.tweak.value) 33 | elif self.value_type == ValueType.Float: 34 | self.tweak.value = float(self.tweak.value) 35 | elif self.value_type == ValueType.Array or self.value_type == ValueType.Dictionary: 36 | # json convert string to array/dict 37 | self.tweak.value = loads(self.tweak.value) 38 | 39 | # apply the tweak after updating the value 40 | plist = self.tweak.apply_tweak(plist) 41 | return plist 42 | 43 | 44 | class CustomGestaltTweaks: 45 | custom_tweaks: list[CustomGestaltTweak] = [] 46 | 47 | def create_tweak(key: str="", value: str="1", value_type: ValueType = ValueType.Integer) -> int: 48 | new_tweak = MobileGestaltTweak(key, value=value) 49 | CustomGestaltTweaks.custom_tweaks.append(CustomGestaltTweak(new_tweak, value_type)) 50 | # return the tweak id 51 | return len(CustomGestaltTweaks.custom_tweaks) - 1 52 | 53 | def set_tweak_key(id: int, key: str): 54 | CustomGestaltTweaks.custom_tweaks[id].tweak.key = key 55 | 56 | def set_tweak_value(id: int, value: str): 57 | CustomGestaltTweaks.custom_tweaks[id].tweak.value = value 58 | 59 | def set_tweak_value_type(id: int, value_type) -> str: 60 | new_value_type = value_type 61 | if isinstance(value_type, str): 62 | # based on string value 63 | new_value_type = ValueType(value_type) 64 | elif isinstance(value_type, int): 65 | # based on index of the string 66 | new_value_type = ValueType(ValueTypeStrings[value_type]) 67 | 68 | CustomGestaltTweaks.custom_tweaks[id].value_type = new_value_type 69 | # update the value to be of the new type 70 | new_value = 1 71 | new_str = "1" 72 | if new_value_type == ValueType.Float: 73 | new_value = 1.0 74 | new_str = "1.0" 75 | elif new_value_type == ValueType.String: 76 | new_value = "" 77 | new_str = "" 78 | elif new_value_type == ValueType.Array: 79 | new_value = [] 80 | new_str = "[ ]" 81 | elif new_value_type == ValueType.Dictionary: 82 | new_value = {} 83 | new_str = "{ }" 84 | CustomGestaltTweaks.custom_tweaks[id].tweak.value = new_value 85 | return new_str 86 | 87 | def deactivate_tweak(id: int): 88 | CustomGestaltTweaks.custom_tweaks[id].deactivated = True 89 | CustomGestaltTweaks.custom_tweaks[id].tweak = None 90 | 91 | def apply_tweaks(plist: dict): 92 | for tweak in CustomGestaltTweaks.custom_tweaks: 93 | plist = tweak.apply_tweak(plist) 94 | return plist -------------------------------------------------------------------------------- /tweaks/daemons_tweak.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | class Daemon(Enum): 4 | thermalmonitord = ["com.apple.thermalmonitord"] 5 | OTA = [ 6 | "com.apple.mobile.softwareupdated", 7 | "com.apple.OTATaskingAgent", 8 | "com.apple.softwareupdateservicesd" 9 | ] 10 | UsageTrackingAgent = ["com.apple.UsageTrackingAgent"] 11 | GameCenter = ["com.apple.gamed"] 12 | ScreenTime = [ 13 | "com.apple.ScreenTimeAgent", 14 | "com.apple.homed", 15 | "com.apple.familycircled" 16 | ] 17 | CrashReports = [ 18 | "com.apple.ReportCrash", 19 | "com.apple.ReportCrash.Jetsam", 20 | "com.apple.ReportMemoryException", 21 | "com.apple.OTACrashCopier", 22 | "com.apple.analyticsd", 23 | "com.apple.wifianalyticsd", 24 | "com.apple.aslmanager", 25 | "com.apple.coresymbolicationd", 26 | "com.apple.crash_mover", 27 | "com.apple.crashreportcopymobile", 28 | "com.apple.DumpBasebandCrash", 29 | "com.apple.DumpPanic", 30 | "com.apple.logd", 31 | "com.apple.logd.admin", 32 | "com.apple.logd.events", 33 | "com.apple.logd.watchdog", 34 | "com.apple.logd_helper", 35 | "com.apple.logd_reporter", 36 | "com.apple.logd_reporter.report_statistics", 37 | "com.apple.system.logger", 38 | "com.apple.hangreporter", 39 | "com.apple.hangtracerd", 40 | "com.apple.spindump", 41 | "com.apple.rtcreportingd", 42 | "com.apple.syslogd" 43 | ] 44 | ATWAKEUP = ["com.apple.atc.atwakeup"] 45 | Tips = ["com.apple.tipsd"] 46 | VPN = ["com.apple.racoon"] 47 | ChineseLAN = [ 48 | "com.apple.wapic", 49 | "com.apple.wifi.wapic" 50 | ] 51 | HealthKit = ["com.apple.healthd"] 52 | AirPrint = ["com.apple.printd"] 53 | AssistiveTouch = ["com.apple.assistivetouchd"] 54 | iCloud = ["com.apple.itunescloudd"] 55 | InternetTethering = ["com.apple.MobileInternetSharing"] 56 | PassBook = ["com.apple.passd"] 57 | Spotlight = ["com.apple.searchd"] 58 | VoiceControl = [ 59 | "com.apple.assistant_service", 60 | "com.apple.assistantd", 61 | "com.apple.voiced" 62 | ] 63 | -------------------------------------------------------------------------------- /tweaks/eligibility_tweak.py: -------------------------------------------------------------------------------- 1 | from .tweak_classes import Tweak 2 | from controllers.files_handler import get_bundle_files 3 | from restore.restore import FileToRestore 4 | from devicemanagement.constants import Version 5 | 6 | import plistlib 7 | import sys 8 | from pathlib import Path 9 | from os import path, getcwd 10 | 11 | class InvalidRegionCodeException(Exception): 12 | "Region code must be exactly 2 characters long!" 13 | pass 14 | 15 | def replace_region_code(plist_path: str, original_code: str = "US", new_code: str = "US"): 16 | with open(plist_path, 'rb') as f: 17 | plist_data = plistlib.load(f) 18 | 19 | plist_str = str(plist_data) 20 | updated_plist_str = plist_str.replace(original_code, new_code) 21 | updated_plist_data = eval(updated_plist_str) # Convert string back to dictionary 22 | 23 | return plistlib.dumps(updated_plist_data) 24 | 25 | class EligibilityTweak(Tweak): 26 | def __init__(self): 27 | super().__init__(key=None, value=["Method 1", "Method 2"]) 28 | self.code = "US" 29 | self.method = 0 # between 0 and 1 30 | 31 | def set_region_code(self, new_code: str): 32 | if new_code == '': 33 | self.code = "US" 34 | else: 35 | self.code = new_code 36 | 37 | def set_selected_option(self, new_method: int): 38 | self.method = new_method % 2 # force it to be either 0 or 1 39 | self.set_enabled(True) 40 | 41 | def get_selected_option(self) -> int: 42 | return self.method 43 | 44 | def apply_tweak(self) -> list[FileToRestore]: 45 | # credit to lrdsnow for EU Enabler 46 | # https://github.com/Lrdsnow/EUEnabler/blob/main/app.py 47 | if not self.enabled: 48 | return None 49 | print(f"Applying EU Enabler for region \'{self.code}\'...") 50 | # get the plists directory 51 | source_dir = get_bundle_files("files/eligibility") 52 | 53 | # start with eligibility.plist 54 | file_path = path.join(source_dir, 'eligibility.plist') 55 | eligibility_data = replace_region_code(file_path, original_code="US", new_code=self.code) 56 | files_to_restore = [ 57 | FileToRestore( 58 | contents=eligibility_data, 59 | restore_path="/var/db/os_eligibility/eligibility.plist", 60 | ) 61 | ] 62 | 63 | # next modify config.plist 64 | file_path = path.join(source_dir, 'Config.plist') 65 | config_data = replace_region_code(file_path, original_code="US", new_code=self.code) 66 | if self.method == 0: 67 | files_to_restore.append( 68 | FileToRestore( 69 | contents=config_data, 70 | restore_path="/var/MobileAsset/AssetsV2/com_apple_MobileAsset_OSEligibility/purpose_auto/c55a421c053e10233e5bfc15c42fa6230e5639a9.asset/AssetData/Config.plist", 71 | ) 72 | ) 73 | elif self.method == 1: 74 | files_to_restore.append( 75 | FileToRestore( 76 | contents=config_data, 77 | restore_path="/var/MobileAsset/AssetsV2/com_apple_MobileAsset_OSEligibility/purpose_auto/247556c634fc4cc4fd742f1b33af9abf194a986e.asset/AssetData/Config.plist", 78 | ) 79 | ) 80 | 81 | # return the new files to restore 82 | return files_to_restore 83 | 84 | 85 | class AITweak(Tweak): 86 | def __init__(self): 87 | super().__init__(key=None, value="") 88 | 89 | def set_language_code(self, lang: str): 90 | self.value = lang 91 | 92 | def apply_tweak(self) -> FileToRestore: 93 | if not self.enabled: 94 | return None 95 | langs = ["en"] 96 | if self.value != "": 97 | langs.append(self.value) 98 | plist = { 99 | "OS_ELIGIBILITY_DOMAIN_CALCIUM": { 100 | "os_eligibility_answer_source_t": 1, 101 | "os_eligibility_answer_t": 2, 102 | "status": { 103 | "OS_ELIGIBILITY_INPUT_CHINA_CELLULAR": 2 104 | } 105 | }, 106 | "OS_ELIGIBILITY_DOMAIN_GREYMATTER": { 107 | "context": { 108 | "OS_ELIGIBILITY_CONTEXT_ELIGIBLE_DEVICE_LANGUAGES": langs 109 | }, 110 | "os_eligibility_answer_source_t": 1, 111 | "os_eligibility_answer_t": 4, 112 | "status": { 113 | "OS_ELIGIBILITY_INPUT_DEVICE_LANGUAGE": 3, 114 | "OS_ELIGIBILITY_INPUT_DEVICE_REGION_CODE": 3, 115 | "OS_ELIGIBILITY_INPUT_EXTERNAL_BOOT_DRIVE": 3, 116 | "OS_ELIGIBILITY_INPUT_GENERATIVE_MODEL_SYSTEM": 3, 117 | "OS_ELIGIBILITY_INPUT_SHARED_IPAD": 3, 118 | "OS_ELIGIBILITY_INPUT_SIRI_LANGUAGE": 3 119 | } 120 | } 121 | } 122 | 123 | return FileToRestore(contents=plistlib.dumps(plist), restore_path="/var/db/eligibilityd/eligibility.plist") -------------------------------------------------------------------------------- /tweaks/posterboard/template_file.py: -------------------------------------------------------------------------------- 1 | import os 2 | import uuid 3 | import zipfile 4 | import fnmatch 5 | 6 | from json import load 7 | from typing import Optional 8 | from tempfile import TemporaryDirectory 9 | from shutil import rmtree 10 | 11 | from .tendie_file import TendieFile 12 | from .template_options import OptionType, TemplateOption, ReplaceOption, RemoveOption, SetOption, PickerOption 13 | from exceptions.posterboard_exceptions import PBTemplateException 14 | 15 | CURRENT_FORMAT = 1 16 | 17 | class TemplateFile(TendieFile): 18 | options: list[TemplateOption] 19 | json_path: str 20 | tmp_dir: str = None 21 | 22 | # TODO: Move these to custom operations 23 | description: Optional[str] = None # description to go under the file 24 | resources: list[str] = [] # list of file paths for embedded resources 25 | previews: dict[str, str] = {} # list of resources to use as previews 26 | preview_layout: str = "horizontal" # the direction to lay out the preview images 27 | 28 | banner_text: Optional[str] = None # text to go as a banner 29 | banner_stylesheet: Optional[str] = None # style sheet of the banner 30 | format_version: int = CURRENT_FORMAT # format version of config 31 | 32 | def __init__(self, path: str): 33 | super().__init__(path=path) 34 | self.options = [] 35 | self.json_path = None 36 | 37 | # find the config.json file 38 | with zipfile.ZipFile(path, mode="r") as archive: 39 | for option in archive.namelist(): 40 | if "config.json" in option.lower() and not "descriptor" in option.lower() and not "container" in option.lower(): 41 | self.json_path = option 42 | break 43 | if self.json_path != None: 44 | file = archive.open(self.json_path) 45 | data = load(file) 46 | # load the options 47 | if not 'options' in data: 48 | raise PBTemplateException(path, "No options were found in the config. Make sure that it is in the correct format.") 49 | if not 'domain' in data or (data['domain'] != "AppDomain-com.apple.PosterBoard" and data['domain'] != "com.apple.PosterBoard"): 50 | # made an oopsie here, only AppDomain-com.apple.PosterBoard should be allowed 51 | # I will allow com.apple.PosterBoard to not break support 52 | raise PBTemplateException(path, "This config is not for the domain \"AppDomain-com.apple.PosterBoard\". Make sure that it is compatible with your version of Nugget.") 53 | self.format_version = int(data['format_version']) 54 | if self.format_version > CURRENT_FORMAT: 55 | raise PBTemplateException(path, "This config requires a newer version of Nugget.") 56 | self.name = f"{data['title']} - by {data['author']}" 57 | if 'description' in data: 58 | self.description = data['description'] 59 | # load the previews 60 | prevs = [] 61 | if 'previews' in data: 62 | prevs = data['previews'] 63 | if 'preview_layout' in data: 64 | self.preview_layout = data['preview_layout'] 65 | # load the banner 66 | if 'banner_text' in data: 67 | self.banner_text = data['banner_text'] 68 | if 'banner_stylesheet' in data: 69 | self.banner_stylesheet = data['banner_stylesheet'] 70 | # load the resources 71 | if 'resources' in data: 72 | self.resources = data['resources'] 73 | # open the resources and put them in temp files 74 | rcs_path = self.json_path.removesuffix("config.json") 75 | for resource in self.resources: 76 | # handle wildcards 77 | rc_pattern = rcs_path + resource 78 | for rc_path in fnmatch.filter(archive.namelist(), rc_pattern): 79 | rc_data = archive.read(rc_path) 80 | if rc_data != None: 81 | # write it to a temp file 82 | if self.tmp_dir == None: 83 | self.tmp_dir = TemporaryDirectory() 84 | rc_full_path = os.path.join(self.tmp_dir.name, rc_path) 85 | os.makedirs(os.path.dirname(rc_full_path), exist_ok=True) 86 | with open(rc_full_path, "wb") as rc_fp: 87 | rc_fp.write(rc_data) 88 | # update the url in the banner stylesheet 89 | clean_path = rc_path.replace(rcs_path, "") 90 | if self.banner_stylesheet != None: 91 | self.banner_stylesheet = self.banner_stylesheet.replace(f"url({clean_path})", f"url({rc_full_path})") 92 | # set the preview images 93 | if clean_path in prevs: 94 | self.previews[clean_path] = rc_full_path 95 | 96 | # TODO: Add error handling 97 | for option in data['options']: 98 | opt_type = OptionType[option['type']] 99 | if opt_type == OptionType.replace: 100 | self.options.append(ReplaceOption(data=option)) 101 | elif opt_type == OptionType.remove: 102 | self.options.append(RemoveOption(data=option)) 103 | elif opt_type == OptionType.set: 104 | self.options.append(SetOption(data=option)) 105 | elif opt_type == OptionType.picker: 106 | self.options.append(PickerOption(data=option)) 107 | else: 108 | raise PBTemplateException(path, "Invalid option type in template") 109 | else: 110 | raise PBTemplateException(path, "No config.json found in file!") 111 | 112 | def clean_files(self): 113 | if self.tmp_dir != None: 114 | try: 115 | rmtree(self.tmp_dir.name) 116 | except Exception as e: 117 | print(f"Error when removing temp dir: {str(e)}") 118 | 119 | def extract(self, output_dir: str): 120 | zip_output = os.path.join(output_dir, str(uuid.uuid4())) 121 | os.makedirs(zip_output) 122 | with zipfile.ZipFile(self.path, 'r') as zip_ref: 123 | zip_ref.extractall(zip_output) 124 | 125 | # apply the options 126 | parent_path = os.path.join(zip_output, os.path.dirname(self.json_path)) 127 | for option in self.options: 128 | option.apply(container_path=parent_path) -------------------------------------------------------------------------------- /tweaks/posterboard/template_options/__init__.py: -------------------------------------------------------------------------------- 1 | from .template_options import OptionType, TemplateOption 2 | from .replace_option import ReplaceOption 3 | from .remove_option import RemoveOption 4 | from .set_option import SetterType, SetOption 5 | from .picker_option import PickerOption -------------------------------------------------------------------------------- /tweaks/posterboard/template_options/picker_option.py: -------------------------------------------------------------------------------- 1 | from . import TemplateOption 2 | 3 | import os 4 | from typing import Optional 5 | from shutil import rmtree, move 6 | from PySide6.QtWidgets import QWidget, QLabel, QVBoxLayout, QHBoxLayout, QComboBox 7 | 8 | from controllers.xml_handler import delete_xml_value 9 | from gui.custom_qt_elements.multicombobox import MultiComboBox 10 | 11 | class PickerElement: 12 | label: str # label to show for the picker 13 | files: list[str] = [] # list of files to include with the picker 14 | associated_preview: Optional[str] = None 15 | preview_lbl: Optional[QLabel] = None 16 | 17 | def __init__(self, option_data: dict): 18 | self.label = option_data['label'] 19 | if 'files' in option_data: 20 | self.files = option_data['files'] 21 | if 'associated_preview' in option_data: 22 | self.associated_preview = option_data['associated_preview'] 23 | 24 | class PickerOption(TemplateOption): 25 | options: list[PickerElement] # list of elements inside the picker 26 | allow_multiple_selection: bool = False # allow choosing multiple options 27 | rename: bool = False # whether or not to rename the chosen files (allow_multiple_selection must be False) 28 | names: list[str] = [] # list of names to rename to 29 | 30 | selection: int|list[str] = [] # chosen user selection 31 | default_val: int|list[int] = [] # the default values 32 | 33 | def __init__(self, data: dict): 34 | super().__init__(data=data) 35 | # get the list of options 36 | self.options = [] 37 | opts = data['options'] 38 | for opt in opts: 39 | self.options.append(PickerElement(opt)) 40 | 41 | # add the other options 42 | if 'allow_multiple_selection' in data: 43 | self.allow_multiple_selection = data['allow_multiple_selection'] 44 | if not self.allow_multiple_selection: 45 | self.selection = 0 46 | if 'rename' in data: 47 | self.rename = data['rename'] 48 | if self.rename: 49 | self.names = data['names'] # only require if rename is true 50 | # default value(s) 51 | if 'default_value' in data: 52 | self.default_val = data['default_value'] 53 | elif 'default_values' in data: 54 | self.default_val = data['default_values'] 55 | 56 | def create_interface(self, options_widget: QWidget, options_layout: QVBoxLayout): 57 | # picker option 58 | picker_widget = QWidget(options_widget) 59 | picker_layout = QHBoxLayout(options_widget) 60 | # add label first 61 | picker_label = QLabel(picker_widget) 62 | picker_label.setText(self.label) 63 | picker_layout.addWidget(picker_label) 64 | if self.allow_multiple_selection: 65 | pickerDrp = MultiComboBox(picker_widget, updateAction=self.onPickerUpdate) 66 | for opt in self.options: 67 | pickerDrp.addItem(opt.label) 68 | pickerDrp.selectIndices(self.default_val) 69 | pickerDrp.updateText() 70 | pickerDrp.setStyleSheet("QWidget { background-color: #3b3b3b; border: 2px solid #3b3b3b; border-radius: 5px; }") 71 | else: 72 | pickerDrp = QComboBox(picker_widget) 73 | for opt in self.options: 74 | pickerDrp.addItem(opt.label) 75 | pickerDrp.activated.connect(self.onPickerUpdate) 76 | pickerDrp.setCurrentIndex(self.default_val) 77 | picker_layout.addWidget(pickerDrp) 78 | picker_widget.setLayout(picker_layout) 79 | options_layout.addWidget(picker_widget) 80 | 81 | def add_potential_preview_lbls(self, lbls: dict[str, QLabel]): 82 | for opt in self.options: 83 | if opt.associated_preview in lbls: 84 | opt.preview_lbl = lbls[opt.associated_preview] 85 | 86 | def update_preview(self): 87 | if self.allow_multiple_selection: 88 | for opt in self.options: 89 | # hide if not in selection or show if it is 90 | if opt.preview_lbl != None: 91 | opt.preview_lbl.setVisible(opt.label in self.selection) 92 | else: 93 | for i in range(len(self.options)): 94 | if self.options[i].preview_lbl != None: 95 | self.options[i].preview_lbl.setVisible(self.selection == i) 96 | 97 | def onPickerUpdate(self, selection: int|list[str]): 98 | self.selection = selection 99 | self.update_preview() 100 | 101 | def apply(self, container_path: str): 102 | # get the list of files 103 | sel_options: list[PickerElement] = [] 104 | if self.allow_multiple_selection: 105 | for opt in self.options: 106 | if opt.label in self.selection: 107 | sel_options.append(opt) 108 | else: 109 | sel_options.append(self.options[self.selection]) 110 | # delete the files that are not in use 111 | for opt in self.options: 112 | if not opt in sel_options: 113 | for file in opt.files: 114 | # delete files or directories 115 | path = os.path.join(container_path, *file.split('/')) 116 | if os.path.isdir(path): 117 | rmtree(path=path, ignore_errors=True) 118 | else: 119 | os.remove(path=path) 120 | # rename the files if needed 121 | if self.rename: 122 | for i in range(len(self.options[self.selection].files)): 123 | # rename files or directories 124 | old_path = os.path.join(container_path, self.options[self.selection].files[i]) 125 | new_path = os.path.join(container_path, self.names[i]) 126 | if os.path.isdir(path): 127 | # rename whole directory 128 | move(old_path, new_path) 129 | else: 130 | os.rename(old_path, new_path) -------------------------------------------------------------------------------- /tweaks/posterboard/template_options/remove_option.py: -------------------------------------------------------------------------------- 1 | from . import TemplateOption 2 | 3 | import os 4 | import glob 5 | from dataclasses import dataclass 6 | from shutil import rmtree 7 | from typing import Optional 8 | from PySide6.QtWidgets import QWidget, QVBoxLayout, QCheckBox 9 | 10 | from controllers.xml_handler import delete_xml_value 11 | 12 | @dataclass 13 | class RemoveOption(TemplateOption): 14 | inverted: bool = False # if set to true, the files will only be deleted if the checkbox is unchecked 15 | value: bool = False # whether or not to delete the file 16 | identifier: Optional[str] = None # nuggetId for properties in caml 17 | use_ca_id: bool = False # whether or not to use the ca id over nuggetId 18 | 19 | def __init__(self, data: dict): 20 | super().__init__(data=data) 21 | if 'inverted' in data: 22 | self.inverted = data['inverted'] 23 | if 'default_value' in data: 24 | self.value = data['default_value'] 25 | if 'identifier' in data: 26 | self.identifier = data['identifier'] 27 | if 'use_ca_id' in data: 28 | self.use_ca_id = data['use_ca_id'] 29 | 30 | def create_interface(self, options_widget: QWidget, options_layout: QVBoxLayout): 31 | # remove object/setter toggle 32 | remove_chk = QCheckBox(options_widget) 33 | remove_chk.setText(self.label) 34 | remove_chk.setChecked(self.value) 35 | remove_chk.toggled.connect(self.set_option) 36 | options_layout.addWidget(remove_chk) 37 | 38 | def set_option(self, checked: bool): 39 | self.value = checked 40 | self.update_preview() 41 | 42 | def update_preview(self): 43 | if self.preview_lbl != None: 44 | to_hide = (self.inverted and not self.value) or (not self.inverted and self.value) 45 | self.preview_lbl.setVisible(not to_hide) 46 | 47 | def apply(self, container_path: str): 48 | if (self.inverted and not self.value) or (not self.inverted and self.value): 49 | for file in self.files: 50 | path = os.path.join(container_path, *file.split('/')) 51 | # wildcard support 52 | for full_path in glob.glob(path, recursive=True): 53 | if self.identifier != None: 54 | # delete properties in xml 55 | # TODO: make sure it isn't a directory 56 | delete_xml_value(file=full_path, id=self.identifier, use_ca_id=self.use_ca_id) 57 | else: 58 | # delete files or directories 59 | if os.path.isdir(full_path): 60 | rmtree(path=full_path, ignore_errors=True) 61 | else: 62 | os.remove(path=full_path) -------------------------------------------------------------------------------- /tweaks/posterboard/template_options/replace_option.py: -------------------------------------------------------------------------------- 1 | from . import TemplateOption 2 | 3 | from exceptions.nugget_exception import NuggetException 4 | 5 | import os 6 | import glob 7 | from dataclasses import dataclass 8 | from typing import Optional 9 | from PySide6 import QtWidgets, QtGui, QtCore 10 | 11 | @dataclass 12 | class ReplaceOption(TemplateOption): 13 | allowed_files: str # Qt format - ex. "Image Files (*.png)" 14 | button_label: str = "Import" 15 | required: bool = False 16 | value: Optional[str] = None # path of file selected by user 17 | default_preview: Optional[QtGui.QPixmap] = None # the default preview image so that it isn't lost when replacing 18 | 19 | def __init__(self, data: dict): 20 | super().__init__(data=data) 21 | self.allowed_files = data['allowed_files'] 22 | if 'button_label' in data: 23 | self.button_label = data['button_label'] 24 | else: 25 | self.button_label = f"Import {self.allowed_files}" 26 | if 'required' in data: 27 | self.required = data['required'] 28 | self.repl_btn = None 29 | self.file_path_lbl = None 30 | self.window = None 31 | self.import_icon = QtGui.QIcon(":/icon/import.svg") 32 | self.remove_icon = QtGui.QIcon(":/icon/trash.svg") 33 | 34 | def update_preview(self): 35 | if self.preview_lbl != None: 36 | if self.value == None: 37 | # set back to default 38 | self.preview_lbl.setPixmap(self.default_preview) 39 | else: 40 | # set to newly selected image 41 | pixmap = QtGui.QPixmap(self.value) 42 | self.preview_lbl.setPixmap(pixmap) 43 | 44 | def add_potential_preview_lbls(self, lbls): 45 | super().add_potential_preview_lbls(lbls) 46 | if self.preview_lbl != None: 47 | self.default_preview = self.preview_lbl.pixmap() 48 | 49 | def create_interface(self, options_widget: QtWidgets.QWidget, options_layout: QtWidgets.QVBoxLayout): 50 | # replacable object 51 | repl_widget = QtWidgets.QWidget(options_widget) 52 | repl_layout = QtWidgets.QHBoxLayout(options_widget) 53 | repl_layout.setContentsMargins(0, 2, 0, 2) 54 | repl_lbl = QtWidgets.QLabel(repl_widget) 55 | req_label = "" 56 | if self.required: 57 | req_label = "* " 58 | repl_lbl.setText(f"{req_label}{self.label}") 59 | repl_layout.addWidget(repl_lbl) 60 | # button for importing files 61 | imp_widget = QtWidgets.QWidget(options_widget) 62 | imp_layout = QtWidgets.QVBoxLayout(options_widget) 63 | imp_widget.setLayoutDirection(QtCore.Qt.LayoutDirection.RightToLeft) 64 | self.repl_btn = QtWidgets.QToolButton(imp_widget) 65 | self.repl_btn.setIcon(self.import_icon) 66 | self.repl_btn.setIconSize(QtCore.QSize(20, 20)) 67 | self.repl_btn.setText(self.button_label) 68 | self.repl_btn.setToolButtonStyle(QtCore.Qt.ToolButtonStyle.ToolButtonTextBesideIcon) 69 | self.repl_btn.clicked.connect(self.on_importReplaceBtn_clicked) 70 | imp_layout.addWidget(self.repl_btn) 71 | # file path name 72 | self.file_path_lbl = QtWidgets.QLabel(imp_widget) 73 | self.file_path_lbl.hide() 74 | imp_layout.addWidget(self.file_path_lbl) 75 | imp_widget.setLayout(imp_layout) 76 | repl_layout.addWidget(imp_widget) 77 | repl_widget.setLayout(repl_layout) 78 | options_layout.addWidget(repl_widget) 79 | 80 | def on_importReplaceBtn_clicked(self): 81 | if self.value == None: 82 | # import image 83 | selected_file, _ = QtWidgets.QFileDialog.getOpenFileName(self.window, "Select File", "", self.allowed_files, options=QtWidgets.QFileDialog.ReadOnly) 84 | if selected_file != None and selected_file != "": 85 | self.value = selected_file 86 | self.file_path_lbl.setText(selected_file) 87 | self.file_path_lbl.show() 88 | self.repl_btn.setText("Remove Selected File") 89 | self.repl_btn.setIcon(self.remove_icon) 90 | else: 91 | # remove current image 92 | self.value = None 93 | self.file_path_lbl.hide() 94 | self.repl_btn.setText(self.button_label) 95 | self.repl_btn.setIcon(self.import_icon) 96 | self.update_preview() 97 | 98 | def apply(self, container_path: str): 99 | if self.value == None: 100 | if not self.required: 101 | return 102 | elif self.required and self.value == None: 103 | raise NuggetException(f"Error applying template:\n\nNo selected file for required option {self.label}") 104 | contents=None 105 | in_path = os.path.join(container_path, self.value) 106 | with open(in_path, "rb") as in_file: 107 | contents = in_file.read() 108 | for file in self.files: 109 | out_path = os.path.join(container_path, *file.split('/')) 110 | # wildcard support 111 | for full_path in glob.glob(out_path, recursive=True): 112 | with open(full_path, "wb") as out_file: 113 | out_file.write(contents) 114 | del contents -------------------------------------------------------------------------------- /tweaks/posterboard/template_options/template_options.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from dataclasses import dataclass 3 | from shutil import rmtree 4 | from typing import Optional 5 | from PySide6.QtWidgets import QVBoxLayout, QWidget, QLabel 6 | 7 | class OptionType(Enum): 8 | replace = "replace" 9 | remove = "remove" 10 | set = "set" 11 | picker = "picker" 12 | 13 | @dataclass 14 | class TemplateOption: 15 | type: OptionType 16 | label: str 17 | files: list[str] 18 | associated_preview: Optional[str] 19 | preview_lbl: Optional[QLabel] 20 | 21 | def __init__(self, data: dict): 22 | self.type = OptionType[data['type']] 23 | self.label = data['label'] 24 | if self.type == OptionType.picker: 25 | # picker should not have files 26 | self.files = [] 27 | else: 28 | self.files = data['files'] 29 | if 'associated_preview' in data: 30 | self.associated_preview = data['associated_preview'] 31 | else: 32 | self.associated_preview = None 33 | self.preview_lbl = None 34 | 35 | def add_potential_preview_lbls(self, lbls: dict[str, QLabel]): 36 | if self.associated_preview != None and self.associated_preview in lbls: 37 | self.preview_lbl = lbls[self.associated_preview] 38 | def update_preview(self): 39 | pass 40 | 41 | def create_interface(self, options_widget: QWidget, options_layout: QVBoxLayout): 42 | raise NotImplementedError 43 | 44 | def apply(self, container_path: str): 45 | raise NotImplementedError -------------------------------------------------------------------------------- /tweaks/posterboard/tendie_file.py: -------------------------------------------------------------------------------- 1 | import os 2 | import uuid 3 | import zipfile 4 | 5 | class TendieFile: 6 | path: str 7 | name: str 8 | descriptor_cnt: int 9 | is_container: bool 10 | unsafe_container: bool 11 | loaded: bool 12 | 13 | def __init__(self, path: str): 14 | self.path = path 15 | self.name = os.path.basename(path) 16 | self.descriptor_cnt = 0 17 | self.is_container = False 18 | self.unsafe_container = False 19 | self.loaded = False 20 | 21 | # read the contents 22 | with zipfile.ZipFile(path, mode="r") as archive: 23 | for option in archive.namelist(): 24 | if "__macosx/" in option.lower(): 25 | continue 26 | if "container" in option.lower(): 27 | self.is_container = True 28 | # check for the unsafe file that requires prb reset 29 | if "PBFPosterExtensionDataStoreSQLiteDatabase.sqlite3" in option: 30 | self.unsafe_container = True 31 | if "descriptor/" in option.lower(): 32 | item = option.lower().split("descriptor/")[1] 33 | if item.count('/') == 1 and item.endswith('/'): 34 | self.descriptor_cnt += 1 35 | elif "descriptors/" in option.lower(): 36 | item = option.lower().split("descriptors/")[1] 37 | if item.count('/') == 1 and item.endswith('/'): 38 | self.descriptor_cnt += 1 39 | 40 | def get_icon(self): 41 | if self.is_container: 42 | # container 43 | return ":/icon/shippingbox.svg" 44 | elif self.descriptor_cnt == 1: 45 | # single descriptor 46 | return ":/icon/photo.svg" 47 | else: 48 | # multiple descriptors 49 | return ":/icon/photo-stack.svg" 50 | 51 | def extract(self, output_dir: str): 52 | zip_output = os.path.join(output_dir, str(uuid.uuid4())) 53 | os.makedirs(zip_output) 54 | with zipfile.ZipFile(self.path, 'r') as zip_ref: 55 | zip_ref.extractall(zip_output) -------------------------------------------------------------------------------- /tweaks/tweak_classes.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from devicemanagement.constants import Version 3 | from .basic_plist_locations import FileLocation 4 | 5 | class Tweak: 6 | def __init__( 7 | self, 8 | key: str, 9 | value: any = 1, 10 | owner: int = 501, group: int = 501 11 | ): 12 | self.key = key 13 | self.value = value 14 | self.owner = owner 15 | self.group = group 16 | self.enabled = False 17 | 18 | def set_enabled(self, value: bool): 19 | self.enabled = value 20 | def toggle_enabled(self): 21 | self.enabled = not self.enabled 22 | def set_value(self, new_value: any, toggle_enabled: bool = True): 23 | self.value = new_value 24 | if toggle_enabled: 25 | self.enabled = True 26 | 27 | def apply_tweak(self): 28 | raise NotImplementedError 29 | 30 | class NullifyFileTweak(Tweak): 31 | def __init__( 32 | self, 33 | file_location: FileLocation, 34 | owner: int = 501, group: int = 501 35 | ): 36 | super().__init__(key=None, value=None, owner=owner, group=group) 37 | self.file_location = file_location 38 | 39 | def apply_tweak(self, other_tweaks: dict): 40 | if self.enabled: 41 | other_tweaks[self.file_location] = b"" 42 | 43 | 44 | class BasicPlistTweak(Tweak): 45 | def __init__( 46 | self, 47 | file_location: FileLocation, 48 | key: str, 49 | value: any = True, 50 | owner: int = 501, group: int = 501, 51 | is_risky: bool = False 52 | ): 53 | super().__init__(key=key, value=value, owner=owner, group=group) 54 | self.file_location = file_location 55 | self.is_risky = is_risky 56 | 57 | def apply_tweak(self, other_tweaks: dict, risky_allowed: bool = False) -> dict: 58 | if not self.enabled or (self.is_risky and not risky_allowed): 59 | return other_tweaks 60 | if self.file_location in other_tweaks: 61 | other_tweaks[self.file_location][self.key] = self.value 62 | else: 63 | other_tweaks[self.file_location] = {self.key: self.value} 64 | return other_tweaks 65 | 66 | class AdvancedPlistTweak(BasicPlistTweak): 67 | def __init__( 68 | self, 69 | file_location: FileLocation, 70 | keyValues: dict, 71 | owner: int = 501, group: int = 501, 72 | is_risky: bool = False 73 | ): 74 | super().__init__(file_location=file_location, key=None, value=keyValues, owner=owner, group=group, is_risky=is_risky) 75 | 76 | def set_multiple_values(self, keys: list[str], value: any): 77 | for key in keys: 78 | self.value[key] = value 79 | 80 | def apply_tweak(self, other_tweaks: dict, risky_allowed: bool = False) -> dict: 81 | if not self.enabled or (self.is_risky and not risky_allowed): 82 | return other_tweaks 83 | plist = {} 84 | for key in self.value: 85 | plist[key] = self.value[key] 86 | other_tweaks[self.file_location] = plist 87 | return other_tweaks 88 | 89 | 90 | class RdarFixTweak(BasicPlistTweak): 91 | def __init__(self): 92 | super().__init__(file_location=FileLocation.resolution, key=None) 93 | self.mode = 0 94 | self.di_type = -1 95 | 96 | def get_rdar_mode(self, model: str) -> int: 97 | if (model == "iPhone11,2" or model == "iPhone11,4" or model == "iPhone11,6" 98 | or model == "iPhone11,8" 99 | or model == "iPhone12,1" or model == "iPhone12,3" or model == "iPhone12,5"): 100 | self.mode = 1 101 | elif (model == "iPhone13,2" or model == "iPhone13,3" or model == "iPhone13,4" 102 | or model == "iPhone14,5" or model == "iPhone14,2" or model == "iPhone14,3" 103 | or model == "iPhone14,7" or model == "iPhone14,8"): 104 | self.mode = 2 105 | elif (model == "iPhone12,8" or model == "iPhone14,6"): 106 | self.mode = 3 107 | return self.mode 108 | 109 | def get_rdar_title(self) -> str: 110 | if self.mode == 1 or self.mode == 3: 111 | if self.di_type == -1: 112 | return "Revert RDAR fix" 113 | return "Fix RDAR" 114 | elif self.mode == 2: 115 | if self.di_type == -1: 116 | return "Revert Status Bar Fix" 117 | return "Dynamic Island Status Bar Fix" 118 | return "hide" 119 | 120 | def set_di_type(self, type: int): 121 | self.di_type = type 122 | 123 | def apply_tweak(self, other_tweaks: dict, risky_allowed: bool = False) -> dict: 124 | if not self.enabled: 125 | return other_tweaks 126 | if self.di_type == -1: 127 | # revert the fix 128 | other_tweaks[self.file_location] = {} 129 | elif self.mode == 1: 130 | # iPhone XR, XS, and 11 131 | plist = { 132 | "canvas_height": 1791, 133 | "canvas_width": 828 134 | } 135 | other_tweaks[self.file_location] = plist 136 | elif self.mode == 3: 137 | # iPhone SEs 138 | plist = { 139 | "canvas_height": 1779, 140 | "canvas_width": 1000 141 | } 142 | other_tweaks[self.file_location] = plist 143 | elif self.mode == 2: 144 | # Status bar fix (iPhone 12+) 145 | width = 2868 146 | height = 1320 147 | if self.di_type == 2556: 148 | width = 1179 149 | height = 2556 150 | elif self.di_type == 2796 or self.di_type == 2976: 151 | width = 1290 152 | height = 2796 153 | elif self.di_type == 2622: 154 | width = 1206 155 | height = 2622 156 | elif self.di_type == 2868: 157 | width = 1320 158 | height = 2868 159 | plist = { 160 | "canvas_height": height, 161 | "canvas_width": width 162 | } 163 | other_tweaks[self.file_location] = plist 164 | return other_tweaks 165 | 166 | 167 | class MobileGestaltTweak(Tweak): 168 | def __init__( 169 | self, 170 | key: str, subkey: str = None, 171 | value: any = 1, 172 | owner: int = 501, group: int = 501 173 | ): 174 | super().__init__(key, value, owner, group) 175 | self.subkey = subkey 176 | 177 | def apply_tweak(self, plist: dict): 178 | if not self.enabled: 179 | return plist 180 | new_value = self.value 181 | if self.subkey == None: 182 | plist["CacheExtra"][self.key] = new_value 183 | else: 184 | plist["CacheExtra"][self.key][self.subkey] = new_value 185 | return plist 186 | 187 | class MobileGestaltPickerTweak(Tweak): 188 | def __init__( 189 | self, 190 | key: str, subkey: str = None, 191 | values: list = [1] 192 | ): 193 | super().__init__(key=key, value=values) 194 | self.subkey = subkey 195 | self.selected_option = 0 # index of the selected option 196 | 197 | def apply_tweak(self, plist: dict): 198 | if not self.enabled or self.value[self.selected_option] == "Placeholder": 199 | return plist 200 | new_value = self.value[self.selected_option] 201 | if self.subkey == None: 202 | plist["CacheExtra"][self.key] = new_value 203 | else: 204 | plist["CacheExtra"][self.key][self.subkey] = new_value 205 | return plist 206 | 207 | def set_selected_option(self, new_option: int, is_enabled: bool = True): 208 | self.selected_option = new_option 209 | self.enabled = is_enabled 210 | 211 | def get_selected_option(self) -> int: 212 | return self.selected_option 213 | 214 | class MobileGestaltMultiTweak(Tweak): 215 | def __init__(self, keyValues: dict): 216 | super().__init__(key=None) 217 | self.keyValues = keyValues 218 | # key values looks like ["key name" = value] 219 | 220 | def apply_tweak(self, plist: dict): 221 | if not self.enabled: 222 | return plist 223 | for key in self.keyValues: 224 | plist["CacheExtra"][key] = self.keyValues[key] 225 | return plist 226 | 227 | class FeatureFlagTweak(Tweak): 228 | def __init__( 229 | self, 230 | flag_category: str, flag_names: list, 231 | is_list: bool=True, inverted: bool=False 232 | ): 233 | super().__init__(key=None) 234 | self.flag_category = flag_category 235 | self.flag_names = flag_names 236 | self.is_list = is_list 237 | self.inverted = inverted 238 | 239 | def apply_tweak(self, plist: dict): 240 | to_enable = self.enabled 241 | if self.inverted: 242 | to_enable = not self.enabled 243 | # create the category list if it doesn't exist 244 | if not self.flag_category in plist: 245 | plist[self.flag_category] = {} 246 | for flag in self.flag_names: 247 | if self.is_list: 248 | plist[self.flag_category][flag] = { 249 | 'Enabled': to_enable 250 | } 251 | else: 252 | plist[self.flag_category][flag] = to_enable 253 | return plist -------------------------------------------------------------------------------- /version.txt: -------------------------------------------------------------------------------- 1 | # UTF-8 2 | # 3 | # For more details about fixed file info 'ffi' see: 4 | # http://msdn.microsoft.com/en-us/library/ms646997.aspx 5 | VSVersionInfo( 6 | ffi=FixedFileInfo( 7 | # filevers and prodvers should be always a tuple with four items: (1, 2, 3, 4) 8 | # Set not needed items to zero 0. 9 | filevers=(1, 0, 0, 0), 10 | prodvers=(1, 0, 0, 0), 11 | # Contains a bitmask that specifies the valid bits 'flags'r 12 | mask=0x3f, 13 | # Contains a bitmask that specifies the Boolean attributes of the file. 14 | flags=0x0, 15 | # The operating system for which this file was designed. 16 | # 0x4 - NT and there is no need to change it. 17 | OS=0x4, 18 | # The general type of file. 19 | # 0x1 - the file is an application. 20 | fileType=0x1, 21 | # The function of the file. 22 | # 0x0 - the function is not defined for this fileType 23 | subtype=0x0, 24 | # Creation date and time stamp. 25 | date=(0, 0) 26 | ), 27 | kids=[ 28 | StringFileInfo( 29 | [ 30 | StringTable( 31 | u'040904B0', 32 | [StringStruct(u'CompanyName', u'Cowabunga'), 33 | StringStruct(u'FileDescription', u'Nugget'), 34 | StringStruct(u'FileVersion', u'5.2'), 35 | StringStruct(u'InternalName', u'Nugget'), 36 | StringStruct(u'LegalCopyright', u'Copyright (c) 2025 LeminLimez'), 37 | StringStruct(u'OriginalFilename', u'Nugget.exe'), 38 | StringStruct(u'ProductName', u'Nugget'), 39 | StringStruct(u'ProductVersion', u'5.2.0')]) 40 | ]), 41 | VarFileInfo([VarStruct(u'Translation', [1033, 1200])]) 42 | ] 43 | ) --------------------------------------------------------------------------------