Supports installing Fugu14-based jailbreaks

If a jailbreak app contains the relevant Fugu14 entries in its Info.plist, AltStore will automatically guide the user through the Fugu14 untether process before installing the jailbreak.
This commit is contained in:
Riley Testut
2021-10-25 22:27:30 -07:00
parent e6ef288a69
commit ed396b400d
23 changed files with 1329 additions and 26 deletions

View File

@@ -107,11 +107,11 @@ class InstallAppOperation: ResultOperation<InstalledApp>
installedApp.appExtensions = installedExtensions
self.context.beginInstallationHandler?(installedApp)
// Temporary directory and resigned .ipa no longer needed, so delete them now to ensure AltStore doesn't quit before we get the chance to.
self.cleanUp()
self.context.beginInstallationHandler?(installedApp)
var activeProfiles: Set<String>?
if let sideloadedAppsLimit = UserDefaults.standard.activeAppsLimit
{

View File

@@ -0,0 +1,19 @@
//
// ALTAppPatcher.h
// AltStore
//
// Created by Riley Testut on 10/18/21.
// Copyright © 2021 Riley Testut. All rights reserved.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface ALTAppPatcher : NSObject
- (BOOL)patchAppBinaryAtURL:(NSURL *)appFileURL withBinaryAtURL:(NSURL *)patchFileURL error:(NSError *_Nullable *)error;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,143 @@
//
// ALTAppPatcher.m
// AltStore
//
// Created by Riley Testut on 10/18/21.
// Copied with minor modifications from sample code provided by Linus Henze.
//
#import "ALTAppPatcher.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@import Roxas;
#define CPU_SUBTYPE_PAC 0x80000000
#define FAT_MAGIC 0xcafebabe
#define ROUND_TO_PAGE(val) (((val % 0x4000) == 0) ? val : (val + (0x4000 - (val & 0x3FFF))))
typedef struct {
uint32_t magic;
uint32_t cpuType;
uint32_t cpuSubType;
// Incomplete, we don't need anything else
} MachOHeader;
typedef struct {
uint32_t cpuType;
uint32_t cpuSubType;
uint32_t fileOffset;
uint32_t size;
uint32_t alignment;
} FatArch;
typedef struct {
uint32_t magic;
uint32_t archCount;
FatArch archs[0];
} FatHeader;
// Given two MachO files, return a FAT file with the following properties:
// 1. installd will still see the original MachO and validate it's code signature
// 2. The kernel will only see the injected MachO instead
//
// Only arm64e for now
void *injectApp(void *originalApp, size_t originalAppSize, void *appToInject, size_t appToInjectSize, size_t *outputSize) {
*outputSize = 0;
// First validate the App to inject: It must be an arm64e application
if (appToInjectSize < sizeof(MachOHeader)) {
return NULL;
}
MachOHeader *injectedHeader = (MachOHeader*) appToInject;
if (injectedHeader->cpuType != CPU_TYPE_ARM64) {
return NULL;
}
if (injectedHeader->cpuSubType != (CPU_SUBTYPE_ARM64E | CPU_SUBTYPE_PAC)) {
return NULL;
}
// Ok, the App to inject is ok
// Now build a fat header
size_t originalAppSizeRounded = ROUND_TO_PAGE(originalAppSize);
size_t appToInjectSizeRounded = ROUND_TO_PAGE(appToInjectSize);
size_t totalSize = 0x4000 /* Fat Header + Alignment */ + originalAppSizeRounded + appToInjectSizeRounded;
void *fatBuf = malloc(totalSize);
if (fatBuf == NULL) {
return NULL;
}
bzero(fatBuf, totalSize);
FatHeader *fatHeader = (FatHeader*) fatBuf;
fatHeader->magic = htonl(FAT_MAGIC);
fatHeader->archCount = htonl(2);
// Write first arch (original app)
fatHeader->archs[0].cpuType = htonl(CPU_TYPE_ARM64);
fatHeader->archs[0].cpuSubType = htonl(CPU_SUBTYPE_ARM64E); /* Note that this is not a valid cpu subtype */
fatHeader->archs[0].fileOffset = htonl(0x4000);
fatHeader->archs[0].size = htonl(originalAppSize);
fatHeader->archs[0].alignment = htonl(0xE);
// Write second arch (injected app)
fatHeader->archs[1].cpuType = htonl(CPU_TYPE_ARM64);
fatHeader->archs[1].cpuSubType = htonl(CPU_SUBTYPE_ARM64E | CPU_SUBTYPE_PAC);
fatHeader->archs[1].fileOffset = htonl(0x4000 + originalAppSizeRounded);
fatHeader->archs[1].size = htonl(appToInjectSize);
fatHeader->archs[1].alignment = htonl(0xE);
// Ok, now write the MachOs
memcpy(fatBuf + 0x4000, originalApp, originalAppSize);
memcpy(fatBuf + 0x4000 + originalAppSizeRounded, appToInject, appToInjectSize);
// We're done!
*outputSize = totalSize;
return fatBuf;
}
@implementation ALTAppPatcher
- (BOOL)patchAppBinaryAtURL:(NSURL *)appFileURL withBinaryAtURL:(NSURL *)patchFileURL error:(NSError *__autoreleasing *)error
{
NSMutableData *originalApp = [NSMutableData dataWithContentsOfURL:appFileURL options:0 error:error];
if (originalApp == nil)
{
return NO;
}
NSMutableData *injectedApp = [NSMutableData dataWithContentsOfURL:patchFileURL options:0 error:error];
if (injectedApp == nil)
{
return NO;
}
size_t outputSize = 0;
void *output = injectApp(originalApp.mutableBytes, originalApp.length, injectedApp.mutableBytes, injectedApp.length, &outputSize);
if (output == NULL)
{
if (error)
{
// If injectApp fails, it means the patch app is in the wrong format.
*error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadCorruptFileError userInfo:@{NSURLErrorKey: patchFileURL}];
}
return NO;
}
NSData *outputData = [NSData dataWithBytesNoCopy:output length:outputSize freeWhenDone:YES];
if (![outputData writeToURL:appFileURL options:NSDataWritingAtomic error:error])
{
return NO;
}
return YES;
}
@end

