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 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
{
// 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
}
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 )
{
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 " ) ! ,
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 " ) ! ,
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 " )
2021-10-25 23:13:25 -07:00
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 ] )
}
2021-10-25 22:27:30 -07:00
defer { fragmentzip_close ( fz ) }
self . progress . becomeCurrent ( withPendingUnitCount : 100 )
2021-10-25 23:13:25 -07:00
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 22:27:30 -07:00
}
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 {
// 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 }
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 ( )
}
}