Merge pull request #127 from lbr77/main

ipa install
This commit is contained in:
LiBr
2026-03-07 23:51:33 +08:00
committed by GitHub
parent 122f2aaf0c
commit 56451c4d53
12 changed files with 1497 additions and 8 deletions
+4 -1
View File
@@ -436,6 +436,7 @@ VPHONED_SRC="$SCRIPT_DIR/vphoned"
VPHONED_BIN="$VPHONED_SRC/vphoned"
VPHONED_SRCS=(
"$VPHONED_SRC/vphoned.m"
"$VPHONED_SRC/vphoned_install.m"
"$VPHONED_SRC/vphoned_protocol.m"
"$VPHONED_SRC/vphoned_hid.m"
"$VPHONED_SRC/vphoned_devmode.m"
@@ -458,7 +459,9 @@ if [[ "$needs_vphoned_build" == "1" ]]; then
xcrun -sdk iphoneos clang -arch arm64 -Os -fobjc-arc \
-I"$VPHONED_SRC" \
-o "$VPHONED_BIN" "${VPHONED_SRCS[@]}" \
-framework Foundation
-framework Foundation \
-framework Security \
-framework CoreServices
fi
cp "$VPHONED_BIN" "$TEMP_DIR/vphoned"
ldid \
+4 -1
View File
@@ -438,6 +438,7 @@ VPHONED_SRC="$SCRIPT_DIR/vphoned"
VPHONED_BIN="$VPHONED_SRC/vphoned"
VPHONED_SRCS=(
"$VPHONED_SRC/vphoned.m"
"$VPHONED_SRC/vphoned_install.m"
"$VPHONED_SRC/vphoned_protocol.m"
"$VPHONED_SRC/vphoned_hid.m"
"$VPHONED_SRC/vphoned_devmode.m"
@@ -460,7 +461,9 @@ if [[ "$needs_vphoned_build" == "1" ]]; then
xcrun -sdk iphoneos clang -arch arm64 -Os -fobjc-arc \
-I"$VPHONED_SRC" \
-o "$VPHONED_BIN" "${VPHONED_SRCS[@]}" \
-framework Foundation
-framework Foundation \
-framework Security \
-framework CoreServices
fi
cp "$VPHONED_BIN" "$TEMP_DIR/vphoned"
ldid_sign_ent "$TEMP_DIR/vphoned" "$VPHONED_SRC/entitlements.plist"
+42 -4
View File
@@ -202,18 +202,56 @@ fi
ssh_cmd "uicache -a 2>/dev/null || true"
echo " [+] uicache refreshed"
# ═══════════ 6/6 APT SETUP ═════════════════════════════════
# ═══════════ 6/7 APT SETUP ═════════════════════════════════
echo ""
echo "[6/6] Running apt setup..."
echo "[6/7] Running apt setup..."
ssh_cmd "apt-get update -qq && apt-get install -y -qq libkrw0-tfp0 2>/dev/null || true"
HAVOC_LIST="/var/jb/etc/apt/sources.list.d/havoc.list"
if ssh_cmd "test -d /etc/apt/sources.list.d && test ! -d /var/jb/etc/apt/sources.list.d"; then
HAVOC_LIST="/etc/apt/sources.list.d/havoc.list"
fi
HAVOC_SOURCES="$(ssh_cmd "grep -RIl 'havoc.app' /etc/apt /var/jb/etc/apt 2>/dev/null || true")"
if [[ -n "$HAVOC_SOURCES" ]]; then
echo " [*] Havoc source already present:"
echo "$HAVOC_SOURCES" | sed 's/^/ - /'
OTHER_HAVOC_SOURCES="$(printf '%s\n' "$HAVOC_SOURCES" | grep -Fvx "$HAVOC_LIST" || true)"
if [[ -n "$OTHER_HAVOC_SOURCES" ]] && ssh_cmd "test -f '$HAVOC_LIST' && grep -q 'https://havoc.app/' '$HAVOC_LIST'"; then
ssh_cmd "rm -f '$HAVOC_LIST'"
echo " [+] Removed duplicate autogenerated Havoc source: $HAVOC_LIST"
fi
else
ssh_cmd "mkdir -p '${HAVOC_LIST:h}'"
ssh_cmd "printf '%s\n' 'deb [trusted=yes] https://havoc.app/ ./' > '$HAVOC_LIST'"
echo " [+] Havoc source added: $HAVOC_LIST"
fi
echo " [*] Allowing unsigned third-party repos during automated apt refresh"
ssh_cmd "DEBIAN_FRONTEND=noninteractive apt-get -o Acquire::AllowInsecureRepositories=true -o Acquire::AllowDowngradeToInsecureRepositories=true update -qq"
ssh_cmd "DEBIAN_FRONTEND=noninteractive apt-get -o APT::Get::AllowUnauthenticated=true install -y -qq libkrw0-tfp0 2>/dev/null || true"
echo " [+] apt update + libkrw0-tfp0 done"
ssh_cmd "apt-get upgrade -y -qq 2>/dev/null || true"
ssh_cmd "DEBIAN_FRONTEND=noninteractive apt-get -o APT::Get::AllowUnauthenticated=true upgrade -y -qq 2>/dev/null || true"
echo " [+] apt upgrade done"
# ═══════════ 7/7 INSTALL TROLLSTORE LITE ═══════════════════
echo ""
echo "[7/7] Installing TrollStore Lite..."
if ssh_cmd "dpkg -s com.opa334.trollstorelite >/dev/null 2>&1"; then
echo " [*] TrollStore Lite already installed, skipping"
else
ssh_cmd "DEBIAN_FRONTEND=noninteractive apt-get -o APT::Get::AllowUnauthenticated=true install -y -qq com.opa334.trollstorelite"
echo " [+] TrollStore Lite installed"
fi
ssh_cmd "uicache -a 2>/dev/null || true"
echo " [+] uicache refreshed"
# ═══════════ DONE ═══════════════════════════════════════════
echo ""
echo "[+] JB finalization complete!"
echo " TrollStore Lite is installed automatically during finalization."
echo " Next: open Sileo on device, add source https://ellekit.space, install ElleKit"
echo " Then reboot the device for full JB environment."
+4 -1
View File
@@ -15,7 +15,10 @@ $(OUT): $(SRCS) $(wildcard *.h)
xcrun -sdk iphoneos clang -arch arm64 -Os -fobjc-arc \
-I. \
-DVPHONED_BUILD_HASH='"$(GIT_HASH)"' \
-o $@ $(SRCS) -framework Foundation
-o $@ $(SRCS) \
-framework Foundation \
-framework Security \
-framework CoreServices
@echo " built OK"
clean:
+218
View File
@@ -16,8 +16,10 @@
#import <Foundation/Foundation.h>
#include <CommonCrypto/CommonDigest.h>
#include <mach-o/dyld.h>
#include <spawn.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <unistd.h>
#import "vphoned_protocol.h"
@@ -25,6 +27,7 @@
#import "vphoned_devmode.h"
#import "vphoned_location.h"
#import "vphoned_files.h"
#import "vphoned_install.h"
#ifndef AF_VSOCK
#define AF_VSOCK 40
@@ -80,6 +83,211 @@ static const char *self_executable_path(void) {
return path;
}
// MARK: - TrollStore Lite Install
static NSString *find_trollstore_lite_helper(void) {
NSFileManager *fm = [NSFileManager defaultManager];
NSArray<NSString *> *fixedPaths = @[
@"/Applications/TrollStoreLite.app/trollstorehelper",
@"/var/jb/Applications/TrollStoreLite.app/trollstorehelper",
];
for (NSString *path in fixedPaths) {
if ([fm isExecutableFileAtPath:path]) return path;
}
NSString *bundleRoot = @"/var/containers/Bundle/Application";
NSDirectoryEnumerator<NSString *> *enumerator = [fm enumeratorAtPath:bundleRoot];
for (NSString *relativePath in enumerator) {
if ([relativePath hasSuffix:@"TrollStoreLite.app/trollstorehelper"]) {
NSString *fullPath = [bundleRoot stringByAppendingPathComponent:relativePath];
if ([fm isExecutableFileAtPath:fullPath]) return fullPath;
}
}
return nil;
}
static NSString *trollstore_lite_marker_path_for_container(NSString *containerPath) {
return [containerPath stringByAppendingPathComponent:@"_TrollStoreLite"];
}
static NSDictionary *info_dictionary_for_app_path(NSString *appPath) {
if (appPath.length == 0) return nil;
return [NSDictionary dictionaryWithContentsOfFile:[appPath stringByAppendingPathComponent:@"Info.plist"]];
}
static NSString *find_trollstore_lite_app_path_for_bundle_id(NSString *bundleId) {
if (bundleId.length == 0) return nil;
NSFileManager *fm = [NSFileManager defaultManager];
NSString *bundleRoot = @"/var/containers/Bundle/Application";
NSArray<NSString *> *containers = [fm contentsOfDirectoryAtPath:bundleRoot error:nil];
for (NSString *container in containers) {
NSString *containerPath = [bundleRoot stringByAppendingPathComponent:container];
BOOL isDirectory = NO;
if (![fm fileExistsAtPath:containerPath isDirectory:&isDirectory] || !isDirectory) continue;
if (![fm fileExistsAtPath:trollstore_lite_marker_path_for_container(containerPath)]) continue;
NSArray<NSString *> *items = [fm contentsOfDirectoryAtPath:containerPath error:nil];
for (NSString *item in items) {
if (![item.pathExtension isEqualToString:@"app"]) continue;
NSString *appPath = [containerPath stringByAppendingPathComponent:item];
NSString *candidateBundleId = info_dictionary_for_app_path(appPath)[@"CFBundleIdentifier"];
if ([candidateBundleId isEqualToString:bundleId]) {
return appPath;
}
}
}
return nil;
}
static NSString *read_all_from_fd(int fd) {
NSMutableData *data = [NSMutableData data];
uint8_t buf[4096];
ssize_t n = 0;
while ((n = read(fd, buf, sizeof(buf))) > 0) {
[data appendBytes:buf length:(NSUInteger)n];
}
if (data.length == 0) return @"";
NSString *string = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
return string ?: @"";
}
static int run_process_with_output(NSString *path, NSArray<NSString *> *args, NSString **output) {
NSUInteger argc = args.count + 2;
char **argv = calloc(argc, sizeof(char *));
if (!argv) return ENOMEM;
argv[0] = strdup(path.fileSystemRepresentation);
for (NSUInteger i = 0; i < args.count; i++) {
argv[i + 1] = strdup(args[i].fileSystemRepresentation);
}
argv[argc - 1] = NULL;
int pipeFds[2] = {-1, -1};
if (pipe(pipeFds) != 0) {
for (NSUInteger i = 0; i < argc - 1; i++) free(argv[i]);
free(argv);
return errno;
}
posix_spawn_file_actions_t actions;
posix_spawn_file_actions_init(&actions);
posix_spawn_file_actions_adddup2(&actions, pipeFds[1], STDOUT_FILENO);
posix_spawn_file_actions_adddup2(&actions, pipeFds[1], STDERR_FILENO);
posix_spawn_file_actions_addclose(&actions, pipeFds[0]);
pid_t pid = 0;
int spawnError = posix_spawn(&pid, path.fileSystemRepresentation, &actions, NULL, argv, NULL);
posix_spawn_file_actions_destroy(&actions);
close(pipeFds[1]);
NSString *captured = read_all_from_fd(pipeFds[0]);
close(pipeFds[0]);
int status = 0;
if (spawnError == 0) {
if (waitpid(pid, &status, 0) < 0) {
spawnError = errno;
}
}
for (NSUInteger i = 0; i < argc - 1; i++) free(argv[i]);
free(argv);
if (output) *output = captured;
if (spawnError != 0) return spawnError;
if (WIFEXITED(status)) return WEXITSTATUS(status);
if (WIFSIGNALED(status)) return 128 + WTERMSIG(status);
return -1;
}
static NSDictionary *handle_trollstore_install(NSDictionary *msg) {
id reqId = msg[@"id"];
NSString *ipaPath = msg[@"path"];
NSString *bundleId = msg[@"bundle_id"];
NSString *registration = msg[@"registration"];
if (ipaPath.length == 0) {
NSMutableDictionary *r = vp_make_response(@"err", reqId);
r[@"msg"] = @"missing ipa path";
return r;
}
if (![[NSFileManager defaultManager] fileExistsAtPath:ipaPath]) {
NSMutableDictionary *r = vp_make_response(@"err", reqId);
r[@"msg"] = [NSString stringWithFormat:@"IPA not found: %@", ipaPath];
return r;
}
NSString *helperPath = find_trollstore_lite_helper();
if (helperPath.length == 0) {
NSMutableDictionary *r = vp_make_response(@"err", reqId);
r[@"msg"] = @"TrollStore Lite helper not found in guest";
return r;
}
NSString *output = @"";
int ret = run_process_with_output(helperPath, @[ @"install", ipaPath ], &output);
if (ret != 0) {
NSMutableDictionary *r = vp_make_response(@"err", reqId);
NSString *trimmed = [output stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
if (trimmed.length > 4000) {
trimmed = [trimmed substringToIndex:4000];
}
r[@"msg"] = trimmed.length > 0
? [NSString stringWithFormat:@"trollstorehelper returned %d\n%@", ret, trimmed]
: [NSString stringWithFormat:@"trollstorehelper returned %d", ret];
return r;
}
if ([registration isEqualToString:@"User"] && bundleId.length > 0) {
NSString *appPath = nil;
for (int attempt = 0; attempt < 10; attempt++) {
appPath = find_trollstore_lite_app_path_for_bundle_id(bundleId);
if (appPath.length > 0) break;
usleep(200 * 1000);
}
if (appPath.length == 0) {
NSMutableDictionary *r = vp_make_response(@"err", reqId);
r[@"msg"] = [NSString stringWithFormat:@"installed but failed to locate app path for %@", bundleId];
return r;
}
NSString *registrationOutput = @"";
int registrationRet = run_process_with_output(
helperPath,
@[ @"modify-registration", appPath, @"User" ],
&registrationOutput
);
if (registrationRet != 0) {
NSMutableDictionary *r = vp_make_response(@"err", reqId);
NSString *trimmed = [registrationOutput stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
if (trimmed.length > 4000) {
trimmed = [trimmed substringToIndex:4000];
}
r[@"msg"] = trimmed.length > 0
? [NSString stringWithFormat:@"installed, but switch-to-user failed (%d)\n%@", registrationRet, trimmed]
: [NSString stringWithFormat:@"installed, but switch-to-user failed (%d)", registrationRet];
return r;
}
}
[[NSFileManager defaultManager] removeItemAtPath:ipaPath error:nil];
NSMutableDictionary *r = vp_make_response(@"ok", reqId);
if ([registration isEqualToString:@"User"]) {
r[@"msg"] = [NSString stringWithFormat:@"Installed via TrollStore Lite and switched to User: %@", ipaPath.lastPathComponent];
} else {
r[@"msg"] = [NSString stringWithFormat:@"Installed via TrollStore Lite: %@", ipaPath.lastPathComponent];
}
return r;
}
// MARK: - Auto-update
/// Receive raw binary from host, write to CACHE_PATH, chmod +x.
@@ -202,6 +410,14 @@ static NSDictionary *handle_command(NSDictionary *msg) {
return r;
}
if ([type isEqualToString:@"tslite_install"]) {
return handle_trollstore_install(msg);
}
if ([type isEqualToString:@"ipa_install"]) {
return vp_handle_custom_install(msg);
}
NSMutableDictionary *r = vp_make_response(@"err", reqId);
r[@"msg"] = [NSString stringWithFormat:@"unknown type: %@", type];
return r;
@@ -251,6 +467,8 @@ static BOOL handle_client(int fd) {
// Build capabilities list
NSMutableArray *caps = [NSMutableArray arrayWithObjects:@"hid", @"devmode", @"file", nil];
if (vp_location_available()) [caps addObject:@"location"];
if (vp_custom_installer_available()) [caps addObject:@"ipa_install"];
if (find_trollstore_lite_helper()) [caps addObject:@"tslite_install"];
NSMutableDictionary *helloResp = [@{
@"v": @PROTOCOL_VERSION,
+4
View File
@@ -0,0 +1,4 @@
#import <Foundation/Foundation.h>
BOOL vp_custom_installer_available(void);
NSDictionary *vp_handle_custom_install(NSDictionary *msg);
+883
View File
@@ -0,0 +1,883 @@
#import "vphoned_install.h"
#import <Security/Security.h>
#include <dlfcn.h>
#include <errno.h>
#include <fcntl.h>
#include <mach-o/fat.h>
#include <mach-o/loader.h>
#include <spawn.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <unistd.h>
#import "vphoned_protocol.h"
typedef struct __SecCode const *SecStaticCodeRef;
typedef CF_OPTIONS(uint32_t, SecCSFlags) {
kSecCSDefaultFlags = 0
};
#define kSecCSRequirementInformation (1 << 2)
OSStatus SecStaticCodeCreateWithPathAndAttributes(
CFURLRef path,
SecCSFlags flags,
CFDictionaryRef attributes,
SecStaticCodeRef *staticCode
);
OSStatus SecCodeCopySigningInformation(SecStaticCodeRef code, SecCSFlags flags, CFDictionaryRef *information);
extern CFStringRef kSecCodeInfoEntitlementsDict;
extern NSString *LSInstallTypeKey;
@interface LSBundleProxy : NSObject
@property (nonatomic, readonly) NSString *bundleIdentifier;
@property (nonatomic) NSURL *dataContainerURL;
@property (nonatomic, readonly) NSURL *bundleContainerURL;
- (NSString *)localizedName;
@end
@interface LSApplicationProxy : LSBundleProxy
+ (instancetype)applicationProxyForIdentifier:(NSString *)identifier;
+ (instancetype)applicationProxyForBundleURL:(NSURL *)bundleURL;
@property NSURL *bundleURL;
@property NSString *bundleType;
@property NSString *canonicalExecutablePath;
@property (nonatomic, readonly) NSDictionary *groupContainerURLs;
@property (nonatomic, readonly) NSArray *plugInKitPlugins;
@property (getter=isInstalled, nonatomic, readonly) BOOL installed;
@property (getter=isPlaceholder, nonatomic, readonly) BOOL placeholder;
@property (getter=isRestricted, nonatomic, readonly) BOOL restricted;
@property (nonatomic, readonly) NSSet *claimedURLSchemes;
@property (nonatomic, readonly) NSString *applicationType;
@end
@interface LSApplicationWorkspace : NSObject
+ (instancetype)defaultWorkspace;
- (BOOL)registerApplicationDictionary:(NSDictionary *)dict;
- (BOOL)unregisterApplication:(id)arg1;
- (BOOL)installApplication:(NSURL *)appPackageURL withOptions:(NSDictionary *)options error:(NSError **)error;
- (BOOL)uninstallApplication:(NSString *)appId withOptions:(NSDictionary *)options;
- (void)enumerateApplicationsOfType:(NSUInteger)type block:(void (^)(LSApplicationProxy *))block;
@end
@interface LSEnumerator : NSEnumerator
@property (nonatomic, copy) NSPredicate *predicate;
+ (instancetype)enumeratorForApplicationProxiesWithOptions:(NSUInteger)options;
@end
@interface MCMContainer : NSObject
+ (id)containerWithIdentifier:(id)arg1 createIfNecessary:(BOOL)arg2 existed:(BOOL *)arg3 error:(id *)arg4;
@property (nonatomic, readonly) NSURL *url;
@end
static NSString *const VPManagedMarker = @"_VPhone";
static void vp_load_private_frameworks(void) {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
dlopen("/System/Library/PrivateFrameworks/MobileContainerManager.framework/MobileContainerManager", RTLD_NOW);
dlopen("/System/Library/Frameworks/CoreServices.framework/CoreServices", RTLD_NOW);
});
}
static NSString *vp_trimmed_output(NSString *string) {
NSString *trimmed = [string stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
if (trimmed.length > 4000) {
return [trimmed substringToIndex:4000];
}
return trimmed;
}
static NSDictionary *vp_info_dictionary_for_app_path(NSString *appPath) {
if (appPath.length == 0) return nil;
return [NSDictionary dictionaryWithContentsOfFile:[appPath stringByAppendingPathComponent:@"Info.plist"]];
}
static NSString *vp_app_id_for_app_path(NSString *appPath) {
return vp_info_dictionary_for_app_path(appPath)[@"CFBundleIdentifier"];
}
static NSString *vp_app_main_executable_path_for_app_path(NSString *appPath) {
NSDictionary *info = vp_info_dictionary_for_app_path(appPath);
NSString *executable = info[@"CFBundleExecutable"];
if (executable.length == 0) return nil;
return [appPath stringByAppendingPathComponent:executable];
}
static NSString *vp_find_app_name_in_bundle_path(NSString *bundlePath) {
NSArray<NSString *> *bundleItems = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:bundlePath error:nil];
for (NSString *bundleItem in bundleItems) {
if ([bundleItem.pathExtension isEqualToString:@"app"]) {
return bundleItem;
}
}
return nil;
}
static NSString *vp_find_app_path_in_bundle_path(NSString *bundlePath) {
NSString *appName = vp_find_app_name_in_bundle_path(bundlePath);
if (appName.length == 0) return nil;
return [bundlePath stringByAppendingPathComponent:appName];
}
static NSURL *vp_find_app_url_in_bundle_url(NSURL *bundleURL) {
NSString *appName = vp_find_app_name_in_bundle_path(bundleURL.path);
if (appName.length == 0) return nil;
return [bundleURL URLByAppendingPathComponent:appName];
}
static BOOL vp_is_macho_file(NSString *filePath) {
FILE *file = fopen(filePath.fileSystemRepresentation, "r");
if (!file) return NO;
uint32_t magic = 0;
fread(&magic, sizeof(uint32_t), 1, file);
fclose(file);
return magic == FAT_MAGIC || magic == FAT_CIGAM || magic == MH_MAGIC_64 || magic == MH_CIGAM_64;
}
static void vp_fix_permissions_of_app_bundle(NSString *appBundlePath) {
NSURL *fileURL = nil;
NSDirectoryEnumerator *enumerator = [[NSFileManager defaultManager]
enumeratorAtURL:[NSURL fileURLWithPath:appBundlePath]
includingPropertiesForKeys:nil
options:0
errorHandler:nil];
while ((fileURL = [enumerator nextObject])) {
NSString *filePath = fileURL.path;
chown(filePath.fileSystemRepresentation, 33, 33);
chmod(filePath.fileSystemRepresentation, 0644);
}
enumerator = [[NSFileManager defaultManager]
enumeratorAtURL:[NSURL fileURLWithPath:appBundlePath]
includingPropertiesForKeys:nil
options:0
errorHandler:nil];
while ((fileURL = [enumerator nextObject])) {
NSString *filePath = fileURL.path;
BOOL isDir = NO;
[[NSFileManager defaultManager] fileExistsAtPath:filePath isDirectory:&isDir];
if (isDir || vp_is_macho_file(filePath)) {
chmod(filePath.fileSystemRepresentation, 0755);
}
}
}
static NSString *vp_read_all_from_fd(int fd) {
NSMutableData *data = [NSMutableData data];
uint8_t buf[4096];
ssize_t n = 0;
while ((n = read(fd, buf, sizeof(buf))) > 0) {
[data appendBytes:buf length:(NSUInteger)n];
}
if (data.length == 0) return @"";
NSString *string = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
return string ?: @"";
}
static int vp_run_process_with_output(NSString *path, NSArray<NSString *> *args, NSString **output) {
NSUInteger argc = args.count + 2;
char **argv = calloc(argc, sizeof(char *));
if (!argv) return ENOMEM;
argv[0] = strdup(path.fileSystemRepresentation);
for (NSUInteger i = 0; i < args.count; i++) {
argv[i + 1] = strdup(args[i].fileSystemRepresentation);
}
argv[argc - 1] = NULL;
int pipeFds[2] = {-1, -1};
if (pipe(pipeFds) != 0) {
for (NSUInteger i = 0; i < argc - 1; i++) free(argv[i]);
free(argv);
return errno;
}
posix_spawn_file_actions_t actions;
posix_spawn_file_actions_init(&actions);
posix_spawn_file_actions_adddup2(&actions, pipeFds[1], STDOUT_FILENO);
posix_spawn_file_actions_adddup2(&actions, pipeFds[1], STDERR_FILENO);
posix_spawn_file_actions_addclose(&actions, pipeFds[0]);
pid_t pid = 0;
int spawnError = posix_spawn(&pid, path.fileSystemRepresentation, &actions, NULL, argv, NULL);
posix_spawn_file_actions_destroy(&actions);
close(pipeFds[1]);
NSString *captured = vp_read_all_from_fd(pipeFds[0]);
close(pipeFds[0]);
int status = 0;
if (spawnError == 0 && waitpid(pid, &status, 0) < 0) {
spawnError = errno;
}
for (NSUInteger i = 0; i < argc - 1; i++) free(argv[i]);
free(argv);
if (output) *output = captured;
if (spawnError != 0) return spawnError;
if (WIFEXITED(status)) return WEXITSTATUS(status);
if (WIFSIGNALED(status)) return 128 + WTERMSIG(status);
return -1;
}
static NSString *vp_find_ldid_path(void) {
NSFileManager *fm = [NSFileManager defaultManager];
for (NSString *path in @[
@"/var/jb/usr/bin/ldid",
@"/iosbinpack64/usr/bin/ldid",
@"/usr/bin/ldid",
]) {
if ([fm isExecutableFileAtPath:path]) {
return path;
}
}
return nil;
}
static NSString *vp_resolve_ldid_path(void) {
return vp_find_ldid_path();
}
static NSString *vp_find_tar_path(void) {
NSFileManager *fm = [NSFileManager defaultManager];
for (NSString *path in @[@"/var/jb/usr/bin/tar", @"/usr/bin/tar"]) {
if ([fm isExecutableFileAtPath:path]) {
return path;
}
}
return nil;
}
static SecStaticCodeRef vp_get_static_code_ref(NSString *binaryPath) {
if (binaryPath.length == 0) return NULL;
CFURLRef binaryURL = CFURLCreateWithFileSystemPath(
kCFAllocatorDefault,
(__bridge CFStringRef)binaryPath,
kCFURLPOSIXPathStyle,
false
);
if (binaryURL == NULL) return NULL;
SecStaticCodeRef codeRef = NULL;
OSStatus result = SecStaticCodeCreateWithPathAndAttributes(binaryURL, kSecCSDefaultFlags, NULL, &codeRef);
CFRelease(binaryURL);
if (result != errSecSuccess) {
return NULL;
}
return codeRef;
}
static NSDictionary *vp_dump_entitlements_from_binary_at_path(NSString *binaryPath) {
SecStaticCodeRef codeRef = vp_get_static_code_ref(binaryPath);
if (codeRef == NULL) return nil;
CFDictionaryRef signingInfo = NULL;
OSStatus result = SecCodeCopySigningInformation(codeRef, kSecCSRequirementInformation, &signingInfo);
CFRelease(codeRef);
if (result != errSecSuccess || signingInfo == NULL) {
if (signingInfo) CFRelease(signingInfo);
return nil;
}
NSDictionary *entitlementsNSDict = nil;
CFDictionaryRef entitlements = CFDictionaryGetValue(signingInfo, kSecCodeInfoEntitlementsDict);
if (entitlements && CFGetTypeID(entitlements) == CFDictionaryGetTypeID()) {
entitlementsNSDict = [(__bridge NSDictionary *)entitlements copy];
}
CFRelease(signingInfo);
return entitlementsNSDict;
}
static int vp_sign_binary(
NSString *filePath,
NSDictionary *entitlements,
NSString *certPath,
NSString *ldidPath,
NSString **errorOutput
) {
if (ldidPath.length == 0) {
if (errorOutput) *errorOutput = @"ldid not found in guest or uploaded payload";
return ENOENT;
}
NSString *entitlementsPath = nil;
NSMutableArray<NSString *> *args = [NSMutableArray array];
if (entitlements) {
NSData *entitlementsXML = [NSPropertyListSerialization
dataWithPropertyList:entitlements
format:NSPropertyListXMLFormat_v1_0
options:0
error:nil];
if (entitlementsXML) {
entitlementsPath = [[NSTemporaryDirectory() stringByAppendingPathComponent:[NSUUID UUID].UUIDString]
stringByAppendingPathExtension:@"plist"];
[entitlementsXML writeToFile:entitlementsPath atomically:NO];
[args addObject:[@"-S" stringByAppendingString:entitlementsPath]];
} else {
[args addObject:@"-S"];
}
} else {
[args addObject:@"-S"];
}
if (certPath.length > 0) {
[args addObject:@"-M"];
[args addObject:[@"-K" stringByAppendingString:certPath]];
}
[args addObject:filePath];
NSString *output = @"";
int ret = vp_run_process_with_output(ldidPath, args, &output);
if (entitlementsPath) {
[[NSFileManager defaultManager] removeItemAtPath:entitlementsPath error:nil];
}
if (errorOutput) *errorOutput = output;
return ret;
}
static int vp_sign_app(NSString *appPath, NSString *certPath, NSString *ldidPath, NSString **errorOutput) {
NSDictionary *appInfoDict = vp_info_dictionary_for_app_path(appPath);
if (!appInfoDict) {
if (errorOutput) *errorOutput = @"missing app Info.plist";
return 172;
}
NSString *mainExecutablePath = vp_app_main_executable_path_for_app_path(appPath);
if (mainExecutablePath.length == 0 || ![[NSFileManager defaultManager] fileExistsAtPath:mainExecutablePath]) {
if (errorOutput) *errorOutput = @"missing main executable";
return 174;
}
NSURL *fileURL = nil;
NSDirectoryEnumerator *enumerator = [[NSFileManager defaultManager]
enumeratorAtURL:[NSURL fileURLWithPath:appPath]
includingPropertiesForKeys:nil
options:0
errorHandler:nil];
while ((fileURL = [enumerator nextObject])) {
NSString *filePath = fileURL.path;
if (![filePath.lastPathComponent isEqualToString:@"Info.plist"]) {
continue;
}
NSDictionary *infoDict = [NSDictionary dictionaryWithContentsOfFile:filePath];
NSString *bundleId = infoDict[@"CFBundleIdentifier"];
NSString *bundleExecutable = infoDict[@"CFBundleExecutable"];
if (bundleId.length == 0 || bundleExecutable.length == 0) {
continue;
}
NSString *bundleMainExecutablePath = [[filePath stringByDeletingLastPathComponent]
stringByAppendingPathComponent:bundleExecutable];
if (![[NSFileManager defaultManager] fileExistsAtPath:bundleMainExecutablePath]) {
continue;
}
NSString *packageType = infoDict[@"CFBundlePackageType"];
if ([packageType isEqualToString:@"FMWK"]) {
continue;
}
NSMutableDictionary *entitlementsToUse = [vp_dump_entitlements_from_binary_at_path(bundleMainExecutablePath) mutableCopy];
if (!entitlementsToUse && [bundleMainExecutablePath isEqualToString:mainExecutablePath]) {
entitlementsToUse = [@{
@"application-identifier": @"TROLLTROLL.*",
@"com.apple.developer.team-identifier": @"TROLLTROLL",
@"get-task-allow": @YES,
@"keychain-access-groups": @[@"TROLLTROLL.*", @"com.apple.token"],
} mutableCopy];
}
if (!entitlementsToUse) {
entitlementsToUse = [NSMutableDictionary dictionary];
}
NSObject *containerRequired = entitlementsToUse[@"com.apple.private.security.container-required"];
BOOL shouldWriteContainerRequired = YES;
if ([containerRequired isKindOfClass:[NSString class]]) {
shouldWriteContainerRequired = NO;
} else if ([containerRequired isKindOfClass:[NSNumber class]]) {
shouldWriteContainerRequired = [(NSNumber *)containerRequired boolValue];
}
BOOL noContainer = [entitlementsToUse[@"com.apple.private.security.no-container"] respondsToSelector:@selector(boolValue)]
? [entitlementsToUse[@"com.apple.private.security.no-container"] boolValue]
: NO;
BOOL noSandbox = [entitlementsToUse[@"com.apple.private.security.no-sandbox"] respondsToSelector:@selector(boolValue)]
? [entitlementsToUse[@"com.apple.private.security.no-sandbox"] boolValue]
: NO;
if (shouldWriteContainerRequired && !noContainer && !noSandbox) {
entitlementsToUse[@"com.apple.private.security.container-required"] = bundleId;
}
entitlementsToUse[@"jb.pmap_cs_custom_trust"] = @"PMAP_CS_APP_STORE";
NSString *signOutput = @"";
int ret = vp_sign_binary(bundleMainExecutablePath, entitlementsToUse, certPath, ldidPath, &signOutput);
if (ret != 0) {
if (errorOutput) *errorOutput = signOutput;
return 173;
}
}
NSString *recursiveOutput = @"";
int recursiveRet = vp_sign_binary(appPath, nil, certPath, ldidPath, &recursiveOutput);
if (recursiveRet != 0) {
if (errorOutput) *errorOutput = recursiveOutput;
return 173;
}
return 0;
}
static NSDictionary *vp_construct_groups_containers_for_entitlements(NSDictionary *entitlements, BOOL systemGroups) {
if (!entitlements) return nil;
NSString *entitlementForGroups = systemGroups
? @"com.apple.security.system-groups"
: @"com.apple.security.application-groups";
Class mcmClass = NSClassFromString(systemGroups ? @"MCMSystemDataContainer" : @"MCMSharedDataContainer");
if (!mcmClass) return nil;
NSArray *groupIDs = entitlements[entitlementForGroups];
if (![groupIDs isKindOfClass:[NSArray class]]) return nil;
NSMutableDictionary *groupContainers = [NSMutableDictionary dictionary];
for (NSString *groupID in groupIDs) {
MCMContainer *container = [mcmClass containerWithIdentifier:groupID createIfNecessary:YES existed:nil error:nil];
if (container.url.path.length > 0) {
groupContainers[groupID] = container.url.path;
}
}
return groupContainers.count > 0 ? groupContainers.copy : nil;
}
static BOOL vp_construct_containerization_for_entitlements(NSDictionary *entitlements, NSString **customContainerOut) {
NSNumber *noContainer = entitlements[@"com.apple.private.security.no-container"];
if ([noContainer isKindOfClass:[NSNumber class]] && noContainer.boolValue) {
return NO;
}
NSObject *containerRequired = entitlements[@"com.apple.private.security.container-required"];
if ([containerRequired isKindOfClass:[NSNumber class]] && ![(NSNumber *)containerRequired boolValue]) {
return NO;
}
if ([containerRequired isKindOfClass:[NSString class]]) {
*customContainerOut = (NSString *)containerRequired;
}
return YES;
}
static NSString *vp_construct_team_identifier_for_entitlements(NSDictionary *entitlements) {
NSString *teamIdentifier = entitlements[@"com.apple.developer.team-identifier"];
return [teamIdentifier isKindOfClass:[NSString class]] ? teamIdentifier : nil;
}
static NSDictionary *vp_construct_environment_variables_for_container_path(NSString *containerPath, BOOL isContainerized) {
NSString *homeDir = isContainerized ? containerPath : @"/var/mobile";
NSString *tmpDir = isContainerized ? [containerPath stringByAppendingPathComponent:@"tmp"] : @"/var/tmp";
return @{
@"CFFIXED_USER_HOME": homeDir,
@"HOME": homeDir,
@"TMPDIR": tmpDir,
};
}
static NSSet<NSString *> *vp_immutable_app_bundle_identifiers(void) {
NSMutableSet<NSString *> *systemAppIdentifiers = [NSMutableSet set];
LSEnumerator *enumerator = [LSEnumerator enumeratorForApplicationProxiesWithOptions:0];
LSApplicationProxy *appProxy = nil;
while ((appProxy = [enumerator nextObject])) {
if (appProxy.installed && ![appProxy.bundleURL.path hasPrefix:@"/private/var/containers"]) {
[systemAppIdentifiers addObject:appProxy.bundleIdentifier.lowercaseString];
}
}
return systemAppIdentifiers.copy;
}
static BOOL vp_register_path(NSString *path, BOOL unregister, BOOL forceSystem) {
if (path.length == 0) return NO;
LSApplicationWorkspace *workspace = [LSApplicationWorkspace defaultWorkspace];
if (unregister && ![[NSFileManager defaultManager] fileExistsAtPath:path]) {
LSApplicationProxy *app = [LSApplicationProxy applicationProxyForIdentifier:path];
if (app.bundleURL.path.length > 0) {
path = app.bundleURL.path;
}
}
path = path.stringByResolvingSymlinksInPath.stringByStandardizingPath;
NSDictionary *appInfoPlist = [NSDictionary dictionaryWithContentsOfFile:[path stringByAppendingPathComponent:@"Info.plist"]];
NSString *appBundleID = appInfoPlist[@"CFBundleIdentifier"];
if (appBundleID.length == 0) return NO;
if ([vp_immutable_app_bundle_identifiers() containsObject:appBundleID.lowercaseString]) return NO;
if (!unregister) {
NSString *appExecutablePath = [path stringByAppendingPathComponent:appInfoPlist[@"CFBundleExecutable"]];
NSDictionary *entitlements = vp_dump_entitlements_from_binary_at_path(appExecutablePath);
NSString *appDataContainerID = appBundleID;
BOOL appContainerized = vp_construct_containerization_for_entitlements(entitlements ?: @{}, &appDataContainerID);
Class appDataContainerClass = NSClassFromString(@"MCMAppDataContainer");
MCMContainer *appDataContainer = [appDataContainerClass
containerWithIdentifier:appDataContainerID
createIfNecessary:YES
existed:nil
error:nil];
NSString *containerPath = appDataContainer.url.path;
BOOL isRemovableSystemApp = [[NSFileManager defaultManager]
fileExistsAtPath:[@"/System/Library/AppSignatures" stringByAppendingPathComponent:appBundleID]];
BOOL registerAsUser = [path hasPrefix:@"/var/containers"] && !isRemovableSystemApp && !forceSystem;
NSMutableDictionary *dictToRegister = [NSMutableDictionary dictionary];
if (entitlements) {
dictToRegister[@"Entitlements"] = entitlements;
}
dictToRegister[@"ApplicationType"] = registerAsUser ? @"User" : @"System";
dictToRegister[@"CFBundleIdentifier"] = appBundleID;
dictToRegister[@"CodeInfoIdentifier"] = appBundleID;
dictToRegister[@"CompatibilityState"] = @0;
dictToRegister[@"IsContainerized"] = @(appContainerized);
if (containerPath.length > 0) {
dictToRegister[@"Container"] = containerPath;
dictToRegister[@"EnvironmentVariables"] =
vp_construct_environment_variables_for_container_path(containerPath, appContainerized);
}
dictToRegister[@"IsDeletable"] = @YES;
dictToRegister[@"Path"] = path;
dictToRegister[@"SignerOrganization"] = @"Apple Inc.";
dictToRegister[@"SignatureVersion"] = @132352;
dictToRegister[@"SignerIdentity"] = @"Apple iPhone OS Application Signing";
dictToRegister[@"IsAdHocSigned"] = @YES;
dictToRegister[@"LSInstallType"] = @1;
dictToRegister[@"HasMIDBasedSINF"] = @0;
dictToRegister[@"MissingSINF"] = @0;
dictToRegister[@"FamilyID"] = @0;
dictToRegister[@"IsOnDemandInstallCapable"] = @0;
NSString *teamIdentifier = vp_construct_team_identifier_for_entitlements(entitlements ?: @{});
if (teamIdentifier.length > 0) {
dictToRegister[@"TeamIdentifier"] = teamIdentifier;
}
NSDictionary *appGroupContainers = vp_construct_groups_containers_for_entitlements(entitlements, NO);
NSDictionary *systemGroupContainers = vp_construct_groups_containers_for_entitlements(entitlements, YES);
NSMutableDictionary *groupContainers = [NSMutableDictionary dictionary];
[groupContainers addEntriesFromDictionary:appGroupContainers];
[groupContainers addEntriesFromDictionary:systemGroupContainers];
if (groupContainers.count > 0) {
if (appGroupContainers.count > 0) {
dictToRegister[@"HasAppGroupContainers"] = @YES;
}
if (systemGroupContainers.count > 0) {
dictToRegister[@"HasSystemGroupContainers"] = @YES;
}
dictToRegister[@"GroupContainers"] = groupContainers.copy;
}
NSString *pluginsPath = [path stringByAppendingPathComponent:@"PlugIns"];
NSArray<NSString *> *plugins = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:pluginsPath error:nil];
NSMutableDictionary *bundlePlugins = [NSMutableDictionary dictionary];
for (NSString *pluginName in plugins) {
NSString *pluginPath = [pluginsPath stringByAppendingPathComponent:pluginName];
NSDictionary *pluginInfoPlist =
[NSDictionary dictionaryWithContentsOfFile:[pluginPath stringByAppendingPathComponent:@"Info.plist"]];
NSString *pluginBundleID = pluginInfoPlist[@"CFBundleIdentifier"];
NSString *pluginExecutable = pluginInfoPlist[@"CFBundleExecutable"];
if (pluginBundleID.length == 0 || pluginExecutable.length == 0) {
continue;
}
NSString *pluginExecutablePath = [pluginPath stringByAppendingPathComponent:pluginExecutable];
NSDictionary *pluginEntitlements = vp_dump_entitlements_from_binary_at_path(pluginExecutablePath);
NSString *pluginDataContainerID = pluginBundleID;
BOOL pluginContainerized =
vp_construct_containerization_for_entitlements(pluginEntitlements ?: @{}, &pluginDataContainerID);
Class pluginContainerClass = NSClassFromString(@"MCMPluginKitPluginDataContainer");
MCMContainer *pluginContainer = [pluginContainerClass
containerWithIdentifier:pluginDataContainerID
createIfNecessary:YES
existed:nil
error:nil];
NSString *pluginContainerPath = pluginContainer.url.path;
NSMutableDictionary *pluginDict = [NSMutableDictionary dictionary];
if (pluginEntitlements) {
pluginDict[@"Entitlements"] = pluginEntitlements;
}
pluginDict[@"ApplicationType"] = @"PluginKitPlugin";
pluginDict[@"CFBundleIdentifier"] = pluginBundleID;
pluginDict[@"CodeInfoIdentifier"] = pluginBundleID;
pluginDict[@"CompatibilityState"] = @0;
pluginDict[@"IsContainerized"] = @(pluginContainerized);
if (pluginContainerPath.length > 0) {
pluginDict[@"Container"] = pluginContainerPath;
pluginDict[@"EnvironmentVariables"] =
vp_construct_environment_variables_for_container_path(pluginContainerPath, pluginContainerized);
}
pluginDict[@"Path"] = pluginPath;
pluginDict[@"PluginOwnerBundleID"] = appBundleID;
pluginDict[@"SignerOrganization"] = @"Apple Inc.";
pluginDict[@"SignatureVersion"] = @132352;
pluginDict[@"SignerIdentity"] = @"Apple iPhone OS Application Signing";
NSString *pluginTeamIdentifier = vp_construct_team_identifier_for_entitlements(pluginEntitlements ?: @{});
if (pluginTeamIdentifier.length > 0) {
pluginDict[@"TeamIdentifier"] = pluginTeamIdentifier;
}
NSDictionary *pluginAppGroupContainers =
vp_construct_groups_containers_for_entitlements(pluginEntitlements, NO);
NSDictionary *pluginSystemGroupContainers =
vp_construct_groups_containers_for_entitlements(pluginEntitlements, YES);
NSMutableDictionary *pluginGroupContainers = [NSMutableDictionary dictionary];
[pluginGroupContainers addEntriesFromDictionary:pluginAppGroupContainers];
[pluginGroupContainers addEntriesFromDictionary:pluginSystemGroupContainers];
if (pluginGroupContainers.count > 0) {
if (pluginAppGroupContainers.count > 0) {
pluginDict[@"HasAppGroupContainers"] = @YES;
}
if (pluginSystemGroupContainers.count > 0) {
pluginDict[@"HasSystemGroupContainers"] = @YES;
}
pluginDict[@"GroupContainers"] = pluginGroupContainers.copy;
}
bundlePlugins[pluginBundleID] = pluginDict;
}
dictToRegister[@"_LSBundlePlugins"] = bundlePlugins;
if (![workspace registerApplicationDictionary:dictToRegister]) {
return NO;
}
return YES;
}
NSURL *url = [NSURL fileURLWithPath:path];
return [workspace unregisterApplication:url];
}
static BOOL vp_container_has_known_marker(NSString *containerPath) {
NSFileManager *fm = [NSFileManager defaultManager];
for (NSString *marker in @[VPManagedMarker, @"_TrollStoreLite", @"_TrollStore"]) {
if ([fm fileExistsAtPath:[containerPath stringByAppendingPathComponent:marker]]) {
return YES;
}
}
return NO;
}
static BOOL vp_mark_container_as_managed(NSString *containerPath) {
NSString *markerPath = [containerPath stringByAppendingPathComponent:VPManagedMarker];
if ([[NSFileManager defaultManager] fileExistsAtPath:markerPath]) {
return YES;
}
return [@"" writeToFile:markerPath atomically:YES encoding:NSUTF8StringEncoding error:nil];
}
static int vp_install_app_from_package(
NSString *appPackagePath,
BOOL forceSystem,
NSString *certPath,
NSString *ldidPath,
NSString **detailOutput
) {
NSString *appPayloadPath = [appPackagePath stringByAppendingPathComponent:@"Payload"];
NSString *appBundleToInstallPath = vp_find_app_path_in_bundle_path(appPayloadPath);
if (appBundleToInstallPath.length == 0) {
if (detailOutput) *detailOutput = @"IPA does not contain an .app payload";
return 167;
}
NSString *appId = vp_app_id_for_app_path(appBundleToInstallPath);
if (appId.length == 0) {
if (detailOutput) *detailOutput = @"missing CFBundleIdentifier";
return 176;
}
if ([vp_immutable_app_bundle_identifiers() containsObject:appId.lowercaseString]) {
if (detailOutput) *detailOutput = @"cannot overwrite immutable system app";
return 179;
}
NSString *signOutput = @"";
int signRet = vp_sign_app(appBundleToInstallPath, certPath, ldidPath, &signOutput);
if (signRet != 0) {
if (detailOutput) *detailOutput = signOutput;
return signRet;
}
Class appContainerClass = NSClassFromString(@"MCMAppContainer");
if (!appContainerClass) {
if (detailOutput) *detailOutput = @"MCMAppContainer unavailable";
return 170;
}
MCMContainer *appContainer = [appContainerClass containerWithIdentifier:appId createIfNecessary:NO existed:nil error:nil];
if (appContainer) {
NSURL *bundleContainerURL = appContainer.url;
NSURL *appBundleURL = vp_find_app_url_in_bundle_url(bundleContainerURL);
if (appBundleURL.path.length > 0 && !vp_container_has_known_marker(bundleContainerURL.path)) {
if (detailOutput) *detailOutput = @"a non-managed app with the same bundle identifier is already installed";
return 171;
}
if (appBundleURL.path.length > 0) {
[[NSFileManager defaultManager] removeItemAtURL:appBundleURL error:nil];
}
} else {
NSError *mcmError = nil;
appContainer = [appContainerClass containerWithIdentifier:appId createIfNecessary:YES existed:nil error:&mcmError];
if (!appContainer || mcmError) {
if (detailOutput) *detailOutput = mcmError.localizedDescription ?: @"failed to create app container";
return 170;
}
}
NSString *newAppBundlePath = [appContainer.url.path stringByAppendingPathComponent:appBundleToInstallPath.lastPathComponent];
NSError *copyError = nil;
if (![[NSFileManager defaultManager] copyItemAtPath:appBundleToInstallPath toPath:newAppBundlePath error:&copyError]) {
if (detailOutput) *detailOutput = copyError.localizedDescription ?: @"failed to copy app bundle";
return 178;
}
if (!vp_mark_container_as_managed(appContainer.url.path)) {
if (detailOutput) *detailOutput = @"installed app but failed to write management marker";
return 177;
}
NSURL *updatedAppURL = vp_find_app_url_in_bundle_url(appContainer.url);
if (updatedAppURL.path.length == 0) {
if (detailOutput) *detailOutput = @"installed app but failed to resolve final app path";
return 178;
}
vp_fix_permissions_of_app_bundle(updatedAppURL.path);
if (!vp_register_path(updatedAppURL.path, NO, forceSystem)) {
if (detailOutput) *detailOutput = @"install copied files but LaunchServices registration failed";
return 181;
}
if (detailOutput) {
*detailOutput = [NSString stringWithFormat:@"%@ (%@)", updatedAppURL.lastPathComponent, appId];
}
return 0;
}
static int vp_extract_package_to_directory(
NSString *fileToExtract,
NSString *packageFormat,
NSString *extractionPath,
NSString **detailOutput
) {
NSString *extractorPath = vp_find_tar_path();
if (extractorPath.length == 0) {
if (detailOutput) *detailOutput = @"no tar extractor found in guest";
return 168;
}
if (packageFormat.length > 0 && ![packageFormat isEqualToString:@"tar"]) {
if (detailOutput) *detailOutput = [NSString stringWithFormat:@"unsupported package format: %@", packageFormat];
return 168;
}
NSArray<NSString *> *args = @[ @"-xf", fileToExtract, @"-C", extractionPath ];
NSString *output = @"";
int ret = vp_run_process_with_output(extractorPath, args, &output);
if (ret != 0) {
if (detailOutput) *detailOutput = output.length > 0 ? output : @"extractor returned a non-zero exit status";
return 168;
}
return 0;
}
BOOL vp_custom_installer_available(void) {
vp_load_private_frameworks();
return vp_find_tar_path().length > 0
&& NSClassFromString(@"MCMAppContainer") != Nil
&& NSClassFromString(@"LSApplicationWorkspace") != Nil;
}
NSDictionary *vp_handle_custom_install(NSDictionary *msg) {
vp_load_private_frameworks();
id reqId = msg[@"id"];
NSString *ipaPath = msg[@"path"];
NSString *registration = msg[@"registration"];
NSString *packageFormat = msg[@"package_format"];
NSString *certPath = msg[@"cert_path"];
NSString *ldidPath = vp_resolve_ldid_path();
BOOL forceSystem = [registration isEqualToString:@"System"];
if (ipaPath.length == 0) {
NSMutableDictionary *response = vp_make_response(@"err", reqId);
response[@"msg"] = @"missing ipa path";
return response;
}
if (![[NSFileManager defaultManager] fileExistsAtPath:ipaPath]) {
NSMutableDictionary *response = vp_make_response(@"err", reqId);
response[@"msg"] = [NSString stringWithFormat:@"IPA not found: %@", ipaPath];
return response;
}
if (!vp_custom_installer_available()) {
NSMutableDictionary *response = vp_make_response(@"err", reqId);
NSMutableArray<NSString *> *missing = [NSMutableArray array];
if (vp_find_tar_path().length == 0) [missing addObject:@"extractor(tar)"];
if (NSClassFromString(@"MCMAppContainer") == Nil) [missing addObject:@"MCMAppContainer"];
if (NSClassFromString(@"LSApplicationWorkspace") == Nil) [missing addObject:@"LSApplicationWorkspace"];
NSString *detail = missing.count > 0 ? [missing componentsJoinedByString:@", "] : @"unknown";
response[@"msg"] = [NSString stringWithFormat:@"Built-in IPA installer prerequisites are missing: %@", detail];
return response;
}
if (ldidPath.length == 0) {
NSMutableDictionary *response = vp_make_response(@"err", reqId);
response[@"msg"] = @"Built-in IPA installer could not find a guest-side iOS ldid.";
return response;
}
if (certPath.length > 0 && ![[NSFileManager defaultManager] fileExistsAtPath:certPath]) {
certPath = nil;
}
NSString *tmpPackagePath = [NSTemporaryDirectory() stringByAppendingPathComponent:[NSUUID UUID].UUIDString];
if (![[NSFileManager defaultManager] createDirectoryAtPath:tmpPackagePath withIntermediateDirectories:NO attributes:nil error:nil]) {
NSMutableDictionary *response = vp_make_response(@"err", reqId);
response[@"msg"] = @"failed to create temporary extraction directory";
return response;
}
NSString *detail = @"";
int extractRet = vp_extract_package_to_directory(ipaPath, packageFormat, tmpPackagePath, &detail);
int installRet = 0;
if (extractRet == 0) {
installRet = vp_install_app_from_package(tmpPackagePath, forceSystem, certPath, ldidPath, &detail);
}
[[NSFileManager defaultManager] removeItemAtPath:tmpPackagePath error:nil];
[[NSFileManager defaultManager] removeItemAtPath:ipaPath error:nil];
if (certPath.length > 0) {
[[NSFileManager defaultManager] removeItemAtPath:certPath error:nil];
}
if (extractRet != 0 || installRet != 0) {
NSMutableDictionary *response = vp_make_response(@"err", reqId);
int retCode = extractRet != 0 ? extractRet : installRet;
NSString *trimmed = vp_trimmed_output(detail ?: @"");
response[@"msg"] = trimmed.length > 0
? [NSString stringWithFormat:@"built-in installer failed (%d)\n%@", retCode, trimmed]
: [NSString stringWithFormat:@"built-in installer failed (%d)", retCode];
return response;
}
NSMutableDictionary *response = vp_make_response(@"ok", reqId);
response[@"msg"] = forceSystem
? [NSString stringWithFormat:@"Installed via built-in installer as System: %@", detail]
: [NSString stringWithFormat:@"Installed via built-in installer as User: %@", detail];
return response;
}
+198 -1
View File
@@ -25,6 +25,8 @@ class VPhoneControl {
private(set) var isConnected = false
private(set) var guestName = ""
private(set) var guestCaps: [String] = []
static let ipaInstallUnavailableMessage =
"Guest is not jailbroken or no IPA installer is available. Skipping IPA install."
/// Path to the signed vphoned binary. When set, enables auto-update.
var guestBinaryURL: URL?
@@ -92,6 +94,104 @@ class VPhoneControl {
}
}
var canInstallIPA: Bool {
isConnected
}
private static func bundleIdentifier(fromIPA url: URL) throws -> String {
let process = Process()
process.executableURL = URL(fileURLWithPath: "/usr/bin/python3")
process.arguments = [
"-c",
"""
import plistlib, sys, zipfile
ipa_path = sys.argv[1]
with zipfile.ZipFile(ipa_path, "r") as zf:
plist_name = next((name for name in zf.namelist() if name.startswith("Payload/") and name.endswith(".app/Info.plist")), None)
if not plist_name:
raise SystemExit("missing app Info.plist in IPA")
info = plistlib.loads(zf.read(plist_name))
bundle_id = info.get("CFBundleIdentifier")
if not bundle_id:
raise SystemExit("missing CFBundleIdentifier in IPA")
print(bundle_id)
""",
url.path,
]
let stdout = Pipe()
let stderr = Pipe()
process.standardOutput = stdout
process.standardError = stderr
try process.run()
process.waitUntilExit()
let output = String(data: stdout.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)?
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
if process.terminationStatus == 0, !output.isEmpty {
return output
}
let errorOutput = String(data: stderr.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)?
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
let detail = errorOutput.isEmpty ? "failed to parse CFBundleIdentifier from IPA" : errorOutput
throw ControlError.protocolError(detail)
}
private static func signCertURL() -> URL? {
let fm = FileManager.default
let candidates = [
Bundle.main.resourceURL?.appendingPathComponent("signcert.p12"),
Bundle.main.bundleURL.appendingPathComponent("Contents/Resources/signcert.p12"),
URL(fileURLWithPath: fm.currentDirectoryPath).appendingPathComponent("scripts/vphoned/signcert.p12"),
URL(fileURLWithPath: fm.currentDirectoryPath).appendingPathComponent("../scripts/vphoned/signcert.p12"),
]
for candidate in candidates.compactMap({ $0 }) {
if fm.fileExists(atPath: candidate.path) {
return candidate
}
}
return nil
}
private static func runHostProcess(executableURL: URL, arguments: [String]) throws -> String {
let process = Process()
process.executableURL = executableURL
process.arguments = arguments
let stdout = Pipe()
let stderr = Pipe()
process.standardOutput = stdout
process.standardError = stderr
try process.run()
process.waitUntilExit()
let output = String(data: stdout.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) ?? ""
let errorOutput = String(data: stderr.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) ?? ""
if process.terminationStatus == 0 {
return output
}
let detail = errorOutput.trimmingCharacters(in: .whitespacesAndNewlines)
throw ControlError.protocolError(detail.isEmpty ? "host tool failed: \(executableURL.lastPathComponent)" : detail)
}
private static func buildInstallArchive(fromIPA ipaURL: URL) throws -> URL {
let fm = FileManager.default
let tempRoot = fm.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true)
let extractDir = tempRoot.appendingPathComponent("extract", isDirectory: true)
try fm.createDirectory(at: extractDir, withIntermediateDirectories: true)
let dittoURL = URL(fileURLWithPath: "/usr/bin/ditto")
_ = try runHostProcess(executableURL: dittoURL, arguments: ["-x", "-k", ipaURL.path, extractDir.path])
let tarURL = tempRoot.appendingPathComponent("package.tar")
let tarTool = URL(fileURLWithPath: "/usr/bin/tar")
_ = try runHostProcess(executableURL: tarTool, arguments: ["-cf", tarURL.path, "-C", extractDir.path, "."])
return tarURL
}
// MARK: - Guest Binary Hash
private func loadGuestBinary() {
@@ -385,6 +485,103 @@ class VPhoneControl {
_ = try await sendRequest(["t": "file_rename", "from": from, "to": to])
}
func installIPA(localURL: URL) async throws -> String {
do {
return try await installIPAWithBuiltInInstaller(localURL: localURL)
} catch let ControlError.guestError(message) where message == "unknown type: ipa_install" {
throw ControlError.guestError(
"Guest vphoned does not support ipa_install yet. Reconnect or reboot the guest so the updated daemon can take over."
)
}
}
private func installIPAWithBuiltInInstaller(localURL: URL) async throws -> String {
let archiveURL = try Self.buildInstallArchive(fromIPA: localURL)
let data: Data
do {
data = try Data(contentsOf: archiveURL)
} catch {
throw ControlError.protocolError("failed to read install archive: \(error)")
}
defer {
try? FileManager.default.removeItem(at: archiveURL.deletingLastPathComponent())
}
let remoteDir = "/var/mobile/Documents/vphone-installs"
let remoteName = "\(UUID().uuidString)-\(localURL.deletingPathExtension().lastPathComponent).tar"
let remotePath = "\(remoteDir)/\(remoteName)"
var cleanupPaths = [remotePath]
defer {
Task {
for cleanupPath in cleanupPaths {
try? await deleteFile(path: cleanupPath)
}
}
}
try await createDirectory(path: remoteDir)
try await uploadFile(path: remotePath, data: data)
var request: [String: Any] = [
"t": "ipa_install",
"path": remotePath,
"registration": "User",
"package_format": "tar",
]
if let signCertURL = Self.signCertURL() {
let signCertData = try Data(contentsOf: signCertURL)
let certRemotePath = "\(remoteDir)/\(UUID().uuidString)-signcert.p12"
cleanupPaths.append(certRemotePath)
try await uploadFile(path: certRemotePath, data: signCertData)
request["cert_path"] = certRemotePath
}
let (resp, _) = try await sendRequest(request)
if let detail = resp["msg"] as? String, !detail.isEmpty {
return detail
}
return "Installed \(localURL.lastPathComponent) through the built-in IPA installer."
}
func installIPAWithTrollStoreLite(localURL: URL) async throws -> String {
let data: Data
do {
data = try Data(contentsOf: localURL)
} catch {
throw ControlError.protocolError("failed to read IPA: \(error)")
}
let bundleIdentifier = try Self.bundleIdentifier(fromIPA: localURL)
let remoteDir = "/var/mobile/Documents/vphone-installs"
let remoteName = "\(UUID().uuidString)-\(localURL.lastPathComponent)"
let remotePath = "\(remoteDir)/\(remoteName)"
defer {
Task {
try? await deleteFile(path: remotePath)
}
}
try await createDirectory(path: remoteDir)
try await uploadFile(path: remotePath, data: data)
do {
let (resp, _) = try await sendRequest([
"t": "tslite_install",
"path": remotePath,
"bundle_id": bundleIdentifier,
"registration": "User",
])
if let detail = resp["msg"] as? String, !detail.isEmpty {
return detail
}
return "Installed \(localURL.lastPathComponent) through TrollStore Lite."
} catch {
throw error
}
}
// MARK: - Location
func sendLocation(
@@ -517,7 +714,7 @@ class VPhoneControl {
private static func timeoutForRequest(type: String) -> TimeInterval {
switch type {
case "file_get", "file_put":
case "file_get", "file_put", "tslite_install", "ipa_install":
transferRequestTimeout
case "devmode", "file_list", "file_delete", "file_rename", "file_mkdir":
slowRequestTimeout
@@ -45,6 +45,7 @@ class VPhoneMenuController {
mainMenu.addItem(buildKeysMenu())
mainMenu.addItem(buildTypeMenu())
mainMenu.addItem(buildConnectMenu())
mainMenu.addItem(buildInstallMenu())
mainMenu.addItem(buildLocationMenu())
mainMenu.addItem(buildRecordMenu())
mainMenu.addItem(buildBatteryMenu())
@@ -0,0 +1,53 @@
import AppKit
import Foundation
import UniformTypeIdentifiers
// MARK: - Install Menu
extension VPhoneMenuController {
func buildInstallMenu() -> NSMenuItem {
let item = NSMenuItem()
let menu = NSMenu(title: "Install")
menu.addItem(makeItem("Install IPA...", action: #selector(installIPAFromDisk)))
item.submenu = menu
return item
}
@objc func installIPAFromDisk() {
guard control.isConnected else {
showAlert(title: "Install IPA", message: "Guest is not connected.", style: .warning)
return
}
guard control.canInstallIPA else {
showAlert(
title: "Install IPA",
message: VPhoneControl.ipaInstallUnavailableMessage,
style: .warning
)
return
}
let panel = NSOpenPanel()
panel.canChooseFiles = true
panel.canChooseDirectories = false
panel.allowsMultipleSelection = false
panel.allowedContentTypes = [
UTType(filenameExtension: "ipa") ?? .data,
]
panel.prompt = "Install"
panel.message = "Choose an IPA to install in the guest."
let response = panel.runModal()
guard response == .OK, let url = panel.url else { return }
Task {
do {
let result = try await control.installIPA(localURL: url)
print("[install] \(result)")
} catch {
showAlert(title: "Install IPA", message: "\(error)", style: .warning)
}
}
}
}
@@ -5,8 +5,10 @@ import Virtualization
class VPhoneVirtualMachineView: VZVirtualMachineView {
var keyHelper: VPhoneKeyHelper?
weak var control: VPhoneControl?
private var currentTouchSwipeAim: Int = 0
private var isDragHighlightVisible = false
// MARK: - Private API Accessors
@@ -42,6 +44,7 @@ class VPhoneVirtualMachineView: VZVirtualMachineView {
super.viewDidMoveToWindow()
// Ensure keyboard events route to VM view right after window attach.
window?.makeFirstResponder(self)
registerForDraggedTypes([.fileURL])
}
override func mouseDown(with event: NSEvent) {
@@ -82,6 +85,88 @@ class VPhoneVirtualMachineView: VZVirtualMachineView {
return super.performKeyEquivalent(with: event)
}
// MARK: - Drag and Drop Install
override func draggingEntered(_ sender: any NSDraggingInfo) -> NSDragOperation {
guard droppedIPAURL(from: sender) != nil else { return [] }
updateDragHighlight(true)
return .copy
}
override func draggingExited(_ sender: (any NSDraggingInfo)?) {
_ = sender
updateDragHighlight(false)
}
override func prepareForDragOperation(_ sender: any NSDraggingInfo) -> Bool {
droppedIPAURL(from: sender) != nil
}
override func performDragOperation(_ sender: any NSDraggingInfo) -> Bool {
updateDragHighlight(false)
guard let url = droppedIPAURL(from: sender) else { return false }
Task { @MainActor in
guard let control else {
showAlert(title: "Install IPA", message: "Guest is not connected.", style: .warning)
return
}
guard control.isConnected else {
showAlert(title: "Install IPA", message: "Guest is not connected.", style: .warning)
return
}
guard control.canInstallIPA else {
showAlert(
title: "Install IPA",
message: VPhoneControl.ipaInstallUnavailableMessage,
style: .warning
)
return
}
do {
let result = try await control.installIPA(localURL: url)
print("[install] \(result)")
} catch {
showAlert(title: "Install IPA", message: "\(error)", style: .warning)
}
}
return true
}
private func droppedIPAURL(from sender: any NSDraggingInfo) -> URL? {
let options: [NSPasteboard.ReadingOptionKey: Any] = [
.urlReadingFileURLsOnly: true,
]
guard let urls = sender.draggingPasteboard.readObjects(forClasses: [NSURL.self], options: options) as? [URL] else {
return nil
}
return urls.first {
let ext = $0.pathExtension.lowercased()
return ext == "ipa" || ext == "tipa"
}
}
private func updateDragHighlight(_ visible: Bool) {
guard isDragHighlightVisible != visible else { return }
isDragHighlightVisible = visible
wantsLayer = true
layer?.borderWidth = visible ? 4 : 0
layer?.borderColor = visible ? NSColor.systemGreen.cgColor : NSColor.clear.cgColor
}
private func showAlert(title: String, message: String, style: NSAlert.Style) {
let alert = NSAlert()
alert.messageText = title
alert.informativeText = message
alert.alertStyle = style
if let window {
alert.beginSheetModal(for: window)
} else {
alert.runModal()
}
}
// MARK: - Legacy Touch Injection (macOS 15)
@discardableResult
@@ -20,6 +20,7 @@ class VPhoneWindowController: NSObject, NSToolbarDelegate {
view.virtualMachine = vm
view.capturesSystemKeys = true
view.keyHelper = keyHelper
view.control = control
let vmView: NSView = view
let scale = CGFloat(screenScale)