View File

@@ -0,0 +1,124 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="19162" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="WBb-E1-bN8">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="19144"/>
<capability name="Named colors" minToolsVersion="9.0"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--Navigation Controller-->
<scene sceneID="dx2-fp-qDX">
<objects>
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="WBb-E1-bN8" sceneMemberID="viewController">
<toolbarItems/>
<navigationBar key="navigationBar" contentMode="scaleToFill" barStyle="black" id="cVa-8m-fW6" customClass="NavigationBar" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="44" width="414" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<color key="barTintColor" name="SettingsBackground"/>
</navigationBar>
<nil name="viewControllers"/>
<connections>
<segue destination="idH-XF-rK8" kind="relationship" relationship="rootViewController" id="hSJ-tL-4nB"/>
</connections>
</navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="i7K-pi-SRe" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="137.68115942028987" y="137.94642857142856"/>
</scene>
<!--Patch View Controller-->
<scene sceneID="gJ4-4F-79r">
<objects>
<viewController id="idH-XF-rK8" customClass="PatchViewController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" id="4bV-S5-z7S">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="RHK-C5-7wu" customClass="RSTPlaceholderView">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
</view>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="PjB-Dc-9n3">
<rect key="frame" x="20" y="736.5" width="374" height="117.5"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text=" " textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="GQF-6P-Fit">
<rect key="frame" x="0.0" y="0.0" width="374" height="20.5"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" name="Text"/>
<nil key="highlightedColor"/>
</label>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="SNn-Ad-ICf" customClass="PillButton" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="28.5" width="374" height="51"/>
<color key="backgroundColor" name="SettingsHighlighted"/>
<constraints>
<constraint firstAttribute="height" constant="51" id="s4X-uf-nl9"/>
</constraints>
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="19"/>
<color key="tintColor" name="SettingsHighlighted"/>
<state key="normal" title="Install Untethered Jailbreak">
<color key="titleColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</state>
<connections>
<action selector="performButtonAction" destination="idH-XF-rK8" eventType="primaryActionTriggered" id="FxO-1Y-IML"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="leJ-09-giz">
<rect key="frame" x="0.0" y="87.5" width="374" height="30"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="15"/>
<state key="normal" title="Install Without Untethering">
<color key="titleColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</state>
<connections>
<action selector="installRegularJailbreak" destination="idH-XF-rK8" eventType="primaryActionTriggered" id="1XB-11-Kdn"/>
</connections>
</button>
</subviews>
</stackView>
</subviews>
<viewLayoutGuide key="safeArea" id="H6g-xA-DdL"/>
<color key="backgroundColor" name="SettingsBackground"/>
<constraints>
<constraint firstItem="RHK-C5-7wu" firstAttribute="top" secondItem="4bV-S5-z7S" secondAttribute="top" id="CVK-6E-iA6"/>
<constraint firstAttribute="trailingMargin" secondItem="PjB-Dc-9n3" secondAttribute="trailing" id="GOg-JU-LIP"/>
<constraint firstItem="RHK-C5-7wu" firstAttribute="bottom" secondItem="4bV-S5-z7S" secondAttribute="bottom" id="LPh-J8-IVx"/>
<constraint firstItem="PjB-Dc-9n3" firstAttribute="leading" secondItem="4bV-S5-z7S" secondAttribute="leadingMargin" id="Rlg-PC-5ZN"/>
<constraint firstItem="RHK-C5-7wu" firstAttribute="trailing" secondItem="H6g-xA-DdL" secondAttribute="trailing" id="XdZ-36-6yS"/>
<constraint firstAttribute="bottomMargin" secondItem="PjB-Dc-9n3" secondAttribute="bottom" id="hTS-nX-0xv"/>
<constraint firstItem="RHK-C5-7wu" firstAttribute="leading" secondItem="H6g-xA-DdL" secondAttribute="leading" id="lzV-fG-Xv6"/>
</constraints>
</view>
<navigationItem key="navigationItem" largeTitleDisplayMode="always" id="0J1-80-RD8">
<barButtonItem key="leftBarButtonItem" systemItem="cancel" id="vtw-PQ-Dk1">
<color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<connections>
<action selector="cancel" destination="idH-XF-rK8" id="4Wk-dv-RYG"/>
</connections>
</barButtonItem>
</navigationItem>
<simulatedNavigationBarMetrics key="simulatedTopBarMetrics" prompted="NO"/>
<connections>
<outlet property="cancelBarButtonItem" destination="vtw-PQ-Dk1" id="8Mh-GU-KD5"/>
<outlet property="cancelButton" destination="leJ-09-giz" id="BNh-I3-vXc"/>
<outlet property="pillButton" destination="SNn-Ad-ICf" id="iJg-TC-p8q"/>
<outlet property="placeholderView" destination="RHK-C5-7wu" id="5x0-sg-HAH"/>
<outlet property="taskDescriptionLabel" destination="GQF-6P-Fit" id="C4c-xy-kvU"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="8ev-19-qsi" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="1001" y="138"/>
</scene>
</scenes>
<resources>
<namedColor name="SettingsBackground">
<color red="0.0039215686274509803" green="0.50196078431372548" blue="0.51764705882352946" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
<namedColor name="SettingsHighlighted">
<color red="0.0080000003799796104" green="0.32199999690055847" blue="0.40400001406669617" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
<namedColor name="Text">
<color red="1" green="1" blue="1" alpha="0.75" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
</resources>
</document>

