2021-10-25 22:27:30 -07:00
//
// P a t c h A p p O p e r a t i o n . s w i f t
// A l t S t o r e
//
// C r e a t e d b y R i l e y T e s t u t o n 1 0 / 1 3 / 2 1 .
// C o p y r i g h t © 2 0 2 1 R i l e y T e s t u t . A l l r i g h t s r e s e r v e d .
//
import AppleArchive
2023-03-01 00:48:36 -05:00
import Combine
2021-10-25 22:27:30 -07:00
import System
2023-03-01 00:48:36 -05:00
import UIKit
2023-03-01 14:36:52 -05:00
import SidePatcher
2021-10-25 22:27:30 -07:00
import AltSign
2023-03-01 00:48:36 -05:00
import SideStoreCore
2023-03-01 14:36:52 -05:00
import RoxasUIKit
2021-10-25 22:27:30 -07:00
@ available ( iOS 14 , * )
2023-03-01 00:48:36 -05:00
protocol PatchAppContext {
2021-10-25 22:27:30 -07:00
var bundleIdentifier : String { get }
var temporaryDirectory : URL { get }
2023-03-01 00:48:36 -05:00
2021-10-25 22:27:30 -07:00
var resignedApp : ALTApplication ? { get }
var error : Error ? { get }
}
2023-03-01 00:48:36 -05:00
enum PatchAppError : LocalizedError {
2021-10-25 22:27:30 -07:00
case unsupportedOperatingSystemVersion ( OperatingSystemVersion )
2023-03-01 00:48:36 -05:00
2021-10-25 22:27:30 -07:00
var errorDescription : String ? {
2023-03-01 00:48:36 -05:00
switch self {
case let . unsupportedOperatingSystemVersion ( osVersion ) :
2021-10-25 22:27:30 -07:00
var osVersionString = " \( osVersion . majorVersion ) . \( osVersion . minorVersion ) "
2023-03-01 00:48:36 -05:00
if osVersion . patchVersion != 0 {
2021-10-25 22:27:30 -07:00
osVersionString += " . \( osVersion . patchVersion ) "
}
2023-03-01 00:48:36 -05:00
2021-10-25 22:27:30 -07:00
let errorDescription = String ( format : NSLocalizedString ( " The OTA download URL for iOS %@ could not be determined. " , comment : " " ) , osVersionString )
return errorDescription
}
}
}
2023-03-01 00:48:36 -05:00
private struct OTAUpdate {
2021-10-25 22:27:30 -07:00
var url : URL
var archivePath : String
}
@ available ( iOS 14 , * )
2023-03-01 19:09:33 -05:00
public final class PatchAppOperation : ResultOperation < Void > {
2021-10-25 22:27:30 -07:00
let context : PatchAppContext
2023-03-01 00:48:36 -05:00
2021-10-25 22:27:30 -07:00
var progressHandler : ( ( Progress , String ) -> Void ) ?
2023-03-01 00:48:36 -05:00
2023-03-01 14:36:52 -05:00
private let appPatcher = SideAppPatcher ( )
2021-10-25 22:27:30 -07:00
private lazy var patchDirectory : URL = self . context . temporaryDirectory . appendingPathComponent ( " Patch " , isDirectory : true )
2023-03-01 00:48:36 -05:00
2021-10-25 22:27:30 -07:00
private var cancellable : AnyCancellable ?
2023-03-01 00:48:36 -05:00
init ( context : PatchAppContext ) {
2021-10-25 22:27:30 -07:00
self . context = context
2023-03-01 00:48:36 -05:00
2021-10-25 22:27:30 -07:00
super . init ( )
2023-03-01 00:48:36 -05:00
progress . totalUnitCount = 100
2021-10-25 22:27:30 -07:00
}
2023-03-01 00:48:36 -05:00
2023-03-01 19:09:33 -05:00
public override func main ( ) {
2021-10-25 22:27:30 -07:00
super . main ( )
2023-03-01 00:48:36 -05:00
if let error = context . error {
finish ( . failure ( error ) )
2021-10-25 22:27:30 -07:00
return
}
2023-03-01 00:48:36 -05:00
guard let resignedApp = context . resignedApp else { return finish ( . failure ( OperationError . invalidParameters ) ) }
progressHandler ? ( progress , NSLocalizedString ( " Downloading iOS firmware... " , comment : " " ) )
cancellable = fetchOTAUpdate ( )
2021-10-25 22:27:30 -07:00
. flatMap { self . downloadArchive ( from : $0 ) }
. flatMap { self . extractSpotlightFromArchive ( at : $0 ) }
. flatMap { self . patch ( resignedApp , withBinaryAt : $0 ) }
. tryMap { try FileManager . default . zipAppBundle ( at : $0 ) }
2023-03-01 00:48:36 -05:00
. tryMap { fileURL in
2021-10-25 22:27:30 -07:00
let app = AnyApp ( name : resignedApp . name , bundleIdentifier : self . context . bundleIdentifier , url : resignedApp . fileURL )
2023-03-01 00:48:36 -05:00
2021-10-25 22:27:30 -07:00
let destinationURL = InstalledApp . refreshedIPAURL ( for : app )
try FileManager . default . copyItem ( at : fileURL , to : destinationURL , shouldReplace : true )
}
. receive ( on : RunLoop . main )
. sink { completion in
2023-03-01 00:48:36 -05:00
switch completion {
case let . failure ( error ) : self . finish ( . failure ( error ) )
2021-10-25 22:27:30 -07:00
case . finished : self . finish ( . success ( ( ) ) )
}
} receiveValue : { _ in }
}
2023-03-01 00:48:36 -05:00
2023-03-01 19:09:33 -05:00
public override func cancel ( ) {
2021-10-25 22:27:30 -07:00
super . cancel ( )
2023-03-01 00:48:36 -05:00
cancellable ? . cancel ( )
cancellable = nil
2021-10-25 22:27:30 -07:00
}
}
2023-03-01 00:48:36 -05:00
private let ALTFragmentZipCallback : @ convention ( c ) ( UInt32 ) -> Void = { percentageComplete in
2021-10-25 22:27:30 -07:00
guard let progress = Progress . current ( ) else { return }
2023-03-01 00:48:36 -05:00
if percentageComplete = = 100 , progress . completedUnitCount = = 0 {
2021-10-25 22:27:30 -07:00
// I g n o r e f i r s t p e r c e n t a g e C o m p l e t e , w h i c h i s a l w a y s 1 0 0 .
return
}
2023-03-01 00:48:36 -05:00
2021-10-25 22:27:30 -07:00
progress . completedUnitCount = Int64 ( percentageComplete )
}
@ available ( iOS 14 , * )
2023-03-01 00:48:36 -05:00
private extension PatchAppOperation {
func fetchOTAUpdate ( ) -> AnyPublisher < OTAUpdate , Error > {
2021-10-25 22:27:30 -07:00
Just ( ( ) ) . tryMap {
let osVersion = ProcessInfo . processInfo . operatingSystemVersion
2023-03-01 00:48:36 -05:00
switch ( osVersion . majorVersion , osVersion . minorVersion ) {
2021-10-26 11:21:44 -07:00
case ( 14 , 3 ) :
return OTAUpdate ( url : URL ( string : " https://updates.cdn-apple.com/2020WinterFCS/patches/001-87330/99E29969-F6B6-422A-B946-70DE2E2D73BE/com_apple_MobileAsset_SoftwareUpdate/67f9e42f5e57a20e0a87eaf81b69dd2a61311d3f.zip " ) ! ,
2023-03-01 00:48:36 -05:00
archivePath : " AssetData/payloadv2/payload.042 " )
2021-10-25 22:27:30 -07:00
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 " ) ! ,
2023-03-01 00:48:36 -05:00
archivePath : " AssetData/payloadv2/payload.042 " )
2021-10-25 22:27:30 -07:00
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 " ) ! ,
2023-03-01 00:48:36 -05:00
archivePath : " AssetData/payloadv2/payload.043 " )
2021-10-25 22:27:30 -07:00
default : throw PatchAppError . unsupportedOperatingSystemVersion ( osVersion )
}
}
. eraseToAnyPublisher ( )
}
2023-03-01 00:48:36 -05:00
func downloadArchive ( from update : OTAUpdate ) -> AnyPublisher < URL , Error > {
2021-10-25 22:27:30 -07:00
Just ( ( ) ) . tryMap {
2022-04-13 20:41:38 -07:00
#if targetEnvironment ( simulator )
2023-03-01 00:48:36 -05:00
throw PatchAppError . unsupportedOperatingSystemVersion ( ProcessInfo . processInfo . operatingSystemVersion )
2022-04-13 20:41:38 -07:00
#else
2023-03-01 00:48:36 -05:00
try FileManager . default . createDirectory ( at : self . patchDirectory , withIntermediateDirectories : true , attributes : nil )
let archiveURL = self . patchDirectory . appendingPathComponent ( " ota.archive " )
try archiveURL . withUnsafeFileSystemRepresentation { archivePath in
guard let fz = fragmentzip_open ( ( update . url . absoluteString as NSString ) . utf8String ! ) else {
throw URLError ( . cannotConnectToHost , userInfo : [ NSLocalizedDescriptionKey : NSLocalizedString ( " The connection failed because a connection cannot be made to the host. " , comment : " " ) ,
NSURLErrorKey : update . url ] )
}
defer { fragmentzip_close ( fz ) }
self . progress . becomeCurrent ( withPendingUnitCount : 100 )
defer { self . progress . resignCurrent ( ) }
guard fragmentzip_download_file ( fz , update . archivePath , archivePath ! , ALTFragmentZipCallback ) = = 0 else {
throw URLError ( . networkConnectionLost , userInfo : [ NSLocalizedDescriptionKey : NSLocalizedString ( " The connection failed because the network connection was lost. " , comment : " " ) ,
NSURLErrorKey : update . url ] )
}
2021-10-25 23:13:25 -07:00
}
2023-03-01 00:48:36 -05:00
print ( " Downloaded OTA archive. " )
return archiveURL
2022-04-13 20:41:38 -07:00
#endif
2021-10-25 22:27:30 -07:00
}
. mapError { ( $0 as NSError ) . withLocalizedFailure ( NSLocalizedString ( " Could not download OTA archive. " , comment : " " ) ) }
. eraseToAnyPublisher ( )
}
2023-03-01 00:48:36 -05:00
func extractSpotlightFromArchive ( at archiveURL : URL ) -> AnyPublisher < URL , Error > {
2021-10-25 22:27:30 -07:00
Just ( ( ) ) . tryMap {
2022-04-13 20:41:38 -07:00
#if targetEnvironment ( simulator )
2023-03-01 00:48:36 -05:00
throw PatchAppError . unsupportedOperatingSystemVersion ( ProcessInfo . processInfo . operatingSystemVersion )
2022-04-13 20:41:38 -07:00
#else
2023-03-01 00:48:36 -05:00
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 ) { _ , filePath , _ in
guard filePath = = FilePath ( spotlightPath ) else { return . skip }
return . ok
}
print ( " Extracted Spotlight from OTA archive. " )
return spotlightFileURL
2022-04-13 20:41:38 -07:00
#endif
2021-10-25 22:27:30 -07:00
}
. mapError { ( $0 as NSError ) . withLocalizedFailure ( NSLocalizedString ( " Could not extract Spotlight from OTA archive. " , comment : " " ) ) }
. eraseToAnyPublisher ( )
}
2023-03-01 00:48:36 -05:00
func patch ( _ app : ALTApplication , withBinaryAt patchFileURL : URL ) -> AnyPublisher < URL , Error > {
2021-10-25 22:27:30 -07:00
Just ( ( ) ) . tryMap {
// e x e c u t a b l e U R L m a y b e n i l , s o u s e i n f o D i c t i o n a r y i n s t e a d t o d e t e r m i n e e x e c u t a b l e n a m e .
// g u a r d l e t a p p N a m e = a p p . b u n d l e . e x e c u t a b l e U R L ? . l a s t P a t h C o m p o n e n t e l s e { t h r o w O p e r a t i o n E r r o r . i n v a l i d A p p }
guard let appName = app . bundle . infoDictionary ? [ kCFBundleExecutableKey as String ] as ? String else { throw OperationError . invalidApp }
2023-03-01 00:48:36 -05:00
2021-10-25 22:27:30 -07:00
let temporaryAppURL = self . patchDirectory . appendingPathComponent ( " Patched.app " , isDirectory : true )
try FileManager . default . copyItem ( at : app . fileURL , to : temporaryAppURL )
2023-03-01 00:48:36 -05:00
2021-10-25 22:27:30 -07:00
let appBinaryURL = temporaryAppURL . appendingPathComponent ( appName , isDirectory : false )
try self . appPatcher . patchAppBinary ( at : appBinaryURL , withBinaryAt : patchFileURL )
2023-03-01 00:48:36 -05:00
2021-10-25 22:27:30 -07:00
print ( " Patched \( app . name ) . " )
return temporaryAppURL
}
. mapError { ( $0 as NSError ) . withLocalizedFailure ( String ( format : NSLocalizedString ( " Could not patch %@ placeholder. " , comment : " " ) , app . name ) ) }
. eraseToAnyPublisher ( )
}
}