diff --git a/scripts/fw_prepare.sh b/scripts/fw_prepare.sh index 81f2dab..821cc69 100755 --- a/scripts/fw_prepare.sh +++ b/scripts/fw_prepare.sh @@ -6,6 +6,7 @@ # - direct iPhone IPSW URLs or local file paths # - version/build selectors for the target device # - listing of all downloadable IPSWs for the target device +# - The "VARIANT" environment variable to download apfs_sealvolume for the patchless mode # # Listing and selection are resolved through the `ipsw` CLI already used # elsewhere in this repo, so the script can work with the full downloadable @@ -410,6 +411,99 @@ extract() { cp -R "$cache" "$out" } +download_apfs_sealvolume() { + local src="$1" + local base ios_version filename PROJECT_DIR TOOLS_PREFIX TMP_DIR bn ver BUILD BUILD_MANIFEST RAMDISK_PATH RAMDISK_IM4P RAMDISK MOUNT + + base="$(basename "$src")" + ios_version="$(awk -F_ 'NF >= 2 { print $2 }' <<<"$base")" + + if [[ -z "$ios_version" ]]; then + echo "Error: could not determine iOS version from filename: $base" >&2 + return 1 + fi + + filename="apfs_sealvolume_${ios_version}" + + PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" + TOOLS_PREFIX="$PROJECT_DIR/.tools" + + if [[ -f "$TOOLS_PREFIX/$filename" ]]; then + echo "$filename already present" + else + echo "Downloading $filename" + ( + TMP_DIR="$(mktemp -d)" + trap 'rm -rf "$TMP_DIR"' EXIT + + # List matching macOS version to the iOS version + while IFS= read -r url; do + bn="$(basename "$url")" + ver="${bn#*_}" + ver="${ver%%_*}" + + [[ "$ver" == "$ios_version" ]] || continue + + BUILD="$(awk -F_ '{print $3}' <<<"$bn")" + break + done < <( + ipsw download appledb \ + --os macOS \ + --version $ios_version \ + --urls + ) + + if [[ -z "${BUILD:-}" ]]; then + echo "Error: failed to determine macOS build from available URLs" >&2 + exit 1 + fi + + # Download BuildManifest first + ipsw download appledb \ + --os macOS \ + --build $BUILD \ + --pattern "^BuildManifest.plist\$" \ + --output "$TMP_DIR" + + BUILD_MANIFEST="$(find "$TMP_DIR" -name BuildManifest.plist -print -quit)" + if [ -z "$BUILD_MANIFEST" ]; then + echo "Failed to locate BuildManifest.plist" + exit 1 + fi + + RAMDISK_PATH="$(/usr/bin/plutil -extract 'BuildIdentities.0.Manifest.RestoreRamDisk.Info.Path' raw -o - "$BUILD_MANIFEST")" + if [ -z "$RAMDISK_PATH" ]; then + echo "Failed to read RestoreRamDisk path from BuildManifest" + exit 1 + fi + + # Download the ramdisk referenced by BuildManifest + ipsw download appledb \ + --os macOS \ + --build $BUILD \ + --pattern "$RAMDISK_PATH" \ + --output "$TMP_DIR" + + RAMDISK_IM4P="$(find "$TMP_DIR" -path "*${RAMDISK_PATH}" -print -quit)" + if [ -z "$RAMDISK_IM4P" ]; then + echo "Failed to locate downloaded ramdisk: $RAMDISK_PATH" + exit 1 + fi + + RAMDISK="$TMP_DIR/ramdisk.dmg" + ipsw img4 im4p extract --output "$RAMDISK" "$RAMDISK_IM4P" + + MOUNT=$(hdiutil attach -readonly -nobrowse "$RAMDISK" | awk 'END{ print$NF}') + cp "$MOUNT/System/Library/Filesystems/apfs.fs/Contents/Resources/apfs_sealvolume" \ + "$TOOLS_PREFIX/$filename" + hdiutil detach "$MOUNT" >/dev/null 2>&1 || true + ) + echo " Downloaded: $TOOLS_PREFIX/$filename" + echo " Resigning $filename" + codesign --force --sign - "$TOOLS_PREFIX/$filename" + fi +} + LIST_FIRMWARES="${LIST_FIRMWARES:-0}" IPHONE_DEVICE="${IPHONE_DEVICE:-$DEFAULT_IPHONE_DEVICE}" IPHONE_VERSION="${IPHONE_VERSION:-}" @@ -537,6 +631,13 @@ echo "" fetch "$IPHONE_SOURCE" "$IPHONE_IPSW_PATH" fetch "$CLOUDOS_SOURCE" "$CLOUDOS_IPSW_PATH" +VARIANT="${VARIANT:-}" +if [[ "$VARIANT" == "less" ]]; then + download_apfs_sealvolume "$IPHONE_SOURCE" +else + echo "==> Downloading apfs sealvolume (skipped — patchless variant only)" +fi + IPHONE_CACHE="${IPSW_DIR}/${IPHONE_DIR}" CLOUDOS_CACHE="${IPSW_DIR}/${CLOUDOS_DIR}" diff --git a/scripts/setup_machine.sh b/scripts/setup_machine.sh index b794a64..33d827a 100755 --- a/scripts/setup_machine.sh +++ b/scripts/setup_machine.sh @@ -1053,11 +1053,7 @@ main() { install_brew_deps ensure_python_linked - if [[ "$LESS_MODE" -eq 1 ]]; then - VARIANT=less run_make "Project setup" setup_tools - else - run_make "Project setup" setup_tools - fi + run_make "Project setup" setup_tools run_make "Project setup" build fi @@ -1066,10 +1062,11 @@ main() { export PATH="$PROJECT_ROOT/.venv/bin:$PATH" run_make "Firmware prep" vm_new - run_make "Firmware prep" fw_prepare if [[ "$LESS_MODE" -eq 0 ]]; then + run_make "Firmware prep" fw_prepare run_make "Firmware patch" "$fw_patch_target" else + VARIANT=less run_make "Firmware prep" fw_prepare run_make_sudo "Firmware patch" "$fw_patch_target" fi diff --git a/scripts/setup_tools.sh b/scripts/setup_tools.sh index 05484cd..d6d3b1b 100755 --- a/scripts/setup_tools.sh +++ b/scripts/setup_tools.sh @@ -25,7 +25,7 @@ ensure_repo_submodule() { # ── Brew packages ────────────────────────────────────────────── -echo "[1/5] Checking brew packages..." +echo "[1/4] Checking brew packages..." BREW_PACKAGES=(aria2 gnu-tar openssl@3 ldid-procursus sshpass zstd) BREW_MISSING=() @@ -45,7 +45,7 @@ fi # ── Trustcache ───────────────────────────────────────────────── -echo "[2/5] trustcache" +echo "[2/4] trustcache" TRUSTCACHE_BIN="$TOOLS_PREFIX/bin/trustcache" if [[ -x "$TRUSTCACHE_BIN" ]]; then @@ -74,7 +74,7 @@ fi # ── insert_dylib ─────────────────────────────────────────────── -echo "[3/5] insert_dylib" +echo "[3/4] insert_dylib" INSERT_DYLIB_BIN="$TOOLS_PREFIX/bin/insert_dylib" if [[ -x "$INSERT_DYLIB_BIN" ]]; then @@ -90,41 +90,8 @@ fi # ── Python venv ──────────────────────────────────────────────── -echo "[4/5] Python venv" +echo "[4/4] Python venv" zsh "$SCRIPT_DIR/setup_venv.sh" -# ── APFS sealvolume (patchless variant only) ────────────────────── - -VARIANT="${VARIANT:-}" - -if [[ "$VARIANT" == "less" ]]; then - echo "[5/5] apfs sealvolume" - if [[ -f "$TOOLS_PREFIX/apfs_sealvolume" ]]; then - echo " apfs_sealvolume already present" - else - TMP_DIR="$(mktemp -d)" - ipsw download appledb \ - --os macOS \ - --build 25D2140 \ - --pattern "094-33864-054.dmg" \ - --output "$TMP_DIR" - - RAMDISK_IM4P="$TMP_DIR/25D2140__MacOS/094-33864-054.dmg" - RAMDISK="$TMP_DIR/ramdisk.dmg" - ipsw img4 im4p extract --output "$RAMDISK" "$RAMDISK_IM4P" - - MOUNT=$(hdiutil attach -readonly -nobrowse "$RAMDISK" | awk 'END{ print$NF}') - cp "$MOUNT/System/Library/Filesystems/apfs.fs/Contents/Resources/apfs_sealvolume" \ - "$TOOLS_PREFIX/apfs_sealvolume" - hdiutil detach "$MOUNT" >/dev/null 2>&1 || true - rm -rf "$TMP_DIR" - echo " Downloaded: $TOOLS_PREFIX/apfs_sealvolume" - echo " Resigning apfs_sealvolume" - codesign --force --sign - "$TOOLS_PREFIX/apfs_sealvolume" - fi -else - echo "[5/5] apfs sealvolume (skipped — patchless variant only)" -fi - echo "" echo "All tools installed." diff --git a/sources/FirmwarePatcher/Filesystem/CryptexFilesystemPatcher.swift b/sources/FirmwarePatcher/Filesystem/CryptexFilesystemPatcher.swift index 74b6683..d9a938e 100644 --- a/sources/FirmwarePatcher/Filesystem/CryptexFilesystemPatcher.swift +++ b/sources/FirmwarePatcher/Filesystem/CryptexFilesystemPatcher.swift @@ -15,6 +15,7 @@ import Img4tool enum ProcessError: Error { case failed(Int32, String) + case notExecutable(String) } extension Data { @@ -394,6 +395,18 @@ public final class CryptexFilesystemPatcher: Patcher { return im4pPath } + private func identifyApfsSealvolume() throws -> URL { + let iosVersion = try getProductVersion() + let path = self.vphoneCliDirectory.appending(path: ".tools/apfs_sealvolume_\(iosVersion)") + guard FileManager.default.fileExists(atPath: path.path) else { + throw FirmwareManifest.ManifestError.fileNotFound(path.path) + } + guard FileManager.default.isExecutableFile(atPath: path.path) else { + throw ProcessError.notExecutable(path.path) + } + return path + } + func createDigestAndHash(filesystem: URL, mtree: URL, remap: Bool) throws -> (URL, URL) { let (device, mount) = try attachImage(path: filesystem) defer { try? detachImage(deviceNode: device) } @@ -402,6 +415,7 @@ public final class CryptexFilesystemPatcher: Patcher { let digestDbPath = tmpDir.appending(path: "digest.db") let rootHashPath = tmpDir.appending(path: "root_hash") let mtreeRemapPath = tmpDir.appending(path: "mtree_remap.xml") + let sealLogPath = tmpDir.appending(path: "seal.log") // We want to get the nanosecond timestamp of the last modification before the mtree collection. // We know that we remove directories in /private/var in removeSpecificSystemFiles last. @@ -423,13 +437,14 @@ public final class CryptexFilesystemPatcher: Patcher { FileManager.default.createFile(atPath: mtreeRemapPath.path, contents: remapContent.data(using: .utf8)) try unmount(mount: mount) - let sealvolume = self.vphoneCliDirectory.appending(path: ".tools/apfs_sealvolume") + let sealvolume = try identifyApfsSealvolume() + FileManager.default.createFile(atPath: sealLogPath.path, contents: nil) _ = try runProcess(sealvolume.path, [ "-R", mtreeRemapPath.path, "-U", digestDbPath.path, // Save digest records "-M", rootHashPath.path, // Save root hash device - ]) + ], output: sealLogPath) return (digestDbPath, rootHashPath) } @@ -615,6 +630,14 @@ public final class CryptexFilesystemPatcher: Patcher { return remaining.joined(separator: "/") } + func getProductVersion() throws -> String { + let root = try parsePlist(data: buildManiest) + guard let productVersion = root["ProductVersion"] as? String else { + throw FirmwareManifest.ManifestError.missingKey("ProductVersion in BuildManifest") + } + return productVersion + } + func getTrustcachePath() throws -> String { let path = self.restoreDir.appending(path: "iPhone-BuildManifest.plist") let manifest = try getBuildIdentityManifest(path: path)