View File

@@ -0,0 +1,223 @@
//
// PatchAppOperation.swift
// AltStore
//
// Created by Riley Testut on 10/13/21.
// Copyright © 2021 Riley Testut. All rights reserved.
//
import UIKit
import Combine
import AppleArchive
import System
import AltStoreCore
import AltSign
import Roxas
@available(iOS 14, *)
protocol PatchAppContext
{
var bundleIdentifier: String { get }
var temporaryDirectory: URL { get }
var resignedApp: ALTApplication? { get }
var error: Error? { get }
}
enum PatchAppError: LocalizedError
{
case unsupportedOperatingSystemVersion(OperatingSystemVersion)
var errorDescription: String? {
switch self
{
case .unsupportedOperatingSystemVersion(let osVersion):
var osVersionString = "\(osVersion.majorVersion).\(osVersion.minorVersion)"
if osVersion.patchVersion != 0
{
osVersionString += ".\(osVersion.patchVersion)"
}
let errorDescription = String(format: NSLocalizedString("The OTA download URL for iOS %@ could not be determined.", comment: ""), osVersionString)
return errorDescription
}
}
}
private struct OTAUpdate
{
var url: URL
var archivePath: String
}
@available(iOS 14, *)
class PatchAppOperation: ResultOperation<Void>
{
let context: PatchAppContext
var progressHandler: ((Progress, String) -> Void)?
private let appPatcher = ALTAppPatcher()
private lazy var patchDirectory: URL = self.context.temporaryDirectory.appendingPathComponent("Patch", isDirectory: true)
private var cancellable: AnyCancellable?
init(context: PatchAppContext)
{
self.context = context
super.init()
self.progress.totalUnitCount = 100
}
override func main()
{
super.main()
if let error = self.context.error
{
self.finish(.failure(error))
return
}
guard let resignedApp = self.context.resignedApp else { return self.finish(.failure(OperationError.invalidParameters)) }
self.progressHandler?(self.progress, NSLocalizedString("Downloading iOS firmware...", comment: ""))
self.cancellable = self.fetchOTAUpdate()
.flatMap { self.downloadArchive(from: $0) }
.flatMap { self.extractSpotlightFromArchive(at: $0) }
.flatMap { self.patch(resignedApp, withBinaryAt: $0) }
.tryMap { try FileManager.default.zipAppBundle(at: $0) }
.tryMap { (fileURL) in
let app = AnyApp(name: resignedApp.name, bundleIdentifier: self.context.bundleIdentifier, url: resignedApp.fileURL)
let destinationURL = InstalledApp.refreshedIPAURL(for: app)
try FileManager.default.copyItem(at: fileURL, to: destinationURL, shouldReplace: true)
}
.receive(on: RunLoop.main)
.sink { completion in
switch completion
{
case .failure(let error): self.finish(.failure(error))
case .finished: self.finish(.success(()))
}
} receiveValue: { _ in }
}
override func cancel()
{
super.cancel()
self.cancellable?.cancel()
self.cancellable = nil
}
}
private let ALTFragmentZipCallback: @convention(c) (UInt32) -> Void = { (percentageComplete) in
guard let progress = Progress.current() else { return }
if percentageComplete == 100 && progress.completedUnitCount == 0
{
// Ignore first percentageComplete, which is always 100.
return
}
progress.completedUnitCount = Int64(percentageComplete)
}
@available(iOS 14, *)
private extension PatchAppOperation
{
func fetchOTAUpdate() -> AnyPublisher<OTAUpdate, Error>
{
Just(()).tryMap {
let osVersion = ProcessInfo.processInfo.operatingSystemVersion
switch (osVersion.majorVersion, osVersion.minorVersion)
{
#if DEBUG
case (14, 3): fallthrough
#endif
case (14, 4):
return OTAUpdate(url: URL(string: "https://updates.cdn-apple.com/2021WinterFCS/patches/001-98606/43AF99A1-F286-43B1-A101-F9F856EA395A/com_apple_MobileAsset_SoftwareUpdate/c4985c32c344beb7b49c61919b4e39d1fd336c90.zip")!,
archivePath: "AssetData/payloadv2/payload.042")
case (14, 5):
return OTAUpdate(url: URL(string: "https://updates.cdn-apple.com/2021SpringFCS/patches/061-84483/AB525139-066E-46F8-8E85-DCE802C03BA8/com_apple_MobileAsset_SoftwareUpdate/788573ae93113881db04269acedeecabbaa643e3.zip")!,
archivePath: "AssetData/payloadv2/payload.043")
default: throw PatchAppError.unsupportedOperatingSystemVersion(osVersion)
}
}
.eraseToAnyPublisher()
}
func downloadArchive(from update: OTAUpdate) -> AnyPublisher<URL, Error>
{
Just(()).tryMap {
try FileManager.default.createDirectory(at: self.patchDirectory, withIntermediateDirectories: true, attributes: nil)
let archiveURL = self.patchDirectory.appendingPathComponent("ota.archive")
archiveURL.withUnsafeFileSystemRepresentation { archivePath in
let fz = fragmentzip_open((update.url.absoluteString as NSString).utf8String!)
defer { fragmentzip_close(fz) }
self.progress.becomeCurrent(withPendingUnitCount: 100)
fragmentzip_download_file(fz, update.archivePath, archivePath!, ALTFragmentZipCallback)
self.progress.resignCurrent()
}
print("Downloaded OTA archive.")
return archiveURL
}
.mapError { ($0 as NSError).withLocalizedFailure(NSLocalizedString("Could not download OTA archive.", comment: "")) }
.eraseToAnyPublisher()
}
func extractSpotlightFromArchive(at archiveURL: URL) -> AnyPublisher<URL, Error>
{
Just(()).tryMap {
let spotlightPath = "Applications/Spotlight.app/Spotlight"
let spotlightFileURL = self.patchDirectory.appendingPathComponent(spotlightPath)
guard let readFileStream = ArchiveByteStream.fileStream(path: FilePath(archiveURL.path), mode: .readOnly, options: [], permissions: FilePermissions(rawValue: 0o644)),
let decompressStream = ArchiveByteStream.decompressionStream(readingFrom: readFileStream),
let decodeStream = ArchiveStream.decodeStream(readingFrom: decompressStream),
let readStream = ArchiveStream.extractStream(extractingTo: FilePath(self.patchDirectory.path))
else { throw CocoaError(.fileReadCorruptFile, userInfo: [NSURLErrorKey: archiveURL]) }
_ = try ArchiveStream.process(readingFrom: decodeStream, writingTo: readStream) { message, filePath, data in
guard filePath == FilePath(spotlightPath) else { return .skip }
return .ok
}
print("Extracted Spotlight from OTA archive.")
return spotlightFileURL
}
.mapError { ($0 as NSError).withLocalizedFailure(NSLocalizedString("Could not extract Spotlight from OTA archive.", comment: "")) }
.eraseToAnyPublisher()
}
func patch(_ app: ALTApplication, withBinaryAt patchFileURL: URL) -> AnyPublisher<URL, Error>
{
Just(()).tryMap {
// executableURL may be nil, so use infoDictionary instead to determine executable name.
// guard let appName = app.bundle.executableURL?.lastPathComponent else { throw OperationError.invalidApp }
guard let appName = app.bundle.infoDictionary?[kCFBundleExecutableKey as String] as? String else { throw OperationError.invalidApp }
let temporaryAppURL = self.patchDirectory.appendingPathComponent("Patched.app", isDirectory: true)
try FileManager.default.copyItem(at: app.fileURL, to: temporaryAppURL)
let appBinaryURL = temporaryAppURL.appendingPathComponent(appName, isDirectory: false)
try self.appPatcher.patchAppBinary(at: appBinaryURL, withBinaryAt: patchFileURL)
print("Patched \(app.name).")
return temporaryAppURL
}
.mapError { ($0 as NSError).withLocalizedFailure(String(format: NSLocalizedString("Could not patch %@ placeholder.", comment: ""), app.name)) }
.eraseToAnyPublisher()
}
}

