├── .gitattributes ├── Build IPSW Updater.applescript ├── IPSW Updater Resources └── Credits.rtf ├── IPSW Updater.jxa ├── LICENSE └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | *.jxa linguist-language=JavaScript 2 | -------------------------------------------------------------------------------- /Build IPSW Updater.applescript: -------------------------------------------------------------------------------- 1 | -- 2 | -- Created by Pico Mitchell (of Free Geek) 3 | -- 4 | -- https://ipsw.app 5 | -- https://github.com/freegeek-pdx/IPSW-Updater 6 | -- 7 | -- MIT License 8 | -- 9 | -- Copyright (c) 2022 Free Geek 10 | -- 11 | -- Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), 12 | -- to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, 13 | -- and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 14 | -- 15 | -- The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 16 | -- 17 | -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | -- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 19 | -- WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | -- 21 | 22 | use AppleScript version "2.7" 23 | use scripting additions 24 | use framework "Foundation" 25 | 26 | set bundleIdentifierPrefix to "org.freegeek." 27 | 28 | set pathToMeInfo to (info for (path to me)) 29 | 30 | try 31 | set infoPlistPath to ((POSIX path of (path to me)) & "Contents/Info.plist") 32 | ((infoPlistPath as POSIX file) as alias) 33 | 34 | set AppleScript's text item delimiters to "-" 35 | set correctBundleIdentifier to bundleIdentifierPrefix & ((words of (name of me)) as text) 36 | try 37 | set currentBundleIdentifier to ((do shell script "/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' " & (quoted form of infoPlistPath)) as text) 38 | if (currentBundleIdentifier is not equal to correctBundleIdentifier) then error "INCORRECT Bundle Identifier" 39 | on error 40 | do shell script "plutil -replace CFBundleIdentifier -string " & (quoted form of correctBundleIdentifier) & " " & (quoted form of infoPlistPath) 41 | 42 | try 43 | set currentCopyright to ((do shell script "/usr/libexec/PlistBuddy -c 'Print :NSHumanReadableCopyright' " & (quoted form of infoPlistPath)) as text) 44 | if (currentCopyright does not contain "Twemoji") then error "INCORRECT Copyright" 45 | on error 46 | do shell script "plutil -replace NSHumanReadableCopyright -string " & (quoted form of ("Copyright © " & (year of (current date)) & " Free Geek 47 | Designed and Developed by Pico Mitchell")) & " " & (quoted form of infoPlistPath) 48 | end try 49 | 50 | try 51 | set minSystemVersion to ((do shell script "/usr/libexec/PlistBuddy -c 'Print :LSMinimumSystemVersion' " & (quoted form of infoPlistPath)) as text) 52 | if (minSystemVersion is not equal to "10.13") then error "INCORRECT Minimum System Version" 53 | on error 54 | do shell script "plutil -remove LSMinimumSystemVersionByArchitecture " & (quoted form of infoPlistPath) & "; plutil -replace LSMinimumSystemVersion -string '10.13' " & (quoted form of infoPlistPath) 55 | end try 56 | 57 | try 58 | set prohibitMultipleInstances to ((do shell script "/usr/libexec/PlistBuddy -c 'Print :LSMultipleInstancesProhibited' " & (quoted form of infoPlistPath)) as number) 59 | if (prohibitMultipleInstances is equal to 0) then error "INCORRECT Multiple Instances Prohibited" 60 | on error 61 | do shell script "plutil -replace LSMultipleInstancesProhibited -bool true " & (quoted form of infoPlistPath) 62 | end try 63 | 64 | try 65 | set allowMixedLocalizations to ((do shell script "/usr/libexec/PlistBuddy -c 'Print :CFBundleAllowMixedLocalizations' " & (quoted form of infoPlistPath)) as number) 66 | if (allowMixedLocalizations is equal to 1) then error "INCORRECT Localization" 67 | on error 68 | do shell script "plutil -replace CFBundleAllowMixedLocalizations -bool false " & (quoted form of infoPlistPath) & "; plutil -replace CFBundleDevelopmentRegion -string 'en_US' " & (quoted form of infoPlistPath) 69 | end try 70 | 71 | try 72 | set currentAppleEventsUsageDescription to ((do shell script "/usr/libexec/PlistBuddy -c 'Print :NSAppleEventsUsageDescription' " & (quoted form of infoPlistPath)) as text) 73 | if (currentAppleEventsUsageDescription does not contain (name of me)) then error "INCORRECT AppleEvents Usage Description" 74 | on error 75 | do shell script "plutil -replace NSAppleEventsUsageDescription -string " & (quoted form of ("You MUST click the “OK” button for “" & (name of me) & "” to be able to function.")) & " " & (quoted form of infoPlistPath) 76 | end try 77 | 78 | try 79 | set currentVersion to ((do shell script "/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' " & (quoted form of infoPlistPath)) as text) 80 | if (currentVersion is equal to "1.0") then error "INCORRECT Version" 81 | on error 82 | set shortCreationDateString to (short date string of (creation date of pathToMeInfo)) 83 | set AppleScript's text item delimiters to "/" 84 | set correctVersion to ((text item 3 of shortCreationDateString) & "." & (text item 1 of shortCreationDateString) & "." & (text item 2 of shortCreationDateString)) 85 | do shell script "plutil -remove CFBundleVersion " & (quoted form of infoPlistPath) & "; plutil -replace CFBundleShortVersionString -string " & (quoted form of correctVersion) & " " & (quoted form of infoPlistPath) 86 | end try 87 | 88 | -- The "main.scpt" must NOT be writable to prevent the code signature from being invalidated: https://developer.apple.com/library/archive/releasenotes/AppleScript/RN-AppleScript/RN-10_8/RN-10_8.html#//apple_ref/doc/uid/TP40000982-CH108-SW8 89 | do shell script "osascript -e 'delay 0.5' -e 'repeat while (application \"" & (POSIX path of (path to me)) & "\" is running)' -e 'delay 0.5' -e 'end repeat' -e 'try' -e 'do shell script \"chmod a-w \\\"" & ((POSIX path of (path to me)) & "Contents/Resources/Scripts/main.scpt") & "\\\"\"' -e 'do shell script \"codesign -fs \\\"Developer ID Application\\\" --strict \\\"" & (POSIX path of (path to me)) & "\\\"\"' -e 'on error codeSignError' -e 'activate' -e 'display alert \"Code Sign Error\" message codeSignError' -e 'end try' -e 'do shell script \"open -na \\\"" & (POSIX path of (path to me)) & "\\\"\"' > /dev/null 2>&1 &" 90 | quit 91 | delay 10 92 | end try 93 | end try 94 | 95 | set builderFileName to (displayed name of pathToMeInfo) 96 | if (builderFileName contains ".") then -- "displayed name" could still contain extension that we need to remove if Finder settings are set to include them. 97 | set builderFileNameExtension to ("." & (name extension of pathToMeInfo)) 98 | set AppleScript's text item delimiters to builderFileNameExtension 99 | set builderFileName to ((first text item of builderFileName) as text) 100 | end if 101 | 102 | set AppleScript's text item delimiters to "Build " 103 | set projectName to ((last text item of builderFileName) as text) 104 | 105 | set AppleScript's text item delimiters to "-" 106 | set projectNameForBundleID to ((words of projectName) as text) 107 | 108 | set projectFolderPath to (POSIX path of (((path to me) as text) & "::")) -- https://apple.stackexchange.com/a/302397 109 | 110 | set appBuildPath to projectFolderPath & "Build/" & projectName & ".app" 111 | 112 | repeat 113 | waitUntilAwakeAndUnlocked() 114 | activate 115 | set buildAlertReply to display alert builderFileName & "?" buttons {"Launch", "Quit", "Build"} cancel button 2 default button 3 116 | delay 0.2 -- Delay a moment to allow the prompt to close. 117 | 118 | try 119 | if (button returned of buildAlertReply is "Build") then 120 | repeat while (application appBuildPath is running) 121 | activate 122 | try 123 | do shell script "afplay /System/Library/Sounds/Basso.aiff > /dev/null 2>&1 &" 124 | on error 125 | beep 126 | end try 127 | display alert "You must Quit " & projectName & " to Build." buttons {"Cancel", "Try Again"} cancel button 1 default button 2 128 | end repeat 129 | 130 | try -- IPSW Updater should never have any TCC permissions, but clear them just in case to always reproduce any un-intented prompts for a new build 131 | do shell script ("tccutil reset All " & (quoted form of (bundleIdentifierPrefix & projectNameForBundleID))) 132 | end try 133 | 134 | do shell script ("rm -rf " & (quoted form of appBuildPath)) 135 | 136 | try 137 | (((projectFolderPath & "Build") as POSIX file) as alias) 138 | on error 139 | try 140 | do shell script ("mkdir -p " & (quoted form of (projectFolderPath & "Build"))) 141 | end try 142 | end try 143 | 144 | try 145 | (((projectFolderPath & projectName & " Resources") as POSIX file) as alias) 146 | on error 147 | try 148 | do shell script ("mkdir -p " & (quoted form of (projectFolderPath & projectName & " Resources"))) 149 | end try 150 | end try 151 | 152 | -- Since JXA cannot be compiled as Run-Only like regular AppleScript can and I wanted to be able to distribute the applet in a way that the source could not be edited. 153 | -- To do this, I'm using Google Closure Compiler to minify the JXA source code, and then compressing that minified source with gzip and then base64 encoding that compressed data. 154 | -- The resuling base64 encoded string of the gzipped and minified source code is stored within a JXA wrapper script and decoded and decompressed 155 | -- back into the minified source code when the applet is launched and the minified source code is then run via "eval()". 156 | -- This is nothing crazy that couldn't be reversed by someone who wanted to to be able to retrieve the raw minified source code, 157 | -- but that doesn't really matter since this project with the un-minified source code all released as open source anyways. 158 | -- The purpose of the is more the make the distributed applet not easily editable rather than protecting the source code from being retrieved. 159 | 160 | set jxaSourcePath to (projectFolderPath & projectName & ".jxa") 161 | 162 | set appVersion to (do shell script "awk -F \"'\" '($1 == \"const appVersion = \") { print $(NF-1); exit }' " & (quoted form of jxaSourcePath)) 163 | if (appVersion is equal to "") then error "FAILED TO GET APP VERSION" 164 | 165 | -- Make sure Google Closure Compiler is installed, and check that it's the latest version and prompt if an update is available. 166 | -- https://developers.google.com/closure/compiler/ & https://search.maven.org/artifact/com.google.javascript/closure-compiler & https://mvnrepository.com/artifact/com.google.javascript/closure-compiler & https://github.com/google/closure-compiler 167 | set newestInstalledGoogleClosureCompilerPath to (do shell script ("ls -t " & (quoted form of projectFolderPath) & "closure-compiler*.jar | head -1")) 168 | set installedGoogleClosureCompileVersion to (do shell script ("java -jar " & (quoted form of newestInstalledGoogleClosureCompilerPath) & " --version | awk -F ': ' '($1 == \"Version\") { print $NF; exit }'")) 169 | if (installedGoogleClosureCompileVersion is equal to "") then 170 | open location "https://maven-badges.herokuapp.com/maven-central/com.google.javascript/closure-compiler" 171 | error "MINIFY JXA ERROR (GOOGLE CLOSURE COMPILER NOT FOUND)" 172 | end if 173 | 174 | set latestGoogleClosureCompileVersion to (do shell script "curl -m 5 --retry 2 -sfw '%{redirect_url}' -o /dev/null 'https://maven-badges.herokuapp.com/maven-central/com.google.javascript/closure-compiler' | awk -F '/' '{ print $7; exit }'") 175 | if (latestGoogleClosureCompileVersion does not start with "v2") then 176 | beep 177 | else if (installedGoogleClosureCompileVersion is not equal to latestGoogleClosureCompileVersion) then 178 | set didChooseDownloadLatestGoogleClosureCompiler to false 179 | try 180 | waitUntilAwakeAndUnlocked() 181 | activate 182 | display alert "Newer Google Closure Compiler Available" message ("Google Closure Compiler version " & latestGoogleClosureCompileVersion & " is now available! 183 | 184 | Google Closure Compiler version " & installedGoogleClosureCompileVersion & " is currently installed.") buttons {("Continue Build with Google Closure Compiler " & installedGoogleClosureCompileVersion), ("Download Google Closure Compiler " & latestGoogleClosureCompileVersion)} cancel button 1 default button 2 185 | set didChooseDownloadLatestGoogleClosureCompiler to true 186 | open location "https://maven-badges.herokuapp.com/maven-central/com.google.javascript/closure-compiler" 187 | end try 188 | 189 | if (didChooseDownloadLatestGoogleClosureCompiler) then 190 | error number -128 -- Simulate user canceled if chose to download latest Google Closure Compiler version. 191 | end if 192 | end if 193 | 194 | -- GOOGLE CLOSURE COMPILER OPTION NOTES: 195 | -- The following compilation/minification results in about 95 KB space savings of the JXA source code (about 180 KB down to about 85 KB). 196 | -- DO NOT SET "--compilation_level 'ADVANCED'" which would result in about 10 KB more space savings, but breaks lots of code because it renames properties which need to NOT be renamed when they come from and are written to JSON files, which SIMPLE compilation (the default setting) does not do. 197 | -- Set "--language_in 'ECMASCRIPT_2018'" so that it will error if any JavaScript features are used that may not be supported on macOS 10.13 High Sierra (which I found to support ES2018 and NOT ES2019 by manually checking for newer language features). 198 | -- Set "--assume_function_wrapper" so that top level global names are also renamed (not just names within functions) to save space since those names are never referenced from the main wrapper script (this results in about 16 KB extra space savings vs not using this option). 199 | -- Set "--formatting 'SINGLE_QUOTES'" only because I prefer defaulting to single quoted strings, but it's not really necessary and shouldn't make a difference to the code either way. 200 | -- NOTE: Even though Google Closure Compiler will rename variables and functions, there should be no conflicts with the variable names in the main wrapper script since the code will be run via "eval()" which appears to be its own block scope and none of the variables or function are called outside of "eval()" after it runs. 201 | set jxaMinifiedSource to (do shell script ("java -jar " & (quoted form of newestInstalledGoogleClosureCompilerPath) & " --language_in 'ECMASCRIPT_2018' --language_out 'ECMASCRIPT_2018' --assume_function_wrapper --formatting 'SINGLE_QUOTES' --js " & (quoted form of jxaSourcePath))) 202 | set jxaMinifiedSourceLength to (length of jxaMinifiedSource) 203 | if (jxaMinifiedSourceLength is equal to 0) then error "MINIFY JXA ERROR (GOOGLE CLOSURE COMPILER FAILED)" 204 | 205 | -- GZIP THE MINIFIED GOOGLE CLOSURE COMPILER OUTPUT TO SAVE EVEN MORE SPACE: 206 | -- As stated in the FAQ, the way Google Closure Compiler minifies code is actually done with the intention of the result being gzipping: https://github.com/google/closure-compiler/wiki/FAQ#closure-compiler-inlined-all-my-strings-which-made-my-code-size-bigger-why-did-it-do-that 207 | -- Gzipping gets the size all the way down to about 25 KB, and then base64 encoding the gzipped data brings it back up to about 30 KB, but still a massive savings over not gzipping. 208 | set jxaObfuscatedSource to (do shell script ("printf '%s' " & (quoted form of jxaMinifiedSource) & " | gzip -9 | base64")) 209 | if (jxaObfuscatedSource is equal to "") then error "GZIP/BASE64 FAILED" 210 | 211 | set initialSourceComments to (do shell script ("awk '($1 == \"//\") { print } ($0 == \"\") { exit }' " & (quoted form of jxaSourcePath))) 212 | if (initialSourceComments is equal to "") then 213 | error "FAILED TO GET INITIAL SOURCE COMMENTS" 214 | else 215 | set initialSourceComments to (initialSourceComments & linefeed & linefeed) 216 | end if 217 | 218 | do shell script "osacompile -l 'JavaScript' -o " & (quoted form of appBuildPath) & " -e " & (quoted form of (initialSourceComments & ¬ 219 | "'use strict';" & ¬ 220 | "ObjC.import('AppKit');" & ¬ 221 | "const a=Application.currentApplication();" & ¬ 222 | "a.includeStandardAdditions=true;" & ¬ 223 | "let s='Failed to Load Source';" & ¬ 224 | "try{" & ¬ 225 | "const ab=$.NSBundle.mainBundle;" & ¬ 226 | "const ap=ab.bundlePath.js;" & ¬ 227 | "if(ap.endsWith('.app')){" & ¬ 228 | "const zt=$.NSTask.alloc.init;" & ¬ 229 | "zt.executableURL=$.NSURL.fileURLWithPath('/usr/bin/zcat');" & ¬ 230 | "zt.standardInput=$.NSPipe.pipe;" & ¬ 231 | "const ztI=zt.standardInput.fileHandleForWriting;" & ¬ 232 | "ztI.writeData($.NSData.alloc.initWithBase64EncodedStringOptions('" & jxaObfuscatedSource & "',$.NSDataBase64DecodingIgnoreUnknownCharacters));" & ¬ 233 | "ztI.closeFile;" & ¬ 234 | "zt.standardOutput=$.NSPipe.pipe;" & ¬ 235 | "zt.launchAndReturnError($());" & ¬ 236 | "const ztO=zt.standardOutput.fileHandleForReading;" & ¬ 237 | "s=$.NSString.alloc.initWithDataEncoding((ztO.respondsToSelector('readDataToEndOfFileAndReturnError:')?ztO.readDataToEndOfFileAndReturnError($()):ztO.readDataToEndOfFile),$.NSUTF8StringEncoding).js;" & ¬ 238 | "if(!s)throw new Error('Source Decode/Decompress Error');" & ¬ 239 | "if(s.length!=" & jxaMinifiedSourceLength & ")throw new Error(`Invalid Decoded/Decompressed Source (${s.length} ≠ " & jxaMinifiedSourceLength & ")`);" & ¬ 240 | "eval(s);" & ¬ 241 | "}else a.doShellScript('/usr/bin/open -nb " & bundleIdentifierPrefix & projectNameForBundleID & "')" & ¬ 242 | "}catch(e){" & ¬ 243 | "if(e.errorNumber!==-128){" & ¬ 244 | "delay(0.5);" & ¬ 245 | "a.activate();" & ¬ 246 | "try{" & ¬ 247 | "a.displayAlert(`" & projectName & ": ${((!s)?'Source Not Decoded/Decompressed':((s.length==" & jxaMinifiedSourceLength & ")?'Runtime Error':s.substring(0,100)))}`,{message:`${e}\\n\\n${JSON.stringify(e,Object.getOwnPropertyNames(e))}`,as:'critical',buttons:['Quit','Re-Download “" & projectName & "”'],cancelButton:1,defaultButton:2});" & ¬ 248 | "a.doShellScript('/usr/bin/open https://ipsw.app/download/')" & ¬ 249 | "}catch(e){}}}")) 250 | 251 | set quotedBuiltAppInfoPlistPath to (quoted form of (appBuildPath & "/Contents/Info.plist")) 252 | -- The "main.scpt" for normal AppleScript applets must NOT be writable to prevent the code signature from being invalidated: https://developer.apple.com/library/archive/releasenotes/AppleScript/RN-AppleScript/RN-10_8/RN-10_8.html#//apple_ref/doc/uid/TP40000982-CH108-SW8 253 | -- I don't think that applies to JXA applets since they do not write back properties to the script file, but still doesn't hurt to set it as not writable. 254 | do shell script (" 255 | chmod a-w " & (quoted form of (appBuildPath & "/Contents/Resources/Scripts/main.scpt")) & " 256 | 257 | plutil -replace CFBundleIdentifier -string " & (quoted form of (bundleIdentifierPrefix & projectNameForBundleID)) & " " & quotedBuiltAppInfoPlistPath & " 258 | plutil -replace CFBundleShortVersionString -string " & (quoted form of appVersion) & " " & quotedBuiltAppInfoPlistPath & " 259 | 260 | plutil -remove LSMinimumSystemVersionByArchitecture " & quotedBuiltAppInfoPlistPath & " 261 | plutil -replace LSMinimumSystemVersion -string '10.13' " & quotedBuiltAppInfoPlistPath & " 262 | 263 | plutil -replace LSMultipleInstancesProhibited -bool true " & quotedBuiltAppInfoPlistPath & " 264 | 265 | plutil -replace NSHumanReadableCopyright -string " & (quoted form of ("Copyright © " & (year of (current date)) & " Free Geek 266 | Designed and Developed by Pico Mitchell")) & " " & quotedBuiltAppInfoPlistPath & " 267 | 268 | # Force English to be able to remove menu items by their English titles (and no text within the app is localized anyways). 269 | plutil -replace CFBundleDevelopmentRegion -string 'en_US' " & quotedBuiltAppInfoPlistPath & " 270 | plutil -replace CFBundleAllowMixedLocalizations -bool false " & quotedBuiltAppInfoPlistPath & " 271 | 272 | plutil -remove NSHomeKitUsageDescription " & quotedBuiltAppInfoPlistPath & " 273 | plutil -remove NSAppleMusicUsageDescription " & quotedBuiltAppInfoPlistPath & " 274 | plutil -remove NSCalendarsUsageDescription " & quotedBuiltAppInfoPlistPath & " 275 | plutil -remove NSSiriUsageDescription " & quotedBuiltAppInfoPlistPath & " 276 | plutil -remove NSCameraUsageDescription " & quotedBuiltAppInfoPlistPath & " 277 | plutil -remove NSMicrophoneUsageDescription " & quotedBuiltAppInfoPlistPath & " 278 | plutil -remove NSAppleEventsUsageDescription " & quotedBuiltAppInfoPlistPath & " 279 | plutil -remove NSRemindersUsageDescription " & quotedBuiltAppInfoPlistPath & " 280 | plutil -remove NSContactsUsageDescription " & quotedBuiltAppInfoPlistPath & " 281 | plutil -remove NSPhotoLibraryUsageDescription " & quotedBuiltAppInfoPlistPath & " 282 | plutil -remove NSSystemAdministrationUsageDescription " & quotedBuiltAppInfoPlistPath & " 283 | 284 | mv " & (quoted form of (appBuildPath & "/Contents/MacOS/applet")) & " " & (quoted form of (appBuildPath & "/Contents/MacOS/" & projectName)) & " 285 | plutil -replace CFBundleExecutable -string " & (quoted form of projectName) & " " & quotedBuiltAppInfoPlistPath & " 286 | 287 | mv " & (quoted form of (appBuildPath & "/Contents/Resources/applet.rsrc")) & " " & (quoted form of (appBuildPath & "/Contents/Resources/" & projectName & ".rsrc")) & " 288 | 289 | rm -f " & (quoted form of (appBuildPath & "/Contents/Resources/applet.icns")) & " 290 | plutil -replace CFBundleIconFile -string " & (quoted form of projectName) & " " & quotedBuiltAppInfoPlistPath & " 291 | 292 | ditto " & (quoted form of (projectFolderPath & projectName & " Resources/")) & " " & (quoted form of (appBuildPath & "/Contents/Resources/")) & " 293 | rm -f " & (quoted form of (appBuildPath & "/Contents/Resources/.DS_Store")) & " 294 | 295 | # Any xattrs MUST be cleared for 'codesign' to not error (this MUST be done BEFORE code signing the following 'Launch IPSW Updater' script since signing shell scripts stores the code signature in the xattrs, and those specific xattrs do not prevent code signing of the app itself). 296 | xattr -crs " & (quoted form of appBuildPath) & " 297 | 298 | touch " & (quoted form of appBuildPath) & " 299 | 300 | # The following 'Launch IPSW Updater' script is created and SIGNED so that it can be used for the LaunchAgent that can be created by the 'IPSW Updater' app, 301 | # and it MUST be signed so that the 'AssociatedBundleIdentifier' key can be used in macOS 13 Ventura so that the LaunchAgent is properly displayed as being for the IPSW Updater app. 302 | # This is because the executable in the LaunchAgent MUST have a Code Signing Team ID that matches the Team ID of the app Bundle ID specified in the 'AssociatedBundleIdentifiers' key (as described in https://developer.apple.com/documentation/servicemanagement/updating_helper_executables_from_earlier_versions_of_macos?language=objc#4065210). 303 | # We DO NOT want to have the LaunchAgent just run the 'IPSW Updater' app binary directly because if the app is launched that way via the LaunchAgent and then the LaunchAgent is removed during that execution the app will be terminated immediately when 'launchctl bootout' is run. 304 | # That issue has always been avoided by using the '/usr/bin/open' binary to launch the app instead. But using '/usr/bin/open' directly in the LaunchAgent on macOS 13 Ventura makes it show as just running 'open' from an unidentified developer in the new Login Items list, which may seem suspicious or confusing. 305 | # Making this simple SIGNED script that just runs '/usr/bin/open' and then using the 'AssociatedBundleIdentifiers' allows the LaunchAgent to be properly displayed as being for the 'IPSW Updater' app. 306 | # When on macOS 12 Monterey and older, the 'AssociatedBundleIdentifiers' will just be ignored and the 'Launch IPSW Updater' will function the same as if we directly specified '/usr/bin/open' with the path to the app in the LaunchAgent. 307 | # Search for 'AssociatedBundleIdentifiers' in the 'IPSW Updater.jxa' script to see the LaunchAgent creation code. 308 | echo '#!/bin/sh 309 | /usr/bin/open -na \"${0%/Contents/*}\"' > " & (quoted form of (appBuildPath & "/Contents/Resources/Launch " & projectName)) & " 310 | chmod +x " & (quoted form of (appBuildPath & "/Contents/Resources/Launch " & projectName)) & " 311 | codesign -s 'Developer ID Application' --identifier " & (quoted form of (bundleIdentifierPrefix & "Launch-" & projectNameForBundleID)) & " --strict " & (quoted form of (appBuildPath & "/Contents/Resources/Launch " & projectName))) 312 | 313 | try 314 | do shell script ("codesign -fs 'Developer ID Application' --strict " & (quoted form of appBuildPath)) -- Code signing will be re-done (with hardened runtime) if notarization is done below, but always run "codesign" here in case NOT notarizing during testing so the app can properly launch and validate the code signature. 315 | 316 | if (appVersion does not end with "-0") then 317 | repeat while (application appBuildPath is running) 318 | delay 0.5 319 | end repeat 320 | 321 | try 322 | waitUntilAwakeAndUnlocked() 323 | activate 324 | display alert ("Notarize " & projectName & " version " & appVersion & "?") buttons {"No", "Yes"} cancel button 1 default button 2 325 | delay 0.2 -- Delay a moment to allow the prompt to close. 326 | 327 | try 328 | set AppleScript's text item delimiters to "" 329 | set appZipName to (((words of projectName) as text) & "-v") 330 | set AppleScript's text item delimiters to {".", "-"} 331 | if ((count of (every text item of appVersion)) is equal to 4) then 332 | set appZipName to (appZipName & ((text item 1 of appVersion) as text)) 333 | if ((length of ((text item 2 of appVersion) as text)) < 2) then 334 | set appZipName to (appZipName & "0" & ((text item 2 of appVersion) as text)) 335 | else 336 | set appZipName to (appZipName & ((text item 2 of appVersion) as text)) 337 | end if 338 | if ((length of ((text item 3 of appVersion) as text)) < 2) then 339 | set appZipName to (appZipName & "0" & ((text item 3 of appVersion) as text)) 340 | else 341 | set appZipName to (appZipName & ((text item 3 of appVersion) as text)) 342 | end if 343 | set appZipName to (appZipName & ((text item 4 of appVersion) as text)) 344 | else 345 | set appZipName to (appZipName & appVersion) 346 | end if 347 | 348 | set appZipPathForNotarization to (projectFolderPath & "Build/" & appZipName & "-NOTARIZATION-SUBMISSION.zip") 349 | set appZipPath to (projectFolderPath & "Build/" & appZipName & ".zip") 350 | 351 | -- Setting up "notarytool": https://scriptingosx.com/2021/07/notarize-a-command-line-tool-with-notarytool/ & https://developer.apple.com/documentation/security/notarizing_macos_software_before_distribution/customizing_the_notarization_workflow 352 | 353 | -- NOTE: Every command is chained with "&&" so that if anything errors, it all stops and all combined output from every command will be included in the error message. 354 | -- Also, the output of "spctl -avv" is checked for "source=Notarized Developer ID" since a signed but unnotarized app could pass the initial assessment (but stapling should have failed before getting the that check anyways). 355 | -- And notarization is also verified with "codesign": https://developer.apple.com/forums/thread/128683?answerId=404727022#404727022 & https://developer.apple.com/forums/thread/130560 356 | -- Information about using "--deep" and "--strict" options during "codesign" verification: 357 | -- https://developer.apple.com/documentation/security/notarizing_macos_software_before_distribution/resolving_common_notarization_issues#3087735 358 | -- https://developer.apple.com/library/archive/technotes/tn2206/_index.html#//apple_ref/doc/uid/DTS40007919-CH1-TNTAG211 359 | -- https://developer.apple.com/library/archive/technotes/tn2206/_index.html#//apple_ref/doc/uid/DTS40007919-CH1-TNTAG404 360 | -- The "--deep" option is DEPRECATED in macOS 13 Ventura for SIGNING but I don't think it's deprecated for VERIFYING since verification is where it was always really intended to be used (as explained in the note in the last link in the list above). 361 | 362 | set notarizationOutput to (do shell script ("rm -f " & (quoted form of appZipPathForNotarization) & " " & (quoted form of appZipPath) & " && 363 | echo 'Code Signing App...' && 364 | codesign -fs 'Developer ID Application' -o runtime --strict " & (quoted form of appBuildPath) & " 2>&1 && 365 | echo ' 366 | Zipping App for Notarization...' && 367 | ditto -ckvV --keepParent " & (quoted form of appBuildPath) & " " & (quoted form of appZipPathForNotarization) & " 2>&1 && 368 | echo ' 369 | Notarizing App...' && 370 | xcrun notarytool submit " & (quoted form of appZipPathForNotarization) & " --keychain-profile 'notarytool App Specific Password' --wait 2>&1 && 371 | rm -f " & (quoted form of appZipPathForNotarization) & " && 372 | echo ' 373 | Stapling Notarization Ticket to App...' && 374 | xcrun stapler staple " & (quoted form of appBuildPath) & " 2>&1 && 375 | echo ' 376 | Assessing Notarized App...' && 377 | spctl_assess_output=\"$(spctl -avv " & (quoted form of appBuildPath) & " 2>&1; true)\" && # Never exit because of spctl error to always show the output and notarization will be checked explicitly below. 378 | echo \"${spctl_assess_output}\" && 379 | codesign -vv --deep --strict -R '=notarized' --check-notarization " & (quoted form of appBuildPath) & " 2>&1 && 380 | echo \"${spctl_assess_output}\" | grep -qxF 'source=Notarized Developer ID' && 381 | echo ' 382 | Zipping Notarized App...' && 383 | ditto -ckvV --keepParent --sequesterRsrc --zlibCompressionLevel 9 " & (quoted form of appBuildPath) & " " & (quoted form of appZipPath) & " 2>&1") without altering line endings) -- VERY IMPORTANT to NOT alter line endings so that "awk" can read each line (which needs "\n" instead of "\r"). 384 | 385 | try 386 | set notarizationSubmissionID to (do shell script ("echo " & (quoted form of notarizationOutput) & " | awk '($1 == \"id:\") { print $NF; exit }'")) 387 | if (notarizationSubmissionID is not equal to "") then 388 | set notarizationOutput to (notarizationOutput & " 389 | 390 | Notarization Log: 391 | " & (do shell script ("xcrun notarytool log " & notarizationSubmissionID & " --keychain-profile 'notarytool App Specific Password' 2>&1"))) 392 | end if 393 | end try 394 | 395 | set appZipChecksum to "UNKNOWN" 396 | try 397 | set appZipChecksum to (last word of (do shell script "openssl dgst -sha512 " & (quoted form of appZipPath))) 398 | set the clipboard to appZipChecksum 399 | end try 400 | 401 | waitUntilAwakeAndUnlocked() 402 | activate 403 | try 404 | do shell script "afplay /System/Library/Sounds/Glass.aiff > /dev/null 2>&1 &" 405 | on error 406 | beep 407 | end try 408 | set notarizationSuccessfulReply to choose from list (paragraphs of notarizationOutput) with prompt ("Successfully Notarized & Zipped " & projectName & " version " & appVersion & "! 409 | 410 | Copied SHA512 Checksum of Zipped File to Clipboard: 411 | " & appZipChecksum & " 412 | 413 | Notarization Output:") cancel button name "Quit" OK button name "Continue" with title (name of me) with empty selection allowed without multiple selections allowed 414 | 415 | if (notarizationSuccessfulReply is false) then 416 | quit 417 | delay 10 418 | end if 419 | 420 | try 421 | do shell script ("open -R " & (quoted form of appZipPath)) 422 | end try 423 | on error notarizationError number notarizationErrorCode 424 | set notarizationLog to "" 425 | try 426 | set notarizationSubmissionID to (do shell script ("echo " & (quoted form of notarizationError) & " | awk '($1 == \"id:\") { print $NF; exit }'")) 427 | if (notarizationSubmissionID is not equal to "") then 428 | set notarizationLog to (" 429 | 430 | Notarization Log: 431 | " & (do shell script ("xcrun notarytool log " & notarizationSubmissionID & " --keychain-profile 'notarytool App Specific Password' 2>&1"))) 432 | end if 433 | end try 434 | 435 | waitUntilAwakeAndUnlocked() 436 | activate 437 | try 438 | do shell script "afplay /System/Library/Sounds/Basso.aiff > /dev/null 2>&1 &" 439 | on error 440 | beep 441 | end try 442 | set notarizationErrorReply to choose from list (paragraphs of (notarizationError & notarizationLog)) with prompt (projectName & " Notarization Error " & notarizationErrorCode & " 443 | 444 | Notarization Error:") cancel button name "Quit" OK button name "Continue" with title (name of me) with empty selection allowed without multiple selections allowed 445 | 446 | if (notarizationErrorReply is false) then 447 | quit 448 | delay 10 449 | end if 450 | end try 451 | end try 452 | end if 453 | on error codeSignError number codeSignErrorCode 454 | waitUntilAwakeAndUnlocked() 455 | activate 456 | try 457 | do shell script "afplay /System/Library/Sounds/Basso.aiff > /dev/null 2>&1 &" 458 | on error 459 | beep 460 | end try 461 | display alert (projectName & " Code Sign Error " & codeSignErrorCode) message codeSignError 462 | end try 463 | end if 464 | 465 | try 466 | do shell script "open -na " & (quoted form of appBuildPath) 467 | on error 468 | try 469 | do shell script "open -na " & (quoted form of ("/Applications/" & projectName & ".app")) 470 | end try 471 | end try 472 | on error buildErrorMessage number buildErrorNumber 473 | if (buildErrorNumber is not equal to -128) then 474 | waitUntilAwakeAndUnlocked() 475 | activate 476 | try 477 | do shell script "afplay /System/Library/Sounds/Basso.aiff > /dev/null 2>&1 &" 478 | on error 479 | beep 480 | end try 481 | display alert (button returned of buildAlertReply) & " Error" message buildErrorMessage 482 | end if 483 | end try 484 | end repeat 485 | 486 | on waitUntilAwakeAndUnlocked() 487 | repeat -- dialogs timeout when screen is asleep or locked (just in case) 488 | set isAwake to true 489 | try 490 | set isAwake to ((run script "ObjC.import('CoreGraphics'); $.CGDisplayIsActive($.CGMainDisplayID())" in "JavaScript") is equal to 1) 491 | end try 492 | 493 | set isUnlocked to true 494 | try 495 | set isUnlocked to ((do shell script ("bash -c " & (quoted form of "/usr/libexec/PlistBuddy -c 'Print :IOConsoleUsers:0:CGSSessionScreenIsLocked' /dev/stdin <<< \"$(ioreg -ac IORegistryEntry -k IOConsoleUsers -d 1)\""))) is not equal to "true") 496 | end try 497 | 498 | if (isAwake and isUnlocked) then 499 | exit repeat 500 | else 501 | delay 1 502 | end if 503 | end repeat 504 | end waitUntilAwakeAndUnlocked 505 | -------------------------------------------------------------------------------- /IPSW Updater Resources/Credits.rtf: -------------------------------------------------------------------------------- 1 | {\rtf1\ansi\ansicpg1252\cocoartf2761 2 | \cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fswiss\fcharset0 Helvetica-BoldOblique;\f1\fswiss\fcharset0 Helvetica-Bold;} 3 | {\colortbl;\red255\green255\blue255;\red52\green52\blue52;} 4 | {\*\expandedcolortbl;;\csgray\c26515;} 5 | \margl1440\margr1440\vieww5480\viewh8400\viewkind0 6 | \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 7 | 8 | \f0\i\b\fs18 \cf2 9 | \fs22 \ 10 | {\field{\*\fldinst{HYPERLINK "https://ipsw.app"}}{\fldrslt 11 | \f1\i0\fs26 IPSW Updater}}\ 12 | 13 | \fs18 14 | \fs22 \ 15 | Made by {\field{\*\fldinst{HYPERLINK "https://www.freegeek.org"}}{\fldrslt Free Geek}}\ 16 | {\field{\*\fldinst{HYPERLINK "https://source.ipsw.app"}}{\fldrslt Open Source}} ({\field{\*\fldinst{HYPERLINK "https://license.ipsw.app"}}{\fldrslt MIT License}})\ 17 | 18 | \fs18 19 | \fs22 \ 20 | Designed and Developed by {\field{\*\fldinst{HYPERLINK "https://randomapplications.com"}}{\fldrslt Pico Mitchell}}\ 21 | 22 | \fs18 23 | \fs22 \ 24 | Uses {\field{\*\fldinst{HYPERLINK "https://ipsw.me"}}{\fldrslt IPSW Downloads API}}\ 25 | by {\field{\*\fldinst{HYPERLINK "https://ipsw.me/about"}}{\fldrslt Callum Jones}}\ 26 | 27 | \fs18 28 | \fs22 \ 29 | App Icon is \'93Phone with Arrow\'94 from {\field{\*\fldinst{HYPERLINK "https://github.com/microsoft/fluentui-emoji"}}{\fldrslt Fluent Emoji}}\ 30 | by {\field{\*\fldinst{HYPERLINK "https://opensource.microsoft.com"}}{\fldrslt Microsoft}} licensed under the {\field{\*\fldinst{HYPERLINK "https://github.com/microsoft/fluentui-emoji/blob/main/LICENSE"}}{\fldrslt MIT License}}\ 31 | \pard\pardeftab720\qc\partightenfactor0 32 | \cf2 \ 33 | \ 34 | \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0 35 | \cf2 Apple, Mac, iTunes, iPhone, iPad, iPod,\ 36 | Apple TV, HomePod, Apple Vision, iBridge,\ 37 | iPadOS, tvOS, audioOS, visionOS,\ 38 | bridgeOS, and macOS are trademarks of\ 39 | Apple Inc., registered in the U.S. and\ 40 | other countries and regions.\ 41 | \ 42 | \pard\pardeftab720\qc\partightenfactor0 43 | \cf2 IOS is a trademark or registered trademark\ 44 | of Cisco in the U.S. and other countries\ 45 | and is used under license.\ 46 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Free Geek 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # IPSW Updater 2 | *Made by [Pico Mitchell](https://randomapplications.com) of [Free Geek](https://www.freegeek.org) using the [IPSW Downloads API](https://ipsw.me) by [Callum Jones](https://ipsw.me/about).* 3 | 4 | ### Visit [ipsw.app](https://ipsw.app) to [view release notes](https://ipsw.app/download/updates.php) and [download the latest release of the compiled app](https://ipsw.app/download/). 5 | 6 | IPSW Updater can batch download all the latest IPSW Firmware files from Apple and place them in the correct iTunes Software Updates or Apple Configurator Firmware folders so that they are automatically found by iTunes/Finder and/or Apple Configurator. 7 | 8 | You can set which IPSW Firmware files to download based on versions as well as product types, such as iPhone, iPad, iPod touch, Apple TV, HomePod mini, Apple Vision, T2 Mac (iBridge Firmware), and Apple Silicon Mac. You can also set IPSW Updater to run automatically at a scheduled time so that all of your IPSW Firmware files will always be kept up-to-date. 9 | 10 | iPhone, iPad, and iPod touch IPSW Firmware files will be stored within iTunes Software Updates folders at `~/Library/iTunes` since they will be found by both iTunes/Finder and Apple Configurator at that location. 11 | 12 | Apple TV, HomePod mini, Apple Vision, and T2 and Apple Silicon Mac IPSW Firmware files will be stored within the Apple Configurator Firmware folder at `~/Library/Group Containers/K36BKF7T3D.group.com.apple.configurator/Library/Caches/Firmware`. This is because unlike iPhone, iPad, and iPod touch IPSW Firmware files, Apple Configurator will not detect or use IPSW Firmware files in the iTunes Software Updates folders for any other device types. Also, prior to macOS 14 Sonoma, T2 and Apple Silicon Macs cannot be restored by iTunes/Finder and can only be restored by Apple Configurator when they are put into DFU mode. 13 | 14 | Any IPSW Firmware files that are already in these folders will become managed by IPSW Updater. That means that IPSW Firmware files in these locations will be moved to the Trash (or deleted, based on your settings) when they become out-of-date after an update for them has been downloaded by IPSW Updater. Also, as described above, any iPhone, iPad, and iPod touch IPSW Firmware files that are currently in the Apple Configurator Firmware folder will be moved into their correct iTunes Software Updates folder and any IPSW Firmware files in the iTunes Software Updates folders for any other device types will be moved into the Apple Configurator Firmware folder. Finally, any IPSW Firmware files currently in these folders that are out-of-date but are still signed by Apple will be left alone and any that are no longer signed by Apple will be moved to the Trash (or deleted). IPSW Updater will check for unsigned IPSW Firmware files each time it runs, so existing files may be trashed (or deleted) in the future if and when Apple no longer signs them. 15 | --------------------------------------------------------------------------------