mirror of
https://github.com/StikDebug/StikDebug.git
synced 2026-10-15 20:10:30 +00:00
892 lines
32 KiB
Swift
892 lines
32 KiB
Swift
//
|
|
// IdeviceFFIBridge.swift
|
|
// StikDebug
|
|
//
|
|
// Created by Stephen on 2026/3/30.
|
|
//
|
|
|
|
import Foundation
|
|
import UIKit
|
|
import idevice
|
|
|
|
private enum IdeviceBridge {
|
|
static let processQueue = DispatchQueue(label: "com.stikdebug.processInspector", qos: .userInitiated)
|
|
|
|
static func makeError(
|
|
domain: String = "StikDebug",
|
|
code: Int = -1,
|
|
message: String
|
|
) -> NSError {
|
|
NSError(
|
|
domain: domain,
|
|
code: code,
|
|
userInfo: [NSLocalizedDescriptionKey: message]
|
|
)
|
|
}
|
|
|
|
static func string(from cString: UnsafePointer<CChar>?) -> String? {
|
|
guard let cString else { return nil }
|
|
return String(validatingUTF8: cString)
|
|
}
|
|
|
|
static func consumeFFIError(
|
|
_ ffiError: UnsafeMutablePointer<IdeviceFfiError>?,
|
|
fallback: String,
|
|
domain: String = "StikDebug"
|
|
) -> NSError {
|
|
guard let ffiError else {
|
|
return makeError(domain: domain, message: fallback)
|
|
}
|
|
|
|
let code = Int(ffiError.pointee.code)
|
|
let detail = string(from: ffiError.pointee.message) ?? fallback
|
|
let message = detail == fallback ? fallback : "\(fallback): \(detail)"
|
|
idevice_error_free(ffiError)
|
|
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<T>(
|
|
for context: JITEnableContext,
|
|
_ body: (OpaquePointer, OpaquePointer) throws -> T
|
|
) throws -> T {
|
|
let handles = try activeTunnelHandles(for: context)
|
|
return try body(handles.adapter, handles.handshake)
|
|
}
|
|
|
|
static func connectClient(
|
|
fallback: String,
|
|
missingClientMessage: String,
|
|
domain: String = "StikDebug",
|
|
connect: (UnsafeMutablePointer<OpaquePointer?>) -> UnsafeMutablePointer<IdeviceFfiError>?
|
|
) throws -> OpaquePointer {
|
|
var client: OpaquePointer?
|
|
if let ffiError = connect(&client) {
|
|
throw consumeFFIError(ffiError, fallback: fallback, domain: domain)
|
|
}
|
|
|
|
guard let client else {
|
|
throw makeError(domain: domain, message: missingClientMessage)
|
|
}
|
|
|
|
return client
|
|
}
|
|
|
|
static func withConnectedClient<T>(
|
|
fallback: String,
|
|
missingClientMessage: String,
|
|
domain: String = "StikDebug",
|
|
connect: (UnsafeMutablePointer<OpaquePointer?>) -> UnsafeMutablePointer<IdeviceFfiError>?,
|
|
cleanup: (OpaquePointer) -> Void,
|
|
_ body: (OpaquePointer) throws -> T
|
|
) throws -> T {
|
|
let client = try connectClient(
|
|
fallback: fallback,
|
|
missingClientMessage: missingClientMessage,
|
|
domain: domain,
|
|
connect: connect
|
|
)
|
|
defer { cleanup(client) }
|
|
return try body(client)
|
|
}
|
|
|
|
static func plistDictionaries(adapter: OpaquePointer, handshake: OpaquePointer) throws -> [[String: Any]] {
|
|
try withConnectedClient(
|
|
fallback: "Failed to connect to installation proxy",
|
|
missingClientMessage: "Installation proxy client was not created",
|
|
connect: { installation_proxy_connect_rsd(adapter, handshake, $0) },
|
|
cleanup: { installation_proxy_client_free($0) }
|
|
) { client in
|
|
var rawApps: UnsafeMutableRawPointer?
|
|
var count = 0
|
|
if let ffiError = installation_proxy_get_apps(client, nil, nil, 0, &rawApps, &count) {
|
|
throw consumeFFIError(ffiError, fallback: "Failed to fetch installed apps")
|
|
}
|
|
|
|
guard let rawApps, count > 0 else { return [] }
|
|
|
|
let apps = rawApps.assumingMemoryBound(to: plist_t?.self)
|
|
defer {
|
|
for index in 0..<count {
|
|
plist_free(apps[index])
|
|
}
|
|
idevice_data_free(
|
|
rawApps.assumingMemoryBound(to: UInt8.self),
|
|
UInt(count * MemoryLayout<plist_t?>.stride)
|
|
)
|
|
}
|
|
|
|
var dictionaries: [[String: Any]] = []
|
|
dictionaries.reserveCapacity(count)
|
|
|
|
for index in 0..<count {
|
|
var binaryPlist: UnsafeMutablePointer<CChar>?
|
|
var binaryLength: UInt32 = 0
|
|
let app = apps[index]
|
|
|
|
guard plist_to_bin(app, &binaryPlist, &binaryLength) == PLIST_ERR_SUCCESS,
|
|
let binaryPlist,
|
|
binaryLength > 0 else {
|
|
continue
|
|
}
|
|
|
|
let data = Data(bytes: binaryPlist, count: Int(binaryLength))
|
|
plist_mem_free(binaryPlist)
|
|
|
|
guard let plist = try? PropertyListSerialization.propertyList(from: data, format: nil),
|
|
let dictionary = plist as? [String: Any] else {
|
|
continue
|
|
}
|
|
|
|
dictionaries.append(dictionary)
|
|
}
|
|
|
|
return dictionaries
|
|
}
|
|
}
|
|
|
|
static func appName(from dictionary: [String: Any]) -> String {
|
|
if let displayName = dictionary["CFBundleDisplayName"] as? String, !displayName.isEmpty {
|
|
return displayName
|
|
}
|
|
if let name = dictionary["CFBundleName"] as? String, !name.isEmpty {
|
|
return name
|
|
}
|
|
return "Unknown"
|
|
}
|
|
|
|
static func hasGetTaskAllow(_ dictionary: [String: Any]) -> Bool {
|
|
guard let entitlements = dictionary["Entitlements"] as? [String: Any] else {
|
|
return false
|
|
}
|
|
|
|
if let flag = entitlements["get-task-allow"] as? Bool {
|
|
return flag
|
|
}
|
|
|
|
if let flag = entitlements["get-task-allow"] as? NSNumber {
|
|
return flag.boolValue
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
static func isHiddenSystemApp(_ dictionary: [String: Any]) -> Bool {
|
|
guard let applicationType = dictionary["ApplicationType"] as? String,
|
|
applicationType == "System" || applicationType == "HiddenSystemApp" else {
|
|
return false
|
|
}
|
|
|
|
if let isHidden = dictionary["IsHidden"] as? Bool, isHidden {
|
|
return true
|
|
}
|
|
|
|
if let isHidden = dictionary["IsHidden"] as? NSNumber, isHidden.boolValue {
|
|
return true
|
|
}
|
|
|
|
guard let tags = dictionary["SBAppTags"] as? [String] else {
|
|
return false
|
|
}
|
|
|
|
return tags.contains("hidden") || tags.contains("hidden-system-app")
|
|
}
|
|
|
|
static func appDictionary(
|
|
adapter: OpaquePointer,
|
|
handshake: OpaquePointer,
|
|
requireGetTaskAllow: Bool,
|
|
filter: (([String: Any]) -> Bool)? = nil
|
|
) throws -> [String: String] {
|
|
let dictionaries = try plistDictionaries(adapter: adapter, handshake: handshake)
|
|
var result: [String: String] = [:]
|
|
result.reserveCapacity(dictionaries.count)
|
|
|
|
for dictionary in dictionaries {
|
|
if requireGetTaskAllow && !hasGetTaskAllow(dictionary) {
|
|
continue
|
|
}
|
|
|
|
if let filter, !filter(dictionary) {
|
|
continue
|
|
}
|
|
|
|
guard let bundleID = dictionary["CFBundleIdentifier"] as? String,
|
|
!bundleID.isEmpty else {
|
|
continue
|
|
}
|
|
|
|
result[bundleID] = appName(from: dictionary)
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
static func activeTunnelHandles(for context: JITEnableContext) throws -> (adapter: OpaquePointer, handshake: OpaquePointer) {
|
|
try context.ensureTunnel()
|
|
|
|
guard let adapterHandle = context.adapterHandle,
|
|
let handshakeHandle = context.handshakeHandle else {
|
|
throw makeError(message: "Tunnel is not connected")
|
|
}
|
|
|
|
return (adapterHandle, handshakeHandle)
|
|
}
|
|
}
|
|
|
|
extension JITEnableContext {
|
|
func isDeveloperDiskImageMounted() throws -> Bool {
|
|
try IdeviceBridge.withTunnelHandles(for: self) { adapter, handshake in
|
|
var cryptexQuerySucceeded = false
|
|
var cryptexQueryError: NSError?
|
|
if DeveloperDiskImageService.usesCryptexDDI {
|
|
var installed: UnsafeMutablePointer<InstalledCryptexC>?
|
|
defer { cryptexd_free_installed_cryptex(installed) }
|
|
if let ffiError = cryptexd_installed_ddi(adapter, handshake, &installed) {
|
|
cryptexQueryError = IdeviceBridge.consumeFFIError(ffiError, fallback: "Failed to query installed DDI cryptex")
|
|
} else {
|
|
cryptexQuerySucceeded = true
|
|
if installed != nil {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
|
|
do {
|
|
return 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) }
|
|
) { client in
|
|
var devices: UnsafeMutablePointer<plist_t?>?
|
|
var deviceCount = 0
|
|
if let error = image_mounter_copy_devices(client, &devices, &deviceCount) {
|
|
throw IdeviceBridge.consumeFFIError(error, fallback: "Failed to fetch mounted devices")
|
|
}
|
|
if let devices {
|
|
for index in 0..<deviceCount {
|
|
plist_free(devices[index])
|
|
}
|
|
idevice_data_free(
|
|
UnsafeMutableRawPointer(devices).assumingMemoryBound(to: UInt8.self),
|
|
UInt(deviceCount * MemoryLayout<plist_t?>.stride)
|
|
)
|
|
}
|
|
return deviceCount > 0
|
|
}
|
|
} catch {
|
|
if cryptexQuerySucceeded {
|
|
return false
|
|
}
|
|
throw cryptexQueryError ?? (error as NSError)
|
|
}
|
|
}
|
|
}
|
|
|
|
func installCryptexDDI(from directoryPath: String) throws {
|
|
var assets: OpaquePointer?
|
|
if let ffiError = cryptex1_assets_load(directoryPath, &assets) {
|
|
throw IdeviceBridge.consumeFFIError(ffiError, fallback: "Failed to load DDI cryptex assets")
|
|
}
|
|
|
|
guard let assets else {
|
|
throw IdeviceBridge.makeError(message: "DDI cryptex assets were not loaded")
|
|
}
|
|
defer { cryptex1_assets_free(assets) }
|
|
|
|
try IdeviceBridge.withTunnelHandles(for: self) { adapter, handshake in
|
|
if let ffiError = cryptexd_install_ddi(adapter, handshake, assets, nil) {
|
|
throw IdeviceBridge.consumeFFIError(ffiError, fallback: "Failed to install DDI cryptex")
|
|
}
|
|
}
|
|
}
|
|
|
|
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(
|
|
fallback: "Failed to connect to misagent",
|
|
missingClientMessage: "Misagent client was not created",
|
|
domain: "profiles",
|
|
connect: { misagent_connect_rsd(adapter, handshake, $0) },
|
|
cleanup: { misagent_client_free($0) }
|
|
) { misagentClient in
|
|
var profilePointers: UnsafeMutablePointer<UnsafeMutablePointer<UInt8>?>?
|
|
var profileLengths: UnsafeMutablePointer<Int>?
|
|
var profileCount = 0
|
|
|
|
if let ffiError = misagent_copy_all(misagentClient, &profilePointers, &profileLengths, &profileCount) {
|
|
throw IdeviceBridge.consumeFFIError(
|
|
ffiError,
|
|
fallback: "Failed to fetch provisioning profiles",
|
|
domain: "profiles"
|
|
)
|
|
}
|
|
|
|
defer {
|
|
if let profilePointers, let profileLengths {
|
|
misagent_free_profiles(profilePointers, profileLengths, profileCount)
|
|
}
|
|
}
|
|
|
|
guard let profilePointers, let profileLengths else { return [] }
|
|
|
|
var result: [Data] = []
|
|
result.reserveCapacity(profileCount)
|
|
|
|
for index in 0..<profileCount {
|
|
guard let bytes = profilePointers[index] else { continue }
|
|
result.append(Data(bytes: bytes, count: profileLengths[index]))
|
|
}
|
|
|
|
return result
|
|
}
|
|
}
|
|
}
|
|
|
|
func removeProfile(withUUID uuid: String) throws {
|
|
try IdeviceBridge.withTunnelHandles(for: self) { adapter, handshake in
|
|
try IdeviceBridge.withConnectedClient(
|
|
fallback: "Failed to connect to misagent",
|
|
missingClientMessage: "Misagent client was not created",
|
|
domain: "profiles",
|
|
connect: { misagent_connect_rsd(adapter, handshake, $0) },
|
|
cleanup: { misagent_client_free($0) }
|
|
) { misagentClient in
|
|
if let ffiError = misagent_remove(misagentClient, uuid) {
|
|
throw IdeviceBridge.consumeFFIError(
|
|
ffiError,
|
|
fallback: "Failed to remove provisioning profile",
|
|
domain: "profiles"
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func addProfile(_ profile: Data) throws {
|
|
try IdeviceBridge.withTunnelHandles(for: self) { adapter, handshake in
|
|
try IdeviceBridge.withConnectedClient(
|
|
fallback: "Failed to connect to misagent",
|
|
missingClientMessage: "Misagent client was not created",
|
|
domain: "profiles",
|
|
connect: { misagent_connect_rsd(adapter, handshake, $0) },
|
|
cleanup: { misagent_client_free($0) }
|
|
) { misagentClient in
|
|
let ffiError = profile.withUnsafeBytes { rawBuffer in
|
|
misagent_install(
|
|
misagentClient,
|
|
rawBuffer.bindMemory(to: UInt8.self).baseAddress,
|
|
profile.count
|
|
)
|
|
}
|
|
|
|
if let ffiError {
|
|
throw IdeviceBridge.consumeFFIError(
|
|
ffiError,
|
|
fallback: "Failed to add provisioning profile",
|
|
domain: "profiles"
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func fetchProcessList() throws -> [NSDictionary] {
|
|
try IdeviceBridge.processQueue.sync {
|
|
try IdeviceBridge.withTunnelHandles(for: self) { adapter, handshake in
|
|
try IdeviceBridge.withConnectedClient(
|
|
fallback: "Unable to open AppService",
|
|
missingClientMessage: "AppService client was not created",
|
|
connect: { app_service_connect_rsd(adapter, handshake, $0) },
|
|
cleanup: { app_service_free($0) }
|
|
) { appService in
|
|
var processes: UnsafeMutablePointer<ProcessTokenC>?
|
|
var count = UInt(0)
|
|
if let ffiError = app_service_list_processes(appService, &processes, &count) {
|
|
throw IdeviceBridge.consumeFFIError(ffiError, fallback: "Failed to list processes")
|
|
}
|
|
|
|
defer {
|
|
if let processes {
|
|
app_service_free_process_list(processes, count)
|
|
}
|
|
}
|
|
|
|
guard let processes else { return [] }
|
|
|
|
var result: [NSDictionary] = []
|
|
result.reserveCapacity(Int(count))
|
|
|
|
for index in 0..<Int(count) {
|
|
let process = processes[index]
|
|
var dictionary: [String: Any] = ["pid": NSNumber(value: process.pid)]
|
|
if let executableURL = IdeviceBridge.string(from: process.executable_url) {
|
|
dictionary["path"] = executableURL
|
|
}
|
|
result.append(dictionary as NSDictionary)
|
|
}
|
|
|
|
return result
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func sendSignal(_ signal: Int32, toProcessWithPID pid: Int32) throws {
|
|
try IdeviceBridge.withTunnelHandles(for: self) { adapter, handshake in
|
|
try IdeviceBridge.withConnectedClient(
|
|
fallback: "Unable to open AppService",
|
|
missingClientMessage: "AppService client was not created",
|
|
connect: { app_service_connect_rsd(adapter, handshake, $0) },
|
|
cleanup: { app_service_free($0) }
|
|
) { appService in
|
|
var response: UnsafeMutablePointer<SignalResponseC>?
|
|
let ffiError = app_service_send_signal(appService, UInt32(pid), UInt32(signal), &response)
|
|
defer {
|
|
if let response {
|
|
app_service_free_signal_response(response)
|
|
}
|
|
}
|
|
|
|
if let ffiError {
|
|
throw IdeviceBridge.consumeFFIError(ffiError, fallback: "Failed to send signal \(signal) to process")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func killProcess(withPID pid: Int32) throws {
|
|
try sendSignal(Int32(SIGKILL), toProcessWithPID: pid)
|
|
}
|
|
|
|
func getAppList() throws -> [String: String] {
|
|
try IdeviceBridge.withTunnelHandles(for: self) { adapter, handshake in
|
|
try IdeviceBridge.appDictionary(
|
|
adapter: adapter,
|
|
handshake: handshake,
|
|
requireGetTaskAllow: true
|
|
)
|
|
}
|
|
}
|
|
|
|
func getAllApps() throws -> [String: String] {
|
|
try IdeviceBridge.withTunnelHandles(for: self) { adapter, handshake in
|
|
try IdeviceBridge.appDictionary(
|
|
adapter: adapter,
|
|
handshake: handshake,
|
|
requireGetTaskAllow: false
|
|
)
|
|
}
|
|
}
|
|
|
|
func getHiddenSystemApps() throws -> [String: String] {
|
|
try IdeviceBridge.withTunnelHandles(for: self) { adapter, handshake in
|
|
try IdeviceBridge.appDictionary(
|
|
adapter: adapter,
|
|
handshake: handshake,
|
|
requireGetTaskAllow: false,
|
|
filter: IdeviceBridge.isHiddenSystemApp
|
|
)
|
|
}
|
|
}
|
|
|
|
func getSideloadedApps() throws -> [NSDictionary] {
|
|
try IdeviceBridge.withTunnelHandles(for: self) { adapter, handshake in
|
|
try IdeviceBridge.plistDictionaries(adapter: adapter, handshake: handshake)
|
|
.filter { $0["ProfileValidated"] != nil }
|
|
.map { $0 as NSDictionary }
|
|
}
|
|
}
|
|
|
|
func getAppIcon(withBundleId bundleId: String) throws -> UIImage {
|
|
try IdeviceBridge.withTunnelHandles(for: self) { adapter, handshake in
|
|
try IdeviceBridge.withConnectedClient(
|
|
fallback: "Failed to connect to SpringBoard Services",
|
|
missingClientMessage: "SpringBoard Services client was not created",
|
|
connect: { springboard_services_connect_rsd(adapter, handshake, $0) },
|
|
cleanup: { springboard_services_free($0) }
|
|
) { client in
|
|
var rawIconData: UnsafeMutableRawPointer?
|
|
var rawIconLength = 0
|
|
if let ffiError = springboard_services_get_icon(client, bundleId, &rawIconData, &rawIconLength) {
|
|
throw IdeviceBridge.consumeFFIError(ffiError, fallback: "Failed to get app icon")
|
|
}
|
|
|
|
guard let rawIconData, rawIconLength > 0 else {
|
|
throw IdeviceBridge.makeError(message: "App icon data was empty")
|
|
}
|
|
|
|
defer { free(rawIconData) }
|
|
|
|
let data = Data(bytes: rawIconData, count: rawIconLength)
|
|
guard let image = UIImage(data: data) else {
|
|
throw IdeviceBridge.makeError(message: "Failed to decode app icon image")
|
|
}
|
|
|
|
return image
|
|
}
|
|
}
|
|
}
|
|
|
|
func ideviceInfoInit() throws -> OpaquePointer {
|
|
try IdeviceBridge.withTunnelHandles(for: self) { adapter, handshake in
|
|
try IdeviceBridge.connectClient(
|
|
fallback: "Failed to connect to lockdownd",
|
|
missingClientMessage: "Lockdownd client was not created",
|
|
domain: "profiles",
|
|
connect: { lockdownd_connect_rsd(adapter, handshake, $0) }
|
|
)
|
|
}
|
|
}
|
|
|
|
func ideviceInfoGetXML(withLockdownClient lockdownClient: OpaquePointer?) throws -> UnsafeMutablePointer<CChar>? {
|
|
guard let lockdownClient else { return nil }
|
|
|
|
var plistObject: plist_t?
|
|
if let ffiError = lockdownd_get_value(lockdownClient, nil, nil, &plistObject) {
|
|
throw IdeviceBridge.consumeFFIError(ffiError, fallback: "Failed to fetch device info")
|
|
}
|
|
|
|
guard let plistObject else {
|
|
return nil
|
|
}
|
|
|
|
defer { plist_free(plistObject) }
|
|
|
|
var xml: UnsafeMutablePointer<CChar>?
|
|
var xmlLength: UInt32 = 0
|
|
guard plist_to_xml(plistObject, &xml, &xmlLength) == PLIST_ERR_SUCCESS,
|
|
let xml,
|
|
xmlLength > 0 else {
|
|
throw IdeviceBridge.makeError(message: "Failed to serialize device info plist")
|
|
}
|
|
|
|
return xml
|
|
}
|
|
}
|
|
|
|
func FetchDeviceProcessList(_ error: NSErrorPointer) -> [NSDictionary]? {
|
|
do {
|
|
return try JITEnableContext.shared.fetchProcessList()
|
|
} catch let nsError as NSError {
|
|
error?.pointee = nsError
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func KillDeviceProcess(_ pid: Int32, _ error: NSErrorPointer) -> Bool {
|
|
do {
|
|
try JITEnableContext.shared.killProcess(withPID: pid)
|
|
return true
|
|
} catch let nsError as NSError {
|
|
error?.pointee = nsError
|
|
return false
|
|
}
|
|
}
|
|
|
|
struct ProcessInfoEntry: Identifiable {
|
|
let pid: Int
|
|
private let rawPath: String
|
|
let bundleID: String?
|
|
let name: String?
|
|
|
|
init?(dictionary: NSDictionary) {
|
|
guard let pidNumber = dictionary["pid"] as? NSNumber else { return nil }
|
|
pid = pidNumber.intValue
|
|
rawPath = dictionary["path"] as? String ?? "Unknown"
|
|
bundleID = dictionary["bundleID"] as? String
|
|
name = dictionary["name"] as? String
|
|
}
|
|
|
|
static func currentEntries(_ error: NSErrorPointer = nil) -> [ProcessInfoEntry] {
|
|
let entries = FetchDeviceProcessList(error) ?? []
|
|
return entries.compactMap(Self.init(dictionary:))
|
|
}
|
|
|
|
var id: Int { pid }
|
|
|
|
var executablePath: String {
|
|
rawPath.replacingOccurrences(of: "file://", with: "")
|
|
}
|
|
|
|
var displayName: String {
|
|
if let name, !name.isEmpty {
|
|
return name
|
|
}
|
|
if let bundleID, !bundleID.isEmpty {
|
|
return bundleID
|
|
}
|
|
if let component = executablePath.split(separator: "/").last {
|
|
return String(component)
|
|
}
|
|
return "Process \(pid)"
|
|
}
|
|
|
|
var stableIdentifier: String {
|
|
if let bundleID, !bundleID.isEmpty {
|
|
return bundleID
|
|
}
|
|
return displayName
|
|
}
|
|
}
|
|
|
|
@objcMembers
|
|
final class CMSDecoderHelper: NSObject {
|
|
static func decodeCMSData(_ cmsData: Data) throws -> Data {
|
|
guard !cmsData.isEmpty else {
|
|
throw IdeviceBridge.makeError(
|
|
domain: NSCocoaErrorDomain,
|
|
code: NSURLErrorBadURL,
|
|
message: "Invalid or empty CMS payload"
|
|
)
|
|
}
|
|
|
|
let xmlStart = Data("<?xml".utf8)
|
|
let plistEnd = Data("</plist>".utf8)
|
|
let binaryMagic = Data("bplist00".utf8)
|
|
|
|
if let startRange = cmsData.range(of: xmlStart),
|
|
let endRange = cmsData.range(of: plistEnd, options: [], in: startRange.lowerBound..<cmsData.endIndex) {
|
|
return cmsData[startRange.lowerBound..<endRange.upperBound]
|
|
}
|
|
|
|
if let binaryRange = cmsData.range(of: binaryMagic) {
|
|
return cmsData[binaryRange.lowerBound..<cmsData.endIndex]
|
|
}
|
|
|
|
throw IdeviceBridge.makeError(
|
|
domain: NSCocoaErrorDomain,
|
|
code: NSFileReadUnknownError,
|
|
message: "Unable to extract plist from CMS payload"
|
|
)
|
|
}
|
|
}
|
|
|
|
private enum LocationSimulationStatus {
|
|
static let ok: Int32 = 0
|
|
static let invalidIP: Int32 = 1
|
|
static let pairingRead: Int32 = 2
|
|
static let providerCreate: Int32 = 3
|
|
static let remoteServer: Int32 = 9
|
|
static let locationSimulation: Int32 = 10
|
|
static let locationSet: Int32 = 11
|
|
static let locationClear: Int32 = 12
|
|
}
|
|
|
|
private enum LocationSimulationState {
|
|
static var adapter: OpaquePointer?
|
|
static var handshake: OpaquePointer?
|
|
static var remoteServer: OpaquePointer?
|
|
static var locationSimulation: OpaquePointer?
|
|
|
|
static func cleanup() {
|
|
if let locationSimulation {
|
|
location_simulation_free(locationSimulation)
|
|
self.locationSimulation = nil
|
|
}
|
|
if let remoteServer {
|
|
remote_server_free(remoteServer)
|
|
self.remoteServer = nil
|
|
}
|
|
if let handshake {
|
|
rsd_handshake_free(handshake)
|
|
self.handshake = nil
|
|
}
|
|
if let adapter {
|
|
adapter_free(adapter)
|
|
self.adapter = nil
|
|
}
|
|
}
|
|
}
|
|
|
|
enum LocationSimulationCommandQueue {
|
|
static let shared = DispatchQueue(label: "com.stik.location-sim", qos: .userInitiated)
|
|
}
|
|
|
|
func simulate_location(_ deviceIP: String, _ latitude: Double, _ longitude: Double, _ pairingFile: String) -> Int32 {
|
|
if let locationSimulation = LocationSimulationState.locationSimulation {
|
|
if let ffiError = location_simulation_set(locationSimulation, latitude, longitude) {
|
|
idevice_error_free(ffiError)
|
|
LocationSimulationState.cleanup()
|
|
} else {
|
|
return LocationSimulationStatus.ok
|
|
}
|
|
}
|
|
|
|
var address = sockaddr_in()
|
|
address.sin_family = sa_family_t(AF_INET)
|
|
address.sin_port = in_port_t(49152).bigEndian
|
|
|
|
let inetResult = deviceIP.withCString { inet_pton(AF_INET, $0, &address.sin_addr) }
|
|
guard inetResult == 1 else {
|
|
return LocationSimulationStatus.invalidIP
|
|
}
|
|
|
|
var pairingHandle: OpaquePointer?
|
|
let pairingError = pairingFile.withCString { rp_pairing_file_read($0, &pairingHandle) }
|
|
if let pairingError {
|
|
idevice_error_free(pairingError)
|
|
return LocationSimulationStatus.pairingRead
|
|
}
|
|
|
|
guard let pairingHandle else {
|
|
return LocationSimulationStatus.pairingRead
|
|
}
|
|
|
|
defer { rp_pairing_file_free(pairingHandle) }
|
|
|
|
let providerError = withUnsafePointer(to: &address) { pointer in
|
|
pointer.withMemoryRebound(to: sockaddr.self, capacity: 1) {
|
|
tunnel_create_rppairing(
|
|
$0,
|
|
socklen_t(MemoryLayout<sockaddr_in>.stride),
|
|
"StikDebugLocation",
|
|
pairingHandle,
|
|
nil,
|
|
nil,
|
|
&LocationSimulationState.adapter,
|
|
&LocationSimulationState.handshake
|
|
)
|
|
}
|
|
}
|
|
|
|
if let providerError {
|
|
idevice_error_free(providerError)
|
|
LocationSimulationState.cleanup()
|
|
return LocationSimulationStatus.providerCreate
|
|
}
|
|
|
|
let remoteServerError = remote_server_connect_rsd(
|
|
LocationSimulationState.adapter,
|
|
LocationSimulationState.handshake,
|
|
&LocationSimulationState.remoteServer
|
|
)
|
|
if let remoteServerError {
|
|
idevice_error_free(remoteServerError)
|
|
LocationSimulationState.cleanup()
|
|
return LocationSimulationStatus.remoteServer
|
|
}
|
|
|
|
let locationSimulationError = location_simulation_new(
|
|
LocationSimulationState.remoteServer,
|
|
&LocationSimulationState.locationSimulation
|
|
)
|
|
if let locationSimulationError {
|
|
idevice_error_free(locationSimulationError)
|
|
LocationSimulationState.cleanup()
|
|
return LocationSimulationStatus.locationSimulation
|
|
}
|
|
|
|
LocationSimulationState.remoteServer = nil
|
|
|
|
let locationSetError = location_simulation_set(
|
|
LocationSimulationState.locationSimulation,
|
|
latitude,
|
|
longitude
|
|
)
|
|
if let locationSetError {
|
|
idevice_error_free(locationSetError)
|
|
LocationSimulationState.cleanup()
|
|
return LocationSimulationStatus.locationSet
|
|
}
|
|
|
|
return LocationSimulationStatus.ok
|
|
}
|
|
|
|
func clear_simulated_location() -> Int32 {
|
|
guard let locationSimulation = LocationSimulationState.locationSimulation else {
|
|
return LocationSimulationStatus.locationClear
|
|
}
|
|
|
|
let ffiError = location_simulation_clear(locationSimulation)
|
|
LocationSimulationState.cleanup()
|
|
|
|
if let ffiError {
|
|
idevice_error_free(ffiError)
|
|
return LocationSimulationStatus.locationClear
|
|
}
|
|
|
|
return LocationSimulationStatus.ok
|
|
}
|