View File

@@ -0,0 +1,499 @@
//
// PatchViewController.swift
// AltStore
//
// Created by Riley Testut on 10/20/21.
// Copyright © 2021 Riley Testut. All rights reserved.
//
import UIKit
import Combine
import AltStoreCore
import AltSign
import Roxas
@available(iOS 14.0, *)
extension PatchViewController
{
enum Step
{
case confirm
case install
case openApp
case patchApp
case reboot
case refresh
case finish
}
}
@available(iOS 14.0, *)
class PatchViewController: UIViewController
{
var patchApp: AnyApp?
var installedApp: InstalledApp?
var completionHandler: ((Result<Void, Error>) -> Void)?
private let context = AuthenticatedOperationContext()
private var currentStep: Step = .confirm {
didSet {
DispatchQueue.main.async {
self.update()
}
}
}
private var buttonHandler: (() -> Void)?
private var resignedApp: ALTApplication?
private lazy var temporaryDirectory: URL = FileManager.default.uniqueTemporaryURL()
private var didEnterBackgroundObservation: NSObjectProtocol?
private weak var cancellableProgress: Progress?
@IBOutlet private var placeholderView: RSTPlaceholderView!
@IBOutlet private var taskDescriptionLabel: UILabel!
@IBOutlet private var pillButton: PillButton!
@IBOutlet private var cancelBarButtonItem: UIBarButtonItem!
@IBOutlet private var cancelButton: UIButton!
override func viewDidLoad()
{
super.viewDidLoad()
self.isModalInPresentation = true
self.placeholderView.stackView.spacing = 20
self.placeholderView.textLabel.textColor = .white
self.placeholderView.detailTextLabel.textAlignment = .left
self.placeholderView.detailTextLabel.textColor = UIColor.white.withAlphaComponent(0.6)
self.buttonHandler = { [weak self] in
self?.startProcess()
}
do
{
try FileManager.default.createDirectory(at: self.temporaryDirectory, withIntermediateDirectories: true, attributes: nil)
}
catch
{
print("Failed to create temporary directory:", error)
}
self.update()
}
override func viewWillAppear(_ animated: Bool)
{
super.viewWillAppear(animated)
if self.installedApp != nil
{
self.refreshApp()
}
}
}
@available(iOS 14.0, *)
private extension PatchViewController
{
func update()
{
self.cancelButton.alpha = 0.0
switch self.currentStep
{
case .confirm:
guard let app = self.patchApp else { break }
if UIDevice.current.isUntetheredJailbreakRequired
{
self.placeholderView.textLabel.text = NSLocalizedString("Jailbreak Requires Untethering", comment: "")
self.placeholderView.detailTextLabel.text = String(format: NSLocalizedString("This jailbreak is untethered, which means %@ will never expire — even after 7 days or rebooting the device.\n\nInstalling an untethered jailbreak requires a few extra steps, but AltStore will walk you through the process.\n\nWould you like to continue? ", comment: ""), app.name)
}
else
{
self.placeholderView.textLabel.text = NSLocalizedString("Jailbreak Supports Untethering", comment: "")
self.placeholderView.detailTextLabel.text = String(format: NSLocalizedString("This jailbreak has an untethered version, which means %@ will never expire — even after 7 days or rebooting the device.\n\nInstalling an untethered jailbreak requires a few extra steps, but AltStore will walk you through the process.\n\nWould you like to continue? ", comment: ""), app.name)
}
self.pillButton.setTitle(NSLocalizedString("Install Untethered Jailbreak", comment: ""), for: .normal)
self.cancelButton.alpha = 1.0
case .install:
guard let app = self.patchApp else { break }
self.placeholderView.textLabel.text = String(format: NSLocalizedString("Installing %@ placeholder…", comment: ""), app.name)
self.placeholderView.detailTextLabel.text = NSLocalizedString("A placeholder app needs to be installed in order to prepare your device for untethering.\n\nThis may take a few moments.", comment: "")
case .openApp:
self.placeholderView.textLabel.text = NSLocalizedString("Continue in App", comment: "")
self.placeholderView.detailTextLabel.text = NSLocalizedString("Please open the placeholder app and follow the instructions to continue jailbreaking your device.", comment: "")
self.pillButton.setTitle(NSLocalizedString("Open Placeholder", comment: ""), for: .normal)
case .patchApp:
guard let app = self.patchApp else { break }
self.placeholderView.textLabel.text = String(format: NSLocalizedString("Patching %@ placeholder…", comment: ""), app.name)
self.placeholderView.detailTextLabel.text = NSLocalizedString("This will take a few moments. Please do not turn off the screen or leave the app until patching is complete.", comment: "")
self.pillButton.setTitle(NSLocalizedString("Patch Placeholder", comment: ""), for: .normal)
case .reboot:
self.placeholderView.textLabel.text = NSLocalizedString("Continue in App", comment: "")
self.placeholderView.detailTextLabel.text = NSLocalizedString("Please open the placeholder app and follow the instructions to continue jailbreaking your device.", comment: "")
self.pillButton.setTitle(NSLocalizedString("Open Placeholder", comment: ""), for: .normal)
case .refresh:
guard let installedApp = self.installedApp else { break }
self.placeholderView.textLabel.text = String(format: NSLocalizedString("Finish installing %@?", comment: ""), installedApp.name)
self.placeholderView.detailTextLabel.text = String(format: NSLocalizedString("In order to finish jailbreaking this device, you need to install %@ then follow the instructions in the app.", comment: ""), installedApp.name)
self.pillButton.setTitle(String(format: NSLocalizedString("Install %@", comment: ""), installedApp.name), for: .normal)
case .finish:
guard let installedApp = self.installedApp else { break }
self.placeholderView.textLabel.text = String(format: NSLocalizedString("Finish in %@", comment: ""), installedApp.name)
self.placeholderView.detailTextLabel.text = String(format: NSLocalizedString("Follow the instructions in %@ to finish jailbreaking this device.", comment: ""), installedApp.name)
self.pillButton.setTitle(String(format: NSLocalizedString("Open %@", comment: ""), installedApp.name), for: .normal)
}
}
func present(_ error: Error, title: String)
{
DispatchQueue.main.async {
let nsError = error as NSError
let alertController = UIAlertController(title: nsError.localizedFailure ?? title, message: error.localizedDescription, preferredStyle: .alert)
alertController.addAction(.ok)
self.present(alertController, animated: true, completion: nil)
self.setProgress(nil, description: nil)
}
}
func setProgress(_ progress: Progress?, description: String?)
{
DispatchQueue.main.async {
self.pillButton.progress = progress
self.taskDescriptionLabel.text = description ?? " " // Use non-empty string to prevent label resizing itself.
}
}
func finish(with result: Result<Void, Error>)
{
do
{
try FileManager.default.removeItem(at: self.temporaryDirectory)
}
catch
{
print("Failed to remove temporary directory:", error)
}
if let observation = self.didEnterBackgroundObservation
{
NotificationCenter.default.removeObserver(observation)
}
self.completionHandler?(result)
self.completionHandler = nil
}
}
@available(iOS 14.0, *)
private extension PatchViewController
{
@IBAction func performButtonAction()
{
self.buttonHandler?()
}
@IBAction func cancel()
{
self.finish(with: .success(()))
self.cancellableProgress?.cancel()
}
@IBAction func installRegularJailbreak()
{
guard let app = self.patchApp else { return }
let title: String
let message: String
if UIDevice.current.isUntetheredJailbreakRequired
{
title = NSLocalizedString("Untethering Required", comment: "")
message = String(format: NSLocalizedString("%@ can not jailbreak this device unless you untether it first. Are you sure you want to install without untethering?", comment: ""), app.name)
}
else
{
title = NSLocalizedString("Untethering Recommended", comment: "")
message = String(format: NSLocalizedString("Untethering this jailbreak will prevent %@ from expiring, even after 7 days or rebooting the device. Are you sure you want to install without untethering?", comment: ""), app.name)
}
let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: NSLocalizedString("Install Without Untethering", comment: ""), style: .default) { _ in
self.finish(with: .failure(OperationError.cancelled))
})
alertController.addAction(.cancel)
self.present(alertController, animated: true, completion: nil)
}
}
@available(iOS 14.0, *)
private extension PatchViewController
{
func startProcess()
{
guard let patchApp = self.patchApp else { return }
self.currentStep = .install
if let progress = AppManager.shared.installationProgress(for: patchApp)
{
// Cancel pending jailbreak app installation so we can start a new one.
progress.cancel()
}
let appURL = InstalledApp.fileURL(for: patchApp)
let cachedAppURL = self.temporaryDirectory.appendingPathComponent("Cached.app")
do
{
// Make copy of original app, so we can replace the cached patch app with it later.
try FileManager.default.copyItem(at: appURL, to: cachedAppURL, shouldReplace: true)
}
catch
{
self.present(error, title: NSLocalizedString("Could not back up jailbreak app.", comment: ""))
return
}
var unzippingError: Error?
let refreshGroup = AppManager.shared.install(patchApp, presentingViewController: self, context: self.context) { result in
do
{
_ = try result.get()
if let unzippingError = unzippingError
{
throw unzippingError
}
// Replace cached patch app with original app so we can resume installing it post-reboot.
try FileManager.default.copyItem(at: cachedAppURL, to: appURL, shouldReplace: true)
self.openApp()
}
catch
{
self.present(error, title: String(format: NSLocalizedString("Could not install %@ placeholder.", comment: ""), patchApp.name))
}
}
refreshGroup.beginInstallationHandler = { (installedApp) in
do
{
// Replace patch app name with correct name.
installedApp.name = patchApp.name
let ipaURL = installedApp.refreshedIPAURL
let resignedAppURL = try FileManager.default.unzipAppBundle(at: ipaURL, toDirectory: self.temporaryDirectory)
self.resignedApp = ALTApplication(fileURL: resignedAppURL)
}
catch
{
print("Error unzipping app bundle:", error)
unzippingError = error
}
}
self.setProgress(refreshGroup.progress, description: nil)
self.cancellableProgress = refreshGroup.progress
}
func openApp()
{
guard let patchApp = self.patchApp else { return }
self.setProgress(nil, description: nil)
self.currentStep = .openApp
// This observation is willEnterForeground because patching starts immediately upon return.
self.didEnterBackgroundObservation = NotificationCenter.default.addObserver(forName: UIApplication.willEnterForegroundNotification, object: nil, queue: .main) { (notification) in
self.didEnterBackgroundObservation.map { NotificationCenter.default.removeObserver($0) }
self.patchApplication()
}
self.buttonHandler = { [weak self] in
guard let self = self else { return }
#if !targetEnvironment(simulator)
let openURL = InstalledApp.openAppURL(for: patchApp)
UIApplication.shared.open(openURL) { success in
guard !success else { return }
self.present(OperationError.openAppFailed(name: patchApp.name), title: String(format: NSLocalizedString("Could not open %@ placeholder.", comment: ""), patchApp.name))
}
#endif
}
}
func patchApplication()
{
guard let resignedApp = self.resignedApp else { return }
self.currentStep = .patchApp
self.buttonHandler = { [weak self] in
self?.patchApplication()
}
let patchAppOperation = AppManager.shared.patch(resignedApp: resignedApp, presentingViewController: self, context: self.context) { result in
switch result
{
case .failure(let error): self.present(error, title: String(format: NSLocalizedString("Could not patch %@ placeholder.", comment: ""), resignedApp.name))
case .success: self.rebootDevice()
}
}
patchAppOperation.progressHandler = { (progress, description) in
self.setProgress(progress, description: description)
}
self.cancellableProgress = patchAppOperation.progress
}
func rebootDevice()
{
guard let patchApp = self.patchApp else { return }
self.setProgress(nil, description: nil)
self.currentStep = .reboot
self.didEnterBackgroundObservation = NotificationCenter.default.addObserver(forName: UIApplication.didEnterBackgroundNotification, object: nil, queue: .main) { (notification) in
self.didEnterBackgroundObservation.map { NotificationCenter.default.removeObserver($0) }
var patchedApps = UserDefaults.standard.patchedApps ?? []
if !patchedApps.contains(patchApp.bundleIdentifier)
{
patchedApps.append(patchApp.bundleIdentifier)
UserDefaults.standard.patchedApps = patchedApps
}
self.finish(with: .success(()))
}
self.buttonHandler = { [weak self] in
guard let self = self else { return }
#if !targetEnvironment(simulator)
let openURL = InstalledApp.openAppURL(for: patchApp)
UIApplication.shared.open(openURL) { success in
guard !success else { return }
self.present(OperationError.openAppFailed(name: patchApp.name), title: String(format: NSLocalizedString("Could not open %@ placeholder.", comment: ""), patchApp.name))
}
#endif
}
}
func refreshApp()
{
guard let installedApp = self.installedApp else { return }
self.currentStep = .refresh
self.buttonHandler = { [weak self] in
guard let self = self else { return }
DatabaseManager.shared.persistentContainer.performBackgroundTask { context in
let tempApp = context.object(with: installedApp.objectID) as! InstalledApp
tempApp.needsResign = true
let errorTitle = String(format: NSLocalizedString("Could not install %@.", comment: ""), tempApp.name)
do
{
try context.save()
installedApp.managedObjectContext?.perform {
// Refreshing ensures we don't attempt to patch the app again,
// since that is only checked when installing a new app.
let refreshGroup = AppManager.shared.refresh([installedApp], presentingViewController: self, group: nil)
refreshGroup.completionHandler = { [weak refreshGroup, weak self] (results) in
guard let self = self else { return }
do
{
guard let (bundleIdentifier, result) = results.first else { throw refreshGroup?.context.error ?? OperationError.unknown }
_ = try result.get()
if var patchedApps = UserDefaults.standard.patchedApps, let index = patchedApps.firstIndex(of: bundleIdentifier)
{
patchedApps.remove(at: index)
UserDefaults.standard.patchedApps = patchedApps
}
self.finish()
}
catch
{
self.present(error, title: errorTitle)
}
}
self.setProgress(refreshGroup.progress, description: String(format: NSLocalizedString("Installing %@...", comment: ""), installedApp.name))
}
}
catch
{
self.present(error, title: errorTitle)
}
}
}
}
func finish()
{
guard let installedApp = self.installedApp else { return }
self.setProgress(nil, description: nil)
self.currentStep = .finish
self.didEnterBackgroundObservation = NotificationCenter.default.addObserver(forName: UIApplication.didEnterBackgroundNotification, object: nil, queue: .main) { (notification) in
self.didEnterBackgroundObservation.map { NotificationCenter.default.removeObserver($0) }
self.finish(with: .success(()))
}
installedApp.managedObjectContext?.perform {
let appName = installedApp.name
let openURL = installedApp.openAppURL
self.buttonHandler = { [weak self] in
guard let self = self else { return }
#if !targetEnvironment(simulator)
UIApplication.shared.open(openURL) { success in
guard !success else { return }
self.present(OperationError.openAppFailed(name: appName), title: String(format: NSLocalizedString("Could not open %@.", comment: ""), appName))
}
#endif
}
}
}
}

