diff --git a/StikDebug/Device/IdeviceFFIBridge.swift b/StikDebug/Device/IdeviceFFIBridge.swift index ef034ea..3e1c98e 100644 --- a/StikDebug/Device/IdeviceFFIBridge.swift +++ b/StikDebug/Device/IdeviceFFIBridge.swift @@ -44,6 +44,35 @@ private enum IdeviceBridge { return makeError(domain: domain, code: code, message: message) } + static func mappedFileData(atPath path: String, description: String) throws -> Data { + let url = URL(fileURLWithPath: path) + + do { + let data = try Data(contentsOf: url, options: .mappedIfSafe) + guard !data.isEmpty else { + throw makeError(message: "\(description) is empty") + } + return data + } catch let error as NSError { + throw makeError(code: error.code, message: "Failed to read \(description): \(error.localizedDescription)") + } + } + + static func uint64Value(from plist: plist_t?, fieldName: String) throws -> UInt64 { + guard let plist else { + throw makeError(message: "\(fieldName) was not returned by lockdownd") + } + + var value: UInt64 = 0 + plist_get_uint_val(plist, &value) + + guard value != 0 else { + throw makeError(message: "Failed to decode \(fieldName)") + } + + return value + } + static func withTunnelHandles( for context: JITEnableContext, _ body: (OpaquePointer, OpaquePointer) throws -> T @@ -295,6 +324,59 @@ extension JITEnableContext { } } + func mountPersonalDDI(withImagePath imagePath: String, trustcachePath: String, manifestPath: String) throws { + let imageData = try IdeviceBridge.mappedFileData(atPath: imagePath, description: "developer disk image") + let trustcacheData = try IdeviceBridge.mappedFileData(atPath: trustcachePath, description: "developer disk image trust cache") + let manifestData = try IdeviceBridge.mappedFileData(atPath: manifestPath, description: "developer disk image manifest") + + try IdeviceBridge.withTunnelHandles(for: self) { adapter, handshake in + let uniqueChipID = try IdeviceBridge.withConnectedClient( + fallback: "Failed to connect to lockdownd", + missingClientMessage: "Lockdownd client was not created", + connect: { lockdownd_connect_rsd(adapter, handshake, $0) }, + cleanup: { lockdownd_client_free($0) } + ) { lockdownClient in + var uniqueChipIDPlist: plist_t? + if let ffiError = lockdownd_get_value(lockdownClient, "UniqueChipID", nil, &uniqueChipIDPlist) { + throw IdeviceBridge.consumeFFIError(ffiError, fallback: "Failed to query UniqueChipID") + } + defer { plist_free(uniqueChipIDPlist) } + return try IdeviceBridge.uint64Value(from: uniqueChipIDPlist, fieldName: "UniqueChipID") + } + + try IdeviceBridge.withConnectedClient( + fallback: "Failed to connect to image mounter", + missingClientMessage: "Image mounter client was not created", + connect: { image_mounter_connect_rsd(adapter, handshake, $0) }, + cleanup: { image_mounter_free($0) } + ) { imageMounterClient in + let ffiError = imageData.withUnsafeBytes { imageBuffer in + trustcacheData.withUnsafeBytes { trustcacheBuffer in + manifestData.withUnsafeBytes { manifestBuffer in + image_mounter_mount_personalized_rsd( + imageMounterClient, + adapter, + handshake, + imageBuffer.bindMemory(to: UInt8.self).baseAddress, + imageData.count, + trustcacheBuffer.bindMemory(to: UInt8.self).baseAddress, + trustcacheData.count, + manifestBuffer.bindMemory(to: UInt8.self).baseAddress, + manifestData.count, + nil, + uniqueChipID + ) + } + } + } + + if let ffiError { + throw IdeviceBridge.consumeFFIError(ffiError, fallback: "Failed to mount personalized DDI") + } + } + } + } + func fetchAllProfiles() throws -> [Data] { try IdeviceBridge.withTunnelHandles(for: self) { adapter, handshake in try IdeviceBridge.withConnectedClient( diff --git a/StikDebug/Device/MountingProgress.swift b/StikDebug/Device/MountingProgress.swift index 0ef26fb..80d31e1 100644 --- a/StikDebug/Device/MountingProgress.swift +++ b/StikDebug/Device/MountingProgress.swift @@ -93,7 +93,7 @@ final class MountingProgress: ObservableObject { self.coolisMounted = true self.checkforMounted() } else if let mountError { - LogManager.shared.addErrorLog("Failed to install DDI cryptex: \(mountError)") + LogManager.shared.addErrorLog("Failed to mount DDI: \(mountError)") showAlert(title: "DDI Mount Failed", message: mountError, showOk: true, showTryAgain: true) { shouldTryAgain in if shouldTryAgain { self.pubMount() diff --git a/StikDebug/Device/mountDDI.swift b/StikDebug/Device/mountDDI.swift index 1b18623..61128b5 100644 --- a/StikDebug/Device/mountDDI.swift +++ b/StikDebug/Device/mountDDI.swift @@ -33,7 +33,15 @@ func checkMountStatus() -> MountCheckResult { func mountDeveloperDiskImage(from directoryPath: String) -> String? { do { - try JITEnableContext.shared.installCryptexDDI(from: directoryPath) + if DeveloperDiskImageService.usesCryptexDDI { + try JITEnableContext.shared.installCryptexDDI(from: directoryPath) + } else { + try JITEnableContext.shared.mountPersonalDDI( + withImagePath: URL(fileURLWithPath: directoryPath).appendingPathComponent("Image.dmg").path, + trustcachePath: URL(fileURLWithPath: directoryPath).appendingPathComponent("Image.dmg.trustcache").path, + manifestPath: URL(fileURLWithPath: directoryPath).appendingPathComponent("BuildManifest.plist").path + ) + } } catch { return error.localizedDescription } diff --git a/StikDebug/Services/DeveloperDiskImageService.swift b/StikDebug/Services/DeveloperDiskImageService.swift index cc8b066..d52ed8d 100644 --- a/StikDebug/Services/DeveloperDiskImageService.swift +++ b/StikDebug/Services/DeveloperDiskImageService.swift @@ -12,7 +12,11 @@ final class DeveloperDiskImageService { static var filesAreReady: Bool { downloadItems.allSatisfy { FileManager.default.fileExists(atPath: directoryURL.appendingPathComponent($0.fileName).path) - } + } && (try? String(contentsOf: mountMethodURL, encoding: .utf8)) == mountMethod.rawValue + } + + static var usesCryptexDDI: Bool { + mountMethod == .cryptex } private let fileManager: FileManager @@ -24,20 +28,21 @@ final class DeveloperDiskImageService { } func downloadMissingFiles() async throws { - let hasCryptexMarker = ["Image.dmg.cryptex_info", "Image.dmg.root_hash"].contains { - fileManager.fileExists(atPath: Self.directoryURL.appendingPathComponent($0).path) - } - let replaceLegacyFiles = !hasCryptexMarker && Self.downloadItems.contains { - fileManager.fileExists(atPath: Self.directoryURL.appendingPathComponent($0.fileName).path) + let installedMethod = try? String(contentsOf: Self.mountMethodURL, encoding: .utf8) + let replaceFiles = installedMethod != Self.mountMethod.rawValue + + if !Self.usesCryptexDDI { + try removeCryptexOnlyFiles() } for item in Self.downloadItems { let destinationURL = Self.directoryURL.appendingPathComponent(item.fileName) - guard replaceLegacyFiles || !fileManager.fileExists(atPath: destinationURL.path) else { + guard replaceFiles || !fileManager.fileExists(atPath: destinationURL.path) else { continue } try await downloadFile(from: item.urlString, to: destinationURL) } + try Self.mountMethod.rawValue.write(to: Self.mountMethodURL, atomically: true, encoding: .utf8) } func downloadFile(from urlString: String, to destinationURL: URL) async throws { @@ -70,6 +75,12 @@ final class DeveloperDiskImageService { var completedStages = 0.0 progressHandler?(0.0, "Removing existing DDI files...") + if fileManager.fileExists(atPath: Self.mountMethodURL.path) { + try fileManager.removeItem(at: Self.mountMethodURL) + } + if !Self.usesCryptexDDI { + try removeCryptexOnlyFiles() + } let completionMarker = Self.directoryURL.appendingPathComponent("Image.dmg.root_hash") if fileManager.fileExists(atPath: completionMarker.path) { try fileManager.removeItem(at: completionMarker) @@ -92,10 +103,40 @@ final class DeveloperDiskImageService { progressHandler?(completedStages / totalStages, "\(item.name) ready") } + try Self.mountMethod.rawValue.write(to: Self.mountMethodURL, atomically: true, encoding: .utf8) + progressHandler?(1.0, "DDI download complete.") } - private static let downloadItems: [DDIDownloadItem] = [ + private func removeCryptexOnlyFiles() throws { + for fileName in Self.cryptexOnlyFileNames { + let fileURL = Self.directoryURL.appendingPathComponent(fileName) + if fileManager.fileExists(atPath: fileURL.path) { + try fileManager.removeItem(at: fileURL) + } + } + } + + private static var mountMethod: DDIMountMethod { + let version = ProcessInfo.processInfo.operatingSystemVersion + return version.majorVersion > 26 || (version.majorVersion == 26 && version.minorVersion >= 4) + ? .cryptex + : .personalized + } + + private static let mountMethodURL = directoryURL.appendingPathComponent("MountMethod") + private static let cryptexOnlyFileNames = ["Image.dmg.cryptex_info", "Image.dmg.root_hash"] + + private static var downloadItems: [DDIDownloadItem] { + switch mountMethod { + case .cryptex: + return cryptexDownloadItems + case .personalized: + return personalizedDownloadItems + } + } + + private static let cryptexDownloadItems: [DDIDownloadItem] = [ .init( name: "Build Manifest", fileName: "BuildManifest.plist", @@ -122,6 +163,29 @@ final class DeveloperDiskImageService { urlString: "https://github.com/doronz88/DeveloperDiskImage/raw/refs/heads/main/PersonalizedImages/Xcode_iOS_DDI_Cryptex/Image.dmg.root_hash" ) ] + + private static let personalizedDownloadItems: [DDIDownloadItem] = [ + .init( + name: "Build Manifest", + fileName: "BuildManifest.plist", + urlString: "https://github.com/doronz88/DeveloperDiskImage/raw/refs/heads/main/PersonalizedImages/Xcode_iOS_DDI_Personalized/BuildManifest.plist" + ), + .init( + name: "Image", + fileName: "Image.dmg", + urlString: "https://github.com/doronz88/DeveloperDiskImage/raw/refs/heads/main/PersonalizedImages/Xcode_iOS_DDI_Personalized/Image.dmg" + ), + .init( + name: "TrustCache", + fileName: "Image.dmg.trustcache", + urlString: "https://github.com/doronz88/DeveloperDiskImage/raw/refs/heads/main/PersonalizedImages/Xcode_iOS_DDI_Personalized/Image.dmg.trustcache" + ) + ] +} + +private enum DDIMountMethod: String { + case cryptex + case personalized } private struct DDIDownloadItem {