mirror of
https://github.com/Lakr233/vphone-cli.git
synced 2026-10-02 08:04:32 +08:00
Use IcliKit for guest app installation
This commit is contained in:
@@ -24,6 +24,10 @@ The guest links IcliKit directly. App registration refresh is available through
|
||||
JPEG with `mime_type`, `width`, and `height`; the current VM produces 1290×2796.
|
||||
The host's Save/Copy Screenshot menu decodes this guest image. It omits the
|
||||
notch and cutout drawn by the host VM window.
|
||||
`apps.install` accepts IPA and TIPA archives. IcliKit 0.6.6 validates and
|
||||
extracts the archive, then calls vphone's signer on the temporary app bundle
|
||||
before IcliKit copies it into a container, registers it, and owns rollback.
|
||||
`apps.uninstall` delegates removal to IcliKit and requires `force=true`.
|
||||
|
||||
## HTTP and WebSocket contract
|
||||
|
||||
@@ -35,7 +39,7 @@ directory then renames it after all chunks have been written. JSON bodies
|
||||
have a 1 MiB limit. Binary transfers stream without loading the entire file
|
||||
into memory.
|
||||
|
||||
`apps.launch` returns a PID and `frontmost_verified`. IcliKit 0.6.5 checks
|
||||
`apps.launch` returns a PID and `frontmost_verified`. IcliKit 0.6.6 checks
|
||||
RunningBoard's live focal assertion and accepts it only when one real app owns
|
||||
it. iOS 26.6.2 uses `SuspendableRole-UIFocal`; older systems may use
|
||||
`Workspace-ForegroundFocal`. The Home screen's widget renderer can also hold
|
||||
@@ -62,7 +66,7 @@ correlate them by `id`. The socket also sends
|
||||
receive pong frames. JSON WebSocket frames are limited to 1 MiB after
|
||||
fragment reassembly.
|
||||
|
||||
SwiftNIO handles parsing, upgrade, masking, and backpressure. IcliKit 0.6.5
|
||||
SwiftNIO handles parsing, upgrade, masking, and backpressure. IcliKit 0.6.6
|
||||
owns general device operations. Each HTTP or WebSocket request runs independently
|
||||
on a concurrent worker queue, so a stalled system service does not block HID,
|
||||
file browsing, or unrelated requests. The host serializes the input events it
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"originHash" : "f64f0141522e4fc1ff1b3bb6ad935f5a786d14cb195f34f75733bb0b4c35ac72",
|
||||
"originHash" : "01b56dbe07461c056f9f787f47fe980bea82d0877db7a293439df58ce597d835",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "applemobiledevicelibrary",
|
||||
@@ -24,8 +24,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/owngoal-dev/icli.git",
|
||||
"state" : {
|
||||
"revision" : "bbb4d238bb06285dacb5385e372b5e990398d688",
|
||||
"version" : "0.6.5"
|
||||
"revision" : "0bc454f61f54d99439e0f8a7f9e1c43f0fb49dc2",
|
||||
"version" : "0.6.6"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -20,8 +20,8 @@ enum GuestAPIError: Error, CustomStringConvertible {
|
||||
}
|
||||
|
||||
/// The API boundary is deliberately small: named operations and JSON values.
|
||||
/// IcliKit owns general device work, including Keychain metadata. Only
|
||||
/// vphone-specific installation crosses into the native daemon code.
|
||||
/// IcliKit owns general device work, including app installation and Keychain
|
||||
/// metadata. vphone signs app code before IcliKit installs it.
|
||||
enum GuestAPI {
|
||||
// Each request executes independently. A synchronous system service such
|
||||
// as powerd may wait during boot; it must not hold up HID or file requests.
|
||||
@@ -149,6 +149,8 @@ enum GuestAPI {
|
||||
]
|
||||
case "apps.terminate":
|
||||
return try killApp(string(params, "bundle_id"), force: true)
|
||||
case "apps.uninstall":
|
||||
return try uninstallApp(string(params, "bundle_id"), force: params["force"] as? Bool == true)
|
||||
case "apps.foreground":
|
||||
let front = frontmostApp()
|
||||
let id = front["bundle_id"] as? String ?? ""
|
||||
@@ -167,12 +169,19 @@ enum GuestAPI {
|
||||
case "apps.open_url":
|
||||
return try openAppURL(string(params, "url"), bundleID: params["bundle_id"] as? String)
|
||||
case "apps.install":
|
||||
return try native([
|
||||
"t": "ipa_install",
|
||||
"path": string(params, "path"),
|
||||
"registration": params["registration"] as? String ?? "User",
|
||||
"cert_path": params["cert_path"] as? String ?? "",
|
||||
])
|
||||
let path = try string(params, "path")
|
||||
let certificate = params["cert_path"] as? String ?? ""
|
||||
let registration: AppRegistrationType = params["registration"] as? String == "System" ? .system : .user
|
||||
defer {
|
||||
try? FileManager.default.removeItem(atPath: path)
|
||||
if !certificate.isEmpty { try? FileManager.default.removeItem(atPath: certificate) }
|
||||
}
|
||||
var result = try installIPAInContainer(path, registration: registration) { app in
|
||||
try signAppForInstall(app, certificate: certificate)
|
||||
}
|
||||
let id = result["bundle_id"] as? String ?? "app"
|
||||
result["msg"] = "Installed \(id) as a \(registration.rawValue) app."
|
||||
return result
|
||||
case "input.touch":
|
||||
guard let phase = (params["phase"] as? String).flatMap(TouchPhase.init(rawValue:)) else {
|
||||
throw GuestAPIError.invalidRequest("phase must be down, move or up")
|
||||
@@ -285,16 +294,16 @@ enum GuestAPI {
|
||||
}
|
||||
}
|
||||
|
||||
private static func native(_ message: [String: Any]) throws -> [String: Any] {
|
||||
let result = vp_native_api_command(message) as? [String: Any] ?? [:]
|
||||
if result["t"] as? String == "err" {
|
||||
throw GuestAPIError.operationFailed(result["msg"] as? String ?? "Guest operation failed")
|
||||
private static func signAppForInstall(_ app: String, certificate: String) throws {
|
||||
let usableCertificate = FileManager.default.fileExists(atPath: certificate) ? certificate : ""
|
||||
let error = app.withCString { appPath in
|
||||
if usableCertificate.isEmpty { return vp_sign_app_for_install(appPath, nil) }
|
||||
return usableCertificate.withCString { vp_sign_app_for_install(appPath, $0) }
|
||||
}
|
||||
if let error {
|
||||
defer { free(error) }
|
||||
throw GuestAPIError.operationFailed(String(cString: error))
|
||||
}
|
||||
var payload = result
|
||||
payload.removeValue(forKey: "v")
|
||||
payload.removeValue(forKey: "t")
|
||||
payload.removeValue(forKey: "id")
|
||||
return payload
|
||||
}
|
||||
|
||||
private static func fileList(_ path: String) throws -> [String: Any] {
|
||||
|
||||
@@ -2,7 +2,7 @@ import Darwin
|
||||
import Foundation
|
||||
import VPhoneSign
|
||||
|
||||
/// The native IPA installer prepares each executable's guest entitlements.
|
||||
/// vphone prepares each executable's guest entitlements before IcliKit installs it.
|
||||
/// Sign them with the same bundled signer used by the host CLI, without a
|
||||
/// package-manager supplied ldid executable inside the VM.
|
||||
@_cdecl("vp_guest_sign_binary")
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#pragma once
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
/// vphone-specific operations that IcliKit does not own.
|
||||
NSDictionary *vp_native_api_command(NSDictionary *message);
|
||||
/// Sign all executable code in an extracted app. Returns a malloc-owned error or NULL.
|
||||
char *vp_sign_app_for_install(const char *appPath, const char *certificatePath);
|
||||
void vp_native_bootstrap_cached_binary(void);
|
||||
void vp_native_confirm_cached_binary(void);
|
||||
void vp_vcam_start(void);
|
||||
|
||||
-82
@@ -1,82 +0,0 @@
|
||||
/*-
|
||||
* Copyright (c) 2003-2010 Tim Kientzle
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions
|
||||
* are met:
|
||||
* 1. Redistributions of source code must retain the above copyright
|
||||
* notice, this list of conditions and the following disclaimer.
|
||||
* 2. Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR
|
||||
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
||||
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
||||
* IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
|
||||
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
||||
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Minimal vendored header — only the API surface used by unarchive.m.
|
||||
* Linked against iOS system libarchive (-larchive).
|
||||
*/
|
||||
|
||||
#ifndef ARCHIVE_H_INCLUDED
|
||||
#define ARCHIVE_H_INCLUDED
|
||||
|
||||
#include <sys/types.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <unistd.h>
|
||||
|
||||
typedef int64_t la_int64_t;
|
||||
typedef ssize_t la_ssize_t;
|
||||
|
||||
struct archive;
|
||||
struct archive_entry;
|
||||
|
||||
/* Status codes */
|
||||
#define ARCHIVE_EOF 1
|
||||
#define ARCHIVE_OK 0
|
||||
#define ARCHIVE_WARN (-20)
|
||||
|
||||
/* Extract flags */
|
||||
#define ARCHIVE_EXTRACT_TIME 0x0004
|
||||
#define ARCHIVE_EXTRACT_PERM 0x0002
|
||||
#define ARCHIVE_EXTRACT_ACL 0x0020
|
||||
#define ARCHIVE_EXTRACT_FFLAGS 0x0040
|
||||
#define ARCHIVE_EXTRACT_SECURE_SYMLINKS 0x0100
|
||||
#define ARCHIVE_EXTRACT_SECURE_NODOTDOT 0x0200
|
||||
#define ARCHIVE_EXTRACT_SECURE_NOABSOLUTEPATHS 0x10000
|
||||
|
||||
/* Error string */
|
||||
const char *archive_error_string(struct archive *);
|
||||
|
||||
/* Read API */
|
||||
struct archive *archive_read_new(void);
|
||||
int archive_read_support_format_all(struct archive *);
|
||||
int archive_read_support_filter_all(struct archive *);
|
||||
int archive_read_open_filename(struct archive *, const char *filename, size_t block_size);
|
||||
int archive_read_next_header(struct archive *, struct archive_entry **);
|
||||
int archive_read_data_block(struct archive *, const void **buf, size_t *size, la_int64_t *offset);
|
||||
int archive_read_close(struct archive *);
|
||||
int archive_read_free(struct archive *);
|
||||
|
||||
/* Write-to-disk API */
|
||||
struct archive *archive_write_disk_new(void);
|
||||
int archive_write_disk_set_options(struct archive *, int flags);
|
||||
int archive_write_disk_set_standard_lookup(struct archive *);
|
||||
int archive_write_header(struct archive *, struct archive_entry *);
|
||||
int archive_write_data_block(struct archive *, const void *buf, size_t size, la_int64_t offset);
|
||||
int archive_write_finish_entry(struct archive *);
|
||||
int archive_write_close(struct archive *);
|
||||
int archive_write_free(struct archive *);
|
||||
|
||||
#endif /* !ARCHIVE_H_INCLUDED */
|
||||
@@ -1,47 +0,0 @@
|
||||
/*-
|
||||
* Copyright (c) 2003-2008 Tim Kientzle
|
||||
* Copyright (c) 2016 Martin Matuska
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions
|
||||
* are met:
|
||||
* 1. Redistributions of source code must retain the above copyright
|
||||
* notice, this list of conditions and the following disclaimer.
|
||||
* 2. Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR
|
||||
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
||||
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
||||
* IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
|
||||
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
||||
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Minimal vendored header — only the API surface used by unarchive.m.
|
||||
* Linked against iOS system libarchive (-larchive).
|
||||
*/
|
||||
|
||||
#ifndef ARCHIVE_ENTRY_H_INCLUDED
|
||||
#define ARCHIVE_ENTRY_H_INCLUDED
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#ifndef la_int64_t
|
||||
typedef int64_t la_int64_t;
|
||||
#endif
|
||||
|
||||
struct archive_entry;
|
||||
|
||||
const char *archive_entry_pathname(struct archive_entry *);
|
||||
void archive_entry_set_pathname(struct archive_entry *, const char *);
|
||||
la_int64_t archive_entry_size(struct archive_entry *);
|
||||
|
||||
#endif /* !ARCHIVE_ENTRY_H_INCLUDED */
|
||||
@@ -1,3 +0,0 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
extern int vp_extract_archive(NSString *archivePath, NSString *extractionPath, NSString **errorOutput);
|
||||
@@ -1,107 +0,0 @@
|
||||
#import "unarchive.h"
|
||||
|
||||
#include <archive.h>
|
||||
#include <archive_entry.h>
|
||||
|
||||
static int copy_data(struct archive *ar, struct archive *aw) {
|
||||
const void *buff;
|
||||
size_t size;
|
||||
la_int64_t offset;
|
||||
|
||||
for (;;) {
|
||||
int r = archive_read_data_block(ar, &buff, &size, &offset);
|
||||
if (r == ARCHIVE_EOF) return ARCHIVE_OK;
|
||||
if (r < ARCHIVE_OK) return r;
|
||||
r = archive_write_data_block(aw, buff, size, offset);
|
||||
if (r < ARCHIVE_OK) {
|
||||
fprintf(stderr, "%s\n", archive_error_string(aw));
|
||||
return r;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int vp_extract_archive(NSString *archivePath, NSString *extractionPath, NSString **errorOutput) {
|
||||
int flags = ARCHIVE_EXTRACT_TIME
|
||||
| ARCHIVE_EXTRACT_PERM
|
||||
| ARCHIVE_EXTRACT_SECURE_NODOTDOT;
|
||||
|
||||
// Resolve symlinks in extractionPath (e.g. /tmp -> /private/tmp on iOS)
|
||||
// so ARCHIVE_EXTRACT_SECURE_SYMLINKS doesn't reject trusted system symlinks.
|
||||
NSString *resolvedPath = [extractionPath stringByResolvingSymlinksInPath];
|
||||
NSLog(@"vphoned: extract %@ -> %@ (resolved: %@)", archivePath, extractionPath, resolvedPath);
|
||||
|
||||
struct archive *a = archive_read_new();
|
||||
archive_read_support_format_all(a);
|
||||
archive_read_support_filter_all(a);
|
||||
|
||||
struct archive *ext = archive_write_disk_new();
|
||||
archive_write_disk_set_options(ext, flags);
|
||||
archive_write_disk_set_standard_lookup(ext);
|
||||
|
||||
int ret = 0;
|
||||
if (archive_read_open_filename(a, archivePath.fileSystemRepresentation, 10240) != ARCHIVE_OK) {
|
||||
NSString *err = [NSString stringWithFormat:@"Unable to open the app package (%s).", archive_error_string(a)];
|
||||
NSLog(@"vphoned: %@", err);
|
||||
if (errorOutput) *errorOutput = err;
|
||||
ret = 1;
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
struct archive_entry *entry;
|
||||
int r = archive_read_next_header(a, &entry);
|
||||
if (r == ARCHIVE_EOF) break;
|
||||
if (r < ARCHIVE_OK)
|
||||
NSLog(@"vphoned: archive_read_next_header: %s", archive_error_string(a));
|
||||
if (r < ARCHIVE_WARN) {
|
||||
if (errorOutput) *errorOutput = [NSString stringWithFormat:@"Unable to read the app package (%s).",
|
||||
archive_error_string(a)];
|
||||
ret = 1; goto cleanup;
|
||||
}
|
||||
|
||||
const char *entryPath = archive_entry_pathname(entry);
|
||||
if (!entryPath) { ret = 1; goto cleanup; }
|
||||
NSString *currentFile = [NSString stringWithUTF8String:entryPath];
|
||||
if (!currentFile) { ret = 1; goto cleanup; }
|
||||
NSString *fullOutputPath = [resolvedPath stringByAppendingPathComponent:currentFile];
|
||||
archive_entry_set_pathname(entry, fullOutputPath.fileSystemRepresentation);
|
||||
|
||||
r = archive_write_header(ext, entry);
|
||||
if (r < ARCHIVE_OK)
|
||||
NSLog(@"vphoned: archive_write_header(%@): %s (r=%d)", currentFile, archive_error_string(ext), r);
|
||||
if (r < ARCHIVE_WARN) {
|
||||
if (errorOutput) *errorOutput = [NSString stringWithFormat:@"Unable to extract %@ (%s).",
|
||||
currentFile,
|
||||
archive_error_string(ext)];
|
||||
ret = 1; goto cleanup;
|
||||
}
|
||||
if (archive_entry_size(entry) > 0) {
|
||||
r = copy_data(a, ext);
|
||||
if (r < ARCHIVE_OK)
|
||||
NSLog(@"vphoned: copy_data(%@): %s (r=%d)", currentFile, archive_error_string(ext), r);
|
||||
if (r < ARCHIVE_WARN) {
|
||||
if (errorOutput) *errorOutput = [NSString stringWithFormat:@"Unable to extract %@ (%s).",
|
||||
currentFile,
|
||||
archive_error_string(ext)];
|
||||
ret = 1; goto cleanup;
|
||||
}
|
||||
}
|
||||
|
||||
r = archive_write_finish_entry(ext);
|
||||
if (r < ARCHIVE_OK)
|
||||
NSLog(@"vphoned: archive_write_finish_entry(%@): %s (r=%d)", currentFile, archive_error_string(ext), r);
|
||||
if (r < ARCHIVE_WARN) {
|
||||
if (errorOutput) *errorOutput = [NSString stringWithFormat:@"Unable to extract %@ (%s).",
|
||||
currentFile,
|
||||
archive_error_string(ext)];
|
||||
ret = 1; goto cleanup;
|
||||
}
|
||||
}
|
||||
|
||||
cleanup:
|
||||
archive_read_close(a);
|
||||
archive_read_free(a);
|
||||
archive_write_close(ext);
|
||||
archive_write_free(ext);
|
||||
return ret;
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
BOOL vp_custom_installer_available(void);
|
||||
NSDictionary *vp_handle_custom_install(NSDictionary *msg);
|
||||
@@ -1,804 +0,0 @@
|
||||
#import "vphoned_install.h"
|
||||
#import "unarchive.h"
|
||||
|
||||
#import <Security/Security.h>
|
||||
#include <dlfcn.h>
|
||||
#include <errno.h>
|
||||
#include <mach-o/fat.h>
|
||||
#include <mach-o/loader.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#import "vphoned_response.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;
|
||||
|
||||
@interface LSApplicationProxy : NSObject
|
||||
+ (instancetype)applicationProxyForIdentifier:(NSString *)identifier;
|
||||
@property (nonatomic, readonly) NSString *bundleIdentifier;
|
||||
@property (nonatomic, readonly) NSURL *bundleURL;
|
||||
@property (getter=isInstalled, nonatomic, readonly) BOOL installed;
|
||||
@end
|
||||
|
||||
@interface LSApplicationWorkspace : NSObject
|
||||
+ (instancetype)defaultWorkspace;
|
||||
- (BOOL)registerApplicationDictionary:(NSDictionary *)dict;
|
||||
- (BOOL)registerContainerizedApplicationWithInfoDictionaries:(NSArray *)infos
|
||||
operationUUID:(NSUUID *)uuid
|
||||
requestContext:(id)context
|
||||
saveObserver:(id)observer
|
||||
registrationError:(NSError **)error;
|
||||
- (BOOL)unregisterApplication:(id)arg1;
|
||||
@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";
|
||||
|
||||
// Implemented in GuestSigner.swift using the shared VPhoneSign target.
|
||||
// A non-null result is a malloc-owned error message.
|
||||
extern char *vp_guest_sign_binary(const char *path, const char *entitlementsPath, const char *certificatePath);
|
||||
|
||||
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 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 **errorOutput
|
||||
) {
|
||||
NSString *entitlementsPath = nil;
|
||||
NSData *entitlementsXML = entitlements ? [NSPropertyListSerialization
|
||||
dataWithPropertyList:entitlements
|
||||
format:NSPropertyListXMLFormat_v1_0
|
||||
options:0
|
||||
error:nil] : nil;
|
||||
if (entitlementsXML) {
|
||||
entitlementsPath = [[NSTemporaryDirectory() stringByAppendingPathComponent:[NSUUID UUID].UUIDString]
|
||||
stringByAppendingPathExtension:@"plist"];
|
||||
if (![entitlementsXML writeToFile:entitlementsPath atomically:YES]) {
|
||||
if (errorOutput) *errorOutput = @"Could not prepare app entitlements.";
|
||||
return EIO;
|
||||
}
|
||||
}
|
||||
|
||||
char *error = vp_guest_sign_binary(
|
||||
filePath.fileSystemRepresentation,
|
||||
entitlementsPath.fileSystemRepresentation,
|
||||
certPath.length > 0 ? certPath.fileSystemRepresentation : NULL
|
||||
);
|
||||
if (entitlementsPath) {
|
||||
[[NSFileManager defaultManager] removeItemAtPath:entitlementsPath error:nil];
|
||||
}
|
||||
if (!error) return 0;
|
||||
if (errorOutput) *errorOutput = [NSString stringWithUTF8String:error] ?: @"Could not sign app executable.";
|
||||
free(error);
|
||||
return EINVAL;
|
||||
}
|
||||
|
||||
static int vp_sign_app(NSString *appPath, NSString *certPath, NSString **errorOutput) {
|
||||
if (!vp_info_dictionary_for_app_path(appPath)) {
|
||||
if (errorOutput) *errorOutput = @"The app package is incomplete and cannot be signed.";
|
||||
return 172;
|
||||
}
|
||||
|
||||
NSString *mainExecutablePath = vp_app_main_executable_path_for_app_path(appPath);
|
||||
if (mainExecutablePath.length == 0 || ![[NSFileManager defaultManager] fileExistsAtPath:mainExecutablePath]) {
|
||||
if (errorOutput) *errorOutput = @"The app package is missing its program and cannot be signed.";
|
||||
return 174;
|
||||
}
|
||||
|
||||
NSMutableSet<NSString *> *signedExecutables = [NSMutableSet set];
|
||||
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, &signOutput);
|
||||
if (ret != 0) {
|
||||
if (errorOutput) *errorOutput = signOutput;
|
||||
return 173;
|
||||
}
|
||||
[signedExecutables addObject:bundleMainExecutablePath];
|
||||
}
|
||||
|
||||
// Sign code without an Info.plist executable declaration, such as dylibs.
|
||||
// The declared executables above already carry their guest entitlements.
|
||||
enumerator = [[NSFileManager defaultManager]
|
||||
enumeratorAtURL:[NSURL fileURLWithPath:appPath]
|
||||
includingPropertiesForKeys:nil
|
||||
options:0
|
||||
errorHandler:nil];
|
||||
while ((fileURL = [enumerator nextObject])) {
|
||||
NSString *filePath = fileURL.path;
|
||||
if ([signedExecutables containsObject:filePath] || !vp_is_macho_file(filePath)) continue;
|
||||
NSString *signOutput = @"";
|
||||
if (vp_sign_binary(filePath, nil, certPath, &signOutput) != 0) {
|
||||
if (errorOutput) *errorOutput = signOutput;
|
||||
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 = [(id)NSClassFromString(@"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;
|
||||
}
|
||||
|
||||
/// Build the LaunchServices registration dictionary shared by an app bundle and its PlugIns.
|
||||
/// The caller adds the keys that differ: ApplicationType, Path, and the app- or plugin-only keys.
|
||||
static NSMutableDictionary *vp_registration_dictionary(
|
||||
NSString *bundleID,
|
||||
NSString *executablePath,
|
||||
Class containerClass
|
||||
) {
|
||||
NSDictionary *entitlements = vp_dump_entitlements_from_binary_at_path(executablePath);
|
||||
|
||||
NSString *dataContainerID = bundleID;
|
||||
BOOL containerized = vp_construct_containerization_for_entitlements(entitlements ?: @{}, &dataContainerID);
|
||||
|
||||
MCMContainer *dataContainer = [containerClass
|
||||
containerWithIdentifier:dataContainerID
|
||||
createIfNecessary:YES
|
||||
existed:nil
|
||||
error:nil];
|
||||
NSString *containerPath = dataContainer.url.path;
|
||||
|
||||
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
|
||||
if (entitlements) {
|
||||
dict[@"Entitlements"] = entitlements;
|
||||
}
|
||||
dict[@"CFBundleIdentifier"] = bundleID;
|
||||
dict[@"CodeInfoIdentifier"] = bundleID;
|
||||
dict[@"CompatibilityState"] = @0;
|
||||
dict[@"IsContainerized"] = @(containerized);
|
||||
if (containerPath.length > 0) {
|
||||
dict[@"Container"] = containerPath;
|
||||
dict[@"EnvironmentVariables"] =
|
||||
vp_construct_environment_variables_for_container_path(containerPath, containerized);
|
||||
}
|
||||
dict[@"SignerOrganization"] = @"Apple Inc.";
|
||||
dict[@"SignatureVersion"] = @132352;
|
||||
dict[@"SignerIdentity"] = @"Apple iPhone OS Application Signing";
|
||||
|
||||
NSString *teamIdentifier = vp_construct_team_identifier_for_entitlements(entitlements ?: @{});
|
||||
if (teamIdentifier.length > 0) {
|
||||
dict[@"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) {
|
||||
dict[@"HasAppGroupContainers"] = @YES;
|
||||
}
|
||||
if (systemGroupContainers.count > 0) {
|
||||
dict[@"HasSystemGroupContainers"] = @YES;
|
||||
}
|
||||
dict[@"GroupContainers"] = groupContainers.copy;
|
||||
}
|
||||
|
||||
return dict;
|
||||
}
|
||||
|
||||
static BOOL vp_register_path(NSString *path, BOOL unregister, BOOL forceSystem) {
|
||||
if (path.length == 0) return NO;
|
||||
|
||||
LSApplicationWorkspace *workspace = [(id)NSClassFromString(@"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"]];
|
||||
NSMutableDictionary *dictToRegister = vp_registration_dictionary(
|
||||
appBundleID,
|
||||
appExecutablePath,
|
||||
NSClassFromString(@"MCMAppDataContainer"));
|
||||
|
||||
BOOL isRemovableSystemApp = [[NSFileManager defaultManager]
|
||||
fileExistsAtPath:[@"/System/Library/AppSignatures" stringByAppendingPathComponent:appBundleID]];
|
||||
BOOL registerAsUser = [path hasPrefix:@"/var/containers"] && !isRemovableSystemApp && !forceSystem;
|
||||
|
||||
dictToRegister[@"ApplicationType"] = registerAsUser ? @"User" : @"System";
|
||||
dictToRegister[@"IsDeletable"] = @YES;
|
||||
dictToRegister[@"Path"] = path;
|
||||
dictToRegister[@"IsAdHocSigned"] = @YES;
|
||||
dictToRegister[@"LSInstallType"] = @1;
|
||||
dictToRegister[@"HasMIDBasedSINF"] = @0;
|
||||
dictToRegister[@"MissingSINF"] = @0;
|
||||
dictToRegister[@"FamilyID"] = @0;
|
||||
dictToRegister[@"IsOnDemandInstallCapable"] = @0;
|
||||
|
||||
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];
|
||||
|
||||
NSMutableDictionary *pluginDict = vp_registration_dictionary(
|
||||
pluginBundleID,
|
||||
pluginExecutablePath,
|
||||
NSClassFromString(@"MCMPluginKitPluginDataContainer"));
|
||||
pluginDict[@"ApplicationType"] = @"PluginKitPlugin";
|
||||
pluginDict[@"Path"] = pluginPath;
|
||||
pluginDict[@"PluginOwnerBundleID"] = appBundleID;
|
||||
|
||||
bundlePlugins[pluginBundleID] = pluginDict;
|
||||
}
|
||||
dictToRegister[@"_LSBundlePlugins"] = bundlePlugins;
|
||||
|
||||
if ([workspace registerApplicationDictionary:dictToRegister]) {
|
||||
return YES;
|
||||
}
|
||||
// iOS 27+: the plain registerApplicationDictionary path is gated off in lsd
|
||||
// (returns NO). Fall back to the containerized registration path, which
|
||||
// works once lsd's clientIsEntitledForEmbeddedRegistrationOperations gate
|
||||
// is patched (cfw_patch_lsd_embedded_reg). It returns NO even on success,
|
||||
// so treat a nil registrationError as success.
|
||||
SEL containerizedSel = @selector(registerContainerizedApplicationWithInfoDictionaries:operationUUID:requestContext:saveObserver:registrationError:);
|
||||
if ([workspace respondsToSelector:containerizedSel]) {
|
||||
NSError *regError = nil;
|
||||
[workspace registerContainerizedApplicationWithInfoDictionaries:@[dictToRegister]
|
||||
operationUUID:[NSUUID UUID]
|
||||
requestContext:nil
|
||||
saveObserver:nil
|
||||
registrationError:®Error];
|
||||
if (regError == nil) {
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
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 void vp_rollback_app_install(
|
||||
NSString *newPath,
|
||||
NSString *oldPath,
|
||||
NSString *backupPath,
|
||||
NSString *markerPath,
|
||||
BOOL markerExisted,
|
||||
BOOL newMoved,
|
||||
BOOL oldMoved,
|
||||
BOOL restoreRegistration,
|
||||
BOOL forceSystem
|
||||
) {
|
||||
NSFileManager *fm = [NSFileManager defaultManager];
|
||||
if (newMoved) [fm removeItemAtPath:newPath error:nil];
|
||||
if (oldMoved && [fm moveItemAtPath:backupPath toPath:oldPath error:nil] && restoreRegistration) {
|
||||
vp_register_path(oldPath, NO, forceSystem);
|
||||
}
|
||||
if (!markerExisted) [fm removeItemAtPath:markerPath error:nil];
|
||||
}
|
||||
|
||||
static int vp_install_app_from_package(
|
||||
NSString *appPackagePath,
|
||||
BOOL forceSystem,
|
||||
NSString *certPath,
|
||||
NSString **detailOutput
|
||||
) {
|
||||
NSString *appPayloadPath = [appPackagePath stringByAppendingPathComponent:@"Payload"];
|
||||
NSString *appBundleToInstallPath = vp_find_app_path_in_bundle_path(appPayloadPath);
|
||||
if (appBundleToInstallPath.length == 0) {
|
||||
if (detailOutput) *detailOutput = @"The app package does not contain an app.";
|
||||
return 167;
|
||||
}
|
||||
|
||||
NSString *appId = vp_app_id_for_app_path(appBundleToInstallPath);
|
||||
if (appId.length == 0) {
|
||||
if (detailOutput) *detailOutput = @"The app package has no bundle identifier.";
|
||||
return 176;
|
||||
}
|
||||
|
||||
if ([vp_immutable_app_bundle_identifiers() containsObject:appId.lowercaseString]) {
|
||||
if (detailOutput) *detailOutput = @"This app is part of iOS and cannot be replaced.";
|
||||
return 179;
|
||||
}
|
||||
|
||||
NSString *signOutput = @"";
|
||||
int signRet = vp_sign_app(appBundleToInstallPath, certPath, &signOutput);
|
||||
if (signRet != 0) {
|
||||
if (detailOutput) *detailOutput = signOutput;
|
||||
return signRet;
|
||||
}
|
||||
|
||||
Class appContainerClass = NSClassFromString(@"MCMAppContainer");
|
||||
if (!appContainerClass) {
|
||||
if (detailOutput) *detailOutput = @"The app container service is unavailable.";
|
||||
return 170;
|
||||
}
|
||||
|
||||
MCMContainer *appContainer = [appContainerClass
|
||||
containerWithIdentifier:appId
|
||||
createIfNecessary:NO
|
||||
existed:nil
|
||||
error:nil];
|
||||
NSString *oldAppPath = 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 = @"An app with the same bundle identifier is already installed. Remove it and try again.";
|
||||
return 171;
|
||||
}
|
||||
oldAppPath = appBundleURL.path;
|
||||
} else {
|
||||
NSError *mcmError = nil;
|
||||
appContainer = [appContainerClass
|
||||
containerWithIdentifier:appId
|
||||
createIfNecessary:YES
|
||||
existed:nil
|
||||
error:&mcmError];
|
||||
if (!appContainer || mcmError) {
|
||||
if (detailOutput) *detailOutput = mcmError.localizedDescription ?: @"Unable to prepare storage for the app.";
|
||||
return 170;
|
||||
}
|
||||
}
|
||||
|
||||
NSFileManager *fm = [NSFileManager defaultManager];
|
||||
NSString *containerPath = appContainer.url.path;
|
||||
NSString *newAppBundlePath =
|
||||
[containerPath stringByAppendingPathComponent:appBundleToInstallPath.lastPathComponent];
|
||||
NSString *stagedPath = [containerPath stringByAppendingPathComponent:
|
||||
[@".vphone-install-" stringByAppendingString:[NSUUID UUID].UUIDString]];
|
||||
NSString *backupPath = oldAppPath.length > 0 ? [containerPath stringByAppendingPathComponent:
|
||||
[@".vphone-backup-" stringByAppendingString:[NSUUID UUID].UUIDString]] : nil;
|
||||
NSString *markerPath = [containerPath stringByAppendingPathComponent:VPManagedMarker];
|
||||
BOOL markerExisted = [fm fileExistsAtPath:markerPath];
|
||||
NSError *copyError = nil;
|
||||
if (![fm copyItemAtPath:appBundleToInstallPath toPath:stagedPath error:©Error]) {
|
||||
[fm removeItemAtPath:stagedPath error:nil];
|
||||
if (detailOutput) *detailOutput = copyError.localizedDescription ?: @"Unable to copy the app onto the guest.";
|
||||
return 178;
|
||||
}
|
||||
|
||||
if (oldAppPath.length > 0 && ![fm moveItemAtPath:oldAppPath toPath:backupPath error:©Error]) {
|
||||
[fm removeItemAtPath:stagedPath error:nil];
|
||||
if (detailOutput) *detailOutput = copyError.localizedDescription ?: @"Unable to back up the existing app.";
|
||||
return 178;
|
||||
}
|
||||
BOOL oldMoved = oldAppPath.length > 0;
|
||||
if (![fm moveItemAtPath:stagedPath toPath:newAppBundlePath error:©Error]) {
|
||||
[fm removeItemAtPath:stagedPath error:nil];
|
||||
vp_rollback_app_install(newAppBundlePath, oldAppPath, backupPath, markerPath,
|
||||
markerExisted, NO, oldMoved, NO, forceSystem);
|
||||
if (detailOutput) *detailOutput = copyError.localizedDescription ?: @"Unable to place the app onto the guest.";
|
||||
return 178;
|
||||
}
|
||||
|
||||
vp_fix_permissions_of_app_bundle(newAppBundlePath);
|
||||
if (!vp_mark_container_as_managed(containerPath)) {
|
||||
vp_rollback_app_install(newAppBundlePath, oldAppPath, backupPath, markerPath,
|
||||
markerExisted, YES, oldMoved, NO, forceSystem);
|
||||
if (detailOutput) *detailOutput = @"The app was copied but could not be marked as managed.";
|
||||
return 177;
|
||||
}
|
||||
if (!vp_register_path(newAppBundlePath, NO, forceSystem)) {
|
||||
vp_rollback_app_install(newAppBundlePath, oldAppPath, backupPath, markerPath,
|
||||
markerExisted, YES, oldMoved, YES, forceSystem);
|
||||
if (detailOutput) *detailOutput = @"The app was copied but could not be registered with the system.";
|
||||
return 181;
|
||||
}
|
||||
|
||||
if (oldMoved) [fm removeItemAtPath:backupPath error:nil];
|
||||
if (detailOutput) {
|
||||
*detailOutput = [NSString stringWithFormat:@"%@ (%@)", newAppBundlePath.lastPathComponent, appId];
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int vp_extract_package_to_directory(
|
||||
NSString *fileToExtract,
|
||||
NSString *extractionPath,
|
||||
NSString **detailOutput
|
||||
) {
|
||||
NSString *archiveError = nil;
|
||||
int ret = vp_extract_archive(fileToExtract, extractionPath, &archiveError);
|
||||
if (ret != 0) {
|
||||
if (detailOutput) *detailOutput = archiveError ?: @"Unable to extract the app package.";
|
||||
return 168;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
BOOL vp_custom_installer_available(void) {
|
||||
vp_load_private_frameworks();
|
||||
return 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 *certPath = msg[@"cert_path"];
|
||||
BOOL forceSystem = [registration isEqualToString:@"System"];
|
||||
|
||||
if (ipaPath.length == 0) {
|
||||
NSMutableDictionary *response = vp_make_response(@"err", reqId);
|
||||
response[@"msg"] = @"No app package was specified.";
|
||||
return response;
|
||||
}
|
||||
if (![[NSFileManager defaultManager] fileExistsAtPath:ipaPath]) {
|
||||
NSMutableDictionary *response = vp_make_response(@"err", reqId);
|
||||
response[@"msg"] = [NSString stringWithFormat:@"App package not found at %@.", ipaPath];
|
||||
return response;
|
||||
}
|
||||
if (!vp_custom_installer_available()) {
|
||||
NSMutableDictionary *response = vp_make_response(@"err", reqId);
|
||||
NSMutableArray<NSString *> *missing = [NSMutableArray array];
|
||||
if (NSClassFromString(@"MCMAppContainer") == Nil) [missing addObject:@"MCMAppContainer"];
|
||||
if (NSClassFromString(@"LSApplicationWorkspace") == Nil) [missing addObject:@"LSApplicationWorkspace"];
|
||||
NSString *detail = missing.count > 0 ? [missing componentsJoinedByString:@", "] : @"unknown";
|
||||
NSLog(@"vphoned: custom installer unavailable: %@", detail);
|
||||
response[@"msg"] = @"This guest cannot install apps. The built-in installer is not supported here.";
|
||||
return response;
|
||||
}
|
||||
if (certPath.length > 0 && ![[NSFileManager defaultManager] fileExistsAtPath:certPath]) {
|
||||
certPath = nil;
|
||||
}
|
||||
|
||||
NSString *tmpPackagePath = [[NSTemporaryDirectory() stringByResolvingSymlinksInPath]
|
||||
stringByAppendingPathComponent:[NSUUID UUID].UUIDString];
|
||||
if (![[NSFileManager defaultManager] createDirectoryAtPath:tmpPackagePath
|
||||
withIntermediateDirectories:NO
|
||||
attributes:nil
|
||||
error:nil]) {
|
||||
NSMutableDictionary *response = vp_make_response(@"err", reqId);
|
||||
response[@"msg"] = @"Unable to prepare the guest for installation. Try again.";
|
||||
return response;
|
||||
}
|
||||
|
||||
NSString *detail = @"";
|
||||
int extractRet = vp_extract_package_to_directory(ipaPath, tmpPackagePath, &detail);
|
||||
int installRet = 0;
|
||||
if (extractRet == 0) {
|
||||
installRet = vp_install_app_from_package(tmpPackagePath, forceSystem, certPath, &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:@"Unable to install the app (code %d).\n%@", retCode, trimmed]
|
||||
: [NSString stringWithFormat:@"Unable to install the app (code %d).", retCode];
|
||||
return response;
|
||||
}
|
||||
|
||||
NSMutableDictionary *response = vp_make_response(@"ok", reqId);
|
||||
response[@"msg"] = forceSystem
|
||||
? [NSString stringWithFormat:@"Installed %@ as a system app.", detail]
|
||||
: [NSString stringWithFormat:@"Installed %@ as a user app.", detail];
|
||||
return response;
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
#import "Include/VphonedNative.h"
|
||||
#import "vphoned_install.h"
|
||||
#import <CommonCrypto/CommonDigest.h>
|
||||
#include <mach-o/dyld.h>
|
||||
#include <unistd.h>
|
||||
@@ -8,12 +7,6 @@
|
||||
static const char *marker = "/var/root/Library/Caches/vphoned.api-v2";
|
||||
static const char *pending = "/var/root/Library/Caches/vphoned.api-v2.pending";
|
||||
|
||||
NSDictionary *vp_native_api_command(NSDictionary *message) {
|
||||
NSString *type = message[@"t"];
|
||||
if ([type isEqualToString:@"ipa_install"]) return vp_handle_custom_install(message);
|
||||
return @{@"t": @"err", @"msg": @"Unknown native operation"};
|
||||
}
|
||||
|
||||
void vp_native_bootstrap_cached_binary(void) {
|
||||
// A cached update gets one attempt to bind. If it fails, launchd restarts
|
||||
// the bundled daemon, which remains the fallback.
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
#import "Include/VphonedNative.h"
|
||||
#import <Security/Security.h>
|
||||
#include <errno.h>
|
||||
#include <mach-o/fat.h>
|
||||
#include <mach-o/loader.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.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;
|
||||
|
||||
// Implemented in GuestSigner.swift using the shared VPhoneSign target.
|
||||
extern char *vp_guest_sign_binary(const char *path, const char *entitlementsPath, const char *certificatePath);
|
||||
|
||||
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_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 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 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 **errorOutput
|
||||
) {
|
||||
NSString *entitlementsPath = nil;
|
||||
NSData *entitlementsXML = entitlements ? [NSPropertyListSerialization
|
||||
dataWithPropertyList:entitlements
|
||||
format:NSPropertyListXMLFormat_v1_0
|
||||
options:0
|
||||
error:nil] : nil;
|
||||
if (entitlementsXML) {
|
||||
entitlementsPath = [[NSTemporaryDirectory() stringByAppendingPathComponent:[NSUUID UUID].UUIDString]
|
||||
stringByAppendingPathExtension:@"plist"];
|
||||
if (![entitlementsXML writeToFile:entitlementsPath atomically:YES]) {
|
||||
if (errorOutput) *errorOutput = @"Could not prepare app entitlements.";
|
||||
return EIO;
|
||||
}
|
||||
}
|
||||
|
||||
char *error = vp_guest_sign_binary(
|
||||
filePath.fileSystemRepresentation,
|
||||
entitlementsPath.fileSystemRepresentation,
|
||||
certPath.length > 0 ? certPath.fileSystemRepresentation : NULL
|
||||
);
|
||||
if (entitlementsPath) {
|
||||
[[NSFileManager defaultManager] removeItemAtPath:entitlementsPath error:nil];
|
||||
}
|
||||
if (!error) return 0;
|
||||
if (errorOutput) *errorOutput = [NSString stringWithUTF8String:error] ?: @"Could not sign app executable.";
|
||||
free(error);
|
||||
return EINVAL;
|
||||
}
|
||||
|
||||
static int vp_sign_app(NSString *appPath, NSString *certPath, NSString **errorOutput) {
|
||||
if (!vp_info_dictionary_for_app_path(appPath)) {
|
||||
if (errorOutput) *errorOutput = @"The app package is incomplete and cannot be signed.";
|
||||
return 172;
|
||||
}
|
||||
|
||||
NSString *mainExecutablePath = vp_app_main_executable_path_for_app_path(appPath);
|
||||
if (mainExecutablePath.length == 0 || ![[NSFileManager defaultManager] fileExistsAtPath:mainExecutablePath]) {
|
||||
if (errorOutput) *errorOutput = @"The app package is missing its program and cannot be signed.";
|
||||
return 174;
|
||||
}
|
||||
|
||||
NSMutableSet<NSString *> *signedExecutables = [NSMutableSet set];
|
||||
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, &signOutput);
|
||||
if (ret != 0) {
|
||||
if (errorOutput) *errorOutput = signOutput;
|
||||
return 173;
|
||||
}
|
||||
[signedExecutables addObject:bundleMainExecutablePath];
|
||||
}
|
||||
|
||||
// Sign code without an Info.plist executable declaration, such as dylibs.
|
||||
// The declared executables above already carry their guest entitlements.
|
||||
enumerator = [[NSFileManager defaultManager]
|
||||
enumeratorAtURL:[NSURL fileURLWithPath:appPath]
|
||||
includingPropertiesForKeys:nil
|
||||
options:0
|
||||
errorHandler:nil];
|
||||
while ((fileURL = [enumerator nextObject])) {
|
||||
NSString *filePath = fileURL.path;
|
||||
if ([signedExecutables containsObject:filePath] || !vp_is_macho_file(filePath)) continue;
|
||||
NSString *signOutput = @"";
|
||||
if (vp_sign_binary(filePath, nil, certPath, &signOutput) != 0) {
|
||||
if (errorOutput) *errorOutput = signOutput;
|
||||
return 173;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
char *vp_sign_app_for_install(const char *appPath, const char *certificatePath) {
|
||||
if (!appPath) return strdup("Missing app path");
|
||||
NSString *failure = nil;
|
||||
NSString *app = [NSString stringWithUTF8String:appPath];
|
||||
NSString *certificate = certificatePath ? [NSString stringWithUTF8String:certificatePath] : nil;
|
||||
if (vp_sign_app(app, certificate, &failure) == 0) return NULL;
|
||||
return strdup((failure ?: @"Could not sign app.").UTF8String);
|
||||
}
|
||||
@@ -13,7 +13,6 @@
|
||||
C10000000000000000000014 /* NIOHTTP1 in Frameworks */ = {isa = PBXBuildFile; productRef = C10000000000000000000063 /* NIOHTTP1 */; };
|
||||
C10000000000000000000015 /* NIOPosix in Frameworks */ = {isa = PBXBuildFile; productRef = C10000000000000000000064 /* NIOPosix */; };
|
||||
C10000000000000000000016 /* NIOWebSocket in Frameworks */ = {isa = PBXBuildFile; productRef = C10000000000000000000065 /* NIOWebSocket */; };
|
||||
C10000000000000000000017 /* LibArchive in Frameworks */ = {isa = PBXBuildFile; productRef = C10000000000000000000066 /* LibArchive */; };
|
||||
C10000000000000000000018 /* IcliSystem in Frameworks */ = {isa = PBXBuildFile; productRef = C10000000000000000000068 /* IcliSystem */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
@@ -56,7 +55,6 @@
|
||||
C10000000000000000000014 /* NIOHTTP1 in Frameworks */,
|
||||
C10000000000000000000015 /* NIOPosix in Frameworks */,
|
||||
C10000000000000000000016 /* NIOWebSocket in Frameworks */,
|
||||
C10000000000000000000017 /* LibArchive in Frameworks */,
|
||||
C10000000000000000000018 /* IcliSystem in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -128,7 +126,6 @@
|
||||
C10000000000000000000063 /* NIOHTTP1 */,
|
||||
C10000000000000000000064 /* NIOPosix */,
|
||||
C10000000000000000000065 /* NIOWebSocket */,
|
||||
C10000000000000000000066 /* LibArchive */,
|
||||
C10000000000000000000068 /* IcliSystem */,
|
||||
);
|
||||
productName = vphoned;
|
||||
@@ -155,7 +152,6 @@
|
||||
packageReferences = (
|
||||
C10000000000000000000071 /* XCRemoteSwiftPackageReference "icli" */,
|
||||
C10000000000000000000072 /* XCRemoteSwiftPackageReference "swift-nio" */,
|
||||
C10000000000000000000073 /* XCRemoteSwiftPackageReference "libarchive.xcframework" */,
|
||||
C10000000000000000000074 /* XCRemoteSwiftPackageReference "swift-collections" */,
|
||||
);
|
||||
projectDirPath = "";
|
||||
@@ -224,7 +220,6 @@
|
||||
HEADER_SEARCH_PATHS = (
|
||||
"$(PROJECT_DIR)/Native",
|
||||
"$(PROJECT_DIR)/Native/Include",
|
||||
"$(PROJECT_DIR)/Native/Vendor/LibArchive",
|
||||
);
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
OTHER_LDFLAGS = "-ObjC";
|
||||
@@ -247,7 +242,6 @@
|
||||
HEADER_SEARCH_PATHS = (
|
||||
"$(PROJECT_DIR)/Native",
|
||||
"$(PROJECT_DIR)/Native/Include",
|
||||
"$(PROJECT_DIR)/Native/Vendor/LibArchive",
|
||||
);
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
OTHER_LDFLAGS = "-ObjC";
|
||||
@@ -351,7 +345,7 @@
|
||||
repositoryURL = "https://github.com/owngoal-dev/icli.git";
|
||||
requirement = {
|
||||
kind = exactVersion;
|
||||
version = 0.6.5;
|
||||
version = 0.6.6;
|
||||
};
|
||||
};
|
||||
C10000000000000000000072 /* XCRemoteSwiftPackageReference "swift-nio" */ = {
|
||||
@@ -362,14 +356,6 @@
|
||||
version = 2.83.0;
|
||||
};
|
||||
};
|
||||
C10000000000000000000073 /* XCRemoteSwiftPackageReference "libarchive.xcframework" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/Lakr233/libarchive.xcframework.git";
|
||||
requirement = {
|
||||
kind = exactVersion;
|
||||
version = 0.1.1;
|
||||
};
|
||||
};
|
||||
C10000000000000000000074 /* XCRemoteSwiftPackageReference "swift-collections" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/apple/swift-collections.git";
|
||||
@@ -406,11 +392,6 @@
|
||||
package = C10000000000000000000072 /* XCRemoteSwiftPackageReference "swift-nio" */;
|
||||
productName = NIOWebSocket;
|
||||
};
|
||||
C10000000000000000000066 /* LibArchive */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = C10000000000000000000073 /* XCRemoteSwiftPackageReference "libarchive.xcframework" */;
|
||||
productName = LibArchive;
|
||||
};
|
||||
C10000000000000000000068 /* IcliSystem */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = C10000000000000000000071 /* XCRemoteSwiftPackageReference "icli" */;
|
||||
|
||||
+2
-2
@@ -6,8 +6,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/owngoal-dev/icli.git",
|
||||
"state" : {
|
||||
"revision" : "bbb4d238bb06285dacb5385e372b5e990398d688",
|
||||
"version" : "0.6.5"
|
||||
"revision" : "0bc454f61f54d99439e0f8a7f9e1c43f0fb49dc2",
|
||||
"version" : "0.6.6"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user