View File

@@ -0,0 +1,18 @@
//
// fragmentzip.h
// AltStore
//
// Created by Riley Testut on 10/25/21.
// Copyright © 2021 Riley Testut. All rights reserved.
//
#ifndef fragmentzip_h
#define fragmentzip_h
typedef void fragmentzip_t;
typedef void (*fragmentzip_process_callback_t)(unsigned int progress);
fragmentzip_t *fragmentzip_open(const char *url);
int fragmentzip_download_file(fragmentzip_t *info, const char *remotepath, const char *savepath, fragmentzip_process_callback_t callback);
void fragmentzip_close(fragmentzip_t *info);
#endif /* fragmentzip_h */

View File

@@ -14,13 +14,13 @@ import AltStoreCore
@objc(SendAppOperation)
class SendAppOperation: ResultOperation<ServerConnection>
{
let context: AppOperationContext
let context: InstallAppOperationContext
private let dispatchQueue = DispatchQueue(label: "com.altstore.SendAppOperation")
private var serverConnection: ServerConnection?
init(context: AppOperationContext)
init(context: InstallAppOperationContext)
{
self.context = context
@@ -39,9 +39,10 @@ class SendAppOperation: ResultOperation<ServerConnection>
return
}
guard let app = self.context.app, let server = self.context.server else { return self.finish(.failure(OperationError.invalidParameters)) }
guard let resignedApp = self.context.resignedApp, let server = self.context.server else { return self.finish(.failure(OperationError.invalidParameters)) }
// self.context.resignedApp.fileURL points to the app bundle, but we want the .ipa.
let app = AnyApp(name: resignedApp.name, bundleIdentifier: self.context.bundleIdentifier, url: resignedApp.url)
let fileURL = InstalledApp.refreshedIPAURL(for: app)
// Connect to server.