feat: migrate DDI mounting to cryptex

This commit is contained in:
CelloSerenity 2026-09-23 21:39:02 -06:00
parent 709898d500
commit 4e08cecab9
6 changed files with 3748 additions and 129 deletions

View file

@ -209,7 +209,7 @@ let paths = DDIPaths.default(
)
```
Use the same `DDIPaths` for preparation, JIT enablement, and cache reset. Do not validate cached DDI versions or contents yourself; StikJIT reuses readable, nonempty files and lets the image mounter decide whether they work.
Use the same `DDIPaths` for preparation, JIT enablement, and cache reset. The cache contains the image, trust cache, build manifest, cryptex info, and root hash. Do not validate cached DDI versions or contents yourself; StikJIT reuses readable, nonempty files and lets the device's cryptex service decide whether they work.
A successful DDI mount persists until the device reboots, so it normally needs to be mounted only once per boot.

View file

@ -1,18 +1,6 @@
import Foundation
@_implementationOnly import idevice
private func ddiMountProgressCallback(progress: size_t, total: size_t, context: UnsafeMutableRawPointer?) {
guard let context else { return }
let progressHandler = Unmanaged<DDIProgressBox>.fromOpaque(context).takeUnretainedValue()
let fraction = total > 0 ? Double(progress) / Double(total) : 0
progressHandler.handler(fraction)
}
private final class DDIProgressBox {
let handler: (Double) -> Void
init(_ handler: @escaping (Double) -> Void) { self.handler = handler }
}
final class DDISession {
private struct Tunnel {
@ -36,6 +24,25 @@ final class DDISession {
let tunnel = try makeTunnel()
defer { tunnel.free() }
var installed: UnsafeMutablePointer<InstalledCryptexC>?
let cryptexError = cryptexd_installed_ddi(tunnel.adapter, tunnel.handshake, &installed)
defer { cryptexd_free_installed_cryptex(installed) }
if installed != nil {
if let cryptexError { idevice_error_free(cryptexError) }
return true
}
if (try? isLegacyDDIMounted(over: tunnel)) == true {
if let cryptexError { idevice_error_free(cryptexError) }
return true
}
if let cryptexError {
try IdeviceFFI.check("failed to query installed DDI cryptex") { cryptexError }
}
return false
}
private func isLegacyDDIMounted(over tunnel: Tunnel) throws -> Bool {
var client: OpaquePointer?
try IdeviceFFI.check("failed to connect to image mounter") {
image_mounter_connect_rsd(tunnel.adapter, tunnel.handshake, &client)
@ -57,81 +64,24 @@ final class DDISession {
return deviceCount > 0
}
func mountDDI(imagePath: String, trustcachePath: String, manifestPath: String, progress: @escaping (Double) -> Void) throws {
let imageData = try mappedFileData(atPath: imagePath, description: "developer disk image")
let trustcacheData = try mappedFileData(atPath: trustcachePath, description: "developer disk image trust cache")
let manifestData = try mappedFileData(atPath: manifestPath, description: "developer disk image manifest")
func mountDDI(from directoryPath: String, progress: @escaping (Double) -> Void) throws {
progress(0)
var assets: OpaquePointer?
try IdeviceFFI.check("failed to load DDI cryptex assets") {
directoryPath.withCString { cryptex1_assets_load($0, &assets) }
}
guard let assets else {
throw StikJITError.ddiFilesMissing("DDI cryptex assets could not be loaded from \(directoryPath)")
}
defer { cryptex1_assets_free(assets) }
let tunnel = try makeTunnel()
defer { tunnel.free() }
let uniqueChipID = try fetchUniqueChipID(over: tunnel)
var client: OpaquePointer?
try IdeviceFFI.check("failed to connect to image mounter") {
image_mounter_connect_rsd(tunnel.adapter, tunnel.handshake, &client)
try IdeviceFFI.check("failed to install DDI cryptex") {
cryptexd_install_ddi(tunnel.adapter, tunnel.handshake, assets, nil)
}
guard let client else { throw StikJITError.debugProxyUnavailable }
defer { image_mounter_free(client) }
let progressBox = DDIProgressBox(progress)
let context = Unmanaged.passUnretained(progressBox).toOpaque()
try IdeviceFFI.check("failed to mount personalized developer disk image") {
imageData.withUnsafeBytes { imageBuffer -> UnsafeMutablePointer<IdeviceFfiError>? in
trustcacheData.withUnsafeBytes { trustcacheBuffer -> UnsafeMutablePointer<IdeviceFfiError>? in
manifestData.withUnsafeBytes { manifestBuffer -> UnsafeMutablePointer<IdeviceFfiError>? in
image_mounter_mount_personalized_with_callback_rsd(
client,
tunnel.adapter,
tunnel.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,
ddiMountProgressCallback,
context)
}
}
}
}
}
private func mappedFileData(atPath path: String, description: String) throws -> Data {
guard FileManager.default.fileExists(atPath: path) else {
throw StikJITError.ddiFilesMissing("\(description) not found at \(path)")
}
let data = try Data(contentsOf: URL(fileURLWithPath: path), options: .mappedIfSafe)
guard !data.isEmpty else {
throw StikJITError.ddiFilesMissing("\(description) is empty at \(path)")
}
return data
}
private func fetchUniqueChipID(over tunnel: Tunnel) throws -> UInt64 {
var client: OpaquePointer?
try IdeviceFFI.check("failed to connect to lockdownd") {
lockdownd_connect_rsd(tunnel.adapter, tunnel.handshake, &client)
}
guard let client else { throw StikJITError.debugProxyUnavailable }
defer { lockdownd_client_free(client) }
var uniqueChipIDPlist: plist_t?
try IdeviceFFI.check("failed to query UniqueChipID") {
"UniqueChipID".withCString { lockdownd_get_value(client, $0, nil, &uniqueChipIDPlist) }
}
guard let uniqueChipIDPlist else {
throw StikJITError.device(code: -1, subCode: 0, message: "UniqueChipID was not returned by lockdownd")
}
defer { plist_free(uniqueChipIDPlist) }
var value: UInt64 = 0
plist_get_uint_val(uniqueChipIDPlist, &value)
return value
progress(1)
}
private func openPairingFile() throws -> OpaquePointer {

View file

@ -8,10 +8,20 @@ public struct DDIPaths: Sendable {
public var manifestPath: String
public init(imagePath: String, trustcachePath: String, manifestPath: String) {
public var cryptexInfoPath: String
public var rootHashPath: String
public init(imagePath: String,
trustcachePath: String,
manifestPath: String,
cryptexInfoPath: String? = nil,
rootHashPath: String? = nil) {
self.imagePath = imagePath
self.trustcachePath = trustcachePath
self.manifestPath = manifestPath
self.cryptexInfoPath = cryptexInfoPath ?? "\(imagePath).cryptex_info"
self.rootHashPath = rootHashPath ?? "\(imagePath).root_hash"
}
public static func `default`(in directory: URL) -> DDIPaths {
@ -35,7 +45,7 @@ public struct DDIPaths: Sendable {
}
var allPaths: [String] {
[imagePath, trustcachePath, manifestPath]
[imagePath, trustcachePath, manifestPath, cryptexInfoPath, rootHashPath]
}
func removeCachedFiles() throws {
@ -53,13 +63,15 @@ struct DDIDownloadItem {
}
enum DDIDownloadCatalog {
private static let baseURL = URL(string: "https://github.com/doronz88/DeveloperDiskImage/raw/refs/heads/main/PersonalizedImages/Xcode_iOS_DDI_Personalized")!
private static let baseURL = URL(string: "https://github.com/doronz88/DeveloperDiskImage/raw/refs/heads/main/PersonalizedImages/Xcode_iOS_DDI_Cryptex")!
static func items(for paths: DDIPaths) -> [DDIDownloadItem] {
[
DDIDownloadItem(name: "BuildManifest.plist", destinationPath: paths.manifestPath, url: baseURL.appendingPathComponent("BuildManifest.plist")),
DDIDownloadItem(name: "Image.dmg", destinationPath: paths.imagePath, url: baseURL.appendingPathComponent("Image.dmg")),
DDIDownloadItem(name: "Image.dmg.trustcache", destinationPath: paths.trustcachePath, url: baseURL.appendingPathComponent("Image.dmg.trustcache")),
DDIDownloadItem(name: "Image.dmg.cryptex_info", destinationPath: paths.cryptexInfoPath, url: baseURL.appendingPathComponent("Image.dmg.cryptex_info")),
DDIDownloadItem(name: "Image.dmg.root_hash", destinationPath: paths.rootHashPath, url: baseURL.appendingPathComponent("Image.dmg.root_hash")),
]
}
}

View file

@ -144,9 +144,7 @@ public enum StikJIT {
progress(.downloadingDDI(fraction: fraction, status: status))
}
}
try session.mountDDI(imagePath: paths.imagePath,
trustcachePath: paths.trustcachePath,
manifestPath: paths.manifestPath) { fraction in
try session.mountDDI(from: URL(fileURLWithPath: paths.manifestPath).deletingLastPathComponent().path) { fraction in
progress(.mountingDDI(fraction: fraction))
}
progress(.verifyingDDI)
@ -183,9 +181,7 @@ public enum StikJIT {
paths: DDIPaths,
progress: @escaping (Double) -> Void = { _ in }) throws {
let session = DDISession(pairingFilePath: pairingFile.path, configuration: configuration)
try session.mountDDI(imagePath: paths.imagePath,
trustcachePath: paths.trustcachePath,
manifestPath: paths.manifestPath,
try session.mountDDI(from: URL(fileURLWithPath: paths.manifestPath).deletingLastPathComponent().path,
progress: progress)
}
}

File diff suppressed because it is too large Load diff

Binary file not shown.